diff --git a/.gitignore b/.gitignore index 9d94932..d1c5689 100644 --- a/.gitignore +++ b/.gitignore @@ -13,7 +13,9 @@ go.work.sum *.log logs/ .DS_Store -.claude/ +# Local agent/tooling state +/.agents/ +/.claude/ .codex/ .cursor/ .playwright-mcp/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..d8580aa --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,15 @@ +# Agent instructions + +Read [README.md](README.md) and [CONTRIBUTING.md](CONTRIBUTING.md) before working here; they define this repository's ownership, build, and validation rules. + +## Writing + +Give human-facing prose a final readability pass. Lead with the outcome, use concrete plain language and active voice, and cut filler, stock framing, repetition, and promotional claims. Preserve meaning, evidence, citations, uncertainty, and established terminology. Never rewrite exact quotations, commands, logs, identifiers, API names, or contractual language. Match the audience and use restrained formatting. + +## Pull requests + +- Never create a pull request unless the developer explicitly asks. +- Use a plain-language Conventional Commit title. In the body, explain the problem before the solution and end with the required AI disclosure, naming the exact model, harness, and tooling used. +- For UI changes, include before-and-after evidence and a video for motion or timing behavior. Upload pull-request evidence to GitHub; never commit PR-only assets such as `.github/pr-assets`. +- Keep one concern per pull request. If the description needs the word "also" for another change, split it. +- When babysitting a pull request, poll for checks and comments newer than the last push. Verify bot findings against the source, fix real issues, and dismiss false positives with a written reason. Stay quiet when nothing is new, and stop when the latest commit is green. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 120000 index 0000000..47dc3e3 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..e2e2361 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,58 @@ +# Contributing to the Prairie Plugin SDK + +The [Prairie contribution guide](https://github.com/prairie-server/prairie-server/blob/main/CONTRIBUTING.md) +covers project-wide coordination, focused changes, evidence, AI disclosure, and +pull request expectations. Those requirements apply here; this guide adds the +SDK-specific workflow. + +## Before you start + +Open an [issue](https://github.com/prairie-server/prairie-plugin-sdk/issues) before +adding or changing a capability, protobuf contract, manifest field, runtime +behavior, or public Go API. The SDK is a versioned contract consumed by +`prairie-server` and every plugin, so identify affected downstream repositories and +the intended compatibility strategy before implementation. + +Host-only behavior belongs in +[`prairie-server`](https://github.com/prairie-server/prairie-server); provider-specific +behavior belongs in the individual plugin repository. + +## Development setup + +Use the Go version declared in `go.mod`. Protobuf regeneration also requires +`protoc`; `make proto` installs the generator tools under `bin/` as needed. +Read [docs/compatibility.md](docs/compatibility.md) before changing a public +contract. + +## Validate your change + +Run focused package tests while iterating. Before opening a pull request, run: + +```sh +go test ./... +go vet ./... +go build ./examples/hello-scheduled-task +go build ./examples/hello-runtime-host +go build ./examples/hello-network-access +gofmt -l . +``` + +For protobuf or generated-code changes, also run: + +```sh +make proto +``` + +`gofmt -l .` should print nothing. If it reports unrelated pre-existing drift, +none of the Go files touched by your change may appear in the output; do not add +to the output, and report what remains. When no contract change is intended, +`make proto` must leave generated code unchanged. Add compatibility coverage +for presence semantics, validation, and existing consumers when a contract +changes. + +## Open the pull request + +Use a Conventional Commit title, explain the compatibility and release impact, +list downstream coordination, and paste the actual validation results. Read the +[AI-assisted contribution policy](https://github.com/prairie-server/prairie-server/blob/main/docs/ai-contributions.md) +and include its disclosure block. diff --git a/README.md b/README.md index 1842e92..f7d98a3 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,11 @@ Public Go SDK for building Prairie plugins. **Not a runtime plugin** — this is a library that plugin authors depend on via `go.mod`. -`prairie-plugin-sdk` is the source of truth for the plugin authoring contract. First-party consumers (Prairie host, `prairie-plugin-tmdb`, `prairie-plugin-metadb`, every other plugin in this repo) pin tagged semver releases. Local multi-repo workspaces may use `go.work` or a temporary `replace`, but CI and release builds resolve the SDK from a published module tag. +`prairie-plugin-sdk` is the source of truth for the plugin authoring contract. +First-party consumers—including the Prairie host and the separate metadata, +marker, autoscan, and watch-provider plugin repositories—pin tagged semantic +versions. Local multi-repository workspaces may use `go.work`, but CI and +release builds resolve the SDK from a published module tag. ## Packages @@ -10,6 +14,8 @@ Public Go SDK for building Prairie plugins. **Not a runtime plugin** — this is - `github.com/prairie-server/prairie-plugin-sdk/pkg/pluginsdk/capability` — stable capability type constants for manifests and peer discovery. - `github.com/prairie-server/prairie-plugin-sdk/pkg/pluginsdk/config` — config-schema helpers. - `github.com/prairie-server/prairie-plugin-sdk/pkg/pluginsdk/convert` — type conversions. +- `github.com/prairie-server/prairie-plugin-sdk/pkg/pluginsdk/httpclient` — credentialed JSON-over-HTTP client with bounded responses and typed status errors. +- `github.com/prairie-server/prairie-plugin-sdk/pkg/pluginsdk/imagevariant` — canonical image-size variant strings. - `github.com/prairie-server/prairie-plugin-sdk/pkg/pluginsdk/manifest` — manifest loading/rendering. - `github.com/prairie-server/prairie-plugin-sdk/pkg/pluginsdk/runtime` — `manifest` subcommand + `Runtime` server scaffolding. - `github.com/prairie-server/prairie-plugin-sdk/pkg/pluginsdk/runtimedefault` — default `Runtime` implementation with `BindHostBroker` already wired; embed it to skip boilerplate. @@ -22,6 +28,7 @@ The SDK ships protobuf contracts for every capability the host understands: - `metadata_provider.v1` - `marker_provider.v1` - `media_analyzer.v1` +- `image_resolver.v1` - `scheduled_task.v1` - `event_consumer.v1` - `auth_provider.v1` @@ -29,6 +36,7 @@ The SDK ships protobuf contracts for every capability the host understands: - `request_router.v1` - `scan_source.v1` - `watch_sync_provider.v1` +- `network_access_provider.v1` - `audiobook_backend.v1` - `ebook_backend.v1` @@ -43,7 +51,7 @@ A typical plugin: 3. Supports the `manifest` subcommand via `pkg/pluginsdk/runtime` so the host can introspect manifests without launching the plugin. 4. Is installed either from a catalog or by uploading a trusted binary to a Prairie server. -For a minimal self-describing plugin, see [`examples/hello-scheduled-task`](examples/hello-scheduled-task). For a plugin that calls back into the host via `RuntimeHost` (publishing events, listing libraries), see [`examples/hello-runtime-host`](examples/hello-runtime-host). +For a minimal self-describing plugin, see [`examples/hello-scheduled-task`](examples/hello-scheduled-task). For a plugin that calls back into the host via `RuntimeHost` (publishing events, listing libraries), see [`examples/hello-runtime-host`](examples/hello-runtime-host). For a stub overlay-network provider, see [`examples/hello-network-access`](examples/hello-network-access). ## Operator-facing presentation @@ -122,19 +130,58 @@ err = host.CallPluginJSON(ctx, runtimehost.CallPluginJSONRequest{ The `auth_provider.v1` capability also exposes OAuth-flow RPCs (`InitAuthorize`, `ExchangeCode`, `RefreshSession`) for plugins that wrap external identity providers. +## Request routers + +`request_router.v1` lets the host hand a media request to a download backend +such as Sonarr, Radarr, or Seerr. The host owns the request lifecycle, policy, +and quality governance; the plugin routes the request to a configured +connection and reports its status. + +A series request may name seasons in `RequestDescriptor.seasons`. Season `0` +is Specials, and an empty list means the whole series. A plugin that fulfils +seasons individually declares it in its manifest: + +```json +{ + "type": "request_router.v1", + "id": "arr", + "request_router": { "supports_seasons": true } +} +``` + +A declaring plugin must acquire only the requested seasons. When the series +already exists upstream, it adds those seasons to what is already tracked and +leaves the other seasons alone, so a request for season 4 never stops tracking +seasons 1–3. Repeating a request must converge rather than add the series +again. `CheckStatus` receives the same descriptor, so status can cover the +requested seasons. + +Plugins without the flag, including every plugin built before it existed, keep +today's whole-series behaviour. The host requests only the missing seasons of a +series it already has from plugins that declare `supports_seasons`, because +any other plugin would add the whole series again. + ## Watch sync providers `watch_sync_provider.v1` lets external plugins participate in Prairie's host-owned watch-provider pipeline. The host owns encrypted per-profile credentials, -OAuth state, durable desired-state events, retries, ordering, and -reconciliation. Plugins are stateless protocol adapters: they receive secrets -only for the duration of an RPC, map rich movie/episode identity to an upstream -service, and return typed apply or retry outcomes. +authorization-code and device-code flow state, durable desired-state events, +retries, ordering, and reconciliation. Plugins are stateless protocol adapters: +they receive secrets only for the duration of an RPC, map rich movie, episode, +and series identity to an upstream service, and return typed apply or retry +outcomes. Watch-sync plugins must not persist or log credentials, authorization codes, provider flow state, or secret configuration. `ApplyEvents` is an at-least-once contract; plugins must treat `event_id` as stable across retries and implement -convergent desired-state updates rather than increments. +convergent desired-state updates rather than increments. That rule also applies +to scrobble stops: replaying the same event ID must not create another play. +For playback events, `completed` is the host's authoritative watched decision; +plugins must not infer completion from `watch_history_id` or percentage alone. +Metadata consumers must likewise check optional `season_number` presence: zero +means Specials, while absence means no season scope. See +[compatibility guidance](docs/compatibility.md#presence-sensitive-optional-fields) +for the request and record rules. Authenticated RPCs receive the same host-owned capability, configuration, and credential data through `WatchSyncAuthenticatedContext`. The context exists @@ -144,6 +191,29 @@ patches. The host validates and persists them before consuming results, pages, or faults—even when the response contains a fault. If credential persistence fails, the host commits no other response data. +Device-code plugins register both `WatchSyncProvider` and the separate +`WatchSyncDeviceAuthorizationService`. Keeping device authorization in a +second service preserves source compatibility for v0.12 Go providers that +implemented `WatchSyncProviderServer` directly. Register it without changing +the released `CapabilityServers` shape: + +```go +runtime.ServeManifestWithOptions(manifestJSON, version, servers, + runtime.WithWatchSyncDeviceAuthorization(deviceAuthServer)) +``` + +A pending poll may replace its opaque provider state, polling interval, and +expiry; the host encrypts and persists those values before the next poll. +Those updates remain part of the same user challenge, so the original user code +and verification URL must stay valid until expiry. An explicitly empty +`provider_state` clears the prior state; omitting it retains the prior state. + +`WatchSyncProviderConfig` is keyed by manifest config key and field, for example +`provider.client_id`. Scalar values are sent as strings and structured values +as JSON. Fields marked secret in the manifest are sent through `secret_values`; +undeclared fields are treated as secret. Plugins must accept configuration from +the RPC context rather than relying on process-global state. + Descriptors and events use the shared `WatchSyncMediaType` enum so advertised support and delivered media cannot drift between string conventions. Apply results pair their delivery status with a typed fault: successful results omit @@ -151,13 +221,49 @@ the fault, temporary retries use `TEMPORARY`, rate limits use `RATE_LIMITED` with an optional delay, and rejected events use a non-retryable fault code. Connection-wide faults such as invalid credentials belong on the RPC response. +A `SERIES` media item describes the show itself: `external_ids`, `title`, and +`year` identify the series, and the `series_*`, season, and episode fields are +unused. + +Ratings are integers from 1 to 10 in every rating field; the host owns +conversion to its own display scale. Plugins convert between the provider's +native scale and 1–10 by rounding half up and clamping to the valid range. +`import_ratings` means `ListRemoteState` returns `RATING` states, and +`export_ratings` means `ApplyEvents` handles both `SET_RATING` and +`REMOVE_RATING`; there is no separate removal flag. `SET_RATING` carries the +value in the event's `rating` field, where zero is never valid. Both operations +are convergent desired-state writes: resending the same value, or removing a +rating that is already absent, must return `APPLIED` or `NO_CHANGE`, never a +fault. The host sends rating events only for media types listed in +`supported_media_types`, and manifest validation requires that list to include +`MOVIE` or `SERIES` when either ratings flag is set. + `ListRemoteState` returns provider-neutral typed subrecords. `watched` carries a play count and last-watched time; `progress` carries a fractional percentage and -paused time. An item may contain either or both. The host keeps the request +paused time; `favorite` and `watchlist` carry list membership; `rating` carries +a 1–10 rating and when it was set. An item may contain multiple state families. +The host requests only the state families a sync phase needs, keeps that phase's `cursor` fixed while following ephemeral page tokens, commits each successful page, and only then persists the final `next_cursor`. `complete_snapshot=true` means the traversal is authoritative; when false, missing items are not -deletions. +deletions. In a complete `RATING` traversal, an item absent from the snapshot is +unrated. An incremental favorite, watchlist, or rating removal is an item whose +corresponding state has `removed=true`; it may omit `media` when +`provider_item_key` identifies a record previously returned to the host. When +`provides_watchlist_order=true`, watchlist traversals must be complete snapshots +and the order of returned watchlist states is the remote list order. Event +`list_position` is presence-aware: an explicit zero means the first position, +while omission means no requested ordering. + +## Network access providers + +`network_access_provider.v1` lets a resident plugin give the deployment an +overlay-network identity (Tailscale, NetBird) and reverse-proxy overlay +traffic to the host's local listeners. The host starts these plugins at boot, +restarts them on crash, stores their per-instance state encrypted, and +aggregates status across the API server and proxy nodes. See +[docs/network-access-provider.md](docs/network-access-provider.md) for the +proxy contract, `GetHostInfo` fields, instance state, and enrollment rules. ## Scan sources @@ -192,10 +298,28 @@ Before downstream repos stop using local workspace overrides, the required SDK c ## Build & test ```bash -make proto # regenerate protobuf code (uses locally vendored buf under ./bin/) +make proto # regenerate protobuf code (installs tools under ./bin/ as needed) go test ./... ``` +## Contributing + +Read [CONTRIBUTING.md](CONTRIBUTING.md) and +[docs/compatibility.md](docs/compatibility.md) before opening a pull request. +Public Go, protobuf, runtime, and manifest changes should start as an issue and +identify affected downstream repositories. + +## Naming and branding + +Give your plugin its own name and mention Prairie in the summary, for example +"Trakt sync plugin for Prairie". Repository and package names such as +`prairie-plugin-trakt` are fine, since "prairie" only describes what the code plugs +into. Avoid "Prairie[word]" product names such as PrairieTrakt, which read as +official, and do not use the Prairie logo as your plugin's icon. Set +`publisher_name` to yourself, not "Prairie", unless the plugin is published by the +project. The full guidance, including what needs no permission, is at +. + ## License Apache-2.0. See [LICENSE](LICENSE). diff --git a/docs/compatibility.md b/docs/compatibility.md index 0251a09..e205142 100644 --- a/docs/compatibility.md +++ b/docs/compatibility.md @@ -13,6 +13,7 @@ The compatibility boundary includes: - manifest helpers in `pkg/pluginsdk/manifest` - config validation helpers in `pkg/pluginsdk/config` - generic capability metadata conversion helpers in `pkg/pluginsdk/convert` +- canonical image-variant strings in `pkg/pluginsdk/imagevariant` ## Versioning Rules @@ -29,11 +30,63 @@ The compatibility boundary includes: - CI and release pipelines should build with `GOWORK=off` and without checking out this repo as a sibling source dependency. - Local `go.work` files and temporary `replace` directives are acceptable for development, but they must not be committed as the release path. +## Open Vocabularies + +Some contract fields carry an open string vocabulary rather than an enum, so +Prairie can add values without a breaking protobuf change. Plugins must tolerate +values they do not recognize. + +Image variants (`ResolveImageURLRequest.variant`, +`ResolveImageURLsRequest.variant`, `ResolveCatalogImageURLsRequest.variant`) are +the current example. The canonical values are exported from +`pkg/pluginsdk/imagevariant`: `card`, `featured`, `large`, `full`, `original`, +listed smallest to largest. `large` (~780px posters and stills, ~1280px logos +and backdrops) was added between `featured` and `full` once Prairie gained +client-selectable image sizes; adding it is an additive change and does not +require a plugin update. + +A plugin receiving an unknown variant MUST degrade gracefully to its nearest +supported size — or its default size — and MUST NOT return an error. Returning +an error turns a slightly-wrong image size into a missing image. Use a `switch` +with a `default` arm rather than an exhaustive match, and do not assume the +constants shipped in any given SDK tag are the complete set. The same rule +applies to any future open vocabulary added to `v1`. + +## Presence-Sensitive Optional Fields + +Some contract fields use proto3 `optional` because absence and zero have +different meanings. Current examples are `WatchSyncEvent.list_position`, +`GetImagesRequest.season_number`, and `ImageRecord.season_number`. + +Consumers must check presence (`nil` pointer in Go or `HasField` in reflective +APIs), never infer it from the numeric value. Calling +`GetSeasonNumber() != 0` silently conflates "no season scope" with "Specials +(season zero) requested." + +Watch-sync rating fields (`WatchSyncEvent.rating` and +`WatchSyncRemoteRatingState.rating`) are deliberately plain `int32`. Valid +ratings run from 1 to 10, so zero never carries a rating and no presence check +is needed. + +`RequestDescriptor.seasons` is a repeated field, so it has no presence: an +empty list means the whole series, and season `0` in a non-empty list means +Specials. A plugin built before the field existed decodes it as an unknown +field and fulfils the whole series. Because the plugin cannot say so on the +wire, the host decides who may receive a season-only request from the +manifest's `RequestRouterDescriptor.supports_seasons` flag instead. An absent +descriptor means the flag is false. + +A season-scoped `GetImagesRequest` is a scope, not a guarantee. Plugins that +can filter by season should do so, and plugins should populate +`ImageRecord.season_number` whenever the season is known. Hosts must bucket and +verify images by the per-image field rather than assume a filtered response. + ## Runtime Compatibility - `prairie_api_version` is the coarse runtime compatibility gate between Prairie and a plugin binary. - Host installs should reject incompatible API versions before runtime startup. - A plugin binary should return the same manifest shape that Prairie installs, except that binaries may compute their checksum dynamically at runtime. +- From this version, `convert.DecodeCapability` ignores fields and enum values it does not know, so a server node built on this SDK or later, in a mixed-version cluster sharing one database, still loads capability metadata written by a newer SDK; it only loses the parts added after its own SDK version. Nodes built on an earlier SDK decode strictly and reject such metadata, so upgrade every node past them before installing plugins that publish newer fields. ## Go Support diff --git a/docs/network-access-provider.md b/docs/network-access-provider.md new file mode 100644 index 0000000..b100b84 --- /dev/null +++ b/docs/network-access-provider.md @@ -0,0 +1,180 @@ +# Network access providers (`network_access_provider.v1`) + +A network access provider gives a Prairie deployment an overlay-network identity +(Tailscale via tsnet first; NetBird later) so clients reach Prairie without port +forwarding or a public reverse proxy. The plugin owns the overlay client and a +reverse proxy in front of the host's local listeners. The host owns +supervision, per-instance state storage, status aggregation, access-path +routing, and the admin API. + +Added in SDK v0.16.0. Server support arrives in stages; check the host's +`/api/v2/network-access/capabilities` before assuming a feature exists. + +## Manifest + +Declare the capability with the typed descriptor. The host lists providers +from the descriptor without launching the plugin. + +```json +{ + "type": "network_access_provider.v1", + "id": "tailscale", + "display_name": "Tailscale", + "network_access_provider": { + "provider": "tailscale", + "display_name": "Tailscale" + } +} +``` + +`provider` is a stable lowercase, path-safe slug; it appears in admin API +paths such as `/api/v2/admin/network-access/{provider}/status`. Reserved +slugs today: `tailscale`, `netbird`. + +## Resident lifecycle + +Plugins declaring this capability are resident. The host: + +- starts them when the API listener binds (and on every proxy node), +- restarts them on exit or failed health with exponential backoff, +- stops them last during shutdown. + +Every host (the API server and each proxy node) runs its own instance of the +same installation. Each instance has its own overlay identity and its own +instance-state scope. Do not assume a single process. + +## gRPC service + +```proto +service NetworkAccessProvider { + rpc Connect(NetworkAccessConnectRequest) returns (NetworkAccessStatus); + rpc Disconnect(NetworkAccessDisconnectRequest) returns (NetworkAccessStatus); + rpc GetStatus(NetworkAccessGetStatusRequest) returns (NetworkAccessStatus); +} +``` + +Register it with `runtime.CapabilityServers{NetworkAccessProvider: ...}`. + +`NetworkAccessStatus.state` is one of `disconnected`, +`awaiting_authorization`, `connecting`, `connected`, `error`. The vocabulary +is open; the host tolerates values it does not know. `Connect` may return +`awaiting_authorization` or `connecting` and finish enrollment in the +background; push each later transition with +`RuntimeHost.ReportNetworkAccessStatus` so the host does not poll. The host +still calls `GetStatus` on demand. + +`desired_connected` is plugin-owned intent. Persist it in instance state (see +below), report it in every status, and reconnect on start when it is true. +The host never writes it. + +## What the host tells you: `GetHostInfo` + +`RuntimeHost.GetHostInfo` carries everything the proxy needs: + +| Field | Meaning | +|---|---| +| `host_role` | `api` or `proxy`. | +| `host_name` | Node name on proxies, server name on the API host. | +| `node_id` | `stream_nodes.id` on proxies, `0` on the API host. | +| `ingress_token` | Per-process-start secret; see below. | +| `listeners` | Local listeners to expose: `name`, loopback `address`, `default_port`. | + +`listeners` always contains `api`. On the API host it also carries `jellyfin` +and `abs` when those listeners are enabled. Proxies report only `api`. +`default_port` is the port the plugin should expose that listener on (443 for +`api` under HTTPS, 8096 for `jellyfin`, 13378 for `abs`); zero means use the +provider default. Expose every listener the host reports and return one +`NetworkAccessListener` per exposed listener in the status; `origin` is the +`api` listener's origin. + +The typed client is `runtimehost.Client.GetHostInfo`, which returns +`runtimehost.HostInfo` with `Listener(name)` for lookup. + +## Proxy contract + +For every request the plugin forwards to a host listener: + +- Preserve the incoming `Host` header. +- Overwrite `X-Forwarded-Proto` with `https`. +- Set `X-Forwarded-For` to the overlay peer address. +- Set `X-Prairie-Ingress-Token` to the value from `GetHostInfo`, replacing any + client-supplied value; the host answers 403 when the header carries more + than one value. + +The host validates the token, strips the header, and records the request's +access path so stream URLs point tailnet clients at overlay origins. The +plugin's loopback source is in the host's default trusted-proxy list, so the +forwarded headers are honoured. + +The ingress token rotates on every host process start and is compared in +constant time. Keep it in memory only; never log or persist it. After a host +or plugin restart, call `GetHostInfo` again before proxying; a stale token is +rejected with `403`. + +## Instance state + +Per-instance state (tsnet node keys, profile state, `desired_connected`) +lives in the host, encrypted, scoped to installation plus host. The plugin +never sees the scope and never writes files. + +```proto +rpc ReadInstanceState(ReadInstanceStateRequest) returns (ReadInstanceStateResponse); +rpc WriteInstanceState(WriteInstanceStateRequest) returns (WriteInstanceStateResponse); +``` + +Limits: key up to 256 bytes, value up to 256 KiB, up to 256 keys per scope. +The SDK client checks the key and value limits before the round trip. + +`runtimehost.InstanceStateStore` exposes the RPCs in the two-method shape of +tailscale's `ipn.StateStore`: + +```go +type StateStore interface { + ReadState(key string) ([]byte, error) // runtimehost.ErrStateNotExist when absent + WriteState(key string, value []byte) error +} +``` + +The SDK does not import tailscale, and `ipn.StateStore` takes `ipn.StateKey` +rather than `string`, so wrap it in the plugin: + +```go +type tsStore struct{ inner *runtimehost.InstanceStateStore } + +func (s tsStore) ReadState(k ipn.StateKey) ([]byte, error) { + b, err := s.inner.ReadState(string(k)) + if errors.Is(err, runtimehost.ErrStateNotExist) { + return nil, ipn.ErrStateNotExist + } + return b, err +} + +func (s tsStore) WriteState(k ipn.StateKey, v []byte) error { + return s.inner.WriteState(string(k), v) +} + +srv := &tsnet.Server{Store: tsStore{inner: host.InstanceStateStore()}} +``` + +Each `ReadState`/`WriteState` call uses a 30 s deadline by default; +`WithTimeout` changes it. + +## Enrollment + +Support both an auth key in plugin config (for hands-off enrollment across +many nodes) and an interactive auth URL as the fallback. While enrollment is +pending, report `state: awaiting_authorization` with `auth_url` set. The +`auth_url` is admin-only: never log it, and clear it once enrollment +completes. + +## Logging + +Log state transitions and errors. Never log `auth_url`, `ingress_token`, +auth keys, or instance state values. + +## Example + +[`examples/hello-network-access`](../examples/hello-network-access) is a stub +provider with no overlay network. It shows the manifest, registration, +instance-state persistence of `desired_connected`, and the status push, and +serves as a test fixture for the host. diff --git a/docs/runtime-host.md b/docs/runtime-host.md index d447f10..ac449a5 100644 --- a/docs/runtime-host.md +++ b/docs/runtime-host.md @@ -11,7 +11,7 @@ plugin, the plugin calls back into the host. | `PublishEvent(name, payload)` | Publish onto the host's event bus. The host stamps `plugin..` in front of `name` server-side. | | `PublishEventTo(target_plugin_id, name, payload)` | Publish an event addressed to one installed plugin by stable `plugin_id`. | | `PublishEventToInstallation(target_installation_id, name, payload)` | Publish an event addressed to one specific plugin installation. | -| `GetHostInfo()` | Return public-safe host URLs for callbacks and plugin-served links. | +| `GetHostInfo()` | Return public-safe host URLs for callbacks and plugin-served links, plus host role, listeners, and the ingress token for network access providers. | | `ListLibraries(user_id)` | Return libraries (optionally scoped to a user). | | `CheckMediaPresence(provider, media_type, ids)` | Batched lookup: which external IDs already exist in the host catalog. v1 supports provider="tmdb" only. | | `ListInstalledPlugins()` | Return installed plugins and their advertised capabilities. | @@ -21,6 +21,9 @@ plugin, the plugin calls back into the host. | `ResolveCatalogImageURLs(paths, variant)` | Resolve stored catalog image paths into host-generated browser URL targets. | | `MintScopedStream(request)` | Mint a short-lived stream grant for plugin-owned public access workflows. | | `CallPluginHTTP(request)` | Invoke another installed plugin's HTTP route through the host control plane. | +| `ReadInstanceState(key)` | Read one key of the calling instance's encrypted, host-scoped state. | +| `WriteInstanceState(key, value)` | Write one key of that state. Key ≤ 256 bytes, value ≤ 256 KiB, ≤ 256 keys per scope. | +| `ReportNetworkAccessStatus(status)` | Push a `network_access_provider.v1` status change so the host does not poll. | ## Using it from a plugin @@ -109,7 +112,9 @@ Catalog image fields are host-owned opaque paths. Pass poster or backdrop paths back unchanged to resolve them into browser targets: ```go -urls, err := host.ResolveCatalogImageURLs(ctx, []string{item.PosterURL, item.BackdropURL}, "thumbnail") +import "github.com/prairie-server/prairie-plugin-sdk/pkg/pluginsdk/imagevariant" + +urls, err := host.ResolveCatalogImageURLs(ctx, []string{item.PosterURL, item.BackdropURL}, imagevariant.Card) if err != nil { return } posterURL := urls[item.PosterURL] ``` @@ -118,6 +123,28 @@ Pass an empty variant to use the host default. Unresolved paths are omitted from the returned map, which is keyed by the original input path. Treat returned URLs as opaque host-generated browser targets. +### Image variants + +The variant is a semantic size hint, not an exact pixel contract. The canonical +vocabulary, smallest to largest, is exported as constants from +`pkg/pluginsdk/imagevariant`: + +| Constant | Value | Intended surface | +| --- | --- | --- | +| `imagevariant.Card` | `card` | grid and list thumbnails, search results, cast rows (~300px posters) | +| `imagevariant.Featured` | `featured` | hero artwork, detail pages, section headers (~500px posters/logos, ~1280px backdrops) | +| `imagevariant.Large` | `large` | high-density displays and client-selected larger sizes (~780px posters/stills, ~1280px logos/backdrops) | +| `imagevariant.Full` | `full` | full-screen and near-source rendering | +| `imagevariant.Original` | `original` | unresized source asset | + +`large` was added after the initial vocabulary; the set is open and may grow +again. A plugin that resolves variants itself (for example a metadata provider +implementing `ResolveImageURL` / `ResolveImageURLs`) must treat an unknown +variant as a request for its nearest supported size — or its default — and must +never return an error. Implement the mapping as a `switch` with a `default` arm +so new variants fall through to a sensible size instead of failing a resolve, +which users see as a missing poster. + ### Peer discovery and calls Use capability constants and discovery helpers instead of hard-coded strings diff --git a/examples/hello-network-access/.gitignore b/examples/hello-network-access/.gitignore new file mode 100644 index 0000000..79d68cb --- /dev/null +++ b/examples/hello-network-access/.gitignore @@ -0,0 +1 @@ +hello-network-access diff --git a/examples/hello-network-access/README.md b/examples/hello-network-access/README.md new file mode 100644 index 0000000..653b743 --- /dev/null +++ b/examples/hello-network-access/README.md @@ -0,0 +1,27 @@ +# Hello Network Access + +A stub `network_access_provider.v1` plugin with no overlay network. It shows +the contract shape and doubles as a host-side test fixture: + +- `Connect` marks the instance connected, builds a fake origin per listener + reported by `RuntimeHost.GetHostInfo`, persists `desired_connected` in + host-provided instance state, and pushes the status with + `ReportNetworkAccessStatus`. +- `Disconnect` reverses it. +- `GetStatus` restores `desired_connected` from instance state on first call + so intent survives a plugin restart. + +There is no tsnet dependency. A real provider is described in +[docs/network-access-provider.md](../../docs/network-access-provider.md). + +## Build + +```sh +go build -o hello-network-access ./examples/hello-network-access +``` + +## Inspect the manifest + +```sh +./hello-network-access manifest +``` diff --git a/examples/hello-network-access/main.go b/examples/hello-network-access/main.go new file mode 100644 index 0000000..746aa98 --- /dev/null +++ b/examples/hello-network-access/main.go @@ -0,0 +1,181 @@ +// Command hello-network-access is a stub network_access_provider.v1 plugin. +// +// It has no overlay network: Connect flips an in-memory state to "connected", +// reports a fake origin built from the host's api listener, persists +// desired_connected in host-provided instance state, and Disconnect reverses +// it. It exists so prairie-server tests can exercise the resident plugin +// supervisor, the instance state RPCs, and the status push without a tsnet +// dependency. A real provider follows docs/network-access-provider.md. +package main + +import ( + "context" + _ "embed" + "fmt" + "os" + "sync" + + "github.com/hashicorp/go-hclog" + + pluginv1 "github.com/prairie-server/prairie-plugin-sdk/pkg/pluginproto/prairie/plugin/v1" + sdkruntime "github.com/prairie-server/prairie-plugin-sdk/pkg/pluginsdk/runtime" + "github.com/prairie-server/prairie-plugin-sdk/pkg/pluginsdk/runtimehost" +) + +//go:embed manifest.json +var manifestJSON []byte + +const ( + version = "0.1.0" + // desiredKey is the instance state key holding plugin-owned intent. + desiredKey = "desired_connected" +) + +type provider struct { + pluginv1.UnimplementedNetworkAccessProviderServer + + logger hclog.Logger + + mu sync.Mutex + connected bool + desired bool + restored bool + hostInfo *runtimehost.HostInfo +} + +func (p *provider) Connect(ctx context.Context, _ *pluginv1.NetworkAccessConnectRequest) (*pluginv1.NetworkAccessStatus, error) { + p.mu.Lock() + defer p.mu.Unlock() + p.restoreLocked(ctx) + host := sdkruntime.Host() + if host != nil { + info, err := host.GetHostInfo(ctx) + if err != nil { + return nil, fmt.Errorf("get host info: %w", err) + } + p.hostInfo = info + } + p.connected = true + p.desired = true + if err := p.persistLocked(ctx, host); err != nil { + return nil, err + } + status := p.statusLocked() + p.reportLocked(ctx, host, status) + return status, nil +} + +func (p *provider) Disconnect(ctx context.Context, _ *pluginv1.NetworkAccessDisconnectRequest) (*pluginv1.NetworkAccessStatus, error) { + p.mu.Lock() + defer p.mu.Unlock() + p.restoreLocked(ctx) + host := sdkruntime.Host() + p.connected = false + p.desired = false + if err := p.persistLocked(ctx, host); err != nil { + return nil, err + } + status := p.statusLocked() + p.reportLocked(ctx, host, status) + return status, nil +} + +func (p *provider) GetStatus(ctx context.Context, _ *pluginv1.NetworkAccessGetStatusRequest) (*pluginv1.NetworkAccessStatus, error) { + p.mu.Lock() + defer p.mu.Unlock() + p.restoreLocked(ctx) + return p.statusLocked(), nil +} + +// restoreLocked loads desired_connected from instance state once per process +// and, when it was true, resumes the "connection" so the intent survives +// plugin restarts. A real provider would bring the overlay up here. +func (p *provider) restoreLocked(ctx context.Context) { + if p.restored { + return + } + host := sdkruntime.Host() + if host == nil { + return + } + value, found, err := host.ReadInstanceState(ctx, desiredKey) + if err != nil { + // Leave restored false so the next RPC tries again; a transient + // host error must not turn persisted intent into "disconnected". + p.logger.Warn("read instance state", "err", err) + return + } + p.restored = true + if found && string(value) == "1" { + p.desired = true + p.connected = true + if info, err := host.GetHostInfo(ctx); err == nil { + p.hostInfo = info + } + } +} + +// persistLocked writes desired_connected. A failure is returned to the RPC +// caller: the admin must know the intent did not survive, or the plugin would +// come back in the wrong state after its next restart. +func (p *provider) persistLocked(ctx context.Context, host *runtimehost.Client) error { + if host == nil { + return nil + } + value := []byte("0") + if p.desired { + value = []byte("1") + } + if err := host.WriteInstanceState(ctx, desiredKey, value); err != nil { + return fmt.Errorf("persist desired_connected: %w", err) + } + return nil +} + +func (p *provider) reportLocked(ctx context.Context, host *runtimehost.Client, status *pluginv1.NetworkAccessStatus) { + if host == nil { + return + } + if err := host.ReportNetworkAccessStatus(ctx, status); err != nil { + p.logger.Warn("report network access status", "err", err) + } +} + +func (p *provider) statusLocked() *pluginv1.NetworkAccessStatus { + status := &pluginv1.NetworkAccessStatus{ + State: runtimehost.NetworkAccessStateDisconnected, + ProviderVersion: "stub " + version, + DesiredConnected: p.desired, + } + if !p.connected { + return status + } + status.State = runtimehost.NetworkAccessStateConnected + status.Hostname = "stub.invalid" + status.Addresses = []string{"100.64.0.1"} + if p.hostInfo != nil { + for _, l := range p.hostInfo.Listeners { + // Zero asks for the provider default; this stub's default is + // HTTPS on 443, which the origin leaves implicit. + origin := "https://" + status.Hostname + if l.DefaultPort != 0 && l.DefaultPort != 443 { + origin = fmt.Sprintf("https://%s:%d", status.Hostname, l.DefaultPort) + } + status.Listeners = append(status.Listeners, &pluginv1.NetworkAccessListener{Name: l.Name, Origin: origin}) + if l.Name == runtimehost.ListenerAPI { + status.Origin = origin + } + } + } + if status.Origin == "" { + status.Origin = "https://" + status.Hostname + } + return status +} + +func main() { + logger := hclog.New(&hclog.LoggerOptions{Name: "hello-network-access", Output: os.Stderr}) + sdkruntime.ServeManifest(manifestJSON, version, sdkruntime.CapabilityServers{ + NetworkAccessProvider: &provider{logger: logger}, + }) +} diff --git a/examples/hello-network-access/manifest.json b/examples/hello-network-access/manifest.json new file mode 100644 index 0000000..8f8f57d --- /dev/null +++ b/examples/hello-network-access/manifest.json @@ -0,0 +1,32 @@ +{ + "plugin_id": "example.hello-network-access", + "version": "0.1.0", + "checksum": "__CHECKSUM__", + "prairie_api_version": "v1", + "supported_platforms": [ + { + "os": "linux", + "arch": "amd64" + }, + { + "os": "linux", + "arch": "arm64" + }, + { + "os": "darwin", + "arch": "arm64" + } + ], + "capabilities": [ + { + "type": "network_access_provider.v1", + "id": "stub", + "display_name": "Stub Overlay", + "description": "A stub network access provider with no real overlay network. Useful as a host test fixture.", + "network_access_provider": { + "provider": "stub", + "display_name": "Stub Overlay" + } + } + ] +} diff --git a/go.mod b/go.mod index 8ea5d9d..db796a7 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/hashicorp/go-hclog v1.6.3 github.com/hashicorp/go-plugin v1.7.0 github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 - google.golang.org/grpc v1.75.1 + google.golang.org/grpc v1.82.1 google.golang.org/protobuf v1.36.11 ) @@ -17,8 +17,8 @@ require ( github.com/mattn/go-colorable v0.1.12 // indirect github.com/mattn/go-isatty v0.0.17 // indirect github.com/oklog/run v1.1.0 // indirect - golang.org/x/net v0.48.0 // indirect - golang.org/x/sys v0.39.0 // indirect - golang.org/x/text v0.32.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect + golang.org/x/net v0.55.0 // indirect + golang.org/x/sys v0.45.0 // indirect + golang.org/x/text v0.37.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 // indirect ) diff --git a/go.sum b/go.sum index f0c0a85..ac30320 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw= github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -41,36 +43,36 @@ github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCw github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= -go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= -go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= -go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= -go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= -go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= -go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc= -go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= -go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= -go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= -golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= -golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= +go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= +go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= +go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= +go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= +go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= +go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= +go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= +go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= +go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= +golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= +golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= -golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= -golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= -gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= -gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= -google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI= -google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= +golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= +golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 h1:RmoJA1ujG+/lRGNfUnOMfhCy5EipVMyvUE+KNbPbTlw= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE= +google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/pkg/pluginproto/prairie/plugin/v1/common.pb.go b/pkg/pluginproto/prairie/plugin/v1/common.pb.go index c509a76..57a214d 100644 --- a/pkg/pluginproto/prairie/plugin/v1/common.pb.go +++ b/pkg/pluginproto/prairie/plugin/v1/common.pb.go @@ -977,8 +977,16 @@ type CapabilityDescriptor struct { // of type "watch_sync_provider.v1". Keeping this typed lets the host build // connection and authorization UI without launching the plugin. WatchSyncProvider *WatchSyncProviderDescriptor `protobuf:"bytes,10,opt,name=watch_sync_provider,json=watchSyncProvider,proto3" json:"watch_sync_provider,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Typed network-access contract metadata. Only meaningful for capabilities + // of type "network_access_provider.v1". The host lists available overlay + // providers from this descriptor without launching the plugin. + NetworkAccessProvider *NetworkAccessProviderDescriptor `protobuf:"bytes,11,opt,name=network_access_provider,json=networkAccessProvider,proto3" json:"network_access_provider,omitempty"` + // Typed request-router contract metadata. Only meaningful for capabilities + // of type "request_router.v1". Optional: an absent descriptor declares no + // optional request-router features. + RequestRouter *RequestRouterDescriptor `protobuf:"bytes,12,opt,name=request_router,json=requestRouter,proto3" json:"request_router,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *CapabilityDescriptor) Reset() { @@ -1081,6 +1089,20 @@ func (x *CapabilityDescriptor) GetWatchSyncProvider() *WatchSyncProviderDescript return nil } +func (x *CapabilityDescriptor) GetNetworkAccessProvider() *NetworkAccessProviderDescriptor { + if x != nil { + return x.NetworkAccessProvider + } + return nil +} + +func (x *CapabilityDescriptor) GetRequestRouter() *RequestRouterDescriptor { + if x != nil { + return x.RequestRouter + } + return nil +} + // PluginPresentation describes a plugin for server operators and links them to // its publisher, source, support, and release history. These values are // self-declared by the plugin and never determine catalog approval. @@ -1599,7 +1621,7 @@ var File_prairie_plugin_v1_common_proto protoreflect.FileDescriptor const file_prairie_plugin_v1_common_proto_rawDesc = "" + "\n" + - "\x1eprairie/plugin/v1/common.proto\x12\x11prairie.plugin.v1\x1a\x1cgoogle/protobuf/struct.proto\x1a+prairie/plugin/v1/watch_sync_provider.proto\"7\n" + + "\x1eprairie/plugin/v1/common.proto\x12\x11prairie.plugin.v1\x1a\x1cgoogle/protobuf/struct.proto\x1a/prairie/plugin/v1/network_access_provider.proto\x1a&prairie/plugin/v1/request_router.proto\x1a+prairie/plugin/v1/watch_sync_provider.proto\"7\n" + "\x11SupportedPlatform\x12\x0e\n" + "\x02os\x18\x01 \x01(\tR\x02os\x12\x12\n" + "\x04arch\x18\x02 \x01(\tR\x04arch\"\xdc\x01\n" + @@ -1676,7 +1698,7 @@ const file_prairie_plugin_v1_common_proto_rawDesc = "" + "\rPackagedAsset\x12\x12\n" + "\x04path\x18\x01 \x01(\tR\x04path\x12!\n" + "\fcontent_type\x18\x02 \x01(\tR\vcontentType\x12\x1c\n" + - "\tintegrity\x18\x03 \x01(\tR\tintegrity\"\xba\x03\n" + + "\tintegrity\x18\x03 \x01(\tR\tintegrity\"\xf9\x04\n" + "\x14CapabilityDescriptor\x12\x12\n" + "\x04type\x18\x01 \x01(\tR\x04type\x12\x0e\n" + "\x02id\x18\x02 \x01(\tR\x02id\x12!\n" + @@ -1689,7 +1711,9 @@ const file_prairie_plugin_v1_common_proto_rawDesc = "" + "auth_modes\x18\b \x03(\tR\tauthModes\x12\x19\n" + "\bicon_url\x18\t \x01(\tR\aiconUrl\x12^\n" + "\x13watch_sync_provider\x18\n" + - " \x01(\v2..prairie.plugin.v1.WatchSyncProviderDescriptorR\x11watchSyncProvider\"\xa2\x03\n" + + " \x01(\v2..prairie.plugin.v1.WatchSyncProviderDescriptorR\x11watchSyncProvider\x12j\n" + + "\x17network_access_provider\x18\v \x01(\v22.prairie.plugin.v1.NetworkAccessProviderDescriptorR\x15networkAccessProvider\x12Q\n" + + "\x0erequest_router\x18\f \x01(\v2*.prairie.plugin.v1.RequestRouterDescriptorR\rrequestRouter\"\xa2\x03\n" + "\x12PluginPresentation\x12!\n" + "\fdisplay_name\x18\x01 \x01(\tR\vdisplayName\x12\x18\n" + "\asummary\x18\x02 \x01(\tR\asummary\x121\n" + @@ -1759,30 +1783,32 @@ func file_prairie_plugin_v1_common_proto_rawDescGZIP() []byte { var file_prairie_plugin_v1_common_proto_enumTypes = make([]protoimpl.EnumInfo, 1) var file_prairie_plugin_v1_common_proto_msgTypes = make([]protoimpl.MessageInfo, 20) var file_prairie_plugin_v1_common_proto_goTypes = []any{ - (AdminFormControl)(0), // 0: prairie.plugin.v1.AdminFormControl - (*SupportedPlatform)(nil), // 1: prairie.plugin.v1.SupportedPlatform - (*ConfigSchema)(nil), // 2: prairie.plugin.v1.ConfigSchema - (*AdminFormOption)(nil), // 3: prairie.plugin.v1.AdminFormOption - (*AdminFormField)(nil), // 4: prairie.plugin.v1.AdminFormField - (*AdminFormCondition)(nil), // 5: prairie.plugin.v1.AdminFormCondition - (*AdminFormValidation)(nil), // 6: prairie.plugin.v1.AdminFormValidation - (*AdminFormSection)(nil), // 7: prairie.plugin.v1.AdminFormSection - (*AdminFormDescriptor)(nil), // 8: prairie.plugin.v1.AdminFormDescriptor - (*ConfigEntry)(nil), // 9: prairie.plugin.v1.ConfigEntry - (*HttpRouteDescriptor)(nil), // 10: prairie.plugin.v1.HttpRouteDescriptor - (*PackagedAsset)(nil), // 11: prairie.plugin.v1.PackagedAsset - (*CapabilityDescriptor)(nil), // 12: prairie.plugin.v1.CapabilityDescriptor - (*PluginPresentation)(nil), // 13: prairie.plugin.v1.PluginPresentation - (*PluginManifest)(nil), // 14: prairie.plugin.v1.PluginManifest - (*GetManifestRequest)(nil), // 15: prairie.plugin.v1.GetManifestRequest - (*GetManifestResponse)(nil), // 16: prairie.plugin.v1.GetManifestResponse - (*ConfigureRequest)(nil), // 17: prairie.plugin.v1.ConfigureRequest - (*ConfigureResponse)(nil), // 18: prairie.plugin.v1.ConfigureResponse - (*BindHostBrokerRequest)(nil), // 19: prairie.plugin.v1.BindHostBrokerRequest - (*BindHostBrokerResponse)(nil), // 20: prairie.plugin.v1.BindHostBrokerResponse - (*structpb.Value)(nil), // 21: google.protobuf.Value - (*structpb.Struct)(nil), // 22: google.protobuf.Struct - (*WatchSyncProviderDescriptor)(nil), // 23: prairie.plugin.v1.WatchSyncProviderDescriptor + (AdminFormControl)(0), // 0: prairie.plugin.v1.AdminFormControl + (*SupportedPlatform)(nil), // 1: prairie.plugin.v1.SupportedPlatform + (*ConfigSchema)(nil), // 2: prairie.plugin.v1.ConfigSchema + (*AdminFormOption)(nil), // 3: prairie.plugin.v1.AdminFormOption + (*AdminFormField)(nil), // 4: prairie.plugin.v1.AdminFormField + (*AdminFormCondition)(nil), // 5: prairie.plugin.v1.AdminFormCondition + (*AdminFormValidation)(nil), // 6: prairie.plugin.v1.AdminFormValidation + (*AdminFormSection)(nil), // 7: prairie.plugin.v1.AdminFormSection + (*AdminFormDescriptor)(nil), // 8: prairie.plugin.v1.AdminFormDescriptor + (*ConfigEntry)(nil), // 9: prairie.plugin.v1.ConfigEntry + (*HttpRouteDescriptor)(nil), // 10: prairie.plugin.v1.HttpRouteDescriptor + (*PackagedAsset)(nil), // 11: prairie.plugin.v1.PackagedAsset + (*CapabilityDescriptor)(nil), // 12: prairie.plugin.v1.CapabilityDescriptor + (*PluginPresentation)(nil), // 13: prairie.plugin.v1.PluginPresentation + (*PluginManifest)(nil), // 14: prairie.plugin.v1.PluginManifest + (*GetManifestRequest)(nil), // 15: prairie.plugin.v1.GetManifestRequest + (*GetManifestResponse)(nil), // 16: prairie.plugin.v1.GetManifestResponse + (*ConfigureRequest)(nil), // 17: prairie.plugin.v1.ConfigureRequest + (*ConfigureResponse)(nil), // 18: prairie.plugin.v1.ConfigureResponse + (*BindHostBrokerRequest)(nil), // 19: prairie.plugin.v1.BindHostBrokerRequest + (*BindHostBrokerResponse)(nil), // 20: prairie.plugin.v1.BindHostBrokerResponse + (*structpb.Value)(nil), // 21: google.protobuf.Value + (*structpb.Struct)(nil), // 22: google.protobuf.Struct + (*WatchSyncProviderDescriptor)(nil), // 23: prairie.plugin.v1.WatchSyncProviderDescriptor + (*NetworkAccessProviderDescriptor)(nil), // 24: prairie.plugin.v1.NetworkAccessProviderDescriptor + (*RequestRouterDescriptor)(nil), // 25: prairie.plugin.v1.RequestRouterDescriptor } var file_prairie_plugin_v1_common_proto_depIdxs = []int32{ 8, // 0: prairie.plugin.v1.ConfigSchema.admin_form:type_name -> prairie.plugin.v1.AdminFormDescriptor @@ -1798,27 +1824,29 @@ var file_prairie_plugin_v1_common_proto_depIdxs = []int32{ 2, // 10: prairie.plugin.v1.CapabilityDescriptor.config_schema:type_name -> prairie.plugin.v1.ConfigSchema 22, // 11: prairie.plugin.v1.CapabilityDescriptor.metadata:type_name -> google.protobuf.Struct 23, // 12: prairie.plugin.v1.CapabilityDescriptor.watch_sync_provider:type_name -> prairie.plugin.v1.WatchSyncProviderDescriptor - 1, // 13: prairie.plugin.v1.PluginManifest.supported_platforms:type_name -> prairie.plugin.v1.SupportedPlatform - 12, // 14: prairie.plugin.v1.PluginManifest.capabilities:type_name -> prairie.plugin.v1.CapabilityDescriptor - 2, // 15: prairie.plugin.v1.PluginManifest.global_config_schema:type_name -> prairie.plugin.v1.ConfigSchema - 2, // 16: prairie.plugin.v1.PluginManifest.user_config_schema:type_name -> prairie.plugin.v1.ConfigSchema - 10, // 17: prairie.plugin.v1.PluginManifest.http_routes:type_name -> prairie.plugin.v1.HttpRouteDescriptor - 11, // 18: prairie.plugin.v1.PluginManifest.assets:type_name -> prairie.plugin.v1.PackagedAsset - 22, // 19: prairie.plugin.v1.PluginManifest.metadata:type_name -> google.protobuf.Struct - 13, // 20: prairie.plugin.v1.PluginManifest.presentation:type_name -> prairie.plugin.v1.PluginPresentation - 14, // 21: prairie.plugin.v1.GetManifestResponse.manifest:type_name -> prairie.plugin.v1.PluginManifest - 9, // 22: prairie.plugin.v1.ConfigureRequest.config:type_name -> prairie.plugin.v1.ConfigEntry - 15, // 23: prairie.plugin.v1.Runtime.GetManifest:input_type -> prairie.plugin.v1.GetManifestRequest - 17, // 24: prairie.plugin.v1.Runtime.Configure:input_type -> prairie.plugin.v1.ConfigureRequest - 19, // 25: prairie.plugin.v1.Runtime.BindHostBroker:input_type -> prairie.plugin.v1.BindHostBrokerRequest - 16, // 26: prairie.plugin.v1.Runtime.GetManifest:output_type -> prairie.plugin.v1.GetManifestResponse - 18, // 27: prairie.plugin.v1.Runtime.Configure:output_type -> prairie.plugin.v1.ConfigureResponse - 20, // 28: prairie.plugin.v1.Runtime.BindHostBroker:output_type -> prairie.plugin.v1.BindHostBrokerResponse - 26, // [26:29] is the sub-list for method output_type - 23, // [23:26] is the sub-list for method input_type - 23, // [23:23] is the sub-list for extension type_name - 23, // [23:23] is the sub-list for extension extendee - 0, // [0:23] is the sub-list for field type_name + 24, // 13: prairie.plugin.v1.CapabilityDescriptor.network_access_provider:type_name -> prairie.plugin.v1.NetworkAccessProviderDescriptor + 25, // 14: prairie.plugin.v1.CapabilityDescriptor.request_router:type_name -> prairie.plugin.v1.RequestRouterDescriptor + 1, // 15: prairie.plugin.v1.PluginManifest.supported_platforms:type_name -> prairie.plugin.v1.SupportedPlatform + 12, // 16: prairie.plugin.v1.PluginManifest.capabilities:type_name -> prairie.plugin.v1.CapabilityDescriptor + 2, // 17: prairie.plugin.v1.PluginManifest.global_config_schema:type_name -> prairie.plugin.v1.ConfigSchema + 2, // 18: prairie.plugin.v1.PluginManifest.user_config_schema:type_name -> prairie.plugin.v1.ConfigSchema + 10, // 19: prairie.plugin.v1.PluginManifest.http_routes:type_name -> prairie.plugin.v1.HttpRouteDescriptor + 11, // 20: prairie.plugin.v1.PluginManifest.assets:type_name -> prairie.plugin.v1.PackagedAsset + 22, // 21: prairie.plugin.v1.PluginManifest.metadata:type_name -> google.protobuf.Struct + 13, // 22: prairie.plugin.v1.PluginManifest.presentation:type_name -> prairie.plugin.v1.PluginPresentation + 14, // 23: prairie.plugin.v1.GetManifestResponse.manifest:type_name -> prairie.plugin.v1.PluginManifest + 9, // 24: prairie.plugin.v1.ConfigureRequest.config:type_name -> prairie.plugin.v1.ConfigEntry + 15, // 25: prairie.plugin.v1.Runtime.GetManifest:input_type -> prairie.plugin.v1.GetManifestRequest + 17, // 26: prairie.plugin.v1.Runtime.Configure:input_type -> prairie.plugin.v1.ConfigureRequest + 19, // 27: prairie.plugin.v1.Runtime.BindHostBroker:input_type -> prairie.plugin.v1.BindHostBrokerRequest + 16, // 28: prairie.plugin.v1.Runtime.GetManifest:output_type -> prairie.plugin.v1.GetManifestResponse + 18, // 29: prairie.plugin.v1.Runtime.Configure:output_type -> prairie.plugin.v1.ConfigureResponse + 20, // 30: prairie.plugin.v1.Runtime.BindHostBroker:output_type -> prairie.plugin.v1.BindHostBrokerResponse + 28, // [28:31] is the sub-list for method output_type + 25, // [25:28] is the sub-list for method input_type + 25, // [25:25] is the sub-list for extension type_name + 25, // [25:25] is the sub-list for extension extendee + 0, // [0:25] is the sub-list for field type_name } func init() { file_prairie_plugin_v1_common_proto_init() } @@ -1826,6 +1854,8 @@ func file_prairie_plugin_v1_common_proto_init() { if File_prairie_plugin_v1_common_proto != nil { return } + file_prairie_plugin_v1_network_access_provider_proto_init() + file_prairie_plugin_v1_request_router_proto_init() file_prairie_plugin_v1_watch_sync_provider_proto_init() type x struct{} out := protoimpl.TypeBuilder{ diff --git a/pkg/pluginproto/prairie/plugin/v1/metadata_provider.pb.go b/pkg/pluginproto/prairie/plugin/v1/metadata_provider.pb.go index 4173b4f..7e4d19b 100644 --- a/pkg/pluginproto/prairie/plugin/v1/metadata_provider.pb.go +++ b/pkg/pluginproto/prairie/plugin/v1/metadata_provider.pb.go @@ -1680,14 +1680,18 @@ func (x *GetEpisodesResponse) GetEpisodes() []*EpisodeRecord { } type ImageRecord struct { - state protoimpl.MessageState `protogen:"open.v1"` - Kind string `protobuf:"bytes,1,opt,name=kind,proto3" json:"kind,omitempty"` - Url string `protobuf:"bytes,2,opt,name=url,proto3" json:"url,omitempty"` - Language string `protobuf:"bytes,3,opt,name=language,proto3" json:"language,omitempty"` - Width int32 `protobuf:"varint,4,opt,name=width,proto3" json:"width,omitempty"` - Height int32 `protobuf:"varint,5,opt,name=height,proto3" json:"height,omitempty"` - Metadata *structpb.Struct `protobuf:"bytes,6,opt,name=metadata,proto3" json:"metadata,omitempty"` - SeasonNumber *int32 `protobuf:"varint,7,opt,name=season_number,json=seasonNumber,proto3,oneof" json:"season_number,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + Kind string `protobuf:"bytes,1,opt,name=kind,proto3" json:"kind,omitempty"` + Url string `protobuf:"bytes,2,opt,name=url,proto3" json:"url,omitempty"` + Language string `protobuf:"bytes,3,opt,name=language,proto3" json:"language,omitempty"` + Width int32 `protobuf:"varint,4,opt,name=width,proto3" json:"width,omitempty"` + Height int32 `protobuf:"varint,5,opt,name=height,proto3" json:"height,omitempty"` + Metadata *structpb.Struct `protobuf:"bytes,6,opt,name=metadata,proto3" json:"metadata,omitempty"` + // Present only when the image is confirmed to belong to one exact season. + // Season zero represents Specials, so presence must not be inferred from the + // numeric value. Plugins should populate this field whenever the season is + // known, even when the request was already season-scoped. + SeasonNumber *int32 `protobuf:"varint,7,opt,name=season_number,json=seasonNumber,proto3,oneof" json:"season_number,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -1777,7 +1781,10 @@ type GetImagesRequest struct { ItemType string `protobuf:"bytes,2,opt,name=item_type,json=itemType,proto3" json:"item_type,omitempty"` ProviderIds *structpb.Struct `protobuf:"bytes,3,opt,name=provider_ids,json=providerIds,proto3" json:"provider_ids,omitempty"` Language string `protobuf:"bytes,4,opt,name=language,proto3" json:"language,omitempty"` - // When set for series items, providers return the exact season artwork gallery + // Present only when requesting artwork for a specific TV season. + // Season zero represents Specials, so presence must not be inferred from + // the numeric value. This scope is a request, not a guarantee: plugins that + // can filter by season should do so, but hosts must verify season attribution // via ImageRecord.season_number rather than assume a filtered response. SeasonNumber *int32 `protobuf:"varint,5,opt,name=season_number,json=seasonNumber,proto3,oneof" json:"season_number,omitempty"` unknownFields protoimpl.UnknownFields @@ -1894,9 +1901,15 @@ func (x *GetImagesResponse) GetImages() []*ImageRecord { } type ResolveImageURLRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"` - Variant string `protobuf:"bytes,2,opt,name=variant,proto3" json:"variant,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"` + // Semantic size hint, not an exact pixel contract. Canonical values, smallest + // to largest: "card", "featured", "large", "full", "original". Empty means + // the plugin default. The vocabulary is open and grows additively, so a + // plugin receiving an unknown variant must degrade gracefully to its nearest + // supported size and must not return an error. See + // pkg/pluginsdk/imagevariant for constants and the full rules. + Variant string `protobuf:"bytes,2,opt,name=variant,proto3" json:"variant,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -1990,9 +2003,15 @@ func (x *ResolveImageURLResponse) GetUrl() string { } type ResolveImageURLsRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Paths []string `protobuf:"bytes,1,rep,name=paths,proto3" json:"paths,omitempty"` - Variant string `protobuf:"bytes,2,opt,name=variant,proto3" json:"variant,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + Paths []string `protobuf:"bytes,1,rep,name=paths,proto3" json:"paths,omitempty"` + // Semantic size hint applied to every path in this request. Canonical values, + // smallest to largest: "card", "featured", "large", "full", "original". Empty + // means the plugin default. The vocabulary is open and grows additively, so a + // plugin receiving an unknown variant must degrade gracefully to its nearest + // supported size and must not return an error. See + // pkg/pluginsdk/imagevariant for constants and the full rules. + Variant string `protobuf:"bytes,2,opt,name=variant,proto3" json:"variant,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } diff --git a/pkg/pluginproto/prairie/plugin/v1/metadata_provider_test.go b/pkg/pluginproto/prairie/plugin/v1/metadata_provider_test.go index 155a655..7ec89dc 100644 --- a/pkg/pluginproto/prairie/plugin/v1/metadata_provider_test.go +++ b/pkg/pluginproto/prairie/plugin/v1/metadata_provider_test.go @@ -7,6 +7,14 @@ import ( "google.golang.org/protobuf/reflect/protoreflect" ) +func assertFieldNumber(t *testing.T, message protoreflect.ProtoMessage, field string, want protoreflect.FieldNumber) { + t.Helper() + got := message.ProtoReflect().Descriptor().Fields().ByName(protoreflect.Name(field)) + if got == nil || got.Number() != want { + t.Fatalf("%T.%s field number = %v, want %d", message, field, got, want) + } +} + func TestMetadataItemDescriptor_IncludesReleaseDate(t *testing.T) { field := (&MetadataItem{}).ProtoReflect().Descriptor().Fields().ByName("release_date") if field == nil { @@ -38,21 +46,14 @@ func TestMetadataTitleDescriptors_IncludeAliasAndLanguageFields(t *testing.T) { } } - assertFieldNumber := func(message protoreflect.ProtoMessage, field string, want protoreflect.FieldNumber) { - t.Helper() - got := message.ProtoReflect().Descriptor().Fields().ByName(protoreflect.Name(field)) - if got == nil || got.Number() != want { - t.Fatalf("%T.%s field number = %v, want %d", message, field, got, want) - } - } - assertFieldNumber(&ProviderSearchResult{}, "title_aliases", 9) - assertFieldNumber(&ProviderSearchResult{}, "title_language", 10) - assertFieldNumber(&ProviderSearchResult{}, "title_is_fallback", 11) - assertFieldNumber(&ProviderSearchResult{}, "original_language", 12) - assertFieldNumber(&MetadataItem{}, "title_aliases", 33) - assertFieldNumber(&MetadataItem{}, "title_language", 34) - assertFieldNumber(&MetadataItem{}, "title_is_fallback", 35) - assertFieldNumber(&MetadataItem{}, "title_aliases_complete", 36) + assertFieldNumber(t, &ProviderSearchResult{}, "title_aliases", 9) + assertFieldNumber(t, &ProviderSearchResult{}, "title_language", 10) + assertFieldNumber(t, &ProviderSearchResult{}, "title_is_fallback", 11) + assertFieldNumber(t, &ProviderSearchResult{}, "original_language", 12) + assertFieldNumber(t, &MetadataItem{}, "title_aliases", 33) + assertFieldNumber(t, &MetadataItem{}, "title_language", 34) + assertFieldNumber(t, &MetadataItem{}, "title_is_fallback", 35) + assertFieldNumber(t, &MetadataItem{}, "title_aliases_complete", 36) } func TestMetadataItem_OldPayloadDecodesWithOptionalTitleFields(t *testing.T) { @@ -123,6 +124,11 @@ func TestMetadataProviderRequestDescriptors_IncludeProviderContext(t *testing.T) fieldName: "language", message: &GetImagesRequest{}, }, + { + name: "GetImagesRequest season_number", + fieldName: "season_number", + message: &GetImagesRequest{}, + }, { name: "SearchMetadataRequest language", fieldName: "language", @@ -149,6 +155,32 @@ func TestMetadataProviderRequestDescriptors_IncludeProviderContext(t *testing.T) } } +func TestGetImagesRequest_SeasonNumberPreservesPresenceAndSpecials(t *testing.T) { + seasonZero := int32(0) + assertOptionalInt32Presence( + t, + &GetImagesRequest{SeasonNumber: &seasonZero}, + &GetImagesRequest{}, + func(message proto.Message) *int32 { + return message.(*GetImagesRequest).SeasonNumber + }, + ) + assertFieldNumber(t, &GetImagesRequest{}, "season_number", 5) +} + +func TestImageRecord_SeasonNumberPreservesPresenceAndSpecials(t *testing.T) { + seasonZero := int32(0) + assertOptionalInt32Presence( + t, + &ImageRecord{SeasonNumber: &seasonZero}, + &ImageRecord{}, + func(message proto.Message) *int32 { + return message.(*ImageRecord).SeasonNumber + }, + ) + assertFieldNumber(t, &ImageRecord{}, "season_number", 7) +} + func TestMetadataProviderServiceDescriptor_IncludesPersonDetailRPC(t *testing.T) { method := File_prairie_plugin_v1_metadata_provider_proto.Services(). ByName("MetadataProvider"). diff --git a/pkg/pluginproto/prairie/plugin/v1/network_access_provider.pb.go b/pkg/pluginproto/prairie/plugin/v1/network_access_provider.pb.go new file mode 100644 index 0000000..6221f6f --- /dev/null +++ b/pkg/pluginproto/prairie/plugin/v1/network_access_provider.pb.go @@ -0,0 +1,454 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc (unknown) +// source: prairie/plugin/v1/network_access_provider.proto + +package pluginv1 + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type NetworkAccessConnectRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *NetworkAccessConnectRequest) Reset() { + *x = NetworkAccessConnectRequest{} + mi := &file_prairie_plugin_v1_network_access_provider_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *NetworkAccessConnectRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*NetworkAccessConnectRequest) ProtoMessage() {} + +func (x *NetworkAccessConnectRequest) ProtoReflect() protoreflect.Message { + mi := &file_prairie_plugin_v1_network_access_provider_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use NetworkAccessConnectRequest.ProtoReflect.Descriptor instead. +func (*NetworkAccessConnectRequest) Descriptor() ([]byte, []int) { + return file_prairie_plugin_v1_network_access_provider_proto_rawDescGZIP(), []int{0} +} + +type NetworkAccessDisconnectRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *NetworkAccessDisconnectRequest) Reset() { + *x = NetworkAccessDisconnectRequest{} + mi := &file_prairie_plugin_v1_network_access_provider_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *NetworkAccessDisconnectRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*NetworkAccessDisconnectRequest) ProtoMessage() {} + +func (x *NetworkAccessDisconnectRequest) ProtoReflect() protoreflect.Message { + mi := &file_prairie_plugin_v1_network_access_provider_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use NetworkAccessDisconnectRequest.ProtoReflect.Descriptor instead. +func (*NetworkAccessDisconnectRequest) Descriptor() ([]byte, []int) { + return file_prairie_plugin_v1_network_access_provider_proto_rawDescGZIP(), []int{1} +} + +type NetworkAccessGetStatusRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *NetworkAccessGetStatusRequest) Reset() { + *x = NetworkAccessGetStatusRequest{} + mi := &file_prairie_plugin_v1_network_access_provider_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *NetworkAccessGetStatusRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*NetworkAccessGetStatusRequest) ProtoMessage() {} + +func (x *NetworkAccessGetStatusRequest) ProtoReflect() protoreflect.Message { + mi := &file_prairie_plugin_v1_network_access_provider_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use NetworkAccessGetStatusRequest.ProtoReflect.Descriptor instead. +func (*NetworkAccessGetStatusRequest) Descriptor() ([]byte, []int) { + return file_prairie_plugin_v1_network_access_provider_proto_rawDescGZIP(), []int{2} +} + +// NetworkAccessStatus is the provider's view of one plugin instance. Every +// host (API server, each proxy node) runs its own instance and reports its own +// status; the host aggregates them. +type NetworkAccessStatus struct { + state protoimpl.MessageState `protogen:"open.v1"` + // One of: disconnected | awaiting_authorization | connecting | connected | error. + State string `protobuf:"bytes,1,opt,name=state,proto3" json:"state,omitempty"` + // Overlay DNS name, e.g. prairie.tail1234.ts.net. + Hostname string `protobuf:"bytes,2,opt,name=hostname,proto3" json:"hostname,omitempty"` + // scheme://host[:port] clients should use for the api listener. + Origin string `protobuf:"bytes,3,opt,name=origin,proto3" json:"origin,omitempty"` + // Overlay IP addresses assigned to this instance. + Addresses []string `protobuf:"bytes,4,rep,name=addresses,proto3" json:"addresses,omitempty"` + // One entry per exposed host listener (see RuntimeHost.GetHostInfo). + Listeners []*NetworkAccessListener `protobuf:"bytes,5,rep,name=listeners,proto3" json:"listeners,omitempty"` + // Interactive enrollment URL. Only set while state is awaiting_authorization. + // Admin-only; the plugin must never log it. + AuthUrl string `protobuf:"bytes,6,opt,name=auth_url,json=authUrl,proto3" json:"auth_url,omitempty"` + // Human-readable failure detail when state is error. + Error string `protobuf:"bytes,7,opt,name=error,proto3" json:"error,omitempty"` + // Provider client version shown to admins, e.g. "tsnet 1.102.4". + ProviderVersion string `protobuf:"bytes,8,opt,name=provider_version,json=providerVersion,proto3" json:"provider_version,omitempty"` + // Plugin-owned intent that survives restarts. The plugin persists it in + // instance state and reconnects on start when true. + DesiredConnected bool `protobuf:"varint,9,opt,name=desired_connected,json=desiredConnected,proto3" json:"desired_connected,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *NetworkAccessStatus) Reset() { + *x = NetworkAccessStatus{} + mi := &file_prairie_plugin_v1_network_access_provider_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *NetworkAccessStatus) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*NetworkAccessStatus) ProtoMessage() {} + +func (x *NetworkAccessStatus) ProtoReflect() protoreflect.Message { + mi := &file_prairie_plugin_v1_network_access_provider_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use NetworkAccessStatus.ProtoReflect.Descriptor instead. +func (*NetworkAccessStatus) Descriptor() ([]byte, []int) { + return file_prairie_plugin_v1_network_access_provider_proto_rawDescGZIP(), []int{3} +} + +func (x *NetworkAccessStatus) GetState() string { + if x != nil { + return x.State + } + return "" +} + +func (x *NetworkAccessStatus) GetHostname() string { + if x != nil { + return x.Hostname + } + return "" +} + +func (x *NetworkAccessStatus) GetOrigin() string { + if x != nil { + return x.Origin + } + return "" +} + +func (x *NetworkAccessStatus) GetAddresses() []string { + if x != nil { + return x.Addresses + } + return nil +} + +func (x *NetworkAccessStatus) GetListeners() []*NetworkAccessListener { + if x != nil { + return x.Listeners + } + return nil +} + +func (x *NetworkAccessStatus) GetAuthUrl() string { + if x != nil { + return x.AuthUrl + } + return "" +} + +func (x *NetworkAccessStatus) GetError() string { + if x != nil { + return x.Error + } + return "" +} + +func (x *NetworkAccessStatus) GetProviderVersion() string { + if x != nil { + return x.ProviderVersion + } + return "" +} + +func (x *NetworkAccessStatus) GetDesiredConnected() bool { + if x != nil { + return x.DesiredConnected + } + return false +} + +// NetworkAccessListener maps one host listener onto its overlay origin. +type NetworkAccessListener struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Listener name from RuntimeHost.GetHostInfo: api | jellyfin | abs. + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // Overlay origin serving that listener, scheme://host[:port]. + Origin string `protobuf:"bytes,2,opt,name=origin,proto3" json:"origin,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *NetworkAccessListener) Reset() { + *x = NetworkAccessListener{} + mi := &file_prairie_plugin_v1_network_access_provider_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *NetworkAccessListener) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*NetworkAccessListener) ProtoMessage() {} + +func (x *NetworkAccessListener) ProtoReflect() protoreflect.Message { + mi := &file_prairie_plugin_v1_network_access_provider_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use NetworkAccessListener.ProtoReflect.Descriptor instead. +func (*NetworkAccessListener) Descriptor() ([]byte, []int) { + return file_prairie_plugin_v1_network_access_provider_proto_rawDescGZIP(), []int{4} +} + +func (x *NetworkAccessListener) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *NetworkAccessListener) GetOrigin() string { + if x != nil { + return x.Origin + } + return "" +} + +// NetworkAccessProviderDescriptor is the typed manifest metadata for a +// network_access_provider.v1 capability. The host lists providers from it +// without launching the plugin. +type NetworkAccessProviderDescriptor struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Stable provider slug, e.g. "tailscale", "netbird". Lowercase, path-safe. + Provider string `protobuf:"bytes,1,opt,name=provider,proto3" json:"provider,omitempty"` + // Operator-facing name, e.g. "Tailscale". + DisplayName string `protobuf:"bytes,2,opt,name=display_name,json=displayName,proto3" json:"display_name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *NetworkAccessProviderDescriptor) Reset() { + *x = NetworkAccessProviderDescriptor{} + mi := &file_prairie_plugin_v1_network_access_provider_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *NetworkAccessProviderDescriptor) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*NetworkAccessProviderDescriptor) ProtoMessage() {} + +func (x *NetworkAccessProviderDescriptor) ProtoReflect() protoreflect.Message { + mi := &file_prairie_plugin_v1_network_access_provider_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use NetworkAccessProviderDescriptor.ProtoReflect.Descriptor instead. +func (*NetworkAccessProviderDescriptor) Descriptor() ([]byte, []int) { + return file_prairie_plugin_v1_network_access_provider_proto_rawDescGZIP(), []int{5} +} + +func (x *NetworkAccessProviderDescriptor) GetProvider() string { + if x != nil { + return x.Provider + } + return "" +} + +func (x *NetworkAccessProviderDescriptor) GetDisplayName() string { + if x != nil { + return x.DisplayName + } + return "" +} + +var File_prairie_plugin_v1_network_access_provider_proto protoreflect.FileDescriptor + +const file_prairie_plugin_v1_network_access_provider_proto_rawDesc = "" + + "\n" + + "/prairie/plugin/v1/network_access_provider.proto\x12\x11prairie.plugin.v1\"\x1d\n" + + "\x1bNetworkAccessConnectRequest\" \n" + + "\x1eNetworkAccessDisconnectRequest\"\x1f\n" + + "\x1dNetworkAccessGetStatusRequest\"\xce\x02\n" + + "\x13NetworkAccessStatus\x12\x14\n" + + "\x05state\x18\x01 \x01(\tR\x05state\x12\x1a\n" + + "\bhostname\x18\x02 \x01(\tR\bhostname\x12\x16\n" + + "\x06origin\x18\x03 \x01(\tR\x06origin\x12\x1c\n" + + "\taddresses\x18\x04 \x03(\tR\taddresses\x12F\n" + + "\tlisteners\x18\x05 \x03(\v2(.prairie.plugin.v1.NetworkAccessListenerR\tlisteners\x12\x19\n" + + "\bauth_url\x18\x06 \x01(\tR\aauthUrl\x12\x14\n" + + "\x05error\x18\a \x01(\tR\x05error\x12)\n" + + "\x10provider_version\x18\b \x01(\tR\x0fproviderVersion\x12+\n" + + "\x11desired_connected\x18\t \x01(\bR\x10desiredConnected\"C\n" + + "\x15NetworkAccessListener\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12\x16\n" + + "\x06origin\x18\x02 \x01(\tR\x06origin\"`\n" + + "\x1fNetworkAccessProviderDescriptor\x12\x1a\n" + + "\bprovider\x18\x01 \x01(\tR\bprovider\x12!\n" + + "\fdisplay_name\x18\x02 \x01(\tR\vdisplayName2\xca\x02\n" + + "\x15NetworkAccessProvider\x12a\n" + + "\aConnect\x12..prairie.plugin.v1.NetworkAccessConnectRequest\x1a&.prairie.plugin.v1.NetworkAccessStatus\x12g\n" + + "\n" + + "Disconnect\x121.prairie.plugin.v1.NetworkAccessDisconnectRequest\x1a&.prairie.plugin.v1.NetworkAccessStatus\x12e\n" + + "\tGetStatus\x120.prairie.plugin.v1.NetworkAccessGetStatusRequest\x1a&.prairie.plugin.v1.NetworkAccessStatusBYZWgithub.com/prairie-server/prairie-plugin-sdk/pkg/pluginproto/prairie/plugin/v1;pluginv1b\x06proto3" + +var ( + file_prairie_plugin_v1_network_access_provider_proto_rawDescOnce sync.Once + file_prairie_plugin_v1_network_access_provider_proto_rawDescData []byte +) + +func file_prairie_plugin_v1_network_access_provider_proto_rawDescGZIP() []byte { + file_prairie_plugin_v1_network_access_provider_proto_rawDescOnce.Do(func() { + file_prairie_plugin_v1_network_access_provider_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_prairie_plugin_v1_network_access_provider_proto_rawDesc), len(file_prairie_plugin_v1_network_access_provider_proto_rawDesc))) + }) + return file_prairie_plugin_v1_network_access_provider_proto_rawDescData +} + +var file_prairie_plugin_v1_network_access_provider_proto_msgTypes = make([]protoimpl.MessageInfo, 6) +var file_prairie_plugin_v1_network_access_provider_proto_goTypes = []any{ + (*NetworkAccessConnectRequest)(nil), // 0: prairie.plugin.v1.NetworkAccessConnectRequest + (*NetworkAccessDisconnectRequest)(nil), // 1: prairie.plugin.v1.NetworkAccessDisconnectRequest + (*NetworkAccessGetStatusRequest)(nil), // 2: prairie.plugin.v1.NetworkAccessGetStatusRequest + (*NetworkAccessStatus)(nil), // 3: prairie.plugin.v1.NetworkAccessStatus + (*NetworkAccessListener)(nil), // 4: prairie.plugin.v1.NetworkAccessListener + (*NetworkAccessProviderDescriptor)(nil), // 5: prairie.plugin.v1.NetworkAccessProviderDescriptor +} +var file_prairie_plugin_v1_network_access_provider_proto_depIdxs = []int32{ + 4, // 0: prairie.plugin.v1.NetworkAccessStatus.listeners:type_name -> prairie.plugin.v1.NetworkAccessListener + 0, // 1: prairie.plugin.v1.NetworkAccessProvider.Connect:input_type -> prairie.plugin.v1.NetworkAccessConnectRequest + 1, // 2: prairie.plugin.v1.NetworkAccessProvider.Disconnect:input_type -> prairie.plugin.v1.NetworkAccessDisconnectRequest + 2, // 3: prairie.plugin.v1.NetworkAccessProvider.GetStatus:input_type -> prairie.plugin.v1.NetworkAccessGetStatusRequest + 3, // 4: prairie.plugin.v1.NetworkAccessProvider.Connect:output_type -> prairie.plugin.v1.NetworkAccessStatus + 3, // 5: prairie.plugin.v1.NetworkAccessProvider.Disconnect:output_type -> prairie.plugin.v1.NetworkAccessStatus + 3, // 6: prairie.plugin.v1.NetworkAccessProvider.GetStatus:output_type -> prairie.plugin.v1.NetworkAccessStatus + 4, // [4:7] is the sub-list for method output_type + 1, // [1:4] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_prairie_plugin_v1_network_access_provider_proto_init() } +func file_prairie_plugin_v1_network_access_provider_proto_init() { + if File_prairie_plugin_v1_network_access_provider_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_prairie_plugin_v1_network_access_provider_proto_rawDesc), len(file_prairie_plugin_v1_network_access_provider_proto_rawDesc)), + NumEnums: 0, + NumMessages: 6, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_prairie_plugin_v1_network_access_provider_proto_goTypes, + DependencyIndexes: file_prairie_plugin_v1_network_access_provider_proto_depIdxs, + MessageInfos: file_prairie_plugin_v1_network_access_provider_proto_msgTypes, + }.Build() + File_prairie_plugin_v1_network_access_provider_proto = out.File + file_prairie_plugin_v1_network_access_provider_proto_goTypes = nil + file_prairie_plugin_v1_network_access_provider_proto_depIdxs = nil +} diff --git a/pkg/pluginproto/prairie/plugin/v1/network_access_provider_grpc.pb.go b/pkg/pluginproto/prairie/plugin/v1/network_access_provider_grpc.pb.go new file mode 100644 index 0000000..3bd0d26 --- /dev/null +++ b/pkg/pluginproto/prairie/plugin/v1/network_access_provider_grpc.pb.go @@ -0,0 +1,219 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.6.1 +// - protoc (unknown) +// source: prairie/plugin/v1/network_access_provider.proto + +package pluginv1 + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + NetworkAccessProvider_Connect_FullMethodName = "/prairie.plugin.v1.NetworkAccessProvider/Connect" + NetworkAccessProvider_Disconnect_FullMethodName = "/prairie.plugin.v1.NetworkAccessProvider/Disconnect" + NetworkAccessProvider_GetStatus_FullMethodName = "/prairie.plugin.v1.NetworkAccessProvider/GetStatus" +) + +// NetworkAccessProviderClient is the client API for NetworkAccessProvider service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +// +// NetworkAccessProvider gives a Prairie deployment an overlay-network identity +// (Tailscale, NetBird, ...). The plugin owns the overlay client and a reverse +// proxy in front of the host listeners it learns from RuntimeHost.GetHostInfo; +// the host owns supervision, per-instance state storage, status aggregation, +// and the admin API. Plugins declaring this capability are resident: the host +// starts them at boot, restarts them on crash, and stops them last. +type NetworkAccessProviderClient interface { + // Connect brings the overlay identity up and starts proxying. It returns the + // status reached so far; enrollment may continue in the background and be + // reported through RuntimeHost.ReportNetworkAccessStatus. + Connect(ctx context.Context, in *NetworkAccessConnectRequest, opts ...grpc.CallOption) (*NetworkAccessStatus, error) + // Disconnect tears the overlay listener down and clears desired_connected. + Disconnect(ctx context.Context, in *NetworkAccessDisconnectRequest, opts ...grpc.CallOption) (*NetworkAccessStatus, error) + // GetStatus returns the current status without changing it. + GetStatus(ctx context.Context, in *NetworkAccessGetStatusRequest, opts ...grpc.CallOption) (*NetworkAccessStatus, error) +} + +type networkAccessProviderClient struct { + cc grpc.ClientConnInterface +} + +func NewNetworkAccessProviderClient(cc grpc.ClientConnInterface) NetworkAccessProviderClient { + return &networkAccessProviderClient{cc} +} + +func (c *networkAccessProviderClient) Connect(ctx context.Context, in *NetworkAccessConnectRequest, opts ...grpc.CallOption) (*NetworkAccessStatus, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(NetworkAccessStatus) + err := c.cc.Invoke(ctx, NetworkAccessProvider_Connect_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *networkAccessProviderClient) Disconnect(ctx context.Context, in *NetworkAccessDisconnectRequest, opts ...grpc.CallOption) (*NetworkAccessStatus, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(NetworkAccessStatus) + err := c.cc.Invoke(ctx, NetworkAccessProvider_Disconnect_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *networkAccessProviderClient) GetStatus(ctx context.Context, in *NetworkAccessGetStatusRequest, opts ...grpc.CallOption) (*NetworkAccessStatus, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(NetworkAccessStatus) + err := c.cc.Invoke(ctx, NetworkAccessProvider_GetStatus_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// NetworkAccessProviderServer is the server API for NetworkAccessProvider service. +// All implementations should embed UnimplementedNetworkAccessProviderServer +// for forward compatibility. +// +// NetworkAccessProvider gives a Prairie deployment an overlay-network identity +// (Tailscale, NetBird, ...). The plugin owns the overlay client and a reverse +// proxy in front of the host listeners it learns from RuntimeHost.GetHostInfo; +// the host owns supervision, per-instance state storage, status aggregation, +// and the admin API. Plugins declaring this capability are resident: the host +// starts them at boot, restarts them on crash, and stops them last. +type NetworkAccessProviderServer interface { + // Connect brings the overlay identity up and starts proxying. It returns the + // status reached so far; enrollment may continue in the background and be + // reported through RuntimeHost.ReportNetworkAccessStatus. + Connect(context.Context, *NetworkAccessConnectRequest) (*NetworkAccessStatus, error) + // Disconnect tears the overlay listener down and clears desired_connected. + Disconnect(context.Context, *NetworkAccessDisconnectRequest) (*NetworkAccessStatus, error) + // GetStatus returns the current status without changing it. + GetStatus(context.Context, *NetworkAccessGetStatusRequest) (*NetworkAccessStatus, error) +} + +// UnimplementedNetworkAccessProviderServer should be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedNetworkAccessProviderServer struct{} + +func (UnimplementedNetworkAccessProviderServer) Connect(context.Context, *NetworkAccessConnectRequest) (*NetworkAccessStatus, error) { + return nil, status.Error(codes.Unimplemented, "method Connect not implemented") +} +func (UnimplementedNetworkAccessProviderServer) Disconnect(context.Context, *NetworkAccessDisconnectRequest) (*NetworkAccessStatus, error) { + return nil, status.Error(codes.Unimplemented, "method Disconnect not implemented") +} +func (UnimplementedNetworkAccessProviderServer) GetStatus(context.Context, *NetworkAccessGetStatusRequest) (*NetworkAccessStatus, error) { + return nil, status.Error(codes.Unimplemented, "method GetStatus not implemented") +} +func (UnimplementedNetworkAccessProviderServer) testEmbeddedByValue() {} + +// UnsafeNetworkAccessProviderServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to NetworkAccessProviderServer will +// result in compilation errors. +type UnsafeNetworkAccessProviderServer interface { + mustEmbedUnimplementedNetworkAccessProviderServer() +} + +func RegisterNetworkAccessProviderServer(s grpc.ServiceRegistrar, srv NetworkAccessProviderServer) { + // If the following call panics, it indicates UnimplementedNetworkAccessProviderServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&NetworkAccessProvider_ServiceDesc, srv) +} + +func _NetworkAccessProvider_Connect_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(NetworkAccessConnectRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(NetworkAccessProviderServer).Connect(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: NetworkAccessProvider_Connect_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(NetworkAccessProviderServer).Connect(ctx, req.(*NetworkAccessConnectRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _NetworkAccessProvider_Disconnect_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(NetworkAccessDisconnectRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(NetworkAccessProviderServer).Disconnect(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: NetworkAccessProvider_Disconnect_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(NetworkAccessProviderServer).Disconnect(ctx, req.(*NetworkAccessDisconnectRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _NetworkAccessProvider_GetStatus_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(NetworkAccessGetStatusRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(NetworkAccessProviderServer).GetStatus(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: NetworkAccessProvider_GetStatus_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(NetworkAccessProviderServer).GetStatus(ctx, req.(*NetworkAccessGetStatusRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// NetworkAccessProvider_ServiceDesc is the grpc.ServiceDesc for NetworkAccessProvider service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var NetworkAccessProvider_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "prairie.plugin.v1.NetworkAccessProvider", + HandlerType: (*NetworkAccessProviderServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Connect", + Handler: _NetworkAccessProvider_Connect_Handler, + }, + { + MethodName: "Disconnect", + Handler: _NetworkAccessProvider_Disconnect_Handler, + }, + { + MethodName: "GetStatus", + Handler: _NetworkAccessProvider_GetStatus_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "prairie/plugin/v1/network_access_provider.proto", +} diff --git a/pkg/pluginproto/prairie/plugin/v1/optional_presence_test.go b/pkg/pluginproto/prairie/plugin/v1/optional_presence_test.go new file mode 100644 index 0000000..38c089a --- /dev/null +++ b/pkg/pluginproto/prairie/plugin/v1/optional_presence_test.go @@ -0,0 +1,36 @@ +package pluginv1 + +import ( + "testing" + + "google.golang.org/protobuf/proto" +) + +func assertOptionalInt32Presence(t *testing.T, withZero proto.Message, empty proto.Message, get func(proto.Message) *int32) { + t.Helper() + + data, err := proto.Marshal(withZero) + if err != nil { + t.Fatalf("marshal %T with explicit zero: %v", withZero, err) + } + decoded := withZero.ProtoReflect().New().Interface() + if err := proto.Unmarshal(data, decoded); err != nil { + t.Fatalf("unmarshal %T with explicit zero: %v", withZero, err) + } + value := get(decoded) + if value == nil || *value != 0 { + t.Fatalf("%T explicit zero = %v, want present value 0", decoded, value) + } + + data, err = proto.Marshal(empty) + if err != nil { + t.Fatalf("marshal empty %T: %v", empty, err) + } + decoded = empty.ProtoReflect().New().Interface() + if err := proto.Unmarshal(data, decoded); err != nil { + t.Fatalf("unmarshal empty %T: %v", empty, err) + } + if value := get(decoded); value != nil { + t.Fatalf("%T absent value = %v, want nil", decoded, value) + } +} diff --git a/pkg/pluginproto/prairie/plugin/v1/request_router.pb.go b/pkg/pluginproto/prairie/plugin/v1/request_router.pb.go index fcaa578..96cae01 100644 --- a/pkg/pluginproto/prairie/plugin/v1/request_router.pb.go +++ b/pkg/pluginproto/prairie/plugin/v1/request_router.pb.go @@ -35,8 +35,14 @@ type RequestDescriptor struct { // on the downstream service (e.g. Seerr). Empty when the host cannot resolve it. RequesterEmail string `protobuf:"bytes,8,opt,name=requester_email,json=requesterEmail,proto3" json:"requester_email,omitempty"` RequesterUsername string `protobuf:"bytes,9,opt,name=requester_username,json=requesterUsername,proto3" json:"requester_username,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // seasons lists the requested season numbers of a series; 0 is Specials. + // Empty means the whole series, which is what every request meant before + // this field existed. Only set when media_type is "series". Plugins that + // declare RequestRouterDescriptor.supports_seasons must fulfil only these + // seasons; see that field for the full contract. + Seasons []int32 `protobuf:"varint,10,rep,packed,name=seasons,proto3" json:"seasons,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *RequestDescriptor) Reset() { @@ -132,6 +138,68 @@ func (x *RequestDescriptor) GetRequesterUsername() string { return "" } +func (x *RequestDescriptor) GetSeasons() []int32 { + if x != nil { + return x.Seasons + } + return nil +} + +// RequestRouterDescriptor is typed request_router.v1 contract metadata that the +// host reads from the manifest without launching the plugin. An absent +// descriptor means every flag is false, which is how the host treats plugins +// built before a flag existed. +type RequestRouterDescriptor struct { + state protoimpl.MessageState `protogen:"open.v1"` + // supports_seasons declares that Fulfill and CheckStatus honour + // RequestDescriptor.seasons. A declaring plugin acquires only the listed + // seasons; when the series already exists upstream it adds those seasons to + // what is already tracked and leaves other seasons unchanged. Repeating a + // request converges rather than duplicating work. The host sends a request + // for only the missing seasons of a series it already has only to plugins + // that set this flag, because other plugins would add the whole series. + SupportsSeasons bool `protobuf:"varint,1,opt,name=supports_seasons,json=supportsSeasons,proto3" json:"supports_seasons,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RequestRouterDescriptor) Reset() { + *x = RequestRouterDescriptor{} + mi := &file_prairie_plugin_v1_request_router_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RequestRouterDescriptor) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RequestRouterDescriptor) ProtoMessage() {} + +func (x *RequestRouterDescriptor) ProtoReflect() protoreflect.Message { + mi := &file_prairie_plugin_v1_request_router_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RequestRouterDescriptor.ProtoReflect.Descriptor instead. +func (*RequestRouterDescriptor) Descriptor() ([]byte, []int) { + return file_prairie_plugin_v1_request_router_proto_rawDescGZIP(), []int{1} +} + +func (x *RequestRouterDescriptor) GetSupportsSeasons() bool { + if x != nil { + return x.SupportsSeasons + } + return false +} + type RouterConnection struct { state protoimpl.MessageState `protogen:"open.v1"` Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` @@ -146,7 +214,7 @@ type RouterConnection struct { func (x *RouterConnection) Reset() { *x = RouterConnection{} - mi := &file_prairie_plugin_v1_request_router_proto_msgTypes[1] + mi := &file_prairie_plugin_v1_request_router_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -158,7 +226,7 @@ func (x *RouterConnection) String() string { func (*RouterConnection) ProtoMessage() {} func (x *RouterConnection) ProtoReflect() protoreflect.Message { - mi := &file_prairie_plugin_v1_request_router_proto_msgTypes[1] + mi := &file_prairie_plugin_v1_request_router_proto_msgTypes[2] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -171,7 +239,7 @@ func (x *RouterConnection) ProtoReflect() protoreflect.Message { // Deprecated: Use RouterConnection.ProtoReflect.Descriptor instead. func (*RouterConnection) Descriptor() ([]byte, []int) { - return file_prairie_plugin_v1_request_router_proto_rawDescGZIP(), []int{1} + return file_prairie_plugin_v1_request_router_proto_rawDescGZIP(), []int{2} } func (x *RouterConnection) GetId() string { @@ -216,7 +284,7 @@ type RequestedQuality struct { func (x *RequestedQuality) Reset() { *x = RequestedQuality{} - mi := &file_prairie_plugin_v1_request_router_proto_msgTypes[2] + mi := &file_prairie_plugin_v1_request_router_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -228,7 +296,7 @@ func (x *RequestedQuality) String() string { func (*RequestedQuality) ProtoMessage() {} func (x *RequestedQuality) ProtoReflect() protoreflect.Message { - mi := &file_prairie_plugin_v1_request_router_proto_msgTypes[2] + mi := &file_prairie_plugin_v1_request_router_proto_msgTypes[3] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -241,7 +309,7 @@ func (x *RequestedQuality) ProtoReflect() protoreflect.Message { // Deprecated: Use RequestedQuality.ProtoReflect.Descriptor instead. func (*RequestedQuality) Descriptor() ([]byte, []int) { - return file_prairie_plugin_v1_request_router_proto_rawDescGZIP(), []int{2} + return file_prairie_plugin_v1_request_router_proto_rawDescGZIP(), []int{3} } func (x *RequestedQuality) GetId() string { @@ -270,7 +338,7 @@ type FulfillRequest struct { func (x *FulfillRequest) Reset() { *x = FulfillRequest{} - mi := &file_prairie_plugin_v1_request_router_proto_msgTypes[3] + mi := &file_prairie_plugin_v1_request_router_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -282,7 +350,7 @@ func (x *FulfillRequest) String() string { func (*FulfillRequest) ProtoMessage() {} func (x *FulfillRequest) ProtoReflect() protoreflect.Message { - mi := &file_prairie_plugin_v1_request_router_proto_msgTypes[3] + mi := &file_prairie_plugin_v1_request_router_proto_msgTypes[4] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -295,7 +363,7 @@ func (x *FulfillRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use FulfillRequest.ProtoReflect.Descriptor instead. func (*FulfillRequest) Descriptor() ([]byte, []int) { - return file_prairie_plugin_v1_request_router_proto_rawDescGZIP(), []int{3} + return file_prairie_plugin_v1_request_router_proto_rawDescGZIP(), []int{4} } func (x *FulfillRequest) GetCapabilityId() string { @@ -343,7 +411,7 @@ type FulfillmentTarget struct { func (x *FulfillmentTarget) Reset() { *x = FulfillmentTarget{} - mi := &file_prairie_plugin_v1_request_router_proto_msgTypes[4] + mi := &file_prairie_plugin_v1_request_router_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -355,7 +423,7 @@ func (x *FulfillmentTarget) String() string { func (*FulfillmentTarget) ProtoMessage() {} func (x *FulfillmentTarget) ProtoReflect() protoreflect.Message { - mi := &file_prairie_plugin_v1_request_router_proto_msgTypes[4] + mi := &file_prairie_plugin_v1_request_router_proto_msgTypes[5] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -368,7 +436,7 @@ func (x *FulfillmentTarget) ProtoReflect() protoreflect.Message { // Deprecated: Use FulfillmentTarget.ProtoReflect.Descriptor instead. func (*FulfillmentTarget) Descriptor() ([]byte, []int) { - return file_prairie_plugin_v1_request_router_proto_rawDescGZIP(), []int{4} + return file_prairie_plugin_v1_request_router_proto_rawDescGZIP(), []int{5} } func (x *FulfillmentTarget) GetQuality() string { @@ -424,7 +492,7 @@ type FulfillResponse struct { func (x *FulfillResponse) Reset() { *x = FulfillResponse{} - mi := &file_prairie_plugin_v1_request_router_proto_msgTypes[5] + mi := &file_prairie_plugin_v1_request_router_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -436,7 +504,7 @@ func (x *FulfillResponse) String() string { func (*FulfillResponse) ProtoMessage() {} func (x *FulfillResponse) ProtoReflect() protoreflect.Message { - mi := &file_prairie_plugin_v1_request_router_proto_msgTypes[5] + mi := &file_prairie_plugin_v1_request_router_proto_msgTypes[6] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -449,7 +517,7 @@ func (x *FulfillResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use FulfillResponse.ProtoReflect.Descriptor instead. func (*FulfillResponse) Descriptor() ([]byte, []int) { - return file_prairie_plugin_v1_request_router_proto_rawDescGZIP(), []int{5} + return file_prairie_plugin_v1_request_router_proto_rawDescGZIP(), []int{6} } func (x *FulfillResponse) GetTargets() []*FulfillmentTarget { @@ -477,7 +545,7 @@ type TargetRef struct { func (x *TargetRef) Reset() { *x = TargetRef{} - mi := &file_prairie_plugin_v1_request_router_proto_msgTypes[6] + mi := &file_prairie_plugin_v1_request_router_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -489,7 +557,7 @@ func (x *TargetRef) String() string { func (*TargetRef) ProtoMessage() {} func (x *TargetRef) ProtoReflect() protoreflect.Message { - mi := &file_prairie_plugin_v1_request_router_proto_msgTypes[6] + mi := &file_prairie_plugin_v1_request_router_proto_msgTypes[7] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -502,7 +570,7 @@ func (x *TargetRef) ProtoReflect() protoreflect.Message { // Deprecated: Use TargetRef.ProtoReflect.Descriptor instead. func (*TargetRef) Descriptor() ([]byte, []int) { - return file_prairie_plugin_v1_request_router_proto_rawDescGZIP(), []int{6} + return file_prairie_plugin_v1_request_router_proto_rawDescGZIP(), []int{7} } func (x *TargetRef) GetQuality() string { @@ -538,7 +606,7 @@ type CheckStatusRequest struct { func (x *CheckStatusRequest) Reset() { *x = CheckStatusRequest{} - mi := &file_prairie_plugin_v1_request_router_proto_msgTypes[7] + mi := &file_prairie_plugin_v1_request_router_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -550,7 +618,7 @@ func (x *CheckStatusRequest) String() string { func (*CheckStatusRequest) ProtoMessage() {} func (x *CheckStatusRequest) ProtoReflect() protoreflect.Message { - mi := &file_prairie_plugin_v1_request_router_proto_msgTypes[7] + mi := &file_prairie_plugin_v1_request_router_proto_msgTypes[8] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -563,7 +631,7 @@ func (x *CheckStatusRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CheckStatusRequest.ProtoReflect.Descriptor instead. func (*CheckStatusRequest) Descriptor() ([]byte, []int) { - return file_prairie_plugin_v1_request_router_proto_rawDescGZIP(), []int{7} + return file_prairie_plugin_v1_request_router_proto_rawDescGZIP(), []int{8} } func (x *CheckStatusRequest) GetCapabilityId() string { @@ -608,7 +676,7 @@ type TargetStatus struct { func (x *TargetStatus) Reset() { *x = TargetStatus{} - mi := &file_prairie_plugin_v1_request_router_proto_msgTypes[8] + mi := &file_prairie_plugin_v1_request_router_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -620,7 +688,7 @@ func (x *TargetStatus) String() string { func (*TargetStatus) ProtoMessage() {} func (x *TargetStatus) ProtoReflect() protoreflect.Message { - mi := &file_prairie_plugin_v1_request_router_proto_msgTypes[8] + mi := &file_prairie_plugin_v1_request_router_proto_msgTypes[9] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -633,7 +701,7 @@ func (x *TargetStatus) ProtoReflect() protoreflect.Message { // Deprecated: Use TargetStatus.ProtoReflect.Descriptor instead. func (*TargetStatus) Descriptor() ([]byte, []int) { - return file_prairie_plugin_v1_request_router_proto_rawDescGZIP(), []int{8} + return file_prairie_plugin_v1_request_router_proto_rawDescGZIP(), []int{9} } func (x *TargetStatus) GetQuality() string { @@ -680,7 +748,7 @@ type CheckStatusResponse struct { func (x *CheckStatusResponse) Reset() { *x = CheckStatusResponse{} - mi := &file_prairie_plugin_v1_request_router_proto_msgTypes[9] + mi := &file_prairie_plugin_v1_request_router_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -692,7 +760,7 @@ func (x *CheckStatusResponse) String() string { func (*CheckStatusResponse) ProtoMessage() {} func (x *CheckStatusResponse) ProtoReflect() protoreflect.Message { - mi := &file_prairie_plugin_v1_request_router_proto_msgTypes[9] + mi := &file_prairie_plugin_v1_request_router_proto_msgTypes[10] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -705,7 +773,7 @@ func (x *CheckStatusResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use CheckStatusResponse.ProtoReflect.Descriptor instead. func (*CheckStatusResponse) Descriptor() ([]byte, []int) { - return file_prairie_plugin_v1_request_router_proto_rawDescGZIP(), []int{9} + return file_prairie_plugin_v1_request_router_proto_rawDescGZIP(), []int{10} } func (x *CheckStatusResponse) GetStatuses() []*TargetStatus { @@ -725,7 +793,7 @@ type ConfigOption struct { func (x *ConfigOption) Reset() { *x = ConfigOption{} - mi := &file_prairie_plugin_v1_request_router_proto_msgTypes[10] + mi := &file_prairie_plugin_v1_request_router_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -737,7 +805,7 @@ func (x *ConfigOption) String() string { func (*ConfigOption) ProtoMessage() {} func (x *ConfigOption) ProtoReflect() protoreflect.Message { - mi := &file_prairie_plugin_v1_request_router_proto_msgTypes[10] + mi := &file_prairie_plugin_v1_request_router_proto_msgTypes[11] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -750,7 +818,7 @@ func (x *ConfigOption) ProtoReflect() protoreflect.Message { // Deprecated: Use ConfigOption.ProtoReflect.Descriptor instead. func (*ConfigOption) Descriptor() ([]byte, []int) { - return file_prairie_plugin_v1_request_router_proto_rawDescGZIP(), []int{10} + return file_prairie_plugin_v1_request_router_proto_rawDescGZIP(), []int{11} } func (x *ConfigOption) GetValue() string { @@ -776,7 +844,7 @@ type ConfigOptionList struct { func (x *ConfigOptionList) Reset() { *x = ConfigOptionList{} - mi := &file_prairie_plugin_v1_request_router_proto_msgTypes[11] + mi := &file_prairie_plugin_v1_request_router_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -788,7 +856,7 @@ func (x *ConfigOptionList) String() string { func (*ConfigOptionList) ProtoMessage() {} func (x *ConfigOptionList) ProtoReflect() protoreflect.Message { - mi := &file_prairie_plugin_v1_request_router_proto_msgTypes[11] + mi := &file_prairie_plugin_v1_request_router_proto_msgTypes[12] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -801,7 +869,7 @@ func (x *ConfigOptionList) ProtoReflect() protoreflect.Message { // Deprecated: Use ConfigOptionList.ProtoReflect.Descriptor instead. func (*ConfigOptionList) Descriptor() ([]byte, []int) { - return file_prairie_plugin_v1_request_router_proto_rawDescGZIP(), []int{11} + return file_prairie_plugin_v1_request_router_proto_rawDescGZIP(), []int{12} } func (x *ConfigOptionList) GetOptions() []*ConfigOption { @@ -821,7 +889,7 @@ type ListConfigOptionsRequest struct { func (x *ListConfigOptionsRequest) Reset() { *x = ListConfigOptionsRequest{} - mi := &file_prairie_plugin_v1_request_router_proto_msgTypes[12] + mi := &file_prairie_plugin_v1_request_router_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -833,7 +901,7 @@ func (x *ListConfigOptionsRequest) String() string { func (*ListConfigOptionsRequest) ProtoMessage() {} func (x *ListConfigOptionsRequest) ProtoReflect() protoreflect.Message { - mi := &file_prairie_plugin_v1_request_router_proto_msgTypes[12] + mi := &file_prairie_plugin_v1_request_router_proto_msgTypes[13] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -846,7 +914,7 @@ func (x *ListConfigOptionsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListConfigOptionsRequest.ProtoReflect.Descriptor instead. func (*ListConfigOptionsRequest) Descriptor() ([]byte, []int) { - return file_prairie_plugin_v1_request_router_proto_rawDescGZIP(), []int{12} + return file_prairie_plugin_v1_request_router_proto_rawDescGZIP(), []int{13} } func (x *ListConfigOptionsRequest) GetCapabilityId() string { @@ -873,7 +941,7 @@ type ListConfigOptionsResponse struct { func (x *ListConfigOptionsResponse) Reset() { *x = ListConfigOptionsResponse{} - mi := &file_prairie_plugin_v1_request_router_proto_msgTypes[13] + mi := &file_prairie_plugin_v1_request_router_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -885,7 +953,7 @@ func (x *ListConfigOptionsResponse) String() string { func (*ListConfigOptionsResponse) ProtoMessage() {} func (x *ListConfigOptionsResponse) ProtoReflect() protoreflect.Message { - mi := &file_prairie_plugin_v1_request_router_proto_msgTypes[13] + mi := &file_prairie_plugin_v1_request_router_proto_msgTypes[14] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -898,7 +966,7 @@ func (x *ListConfigOptionsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListConfigOptionsResponse.ProtoReflect.Descriptor instead. func (*ListConfigOptionsResponse) Descriptor() ([]byte, []int) { - return file_prairie_plugin_v1_request_router_proto_rawDescGZIP(), []int{13} + return file_prairie_plugin_v1_request_router_proto_rawDescGZIP(), []int{14} } func (x *ListConfigOptionsResponse) GetOptionsByField() map[string]*ConfigOptionList { @@ -918,7 +986,7 @@ type TestConnectionRequest struct { func (x *TestConnectionRequest) Reset() { *x = TestConnectionRequest{} - mi := &file_prairie_plugin_v1_request_router_proto_msgTypes[14] + mi := &file_prairie_plugin_v1_request_router_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -930,7 +998,7 @@ func (x *TestConnectionRequest) String() string { func (*TestConnectionRequest) ProtoMessage() {} func (x *TestConnectionRequest) ProtoReflect() protoreflect.Message { - mi := &file_prairie_plugin_v1_request_router_proto_msgTypes[14] + mi := &file_prairie_plugin_v1_request_router_proto_msgTypes[15] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -943,7 +1011,7 @@ func (x *TestConnectionRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use TestConnectionRequest.ProtoReflect.Descriptor instead. func (*TestConnectionRequest) Descriptor() ([]byte, []int) { - return file_prairie_plugin_v1_request_router_proto_rawDescGZIP(), []int{14} + return file_prairie_plugin_v1_request_router_proto_rawDescGZIP(), []int{15} } func (x *TestConnectionRequest) GetCapabilityId() string { @@ -970,7 +1038,7 @@ type TestConnectionResponse struct { func (x *TestConnectionResponse) Reset() { *x = TestConnectionResponse{} - mi := &file_prairie_plugin_v1_request_router_proto_msgTypes[15] + mi := &file_prairie_plugin_v1_request_router_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -982,7 +1050,7 @@ func (x *TestConnectionResponse) String() string { func (*TestConnectionResponse) ProtoMessage() {} func (x *TestConnectionResponse) ProtoReflect() protoreflect.Message { - mi := &file_prairie_plugin_v1_request_router_proto_msgTypes[15] + mi := &file_prairie_plugin_v1_request_router_proto_msgTypes[16] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -995,7 +1063,7 @@ func (x *TestConnectionResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use TestConnectionResponse.ProtoReflect.Descriptor instead. func (*TestConnectionResponse) Descriptor() ([]byte, []int) { - return file_prairie_plugin_v1_request_router_proto_rawDescGZIP(), []int{15} + return file_prairie_plugin_v1_request_router_proto_rawDescGZIP(), []int{16} } func (x *TestConnectionResponse) GetOk() bool { @@ -1027,7 +1095,7 @@ type ValidateRequest struct { func (x *ValidateRequest) Reset() { *x = ValidateRequest{} - mi := &file_prairie_plugin_v1_request_router_proto_msgTypes[16] + mi := &file_prairie_plugin_v1_request_router_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1039,7 +1107,7 @@ func (x *ValidateRequest) String() string { func (*ValidateRequest) ProtoMessage() {} func (x *ValidateRequest) ProtoReflect() protoreflect.Message { - mi := &file_prairie_plugin_v1_request_router_proto_msgTypes[16] + mi := &file_prairie_plugin_v1_request_router_proto_msgTypes[17] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1052,7 +1120,7 @@ func (x *ValidateRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ValidateRequest.ProtoReflect.Descriptor instead. func (*ValidateRequest) Descriptor() ([]byte, []int) { - return file_prairie_plugin_v1_request_router_proto_rawDescGZIP(), []int{16} + return file_prairie_plugin_v1_request_router_proto_rawDescGZIP(), []int{17} } func (x *ValidateRequest) GetCapabilityId() string { @@ -1086,7 +1154,7 @@ type ValidateResponse struct { func (x *ValidateResponse) Reset() { *x = ValidateResponse{} - mi := &file_prairie_plugin_v1_request_router_proto_msgTypes[17] + mi := &file_prairie_plugin_v1_request_router_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1098,7 +1166,7 @@ func (x *ValidateResponse) String() string { func (*ValidateResponse) ProtoMessage() {} func (x *ValidateResponse) ProtoReflect() protoreflect.Message { - mi := &file_prairie_plugin_v1_request_router_proto_msgTypes[17] + mi := &file_prairie_plugin_v1_request_router_proto_msgTypes[18] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1111,7 +1179,7 @@ func (x *ValidateResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ValidateResponse.ProtoReflect.Descriptor instead. func (*ValidateResponse) Descriptor() ([]byte, []int) { - return file_prairie_plugin_v1_request_router_proto_rawDescGZIP(), []int{17} + return file_prairie_plugin_v1_request_router_proto_rawDescGZIP(), []int{18} } func (x *ValidateResponse) GetFieldErrors() map[string]string { @@ -1132,7 +1200,7 @@ var File_prairie_plugin_v1_request_router_proto protoreflect.FileDescriptor const file_prairie_plugin_v1_request_router_proto_rawDesc = "" + "\n" + - "&prairie/plugin/v1/request_router.proto\x12\x11prairie.plugin.v1\x1a\x1cgoogle/protobuf/struct.proto\"\xc7\x03\n" + + "&prairie/plugin/v1/request_router.proto\x12\x11prairie.plugin.v1\x1a\x1cgoogle/protobuf/struct.proto\"\xe1\x03\n" + "\x11RequestDescriptor\x12\x1d\n" + "\n" + "media_type\x18\x01 \x01(\tR\tmediaType\x12\x14\n" + @@ -1143,10 +1211,14 @@ const file_prairie_plugin_v1_request_router_proto_rawDesc = "" + "\x11requester_user_id\x18\x06 \x01(\x03R\x0frequesterUserId\x120\n" + "\x14requester_profile_id\x18\a \x01(\tR\x12requesterProfileId\x12'\n" + "\x0frequester_email\x18\b \x01(\tR\x0erequesterEmail\x12-\n" + - "\x12requester_username\x18\t \x01(\tR\x11requesterUsername\x1a>\n" + + "\x12requester_username\x18\t \x01(\tR\x11requesterUsername\x12\x18\n" + + "\aseasons\x18\n" + + " \x03(\x05R\aseasons\x1a>\n" + "\x10ExternalIdsEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + - "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\x87\x01\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"D\n" + + "\x17RequestRouterDescriptor\x12)\n" + + "\x10supports_seasons\x18\x01 \x01(\bR\x0fsupportsSeasons\"\x87\x01\n" + "\x10RouterConnection\x12\x0e\n" + "\x02id\x18\x01 \x01(\tR\x02id\x12\x19\n" + "\bbase_url\x18\x02 \x01(\tR\abaseUrl\x12\x17\n" + @@ -1244,60 +1316,61 @@ func file_prairie_plugin_v1_request_router_proto_rawDescGZIP() []byte { return file_prairie_plugin_v1_request_router_proto_rawDescData } -var file_prairie_plugin_v1_request_router_proto_msgTypes = make([]protoimpl.MessageInfo, 21) +var file_prairie_plugin_v1_request_router_proto_msgTypes = make([]protoimpl.MessageInfo, 22) var file_prairie_plugin_v1_request_router_proto_goTypes = []any{ (*RequestDescriptor)(nil), // 0: prairie.plugin.v1.RequestDescriptor - (*RouterConnection)(nil), // 1: prairie.plugin.v1.RouterConnection - (*RequestedQuality)(nil), // 2: prairie.plugin.v1.RequestedQuality - (*FulfillRequest)(nil), // 3: prairie.plugin.v1.FulfillRequest - (*FulfillmentTarget)(nil), // 4: prairie.plugin.v1.FulfillmentTarget - (*FulfillResponse)(nil), // 5: prairie.plugin.v1.FulfillResponse - (*TargetRef)(nil), // 6: prairie.plugin.v1.TargetRef - (*CheckStatusRequest)(nil), // 7: prairie.plugin.v1.CheckStatusRequest - (*TargetStatus)(nil), // 8: prairie.plugin.v1.TargetStatus - (*CheckStatusResponse)(nil), // 9: prairie.plugin.v1.CheckStatusResponse - (*ConfigOption)(nil), // 10: prairie.plugin.v1.ConfigOption - (*ConfigOptionList)(nil), // 11: prairie.plugin.v1.ConfigOptionList - (*ListConfigOptionsRequest)(nil), // 12: prairie.plugin.v1.ListConfigOptionsRequest - (*ListConfigOptionsResponse)(nil), // 13: prairie.plugin.v1.ListConfigOptionsResponse - (*TestConnectionRequest)(nil), // 14: prairie.plugin.v1.TestConnectionRequest - (*TestConnectionResponse)(nil), // 15: prairie.plugin.v1.TestConnectionResponse - (*ValidateRequest)(nil), // 16: prairie.plugin.v1.ValidateRequest - (*ValidateResponse)(nil), // 17: prairie.plugin.v1.ValidateResponse - nil, // 18: prairie.plugin.v1.RequestDescriptor.ExternalIdsEntry - nil, // 19: prairie.plugin.v1.ListConfigOptionsResponse.OptionsByFieldEntry - nil, // 20: prairie.plugin.v1.ValidateResponse.FieldErrorsEntry - (*structpb.Struct)(nil), // 21: google.protobuf.Struct + (*RequestRouterDescriptor)(nil), // 1: prairie.plugin.v1.RequestRouterDescriptor + (*RouterConnection)(nil), // 2: prairie.plugin.v1.RouterConnection + (*RequestedQuality)(nil), // 3: prairie.plugin.v1.RequestedQuality + (*FulfillRequest)(nil), // 4: prairie.plugin.v1.FulfillRequest + (*FulfillmentTarget)(nil), // 5: prairie.plugin.v1.FulfillmentTarget + (*FulfillResponse)(nil), // 6: prairie.plugin.v1.FulfillResponse + (*TargetRef)(nil), // 7: prairie.plugin.v1.TargetRef + (*CheckStatusRequest)(nil), // 8: prairie.plugin.v1.CheckStatusRequest + (*TargetStatus)(nil), // 9: prairie.plugin.v1.TargetStatus + (*CheckStatusResponse)(nil), // 10: prairie.plugin.v1.CheckStatusResponse + (*ConfigOption)(nil), // 11: prairie.plugin.v1.ConfigOption + (*ConfigOptionList)(nil), // 12: prairie.plugin.v1.ConfigOptionList + (*ListConfigOptionsRequest)(nil), // 13: prairie.plugin.v1.ListConfigOptionsRequest + (*ListConfigOptionsResponse)(nil), // 14: prairie.plugin.v1.ListConfigOptionsResponse + (*TestConnectionRequest)(nil), // 15: prairie.plugin.v1.TestConnectionRequest + (*TestConnectionResponse)(nil), // 16: prairie.plugin.v1.TestConnectionResponse + (*ValidateRequest)(nil), // 17: prairie.plugin.v1.ValidateRequest + (*ValidateResponse)(nil), // 18: prairie.plugin.v1.ValidateResponse + nil, // 19: prairie.plugin.v1.RequestDescriptor.ExternalIdsEntry + nil, // 20: prairie.plugin.v1.ListConfigOptionsResponse.OptionsByFieldEntry + nil, // 21: prairie.plugin.v1.ValidateResponse.FieldErrorsEntry + (*structpb.Struct)(nil), // 22: google.protobuf.Struct } var file_prairie_plugin_v1_request_router_proto_depIdxs = []int32{ - 18, // 0: prairie.plugin.v1.RequestDescriptor.external_ids:type_name -> prairie.plugin.v1.RequestDescriptor.ExternalIdsEntry - 21, // 1: prairie.plugin.v1.RouterConnection.config:type_name -> google.protobuf.Struct + 19, // 0: prairie.plugin.v1.RequestDescriptor.external_ids:type_name -> prairie.plugin.v1.RequestDescriptor.ExternalIdsEntry + 22, // 1: prairie.plugin.v1.RouterConnection.config:type_name -> google.protobuf.Struct 0, // 2: prairie.plugin.v1.FulfillRequest.request:type_name -> prairie.plugin.v1.RequestDescriptor - 2, // 3: prairie.plugin.v1.FulfillRequest.qualities:type_name -> prairie.plugin.v1.RequestedQuality - 1, // 4: prairie.plugin.v1.FulfillRequest.connections:type_name -> prairie.plugin.v1.RouterConnection - 4, // 5: prairie.plugin.v1.FulfillResponse.targets:type_name -> prairie.plugin.v1.FulfillmentTarget + 3, // 3: prairie.plugin.v1.FulfillRequest.qualities:type_name -> prairie.plugin.v1.RequestedQuality + 2, // 4: prairie.plugin.v1.FulfillRequest.connections:type_name -> prairie.plugin.v1.RouterConnection + 5, // 5: prairie.plugin.v1.FulfillResponse.targets:type_name -> prairie.plugin.v1.FulfillmentTarget 0, // 6: prairie.plugin.v1.CheckStatusRequest.request:type_name -> prairie.plugin.v1.RequestDescriptor - 6, // 7: prairie.plugin.v1.CheckStatusRequest.targets:type_name -> prairie.plugin.v1.TargetRef - 1, // 8: prairie.plugin.v1.CheckStatusRequest.connections:type_name -> prairie.plugin.v1.RouterConnection - 8, // 9: prairie.plugin.v1.CheckStatusResponse.statuses:type_name -> prairie.plugin.v1.TargetStatus - 10, // 10: prairie.plugin.v1.ConfigOptionList.options:type_name -> prairie.plugin.v1.ConfigOption - 1, // 11: prairie.plugin.v1.ListConfigOptionsRequest.connection:type_name -> prairie.plugin.v1.RouterConnection - 19, // 12: prairie.plugin.v1.ListConfigOptionsResponse.options_by_field:type_name -> prairie.plugin.v1.ListConfigOptionsResponse.OptionsByFieldEntry - 1, // 13: prairie.plugin.v1.TestConnectionRequest.connection:type_name -> prairie.plugin.v1.RouterConnection - 1, // 14: prairie.plugin.v1.ValidateRequest.connection:type_name -> prairie.plugin.v1.RouterConnection - 1, // 15: prairie.plugin.v1.ValidateRequest.siblings:type_name -> prairie.plugin.v1.RouterConnection - 20, // 16: prairie.plugin.v1.ValidateResponse.field_errors:type_name -> prairie.plugin.v1.ValidateResponse.FieldErrorsEntry - 11, // 17: prairie.plugin.v1.ListConfigOptionsResponse.OptionsByFieldEntry.value:type_name -> prairie.plugin.v1.ConfigOptionList - 3, // 18: prairie.plugin.v1.RequestRouter.Fulfill:input_type -> prairie.plugin.v1.FulfillRequest - 7, // 19: prairie.plugin.v1.RequestRouter.CheckStatus:input_type -> prairie.plugin.v1.CheckStatusRequest - 12, // 20: prairie.plugin.v1.RequestRouter.ListConfigOptions:input_type -> prairie.plugin.v1.ListConfigOptionsRequest - 14, // 21: prairie.plugin.v1.RequestRouter.TestConnection:input_type -> prairie.plugin.v1.TestConnectionRequest - 16, // 22: prairie.plugin.v1.RequestRouter.Validate:input_type -> prairie.plugin.v1.ValidateRequest - 5, // 23: prairie.plugin.v1.RequestRouter.Fulfill:output_type -> prairie.plugin.v1.FulfillResponse - 9, // 24: prairie.plugin.v1.RequestRouter.CheckStatus:output_type -> prairie.plugin.v1.CheckStatusResponse - 13, // 25: prairie.plugin.v1.RequestRouter.ListConfigOptions:output_type -> prairie.plugin.v1.ListConfigOptionsResponse - 15, // 26: prairie.plugin.v1.RequestRouter.TestConnection:output_type -> prairie.plugin.v1.TestConnectionResponse - 17, // 27: prairie.plugin.v1.RequestRouter.Validate:output_type -> prairie.plugin.v1.ValidateResponse + 7, // 7: prairie.plugin.v1.CheckStatusRequest.targets:type_name -> prairie.plugin.v1.TargetRef + 2, // 8: prairie.plugin.v1.CheckStatusRequest.connections:type_name -> prairie.plugin.v1.RouterConnection + 9, // 9: prairie.plugin.v1.CheckStatusResponse.statuses:type_name -> prairie.plugin.v1.TargetStatus + 11, // 10: prairie.plugin.v1.ConfigOptionList.options:type_name -> prairie.plugin.v1.ConfigOption + 2, // 11: prairie.plugin.v1.ListConfigOptionsRequest.connection:type_name -> prairie.plugin.v1.RouterConnection + 20, // 12: prairie.plugin.v1.ListConfigOptionsResponse.options_by_field:type_name -> prairie.plugin.v1.ListConfigOptionsResponse.OptionsByFieldEntry + 2, // 13: prairie.plugin.v1.TestConnectionRequest.connection:type_name -> prairie.plugin.v1.RouterConnection + 2, // 14: prairie.plugin.v1.ValidateRequest.connection:type_name -> prairie.plugin.v1.RouterConnection + 2, // 15: prairie.plugin.v1.ValidateRequest.siblings:type_name -> prairie.plugin.v1.RouterConnection + 21, // 16: prairie.plugin.v1.ValidateResponse.field_errors:type_name -> prairie.plugin.v1.ValidateResponse.FieldErrorsEntry + 12, // 17: prairie.plugin.v1.ListConfigOptionsResponse.OptionsByFieldEntry.value:type_name -> prairie.plugin.v1.ConfigOptionList + 4, // 18: prairie.plugin.v1.RequestRouter.Fulfill:input_type -> prairie.plugin.v1.FulfillRequest + 8, // 19: prairie.plugin.v1.RequestRouter.CheckStatus:input_type -> prairie.plugin.v1.CheckStatusRequest + 13, // 20: prairie.plugin.v1.RequestRouter.ListConfigOptions:input_type -> prairie.plugin.v1.ListConfigOptionsRequest + 15, // 21: prairie.plugin.v1.RequestRouter.TestConnection:input_type -> prairie.plugin.v1.TestConnectionRequest + 17, // 22: prairie.plugin.v1.RequestRouter.Validate:input_type -> prairie.plugin.v1.ValidateRequest + 6, // 23: prairie.plugin.v1.RequestRouter.Fulfill:output_type -> prairie.plugin.v1.FulfillResponse + 10, // 24: prairie.plugin.v1.RequestRouter.CheckStatus:output_type -> prairie.plugin.v1.CheckStatusResponse + 14, // 25: prairie.plugin.v1.RequestRouter.ListConfigOptions:output_type -> prairie.plugin.v1.ListConfigOptionsResponse + 16, // 26: prairie.plugin.v1.RequestRouter.TestConnection:output_type -> prairie.plugin.v1.TestConnectionResponse + 18, // 27: prairie.plugin.v1.RequestRouter.Validate:output_type -> prairie.plugin.v1.ValidateResponse 23, // [23:28] is the sub-list for method output_type 18, // [18:23] is the sub-list for method input_type 18, // [18:18] is the sub-list for extension type_name @@ -1316,7 +1389,7 @@ func file_prairie_plugin_v1_request_router_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_prairie_plugin_v1_request_router_proto_rawDesc), len(file_prairie_plugin_v1_request_router_proto_rawDesc)), NumEnums: 0, - NumMessages: 21, + NumMessages: 22, NumExtensions: 0, NumServices: 1, }, diff --git a/pkg/pluginproto/prairie/plugin/v1/runtime_host.pb.go b/pkg/pluginproto/prairie/plugin/v1/runtime_host.pb.go index 8533931..5cb61dd 100644 --- a/pkg/pluginproto/prairie/plugin/v1/runtime_host.pb.go +++ b/pkg/pluginproto/prairie/plugin/v1/runtime_host.pb.go @@ -350,8 +350,23 @@ type GetHostInfoResponse struct { PublicBaseUrl string `protobuf:"bytes,1,opt,name=public_base_url,json=publicBaseUrl,proto3" json:"public_base_url,omitempty"` InternalBaseUrl string `protobuf:"bytes,2,opt,name=internal_base_url,json=internalBaseUrl,proto3" json:"internal_base_url,omitempty"` PluginProxyBaseUrl string `protobuf:"bytes,3,opt,name=plugin_proxy_base_url,json=pluginProxyBaseUrl,proto3" json:"plugin_proxy_base_url,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Role of the process hosting this plugin instance: api | proxy. + HostRole string `protobuf:"bytes,4,opt,name=host_role,json=hostRole,proto3" json:"host_role,omitempty"` + // Node name on proxies, or the server name on the api host. + HostName string `protobuf:"bytes,5,opt,name=host_name,json=hostName,proto3" json:"host_name,omitempty"` + // stream_nodes.id on proxies; 0 on the api host. + NodeId int64 `protobuf:"varint,6,opt,name=node_id,json=nodeId,proto3" json:"node_id,omitempty"` + // Per-process-start secret that a network access provider stamps on + // proxied requests as X-Prairie-Ingress-Token. Treat as a credential: keep it + // in memory, never log or persist it, and re-read GetHostInfo after a + // restart because the host rotates it on every start. + IngressToken string `protobuf:"bytes,7,opt,name=ingress_token,json=ingressToken,proto3" json:"ingress_token,omitempty"` + // Host listeners a network access provider should expose. Always contains + // "api"; "jellyfin" and "abs" appear when those listeners are enabled. + // Proxies expose only "api". + Listeners []*HostListener `protobuf:"bytes,8,rep,name=listeners,proto3" json:"listeners,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *GetHostInfoResponse) Reset() { @@ -405,6 +420,107 @@ func (x *GetHostInfoResponse) GetPluginProxyBaseUrl() string { return "" } +func (x *GetHostInfoResponse) GetHostRole() string { + if x != nil { + return x.HostRole + } + return "" +} + +func (x *GetHostInfoResponse) GetHostName() string { + if x != nil { + return x.HostName + } + return "" +} + +func (x *GetHostInfoResponse) GetNodeId() int64 { + if x != nil { + return x.NodeId + } + return 0 +} + +func (x *GetHostInfoResponse) GetIngressToken() string { + if x != nil { + return x.IngressToken + } + return "" +} + +func (x *GetHostInfoResponse) GetListeners() []*HostListener { + if x != nil { + return x.Listeners + } + return nil +} + +// HostListener is one local listener the host asks a network access provider +// to expose on the overlay network. +type HostListener struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Listener name: api | jellyfin | abs. + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // Loopback dial address for the listener, host:port. + Address string `protobuf:"bytes,2,opt,name=address,proto3" json:"address,omitempty"` + // Port the plugin should expose this listener on (e.g. 443 for api under + // HTTPS, 8096 for jellyfin, 13378 for abs). Zero means provider default. + DefaultPort int32 `protobuf:"varint,3,opt,name=default_port,json=defaultPort,proto3" json:"default_port,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *HostListener) Reset() { + *x = HostListener{} + mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HostListener) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HostListener) ProtoMessage() {} + +func (x *HostListener) ProtoReflect() protoreflect.Message { + mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HostListener.ProtoReflect.Descriptor instead. +func (*HostListener) Descriptor() ([]byte, []int) { + return file_prairie_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{8} +} + +func (x *HostListener) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *HostListener) GetAddress() string { + if x != nil { + return x.Address + } + return "" +} + +func (x *HostListener) GetDefaultPort() int32 { + if x != nil { + return x.DefaultPort + } + return 0 +} + type ListLibrariesRequest struct { state protoimpl.MessageState `protogen:"open.v1"` // Optional: scope results to libraries this user is allowed to see. @@ -415,7 +531,7 @@ type ListLibrariesRequest struct { func (x *ListLibrariesRequest) Reset() { *x = ListLibrariesRequest{} - mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[8] + mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -427,7 +543,7 @@ func (x *ListLibrariesRequest) String() string { func (*ListLibrariesRequest) ProtoMessage() {} func (x *ListLibrariesRequest) ProtoReflect() protoreflect.Message { - mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[8] + mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[9] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -440,7 +556,7 @@ func (x *ListLibrariesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListLibrariesRequest.ProtoReflect.Descriptor instead. func (*ListLibrariesRequest) Descriptor() ([]byte, []int) { - return file_prairie_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{8} + return file_prairie_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{9} } func (x *ListLibrariesRequest) GetUserId() string { @@ -459,7 +575,7 @@ type ListLibrariesResponse struct { func (x *ListLibrariesResponse) Reset() { *x = ListLibrariesResponse{} - mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[9] + mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -471,7 +587,7 @@ func (x *ListLibrariesResponse) String() string { func (*ListLibrariesResponse) ProtoMessage() {} func (x *ListLibrariesResponse) ProtoReflect() protoreflect.Message { - mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[9] + mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[10] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -484,7 +600,7 @@ func (x *ListLibrariesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListLibrariesResponse.ProtoReflect.Descriptor instead. func (*ListLibrariesResponse) Descriptor() ([]byte, []int) { - return file_prairie_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{9} + return file_prairie_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{10} } func (x *ListLibrariesResponse) GetLibraries() []*Library { @@ -506,7 +622,7 @@ type Library struct { func (x *Library) Reset() { *x = Library{} - mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[10] + mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -518,7 +634,7 @@ func (x *Library) String() string { func (*Library) ProtoMessage() {} func (x *Library) ProtoReflect() protoreflect.Message { - mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[10] + mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[11] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -531,7 +647,7 @@ func (x *Library) ProtoReflect() protoreflect.Message { // Deprecated: Use Library.ProtoReflect.Descriptor instead. func (*Library) Descriptor() ([]byte, []int) { - return file_prairie_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{10} + return file_prairie_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{11} } func (x *Library) GetId() string { @@ -570,7 +686,7 @@ type CheckMediaPresenceRequest struct { func (x *CheckMediaPresenceRequest) Reset() { *x = CheckMediaPresenceRequest{} - mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[11] + mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -582,7 +698,7 @@ func (x *CheckMediaPresenceRequest) String() string { func (*CheckMediaPresenceRequest) ProtoMessage() {} func (x *CheckMediaPresenceRequest) ProtoReflect() protoreflect.Message { - mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[11] + mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[12] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -595,7 +711,7 @@ func (x *CheckMediaPresenceRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CheckMediaPresenceRequest.ProtoReflect.Descriptor instead. func (*CheckMediaPresenceRequest) Descriptor() ([]byte, []int) { - return file_prairie_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{11} + return file_prairie_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{12} } func (x *CheckMediaPresenceRequest) GetProvider() string { @@ -630,7 +746,7 @@ type CheckMediaPresenceResponse struct { func (x *CheckMediaPresenceResponse) Reset() { *x = CheckMediaPresenceResponse{} - mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[12] + mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -642,7 +758,7 @@ func (x *CheckMediaPresenceResponse) String() string { func (*CheckMediaPresenceResponse) ProtoMessage() {} func (x *CheckMediaPresenceResponse) ProtoReflect() protoreflect.Message { - mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[12] + mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[13] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -655,7 +771,7 @@ func (x *CheckMediaPresenceResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use CheckMediaPresenceResponse.ProtoReflect.Descriptor instead. func (*CheckMediaPresenceResponse) Descriptor() ([]byte, []int) { - return file_prairie_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{12} + return file_prairie_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{13} } func (x *CheckMediaPresenceResponse) GetPresent() []*MediaPresence { @@ -677,7 +793,7 @@ type MediaPresence struct { func (x *MediaPresence) Reset() { *x = MediaPresence{} - mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[13] + mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -689,7 +805,7 @@ func (x *MediaPresence) String() string { func (*MediaPresence) ProtoMessage() {} func (x *MediaPresence) ProtoReflect() protoreflect.Message { - mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[13] + mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[14] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -702,7 +818,7 @@ func (x *MediaPresence) ProtoReflect() protoreflect.Message { // Deprecated: Use MediaPresence.ProtoReflect.Descriptor instead. func (*MediaPresence) Descriptor() ([]byte, []int) { - return file_prairie_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{13} + return file_prairie_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{14} } func (x *MediaPresence) GetExternalId() string { @@ -741,7 +857,7 @@ type ListInstalledPluginsRequest struct { func (x *ListInstalledPluginsRequest) Reset() { *x = ListInstalledPluginsRequest{} - mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[14] + mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -753,7 +869,7 @@ func (x *ListInstalledPluginsRequest) String() string { func (*ListInstalledPluginsRequest) ProtoMessage() {} func (x *ListInstalledPluginsRequest) ProtoReflect() protoreflect.Message { - mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[14] + mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[15] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -766,7 +882,7 @@ func (x *ListInstalledPluginsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListInstalledPluginsRequest.ProtoReflect.Descriptor instead. func (*ListInstalledPluginsRequest) Descriptor() ([]byte, []int) { - return file_prairie_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{14} + return file_prairie_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{15} } type ListInstalledPluginsResponse struct { @@ -778,7 +894,7 @@ type ListInstalledPluginsResponse struct { func (x *ListInstalledPluginsResponse) Reset() { *x = ListInstalledPluginsResponse{} - mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[15] + mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -790,7 +906,7 @@ func (x *ListInstalledPluginsResponse) String() string { func (*ListInstalledPluginsResponse) ProtoMessage() {} func (x *ListInstalledPluginsResponse) ProtoReflect() protoreflect.Message { - mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[15] + mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[16] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -803,7 +919,7 @@ func (x *ListInstalledPluginsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListInstalledPluginsResponse.ProtoReflect.Descriptor instead. func (*ListInstalledPluginsResponse) Descriptor() ([]byte, []int) { - return file_prairie_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{15} + return file_prairie_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{16} } func (x *ListInstalledPluginsResponse) GetPlugins() []*InstalledPlugin { @@ -826,7 +942,7 @@ type InstalledPlugin struct { func (x *InstalledPlugin) Reset() { *x = InstalledPlugin{} - mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[16] + mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -838,7 +954,7 @@ func (x *InstalledPlugin) String() string { func (*InstalledPlugin) ProtoMessage() {} func (x *InstalledPlugin) ProtoReflect() protoreflect.Message { - mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[16] + mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[17] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -851,7 +967,7 @@ func (x *InstalledPlugin) ProtoReflect() protoreflect.Message { // Deprecated: Use InstalledPlugin.ProtoReflect.Descriptor instead. func (*InstalledPlugin) Descriptor() ([]byte, []int) { - return file_prairie_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{16} + return file_prairie_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{17} } func (x *InstalledPlugin) GetInstallationId() int64 { @@ -899,7 +1015,7 @@ type SetGlobalConfigEntryRequest struct { func (x *SetGlobalConfigEntryRequest) Reset() { *x = SetGlobalConfigEntryRequest{} - mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[17] + mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -911,7 +1027,7 @@ func (x *SetGlobalConfigEntryRequest) String() string { func (*SetGlobalConfigEntryRequest) ProtoMessage() {} func (x *SetGlobalConfigEntryRequest) ProtoReflect() protoreflect.Message { - mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[17] + mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[18] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -924,7 +1040,7 @@ func (x *SetGlobalConfigEntryRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use SetGlobalConfigEntryRequest.ProtoReflect.Descriptor instead. func (*SetGlobalConfigEntryRequest) Descriptor() ([]byte, []int) { - return file_prairie_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{17} + return file_prairie_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{18} } func (x *SetGlobalConfigEntryRequest) GetKey() string { @@ -949,7 +1065,7 @@ type SetGlobalConfigEntryResponse struct { func (x *SetGlobalConfigEntryResponse) Reset() { *x = SetGlobalConfigEntryResponse{} - mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[18] + mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -961,7 +1077,7 @@ func (x *SetGlobalConfigEntryResponse) String() string { func (*SetGlobalConfigEntryResponse) ProtoMessage() {} func (x *SetGlobalConfigEntryResponse) ProtoReflect() protoreflect.Message { - mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[18] + mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[19] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -974,7 +1090,7 @@ func (x *SetGlobalConfigEntryResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use SetGlobalConfigEntryResponse.ProtoReflect.Descriptor instead. func (*SetGlobalConfigEntryResponse) Descriptor() ([]byte, []int) { - return file_prairie_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{18} + return file_prairie_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{19} } type ListLibraryMediaRequest struct { @@ -998,7 +1114,7 @@ type ListLibraryMediaRequest struct { func (x *ListLibraryMediaRequest) Reset() { *x = ListLibraryMediaRequest{} - mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[19] + mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1010,7 +1126,7 @@ func (x *ListLibraryMediaRequest) String() string { func (*ListLibraryMediaRequest) ProtoMessage() {} func (x *ListLibraryMediaRequest) ProtoReflect() protoreflect.Message { - mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[19] + mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[20] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1023,7 +1139,7 @@ func (x *ListLibraryMediaRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListLibraryMediaRequest.ProtoReflect.Descriptor instead. func (*ListLibraryMediaRequest) Descriptor() ([]byte, []int) { - return file_prairie_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{19} + return file_prairie_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{20} } func (x *ListLibraryMediaRequest) GetLibraryIds() []string { @@ -1107,7 +1223,7 @@ type ListLibraryMediaResponse struct { func (x *ListLibraryMediaResponse) Reset() { *x = ListLibraryMediaResponse{} - mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[20] + mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1119,7 +1235,7 @@ func (x *ListLibraryMediaResponse) String() string { func (*ListLibraryMediaResponse) ProtoMessage() {} func (x *ListLibraryMediaResponse) ProtoReflect() protoreflect.Message { - mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[20] + mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[21] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1132,7 +1248,7 @@ func (x *ListLibraryMediaResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListLibraryMediaResponse.ProtoReflect.Descriptor instead. func (*ListLibraryMediaResponse) Descriptor() ([]byte, []int) { - return file_prairie_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{20} + return file_prairie_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{21} } func (x *ListLibraryMediaResponse) GetItems() []*CatalogMediaItem { @@ -1179,7 +1295,7 @@ type CatalogMediaItem struct { func (x *CatalogMediaItem) Reset() { *x = CatalogMediaItem{} - mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[21] + mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1191,7 +1307,7 @@ func (x *CatalogMediaItem) String() string { func (*CatalogMediaItem) ProtoMessage() {} func (x *CatalogMediaItem) ProtoReflect() protoreflect.Message { - mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[21] + mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[22] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1204,7 +1320,7 @@ func (x *CatalogMediaItem) ProtoReflect() protoreflect.Message { // Deprecated: Use CatalogMediaItem.ProtoReflect.Descriptor instead. func (*CatalogMediaItem) Descriptor() ([]byte, []int) { - return file_prairie_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{21} + return file_prairie_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{22} } func (x *CatalogMediaItem) GetMediaId() string { @@ -1322,7 +1438,7 @@ type GetCatalogStatsRequest struct { func (x *GetCatalogStatsRequest) Reset() { *x = GetCatalogStatsRequest{} - mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[22] + mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1334,7 +1450,7 @@ func (x *GetCatalogStatsRequest) String() string { func (*GetCatalogStatsRequest) ProtoMessage() {} func (x *GetCatalogStatsRequest) ProtoReflect() protoreflect.Message { - mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[22] + mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[23] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1347,7 +1463,7 @@ func (x *GetCatalogStatsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetCatalogStatsRequest.ProtoReflect.Descriptor instead. func (*GetCatalogStatsRequest) Descriptor() ([]byte, []int) { - return file_prairie_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{22} + return file_prairie_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{23} } func (x *GetCatalogStatsRequest) GetLibraryIds() []string { @@ -1368,7 +1484,7 @@ type GetCatalogStatsResponse struct { func (x *GetCatalogStatsResponse) Reset() { *x = GetCatalogStatsResponse{} - mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[23] + mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1380,7 +1496,7 @@ func (x *GetCatalogStatsResponse) String() string { func (*GetCatalogStatsResponse) ProtoMessage() {} func (x *GetCatalogStatsResponse) ProtoReflect() protoreflect.Message { - mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[23] + mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[24] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1393,7 +1509,7 @@ func (x *GetCatalogStatsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetCatalogStatsResponse.ProtoReflect.Descriptor instead. func (*GetCatalogStatsResponse) Descriptor() ([]byte, []int) { - return file_prairie_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{23} + return file_prairie_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{24} } func (x *GetCatalogStatsResponse) GetTotalItems() int32 { @@ -1427,7 +1543,7 @@ type CatalogTypeCount struct { func (x *CatalogTypeCount) Reset() { *x = CatalogTypeCount{} - mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[24] + mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1439,7 +1555,7 @@ func (x *CatalogTypeCount) String() string { func (*CatalogTypeCount) ProtoMessage() {} func (x *CatalogTypeCount) ProtoReflect() protoreflect.Message { - mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[24] + mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[25] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1452,7 +1568,7 @@ func (x *CatalogTypeCount) ProtoReflect() protoreflect.Message { // Deprecated: Use CatalogTypeCount.ProtoReflect.Descriptor instead. func (*CatalogTypeCount) Descriptor() ([]byte, []int) { - return file_prairie_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{24} + return file_prairie_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{25} } func (x *CatalogTypeCount) GetMediaType() string { @@ -1481,7 +1597,7 @@ type CatalogLibraryCount struct { func (x *CatalogLibraryCount) Reset() { *x = CatalogLibraryCount{} - mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[25] + mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[26] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1493,7 +1609,7 @@ func (x *CatalogLibraryCount) String() string { func (*CatalogLibraryCount) ProtoMessage() {} func (x *CatalogLibraryCount) ProtoReflect() protoreflect.Message { - mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[25] + mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[26] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1506,7 +1622,7 @@ func (x *CatalogLibraryCount) ProtoReflect() protoreflect.Message { // Deprecated: Use CatalogLibraryCount.ProtoReflect.Descriptor instead. func (*CatalogLibraryCount) Descriptor() ([]byte, []int) { - return file_prairie_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{25} + return file_prairie_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{26} } func (x *CatalogLibraryCount) GetLibraryId() string { @@ -1543,9 +1659,13 @@ type ResolveCatalogImageURLsRequest struct { // backdrop_url values returned by ListLibraryMedia. Paths are host-owned // opaque strings; plugins should pass them back unchanged. Paths []string `protobuf:"bytes,1,rep,name=paths,proto3" json:"paths,omitempty"` - // Optional image variant name understood by the host resolver. Empty uses the - // host default. Common values include "thumbnail" and "original"; unsupported - // variants are handled host-side. + // Optional semantic size hint understood by the host resolver, not an exact + // pixel contract. Canonical values, smallest to largest: "card", "featured", + // "large", "full", "original". Empty uses the host default. The vocabulary is + // open and grows additively; unsupported variants are degraded host-side to + // the nearest available size rather than erroring, and the same graceful + // degradation is required of plugins that resolve variants themselves. See + // pkg/pluginsdk/imagevariant for constants and the full rules. Variant string `protobuf:"bytes,2,opt,name=variant,proto3" json:"variant,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -1553,7 +1673,7 @@ type ResolveCatalogImageURLsRequest struct { func (x *ResolveCatalogImageURLsRequest) Reset() { *x = ResolveCatalogImageURLsRequest{} - mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[26] + mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[27] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1565,7 +1685,7 @@ func (x *ResolveCatalogImageURLsRequest) String() string { func (*ResolveCatalogImageURLsRequest) ProtoMessage() {} func (x *ResolveCatalogImageURLsRequest) ProtoReflect() protoreflect.Message { - mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[26] + mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[27] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1578,7 +1698,7 @@ func (x *ResolveCatalogImageURLsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ResolveCatalogImageURLsRequest.ProtoReflect.Descriptor instead. func (*ResolveCatalogImageURLsRequest) Descriptor() ([]byte, []int) { - return file_prairie_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{26} + return file_prairie_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{27} } func (x *ResolveCatalogImageURLsRequest) GetPaths() []string { @@ -1607,7 +1727,7 @@ type ResolveCatalogImageURLsResponse struct { func (x *ResolveCatalogImageURLsResponse) Reset() { *x = ResolveCatalogImageURLsResponse{} - mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[27] + mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[28] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1619,7 +1739,7 @@ func (x *ResolveCatalogImageURLsResponse) String() string { func (*ResolveCatalogImageURLsResponse) ProtoMessage() {} func (x *ResolveCatalogImageURLsResponse) ProtoReflect() protoreflect.Message { - mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[27] + mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[28] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1632,7 +1752,7 @@ func (x *ResolveCatalogImageURLsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ResolveCatalogImageURLsResponse.ProtoReflect.Descriptor instead. func (*ResolveCatalogImageURLsResponse) Descriptor() ([]byte, []int) { - return file_prairie_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{27} + return file_prairie_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{28} } func (x *ResolveCatalogImageURLsResponse) GetUrls() map[string]string { @@ -1662,7 +1782,7 @@ type MintScopedStreamRequest struct { func (x *MintScopedStreamRequest) Reset() { *x = MintScopedStreamRequest{} - mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[28] + mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[29] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1674,7 +1794,7 @@ func (x *MintScopedStreamRequest) String() string { func (*MintScopedStreamRequest) ProtoMessage() {} func (x *MintScopedStreamRequest) ProtoReflect() protoreflect.Message { - mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[28] + mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[29] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1687,7 +1807,7 @@ func (x *MintScopedStreamRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use MintScopedStreamRequest.ProtoReflect.Descriptor instead. func (*MintScopedStreamRequest) Descriptor() ([]byte, []int) { - return file_prairie_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{28} + return file_prairie_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{29} } func (x *MintScopedStreamRequest) GetMediaFileId() int64 { @@ -1785,7 +1905,7 @@ type MintScopedStreamResponse struct { func (x *MintScopedStreamResponse) Reset() { *x = MintScopedStreamResponse{} - mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[29] + mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[30] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1797,7 +1917,7 @@ func (x *MintScopedStreamResponse) String() string { func (*MintScopedStreamResponse) ProtoMessage() {} func (x *MintScopedStreamResponse) ProtoReflect() protoreflect.Message { - mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[29] + mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[30] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1810,7 +1930,7 @@ func (x *MintScopedStreamResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use MintScopedStreamResponse.ProtoReflect.Descriptor instead. func (*MintScopedStreamResponse) Descriptor() ([]byte, []int) { - return file_prairie_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{29} + return file_prairie_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{30} } func (x *MintScopedStreamResponse) GetStreamUrl() string { @@ -1848,7 +1968,7 @@ type CallPluginHTTPRequest struct { func (x *CallPluginHTTPRequest) Reset() { *x = CallPluginHTTPRequest{} - mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[30] + mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[31] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1860,7 +1980,7 @@ func (x *CallPluginHTTPRequest) String() string { func (*CallPluginHTTPRequest) ProtoMessage() {} func (x *CallPluginHTTPRequest) ProtoReflect() protoreflect.Message { - mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[30] + mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[31] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1873,7 +1993,7 @@ func (x *CallPluginHTTPRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CallPluginHTTPRequest.ProtoReflect.Descriptor instead. func (*CallPluginHTTPRequest) Descriptor() ([]byte, []int) { - return file_prairie_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{30} + return file_prairie_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{31} } func (x *CallPluginHTTPRequest) GetInstallationId() int32 { @@ -1929,7 +2049,7 @@ type CallPluginHTTPResponse struct { func (x *CallPluginHTTPResponse) Reset() { *x = CallPluginHTTPResponse{} - mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[31] + mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[32] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1941,7 +2061,7 @@ func (x *CallPluginHTTPResponse) String() string { func (*CallPluginHTTPResponse) ProtoMessage() {} func (x *CallPluginHTTPResponse) ProtoReflect() protoreflect.Message { - mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[31] + mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[32] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1954,7 +2074,7 @@ func (x *CallPluginHTTPResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use CallPluginHTTPResponse.ProtoReflect.Descriptor instead. func (*CallPluginHTTPResponse) Descriptor() ([]byte, []int) { - return file_prairie_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{31} + return file_prairie_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{32} } func (x *CallPluginHTTPResponse) GetStatusCode() int32 { @@ -1978,11 +2098,280 @@ func (x *CallPluginHTTPResponse) GetBody() []byte { return nil } +type ReadInstanceStateRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // State key, <= 256 bytes. + Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ReadInstanceStateRequest) Reset() { + *x = ReadInstanceStateRequest{} + mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[33] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ReadInstanceStateRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ReadInstanceStateRequest) ProtoMessage() {} + +func (x *ReadInstanceStateRequest) ProtoReflect() protoreflect.Message { + mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[33] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ReadInstanceStateRequest.ProtoReflect.Descriptor instead. +func (*ReadInstanceStateRequest) Descriptor() ([]byte, []int) { + return file_prairie_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{33} +} + +func (x *ReadInstanceStateRequest) GetKey() string { + if x != nil { + return x.Key + } + return "" +} + +type ReadInstanceStateResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Stored value; empty when found is false. + Value []byte `protobuf:"bytes,1,opt,name=value,proto3" json:"value,omitempty"` + // False when no value is stored under key in this instance's scope. + Found bool `protobuf:"varint,2,opt,name=found,proto3" json:"found,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ReadInstanceStateResponse) Reset() { + *x = ReadInstanceStateResponse{} + mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[34] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ReadInstanceStateResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ReadInstanceStateResponse) ProtoMessage() {} + +func (x *ReadInstanceStateResponse) ProtoReflect() protoreflect.Message { + mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[34] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ReadInstanceStateResponse.ProtoReflect.Descriptor instead. +func (*ReadInstanceStateResponse) Descriptor() ([]byte, []int) { + return file_prairie_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{34} +} + +func (x *ReadInstanceStateResponse) GetValue() []byte { + if x != nil { + return x.Value + } + return nil +} + +func (x *ReadInstanceStateResponse) GetFound() bool { + if x != nil { + return x.Found + } + return false +} + +type WriteInstanceStateRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // State key, <= 256 bytes. + Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` + // Value to store, <= 256 KiB. + Value []byte `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *WriteInstanceStateRequest) Reset() { + *x = WriteInstanceStateRequest{} + mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[35] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *WriteInstanceStateRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WriteInstanceStateRequest) ProtoMessage() {} + +func (x *WriteInstanceStateRequest) ProtoReflect() protoreflect.Message { + mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[35] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WriteInstanceStateRequest.ProtoReflect.Descriptor instead. +func (*WriteInstanceStateRequest) Descriptor() ([]byte, []int) { + return file_prairie_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{35} +} + +func (x *WriteInstanceStateRequest) GetKey() string { + if x != nil { + return x.Key + } + return "" +} + +func (x *WriteInstanceStateRequest) GetValue() []byte { + if x != nil { + return x.Value + } + return nil +} + +type WriteInstanceStateResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *WriteInstanceStateResponse) Reset() { + *x = WriteInstanceStateResponse{} + mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[36] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *WriteInstanceStateResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WriteInstanceStateResponse) ProtoMessage() {} + +func (x *WriteInstanceStateResponse) ProtoReflect() protoreflect.Message { + mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[36] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WriteInstanceStateResponse.ProtoReflect.Descriptor instead. +func (*WriteInstanceStateResponse) Descriptor() ([]byte, []int) { + return file_prairie_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{36} +} + +type ReportNetworkAccessStatusRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Status *NetworkAccessStatus `protobuf:"bytes,1,opt,name=status,proto3" json:"status,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ReportNetworkAccessStatusRequest) Reset() { + *x = ReportNetworkAccessStatusRequest{} + mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[37] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ReportNetworkAccessStatusRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ReportNetworkAccessStatusRequest) ProtoMessage() {} + +func (x *ReportNetworkAccessStatusRequest) ProtoReflect() protoreflect.Message { + mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[37] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ReportNetworkAccessStatusRequest.ProtoReflect.Descriptor instead. +func (*ReportNetworkAccessStatusRequest) Descriptor() ([]byte, []int) { + return file_prairie_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{37} +} + +func (x *ReportNetworkAccessStatusRequest) GetStatus() *NetworkAccessStatus { + if x != nil { + return x.Status + } + return nil +} + +type ReportNetworkAccessStatusResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ReportNetworkAccessStatusResponse) Reset() { + *x = ReportNetworkAccessStatusResponse{} + mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[38] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ReportNetworkAccessStatusResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ReportNetworkAccessStatusResponse) ProtoMessage() {} + +func (x *ReportNetworkAccessStatusResponse) ProtoReflect() protoreflect.Message { + mi := &file_prairie_plugin_v1_runtime_host_proto_msgTypes[38] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ReportNetworkAccessStatusResponse.ProtoReflect.Descriptor instead. +func (*ReportNetworkAccessStatusResponse) Descriptor() ([]byte, []int) { + return file_prairie_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{38} +} + var File_prairie_plugin_v1_runtime_host_proto protoreflect.FileDescriptor const file_prairie_plugin_v1_runtime_host_proto_rawDesc = "" + "\n" + - "$prairie/plugin/v1/runtime_host.proto\x12\x11prairie.plugin.v1\x1a\x1cgoogle/protobuf/struct.proto\x1a\x1eprairie/plugin/v1/common.proto\"g\n" + + "$prairie/plugin/v1/runtime_host.proto\x12\x11prairie.plugin.v1\x1a\x1cgoogle/protobuf/struct.proto\x1a\x1eprairie/plugin/v1/common.proto\x1a/prairie/plugin/v1/network_access_provider.proto\"g\n" + "\x13PublishEventRequest\x12\x1d\n" + "\n" + "event_name\x18\x01 \x01(\tR\teventName\x121\n" + @@ -2000,11 +2389,20 @@ const file_prairie_plugin_v1_runtime_host_proto_rawDesc = "" + "event_name\x18\x02 \x01(\tR\teventName\x121\n" + "\apayload\x18\x03 \x01(\v2\x17.google.protobuf.StructR\apayload\"$\n" + "\"PublishEventToInstallationResponse\"\x14\n" + - "\x12GetHostInfoRequest\"\x9c\x01\n" + + "\x12GetHostInfoRequest\"\xd3\x02\n" + "\x13GetHostInfoResponse\x12&\n" + "\x0fpublic_base_url\x18\x01 \x01(\tR\rpublicBaseUrl\x12*\n" + "\x11internal_base_url\x18\x02 \x01(\tR\x0finternalBaseUrl\x121\n" + - "\x15plugin_proxy_base_url\x18\x03 \x01(\tR\x12pluginProxyBaseUrl\"/\n" + + "\x15plugin_proxy_base_url\x18\x03 \x01(\tR\x12pluginProxyBaseUrl\x12\x1b\n" + + "\thost_role\x18\x04 \x01(\tR\bhostRole\x12\x1b\n" + + "\thost_name\x18\x05 \x01(\tR\bhostName\x12\x17\n" + + "\anode_id\x18\x06 \x01(\x03R\x06nodeId\x12#\n" + + "\ringress_token\x18\a \x01(\tR\fingressToken\x12=\n" + + "\tlisteners\x18\b \x03(\v2\x1f.prairie.plugin.v1.HostListenerR\tlisteners\"_\n" + + "\fHostListener\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12\x18\n" + + "\aaddress\x18\x02 \x01(\tR\aaddress\x12!\n" + + "\fdefault_port\x18\x03 \x01(\x05R\vdefaultPort\"/\n" + "\x14ListLibrariesRequest\x12\x17\n" + "\auser_id\x18\x01 \x01(\tR\x06userId\"Q\n" + "\x15ListLibrariesResponse\x128\n" + @@ -2149,7 +2547,19 @@ const file_prairie_plugin_v1_runtime_host_proto_rawDesc = "" + "\x04body\x18\x03 \x01(\fR\x04body\x1a:\n" + "\fHeadersEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + - "\x05value\x18\x02 \x01(\tR\x05value:\x028\x012\xb6\v\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\",\n" + + "\x18ReadInstanceStateRequest\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\"G\n" + + "\x19ReadInstanceStateResponse\x12\x14\n" + + "\x05value\x18\x01 \x01(\fR\x05value\x12\x14\n" + + "\x05found\x18\x02 \x01(\bR\x05found\"C\n" + + "\x19WriteInstanceStateRequest\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\fR\x05value\"\x1c\n" + + "\x1aWriteInstanceStateResponse\"b\n" + + " ReportNetworkAccessStatusRequest\x12>\n" + + "\x06status\x18\x01 \x01(\v2&.prairie.plugin.v1.NetworkAccessStatusR\x06status\"#\n" + + "!ReportNetworkAccessStatusResponse2\xa2\x0e\n" + "\vRuntimeHost\x12_\n" + "\fPublishEvent\x12&.prairie.plugin.v1.PublishEventRequest\x1a'.prairie.plugin.v1.PublishEventResponse\x12e\n" + "\x0ePublishEventTo\x12(.prairie.plugin.v1.PublishEventToRequest\x1a).prairie.plugin.v1.PublishEventToResponse\x12\x89\x01\n" + @@ -2163,7 +2573,10 @@ const file_prairie_plugin_v1_runtime_host_proto_rawDesc = "" + "\x0fGetCatalogStats\x12).prairie.plugin.v1.GetCatalogStatsRequest\x1a*.prairie.plugin.v1.GetCatalogStatsResponse\x12\x80\x01\n" + "\x17ResolveCatalogImageURLs\x121.prairie.plugin.v1.ResolveCatalogImageURLsRequest\x1a2.prairie.plugin.v1.ResolveCatalogImageURLsResponse\x12k\n" + "\x10MintScopedStream\x12*.prairie.plugin.v1.MintScopedStreamRequest\x1a+.prairie.plugin.v1.MintScopedStreamResponse\x12e\n" + - "\x0eCallPluginHTTP\x12(.prairie.plugin.v1.CallPluginHTTPRequest\x1a).prairie.plugin.v1.CallPluginHTTPResponseBYZWgithub.com/prairie-server/prairie-plugin-sdk/pkg/pluginproto/prairie/plugin/v1;pluginv1b\x06proto3" + "\x0eCallPluginHTTP\x12(.prairie.plugin.v1.CallPluginHTTPRequest\x1a).prairie.plugin.v1.CallPluginHTTPResponse\x12n\n" + + "\x11ReadInstanceState\x12+.prairie.plugin.v1.ReadInstanceStateRequest\x1a,.prairie.plugin.v1.ReadInstanceStateResponse\x12q\n" + + "\x12WriteInstanceState\x12,.prairie.plugin.v1.WriteInstanceStateRequest\x1a-.prairie.plugin.v1.WriteInstanceStateResponse\x12\x86\x01\n" + + "\x19ReportNetworkAccessStatus\x123.prairie.plugin.v1.ReportNetworkAccessStatusRequest\x1a4.prairie.plugin.v1.ReportNetworkAccessStatusResponseBYZWgithub.com/prairie-server/prairie-plugin-sdk/pkg/pluginproto/prairie/plugin/v1;pluginv1b\x06proto3" var ( file_prairie_plugin_v1_runtime_host_proto_rawDescOnce sync.Once @@ -2177,7 +2590,7 @@ func file_prairie_plugin_v1_runtime_host_proto_rawDescGZIP() []byte { return file_prairie_plugin_v1_runtime_host_proto_rawDescData } -var file_prairie_plugin_v1_runtime_host_proto_msgTypes = make([]protoimpl.MessageInfo, 35) +var file_prairie_plugin_v1_runtime_host_proto_msgTypes = make([]protoimpl.MessageInfo, 42) var file_prairie_plugin_v1_runtime_host_proto_goTypes = []any{ (*PublishEventRequest)(nil), // 0: prairie.plugin.v1.PublishEventRequest (*PublishEventResponse)(nil), // 1: prairie.plugin.v1.PublishEventResponse @@ -2187,83 +2600,99 @@ var file_prairie_plugin_v1_runtime_host_proto_goTypes = []any{ (*PublishEventToInstallationResponse)(nil), // 5: prairie.plugin.v1.PublishEventToInstallationResponse (*GetHostInfoRequest)(nil), // 6: prairie.plugin.v1.GetHostInfoRequest (*GetHostInfoResponse)(nil), // 7: prairie.plugin.v1.GetHostInfoResponse - (*ListLibrariesRequest)(nil), // 8: prairie.plugin.v1.ListLibrariesRequest - (*ListLibrariesResponse)(nil), // 9: prairie.plugin.v1.ListLibrariesResponse - (*Library)(nil), // 10: prairie.plugin.v1.Library - (*CheckMediaPresenceRequest)(nil), // 11: prairie.plugin.v1.CheckMediaPresenceRequest - (*CheckMediaPresenceResponse)(nil), // 12: prairie.plugin.v1.CheckMediaPresenceResponse - (*MediaPresence)(nil), // 13: prairie.plugin.v1.MediaPresence - (*ListInstalledPluginsRequest)(nil), // 14: prairie.plugin.v1.ListInstalledPluginsRequest - (*ListInstalledPluginsResponse)(nil), // 15: prairie.plugin.v1.ListInstalledPluginsResponse - (*InstalledPlugin)(nil), // 16: prairie.plugin.v1.InstalledPlugin - (*SetGlobalConfigEntryRequest)(nil), // 17: prairie.plugin.v1.SetGlobalConfigEntryRequest - (*SetGlobalConfigEntryResponse)(nil), // 18: prairie.plugin.v1.SetGlobalConfigEntryResponse - (*ListLibraryMediaRequest)(nil), // 19: prairie.plugin.v1.ListLibraryMediaRequest - (*ListLibraryMediaResponse)(nil), // 20: prairie.plugin.v1.ListLibraryMediaResponse - (*CatalogMediaItem)(nil), // 21: prairie.plugin.v1.CatalogMediaItem - (*GetCatalogStatsRequest)(nil), // 22: prairie.plugin.v1.GetCatalogStatsRequest - (*GetCatalogStatsResponse)(nil), // 23: prairie.plugin.v1.GetCatalogStatsResponse - (*CatalogTypeCount)(nil), // 24: prairie.plugin.v1.CatalogTypeCount - (*CatalogLibraryCount)(nil), // 25: prairie.plugin.v1.CatalogLibraryCount - (*ResolveCatalogImageURLsRequest)(nil), // 26: prairie.plugin.v1.ResolveCatalogImageURLsRequest - (*ResolveCatalogImageURLsResponse)(nil), // 27: prairie.plugin.v1.ResolveCatalogImageURLsResponse - (*MintScopedStreamRequest)(nil), // 28: prairie.plugin.v1.MintScopedStreamRequest - (*MintScopedStreamResponse)(nil), // 29: prairie.plugin.v1.MintScopedStreamResponse - (*CallPluginHTTPRequest)(nil), // 30: prairie.plugin.v1.CallPluginHTTPRequest - (*CallPluginHTTPResponse)(nil), // 31: prairie.plugin.v1.CallPluginHTTPResponse - nil, // 32: prairie.plugin.v1.ResolveCatalogImageURLsResponse.UrlsEntry - nil, // 33: prairie.plugin.v1.CallPluginHTTPRequest.HeadersEntry - nil, // 34: prairie.plugin.v1.CallPluginHTTPResponse.HeadersEntry - (*structpb.Struct)(nil), // 35: google.protobuf.Struct - (*CapabilityDescriptor)(nil), // 36: prairie.plugin.v1.CapabilityDescriptor + (*HostListener)(nil), // 8: prairie.plugin.v1.HostListener + (*ListLibrariesRequest)(nil), // 9: prairie.plugin.v1.ListLibrariesRequest + (*ListLibrariesResponse)(nil), // 10: prairie.plugin.v1.ListLibrariesResponse + (*Library)(nil), // 11: prairie.plugin.v1.Library + (*CheckMediaPresenceRequest)(nil), // 12: prairie.plugin.v1.CheckMediaPresenceRequest + (*CheckMediaPresenceResponse)(nil), // 13: prairie.plugin.v1.CheckMediaPresenceResponse + (*MediaPresence)(nil), // 14: prairie.plugin.v1.MediaPresence + (*ListInstalledPluginsRequest)(nil), // 15: prairie.plugin.v1.ListInstalledPluginsRequest + (*ListInstalledPluginsResponse)(nil), // 16: prairie.plugin.v1.ListInstalledPluginsResponse + (*InstalledPlugin)(nil), // 17: prairie.plugin.v1.InstalledPlugin + (*SetGlobalConfigEntryRequest)(nil), // 18: prairie.plugin.v1.SetGlobalConfigEntryRequest + (*SetGlobalConfigEntryResponse)(nil), // 19: prairie.plugin.v1.SetGlobalConfigEntryResponse + (*ListLibraryMediaRequest)(nil), // 20: prairie.plugin.v1.ListLibraryMediaRequest + (*ListLibraryMediaResponse)(nil), // 21: prairie.plugin.v1.ListLibraryMediaResponse + (*CatalogMediaItem)(nil), // 22: prairie.plugin.v1.CatalogMediaItem + (*GetCatalogStatsRequest)(nil), // 23: prairie.plugin.v1.GetCatalogStatsRequest + (*GetCatalogStatsResponse)(nil), // 24: prairie.plugin.v1.GetCatalogStatsResponse + (*CatalogTypeCount)(nil), // 25: prairie.plugin.v1.CatalogTypeCount + (*CatalogLibraryCount)(nil), // 26: prairie.plugin.v1.CatalogLibraryCount + (*ResolveCatalogImageURLsRequest)(nil), // 27: prairie.plugin.v1.ResolveCatalogImageURLsRequest + (*ResolveCatalogImageURLsResponse)(nil), // 28: prairie.plugin.v1.ResolveCatalogImageURLsResponse + (*MintScopedStreamRequest)(nil), // 29: prairie.plugin.v1.MintScopedStreamRequest + (*MintScopedStreamResponse)(nil), // 30: prairie.plugin.v1.MintScopedStreamResponse + (*CallPluginHTTPRequest)(nil), // 31: prairie.plugin.v1.CallPluginHTTPRequest + (*CallPluginHTTPResponse)(nil), // 32: prairie.plugin.v1.CallPluginHTTPResponse + (*ReadInstanceStateRequest)(nil), // 33: prairie.plugin.v1.ReadInstanceStateRequest + (*ReadInstanceStateResponse)(nil), // 34: prairie.plugin.v1.ReadInstanceStateResponse + (*WriteInstanceStateRequest)(nil), // 35: prairie.plugin.v1.WriteInstanceStateRequest + (*WriteInstanceStateResponse)(nil), // 36: prairie.plugin.v1.WriteInstanceStateResponse + (*ReportNetworkAccessStatusRequest)(nil), // 37: prairie.plugin.v1.ReportNetworkAccessStatusRequest + (*ReportNetworkAccessStatusResponse)(nil), // 38: prairie.plugin.v1.ReportNetworkAccessStatusResponse + nil, // 39: prairie.plugin.v1.ResolveCatalogImageURLsResponse.UrlsEntry + nil, // 40: prairie.plugin.v1.CallPluginHTTPRequest.HeadersEntry + nil, // 41: prairie.plugin.v1.CallPluginHTTPResponse.HeadersEntry + (*structpb.Struct)(nil), // 42: google.protobuf.Struct + (*CapabilityDescriptor)(nil), // 43: prairie.plugin.v1.CapabilityDescriptor + (*NetworkAccessStatus)(nil), // 44: prairie.plugin.v1.NetworkAccessStatus } var file_prairie_plugin_v1_runtime_host_proto_depIdxs = []int32{ - 35, // 0: prairie.plugin.v1.PublishEventRequest.payload:type_name -> google.protobuf.Struct - 35, // 1: prairie.plugin.v1.PublishEventToRequest.payload:type_name -> google.protobuf.Struct - 35, // 2: prairie.plugin.v1.PublishEventToInstallationRequest.payload:type_name -> google.protobuf.Struct - 10, // 3: prairie.plugin.v1.ListLibrariesResponse.libraries:type_name -> prairie.plugin.v1.Library - 13, // 4: prairie.plugin.v1.CheckMediaPresenceResponse.present:type_name -> prairie.plugin.v1.MediaPresence - 16, // 5: prairie.plugin.v1.ListInstalledPluginsResponse.plugins:type_name -> prairie.plugin.v1.InstalledPlugin - 36, // 6: prairie.plugin.v1.InstalledPlugin.capabilities:type_name -> prairie.plugin.v1.CapabilityDescriptor - 35, // 7: prairie.plugin.v1.SetGlobalConfigEntryRequest.value:type_name -> google.protobuf.Struct - 21, // 8: prairie.plugin.v1.ListLibraryMediaResponse.items:type_name -> prairie.plugin.v1.CatalogMediaItem - 24, // 9: prairie.plugin.v1.GetCatalogStatsResponse.media_type_counts:type_name -> prairie.plugin.v1.CatalogTypeCount - 25, // 10: prairie.plugin.v1.GetCatalogStatsResponse.library_counts:type_name -> prairie.plugin.v1.CatalogLibraryCount - 32, // 11: prairie.plugin.v1.ResolveCatalogImageURLsResponse.urls:type_name -> prairie.plugin.v1.ResolveCatalogImageURLsResponse.UrlsEntry - 33, // 12: prairie.plugin.v1.CallPluginHTTPRequest.headers:type_name -> prairie.plugin.v1.CallPluginHTTPRequest.HeadersEntry - 35, // 13: prairie.plugin.v1.CallPluginHTTPRequest.query:type_name -> google.protobuf.Struct - 34, // 14: prairie.plugin.v1.CallPluginHTTPResponse.headers:type_name -> prairie.plugin.v1.CallPluginHTTPResponse.HeadersEntry - 0, // 15: prairie.plugin.v1.RuntimeHost.PublishEvent:input_type -> prairie.plugin.v1.PublishEventRequest - 2, // 16: prairie.plugin.v1.RuntimeHost.PublishEventTo:input_type -> prairie.plugin.v1.PublishEventToRequest - 4, // 17: prairie.plugin.v1.RuntimeHost.PublishEventToInstallation:input_type -> prairie.plugin.v1.PublishEventToInstallationRequest - 6, // 18: prairie.plugin.v1.RuntimeHost.GetHostInfo:input_type -> prairie.plugin.v1.GetHostInfoRequest - 8, // 19: prairie.plugin.v1.RuntimeHost.ListLibraries:input_type -> prairie.plugin.v1.ListLibrariesRequest - 11, // 20: prairie.plugin.v1.RuntimeHost.CheckMediaPresence:input_type -> prairie.plugin.v1.CheckMediaPresenceRequest - 14, // 21: prairie.plugin.v1.RuntimeHost.ListInstalledPlugins:input_type -> prairie.plugin.v1.ListInstalledPluginsRequest - 17, // 22: prairie.plugin.v1.RuntimeHost.SetGlobalConfigEntry:input_type -> prairie.plugin.v1.SetGlobalConfigEntryRequest - 19, // 23: prairie.plugin.v1.RuntimeHost.ListLibraryMedia:input_type -> prairie.plugin.v1.ListLibraryMediaRequest - 22, // 24: prairie.plugin.v1.RuntimeHost.GetCatalogStats:input_type -> prairie.plugin.v1.GetCatalogStatsRequest - 26, // 25: prairie.plugin.v1.RuntimeHost.ResolveCatalogImageURLs:input_type -> prairie.plugin.v1.ResolveCatalogImageURLsRequest - 28, // 26: prairie.plugin.v1.RuntimeHost.MintScopedStream:input_type -> prairie.plugin.v1.MintScopedStreamRequest - 30, // 27: prairie.plugin.v1.RuntimeHost.CallPluginHTTP:input_type -> prairie.plugin.v1.CallPluginHTTPRequest - 1, // 28: prairie.plugin.v1.RuntimeHost.PublishEvent:output_type -> prairie.plugin.v1.PublishEventResponse - 3, // 29: prairie.plugin.v1.RuntimeHost.PublishEventTo:output_type -> prairie.plugin.v1.PublishEventToResponse - 5, // 30: prairie.plugin.v1.RuntimeHost.PublishEventToInstallation:output_type -> prairie.plugin.v1.PublishEventToInstallationResponse - 7, // 31: prairie.plugin.v1.RuntimeHost.GetHostInfo:output_type -> prairie.plugin.v1.GetHostInfoResponse - 9, // 32: prairie.plugin.v1.RuntimeHost.ListLibraries:output_type -> prairie.plugin.v1.ListLibrariesResponse - 12, // 33: prairie.plugin.v1.RuntimeHost.CheckMediaPresence:output_type -> prairie.plugin.v1.CheckMediaPresenceResponse - 15, // 34: prairie.plugin.v1.RuntimeHost.ListInstalledPlugins:output_type -> prairie.plugin.v1.ListInstalledPluginsResponse - 18, // 35: prairie.plugin.v1.RuntimeHost.SetGlobalConfigEntry:output_type -> prairie.plugin.v1.SetGlobalConfigEntryResponse - 20, // 36: prairie.plugin.v1.RuntimeHost.ListLibraryMedia:output_type -> prairie.plugin.v1.ListLibraryMediaResponse - 23, // 37: prairie.plugin.v1.RuntimeHost.GetCatalogStats:output_type -> prairie.plugin.v1.GetCatalogStatsResponse - 27, // 38: prairie.plugin.v1.RuntimeHost.ResolveCatalogImageURLs:output_type -> prairie.plugin.v1.ResolveCatalogImageURLsResponse - 29, // 39: prairie.plugin.v1.RuntimeHost.MintScopedStream:output_type -> prairie.plugin.v1.MintScopedStreamResponse - 31, // 40: prairie.plugin.v1.RuntimeHost.CallPluginHTTP:output_type -> prairie.plugin.v1.CallPluginHTTPResponse - 28, // [28:41] is the sub-list for method output_type - 15, // [15:28] is the sub-list for method input_type - 15, // [15:15] is the sub-list for extension type_name - 15, // [15:15] is the sub-list for extension extendee - 0, // [0:15] is the sub-list for field type_name + 42, // 0: prairie.plugin.v1.PublishEventRequest.payload:type_name -> google.protobuf.Struct + 42, // 1: prairie.plugin.v1.PublishEventToRequest.payload:type_name -> google.protobuf.Struct + 42, // 2: prairie.plugin.v1.PublishEventToInstallationRequest.payload:type_name -> google.protobuf.Struct + 8, // 3: prairie.plugin.v1.GetHostInfoResponse.listeners:type_name -> prairie.plugin.v1.HostListener + 11, // 4: prairie.plugin.v1.ListLibrariesResponse.libraries:type_name -> prairie.plugin.v1.Library + 14, // 5: prairie.plugin.v1.CheckMediaPresenceResponse.present:type_name -> prairie.plugin.v1.MediaPresence + 17, // 6: prairie.plugin.v1.ListInstalledPluginsResponse.plugins:type_name -> prairie.plugin.v1.InstalledPlugin + 43, // 7: prairie.plugin.v1.InstalledPlugin.capabilities:type_name -> prairie.plugin.v1.CapabilityDescriptor + 42, // 8: prairie.plugin.v1.SetGlobalConfigEntryRequest.value:type_name -> google.protobuf.Struct + 22, // 9: prairie.plugin.v1.ListLibraryMediaResponse.items:type_name -> prairie.plugin.v1.CatalogMediaItem + 25, // 10: prairie.plugin.v1.GetCatalogStatsResponse.media_type_counts:type_name -> prairie.plugin.v1.CatalogTypeCount + 26, // 11: prairie.plugin.v1.GetCatalogStatsResponse.library_counts:type_name -> prairie.plugin.v1.CatalogLibraryCount + 39, // 12: prairie.plugin.v1.ResolveCatalogImageURLsResponse.urls:type_name -> prairie.plugin.v1.ResolveCatalogImageURLsResponse.UrlsEntry + 40, // 13: prairie.plugin.v1.CallPluginHTTPRequest.headers:type_name -> prairie.plugin.v1.CallPluginHTTPRequest.HeadersEntry + 42, // 14: prairie.plugin.v1.CallPluginHTTPRequest.query:type_name -> google.protobuf.Struct + 41, // 15: prairie.plugin.v1.CallPluginHTTPResponse.headers:type_name -> prairie.plugin.v1.CallPluginHTTPResponse.HeadersEntry + 44, // 16: prairie.plugin.v1.ReportNetworkAccessStatusRequest.status:type_name -> prairie.plugin.v1.NetworkAccessStatus + 0, // 17: prairie.plugin.v1.RuntimeHost.PublishEvent:input_type -> prairie.plugin.v1.PublishEventRequest + 2, // 18: prairie.plugin.v1.RuntimeHost.PublishEventTo:input_type -> prairie.plugin.v1.PublishEventToRequest + 4, // 19: prairie.plugin.v1.RuntimeHost.PublishEventToInstallation:input_type -> prairie.plugin.v1.PublishEventToInstallationRequest + 6, // 20: prairie.plugin.v1.RuntimeHost.GetHostInfo:input_type -> prairie.plugin.v1.GetHostInfoRequest + 9, // 21: prairie.plugin.v1.RuntimeHost.ListLibraries:input_type -> prairie.plugin.v1.ListLibrariesRequest + 12, // 22: prairie.plugin.v1.RuntimeHost.CheckMediaPresence:input_type -> prairie.plugin.v1.CheckMediaPresenceRequest + 15, // 23: prairie.plugin.v1.RuntimeHost.ListInstalledPlugins:input_type -> prairie.plugin.v1.ListInstalledPluginsRequest + 18, // 24: prairie.plugin.v1.RuntimeHost.SetGlobalConfigEntry:input_type -> prairie.plugin.v1.SetGlobalConfigEntryRequest + 20, // 25: prairie.plugin.v1.RuntimeHost.ListLibraryMedia:input_type -> prairie.plugin.v1.ListLibraryMediaRequest + 23, // 26: prairie.plugin.v1.RuntimeHost.GetCatalogStats:input_type -> prairie.plugin.v1.GetCatalogStatsRequest + 27, // 27: prairie.plugin.v1.RuntimeHost.ResolveCatalogImageURLs:input_type -> prairie.plugin.v1.ResolveCatalogImageURLsRequest + 29, // 28: prairie.plugin.v1.RuntimeHost.MintScopedStream:input_type -> prairie.plugin.v1.MintScopedStreamRequest + 31, // 29: prairie.plugin.v1.RuntimeHost.CallPluginHTTP:input_type -> prairie.plugin.v1.CallPluginHTTPRequest + 33, // 30: prairie.plugin.v1.RuntimeHost.ReadInstanceState:input_type -> prairie.plugin.v1.ReadInstanceStateRequest + 35, // 31: prairie.plugin.v1.RuntimeHost.WriteInstanceState:input_type -> prairie.plugin.v1.WriteInstanceStateRequest + 37, // 32: prairie.plugin.v1.RuntimeHost.ReportNetworkAccessStatus:input_type -> prairie.plugin.v1.ReportNetworkAccessStatusRequest + 1, // 33: prairie.plugin.v1.RuntimeHost.PublishEvent:output_type -> prairie.plugin.v1.PublishEventResponse + 3, // 34: prairie.plugin.v1.RuntimeHost.PublishEventTo:output_type -> prairie.plugin.v1.PublishEventToResponse + 5, // 35: prairie.plugin.v1.RuntimeHost.PublishEventToInstallation:output_type -> prairie.plugin.v1.PublishEventToInstallationResponse + 7, // 36: prairie.plugin.v1.RuntimeHost.GetHostInfo:output_type -> prairie.plugin.v1.GetHostInfoResponse + 10, // 37: prairie.plugin.v1.RuntimeHost.ListLibraries:output_type -> prairie.plugin.v1.ListLibrariesResponse + 13, // 38: prairie.plugin.v1.RuntimeHost.CheckMediaPresence:output_type -> prairie.plugin.v1.CheckMediaPresenceResponse + 16, // 39: prairie.plugin.v1.RuntimeHost.ListInstalledPlugins:output_type -> prairie.plugin.v1.ListInstalledPluginsResponse + 19, // 40: prairie.plugin.v1.RuntimeHost.SetGlobalConfigEntry:output_type -> prairie.plugin.v1.SetGlobalConfigEntryResponse + 21, // 41: prairie.plugin.v1.RuntimeHost.ListLibraryMedia:output_type -> prairie.plugin.v1.ListLibraryMediaResponse + 24, // 42: prairie.plugin.v1.RuntimeHost.GetCatalogStats:output_type -> prairie.plugin.v1.GetCatalogStatsResponse + 28, // 43: prairie.plugin.v1.RuntimeHost.ResolveCatalogImageURLs:output_type -> prairie.plugin.v1.ResolveCatalogImageURLsResponse + 30, // 44: prairie.plugin.v1.RuntimeHost.MintScopedStream:output_type -> prairie.plugin.v1.MintScopedStreamResponse + 32, // 45: prairie.plugin.v1.RuntimeHost.CallPluginHTTP:output_type -> prairie.plugin.v1.CallPluginHTTPResponse + 34, // 46: prairie.plugin.v1.RuntimeHost.ReadInstanceState:output_type -> prairie.plugin.v1.ReadInstanceStateResponse + 36, // 47: prairie.plugin.v1.RuntimeHost.WriteInstanceState:output_type -> prairie.plugin.v1.WriteInstanceStateResponse + 38, // 48: prairie.plugin.v1.RuntimeHost.ReportNetworkAccessStatus:output_type -> prairie.plugin.v1.ReportNetworkAccessStatusResponse + 33, // [33:49] is the sub-list for method output_type + 17, // [17:33] is the sub-list for method input_type + 17, // [17:17] is the sub-list for extension type_name + 17, // [17:17] is the sub-list for extension extendee + 0, // [0:17] is the sub-list for field type_name } func init() { file_prairie_plugin_v1_runtime_host_proto_init() } @@ -2272,13 +2701,14 @@ func file_prairie_plugin_v1_runtime_host_proto_init() { return } file_prairie_plugin_v1_common_proto_init() + file_prairie_plugin_v1_network_access_provider_proto_init() type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_prairie_plugin_v1_runtime_host_proto_rawDesc), len(file_prairie_plugin_v1_runtime_host_proto_rawDesc)), NumEnums: 0, - NumMessages: 35, + NumMessages: 42, NumExtensions: 0, NumServices: 1, }, diff --git a/pkg/pluginproto/prairie/plugin/v1/runtime_host_grpc.pb.go b/pkg/pluginproto/prairie/plugin/v1/runtime_host_grpc.pb.go index 7af018d..832b95e 100644 --- a/pkg/pluginproto/prairie/plugin/v1/runtime_host_grpc.pb.go +++ b/pkg/pluginproto/prairie/plugin/v1/runtime_host_grpc.pb.go @@ -32,6 +32,9 @@ const ( RuntimeHost_ResolveCatalogImageURLs_FullMethodName = "/prairie.plugin.v1.RuntimeHost/ResolveCatalogImageURLs" RuntimeHost_MintScopedStream_FullMethodName = "/prairie.plugin.v1.RuntimeHost/MintScopedStream" RuntimeHost_CallPluginHTTP_FullMethodName = "/prairie.plugin.v1.RuntimeHost/CallPluginHTTP" + RuntimeHost_ReadInstanceState_FullMethodName = "/prairie.plugin.v1.RuntimeHost/ReadInstanceState" + RuntimeHost_WriteInstanceState_FullMethodName = "/prairie.plugin.v1.RuntimeHost/WriteInstanceState" + RuntimeHost_ReportNetworkAccessStatus_FullMethodName = "/prairie.plugin.v1.RuntimeHost/ReportNetworkAccessStatus" ) // RuntimeHostClient is the client API for RuntimeHost service. @@ -83,6 +86,18 @@ type RuntimeHostClient interface { // through the host control plane. Calls are treated as authenticated // service-to-service traffic, but never as admin traffic. CallPluginHTTP(ctx context.Context, in *CallPluginHTTPRequest, opts ...grpc.CallOption) (*CallPluginHTTPResponse, error) + // ReadInstanceState reads one key of the calling plugin instance's private + // state. The scope (installation plus host) is derived by the host and never + // supplied by the plugin. See docs/network-access-provider.md. + ReadInstanceState(ctx context.Context, in *ReadInstanceStateRequest, opts ...grpc.CallOption) (*ReadInstanceStateResponse, error) + // WriteInstanceState writes one key of the calling plugin instance's private + // state. The host stores values encrypted and never returns them through + // any API. Limits: key <= 256 bytes, value <= 256 KiB, <= 256 keys per scope. + WriteInstanceState(ctx context.Context, in *WriteInstanceStateRequest, opts ...grpc.CallOption) (*WriteInstanceStateResponse, error) + // ReportNetworkAccessStatus pushes a network_access_provider.v1 status + // change to the host so it does not have to poll. Plugins call it on every + // state transition; the host may still call GetStatus on demand. + ReportNetworkAccessStatus(ctx context.Context, in *ReportNetworkAccessStatusRequest, opts ...grpc.CallOption) (*ReportNetworkAccessStatusResponse, error) } type runtimeHostClient struct { @@ -223,6 +238,36 @@ func (c *runtimeHostClient) CallPluginHTTP(ctx context.Context, in *CallPluginHT return out, nil } +func (c *runtimeHostClient) ReadInstanceState(ctx context.Context, in *ReadInstanceStateRequest, opts ...grpc.CallOption) (*ReadInstanceStateResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ReadInstanceStateResponse) + err := c.cc.Invoke(ctx, RuntimeHost_ReadInstanceState_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *runtimeHostClient) WriteInstanceState(ctx context.Context, in *WriteInstanceStateRequest, opts ...grpc.CallOption) (*WriteInstanceStateResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(WriteInstanceStateResponse) + err := c.cc.Invoke(ctx, RuntimeHost_WriteInstanceState_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *runtimeHostClient) ReportNetworkAccessStatus(ctx context.Context, in *ReportNetworkAccessStatusRequest, opts ...grpc.CallOption) (*ReportNetworkAccessStatusResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ReportNetworkAccessStatusResponse) + err := c.cc.Invoke(ctx, RuntimeHost_ReportNetworkAccessStatus_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + // RuntimeHostServer is the server API for RuntimeHost service. // All implementations should embed UnimplementedRuntimeHostServer // for forward compatibility. @@ -272,6 +317,18 @@ type RuntimeHostServer interface { // through the host control plane. Calls are treated as authenticated // service-to-service traffic, but never as admin traffic. CallPluginHTTP(context.Context, *CallPluginHTTPRequest) (*CallPluginHTTPResponse, error) + // ReadInstanceState reads one key of the calling plugin instance's private + // state. The scope (installation plus host) is derived by the host and never + // supplied by the plugin. See docs/network-access-provider.md. + ReadInstanceState(context.Context, *ReadInstanceStateRequest) (*ReadInstanceStateResponse, error) + // WriteInstanceState writes one key of the calling plugin instance's private + // state. The host stores values encrypted and never returns them through + // any API. Limits: key <= 256 bytes, value <= 256 KiB, <= 256 keys per scope. + WriteInstanceState(context.Context, *WriteInstanceStateRequest) (*WriteInstanceStateResponse, error) + // ReportNetworkAccessStatus pushes a network_access_provider.v1 status + // change to the host so it does not have to poll. Plugins call it on every + // state transition; the host may still call GetStatus on demand. + ReportNetworkAccessStatus(context.Context, *ReportNetworkAccessStatusRequest) (*ReportNetworkAccessStatusResponse, error) } // UnimplementedRuntimeHostServer should be embedded to have @@ -320,6 +377,15 @@ func (UnimplementedRuntimeHostServer) MintScopedStream(context.Context, *MintSco func (UnimplementedRuntimeHostServer) CallPluginHTTP(context.Context, *CallPluginHTTPRequest) (*CallPluginHTTPResponse, error) { return nil, status.Error(codes.Unimplemented, "method CallPluginHTTP not implemented") } +func (UnimplementedRuntimeHostServer) ReadInstanceState(context.Context, *ReadInstanceStateRequest) (*ReadInstanceStateResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ReadInstanceState not implemented") +} +func (UnimplementedRuntimeHostServer) WriteInstanceState(context.Context, *WriteInstanceStateRequest) (*WriteInstanceStateResponse, error) { + return nil, status.Error(codes.Unimplemented, "method WriteInstanceState not implemented") +} +func (UnimplementedRuntimeHostServer) ReportNetworkAccessStatus(context.Context, *ReportNetworkAccessStatusRequest) (*ReportNetworkAccessStatusResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ReportNetworkAccessStatus not implemented") +} func (UnimplementedRuntimeHostServer) testEmbeddedByValue() {} // UnsafeRuntimeHostServer may be embedded to opt out of forward compatibility for this service. @@ -574,6 +640,60 @@ func _RuntimeHost_CallPluginHTTP_Handler(srv interface{}, ctx context.Context, d return interceptor(ctx, in, info, handler) } +func _RuntimeHost_ReadInstanceState_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ReadInstanceStateRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(RuntimeHostServer).ReadInstanceState(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: RuntimeHost_ReadInstanceState_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(RuntimeHostServer).ReadInstanceState(ctx, req.(*ReadInstanceStateRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _RuntimeHost_WriteInstanceState_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(WriteInstanceStateRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(RuntimeHostServer).WriteInstanceState(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: RuntimeHost_WriteInstanceState_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(RuntimeHostServer).WriteInstanceState(ctx, req.(*WriteInstanceStateRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _RuntimeHost_ReportNetworkAccessStatus_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ReportNetworkAccessStatusRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(RuntimeHostServer).ReportNetworkAccessStatus(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: RuntimeHost_ReportNetworkAccessStatus_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(RuntimeHostServer).ReportNetworkAccessStatus(ctx, req.(*ReportNetworkAccessStatusRequest)) + } + return interceptor(ctx, in, info, handler) +} + // RuntimeHost_ServiceDesc is the grpc.ServiceDesc for RuntimeHost service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -633,6 +753,18 @@ var RuntimeHost_ServiceDesc = grpc.ServiceDesc{ MethodName: "CallPluginHTTP", Handler: _RuntimeHost_CallPluginHTTP_Handler, }, + { + MethodName: "ReadInstanceState", + Handler: _RuntimeHost_ReadInstanceState_Handler, + }, + { + MethodName: "WriteInstanceState", + Handler: _RuntimeHost_WriteInstanceState_Handler, + }, + { + MethodName: "ReportNetworkAccessStatus", + Handler: _RuntimeHost_ReportNetworkAccessStatus_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "prairie/plugin/v1/runtime_host.proto", diff --git a/pkg/pluginproto/prairie/plugin/v1/watch_sync_provider.pb.go b/pkg/pluginproto/prairie/plugin/v1/watch_sync_provider.pb.go index 6393175..e330800 100644 --- a/pkg/pluginproto/prairie/plugin/v1/watch_sync_provider.pb.go +++ b/pkg/pluginproto/prairie/plugin/v1/watch_sync_provider.pb.go @@ -82,6 +82,10 @@ const ( WatchSyncMediaType_WATCH_SYNC_MEDIA_TYPE_UNSPECIFIED WatchSyncMediaType = 0 WatchSyncMediaType_WATCH_SYNC_MEDIA_TYPE_MOVIE WatchSyncMediaType = 1 WatchSyncMediaType_WATCH_SYNC_MEDIA_TYPE_EPISODE WatchSyncMediaType = 2 + // A series-level item. In WatchSyncMedia, external_ids, title, and year + // describe the series itself; the series_* fields, season_number, + // episode_number, and absolute_episode_number are unused. + WatchSyncMediaType_WATCH_SYNC_MEDIA_TYPE_SERIES WatchSyncMediaType = 3 ) // Enum value maps for WatchSyncMediaType. @@ -90,11 +94,13 @@ var ( 0: "WATCH_SYNC_MEDIA_TYPE_UNSPECIFIED", 1: "WATCH_SYNC_MEDIA_TYPE_MOVIE", 2: "WATCH_SYNC_MEDIA_TYPE_EPISODE", + 3: "WATCH_SYNC_MEDIA_TYPE_SERIES", } WatchSyncMediaType_value = map[string]int32{ "WATCH_SYNC_MEDIA_TYPE_UNSPECIFIED": 0, "WATCH_SYNC_MEDIA_TYPE_MOVIE": 1, "WATCH_SYNC_MEDIA_TYPE_EPISODE": 2, + "WATCH_SYNC_MEDIA_TYPE_SERIES": 3, } ) @@ -138,21 +144,27 @@ const ( WatchSyncOperation_WATCH_SYNC_OPERATION_SCROBBLE_START WatchSyncOperation = 7 WatchSyncOperation_WATCH_SYNC_OPERATION_SCROBBLE_PAUSE WatchSyncOperation = 8 WatchSyncOperation_WATCH_SYNC_OPERATION_SCROBBLE_STOP WatchSyncOperation = 9 + // Rating writes carry the desired value in WatchSyncEvent.rating. Both are + // convergent desired-state operations; see WatchSyncEvent.rating. + WatchSyncOperation_WATCH_SYNC_OPERATION_SET_RATING WatchSyncOperation = 10 + WatchSyncOperation_WATCH_SYNC_OPERATION_REMOVE_RATING WatchSyncOperation = 11 ) // Enum value maps for WatchSyncOperation. var ( WatchSyncOperation_name = map[int32]string{ - 0: "WATCH_SYNC_OPERATION_UNSPECIFIED", - 1: "WATCH_SYNC_OPERATION_MARK_WATCHED", - 2: "WATCH_SYNC_OPERATION_MARK_UNWATCHED", - 3: "WATCH_SYNC_OPERATION_ADD_FAVORITE", - 4: "WATCH_SYNC_OPERATION_REMOVE_FAVORITE", - 5: "WATCH_SYNC_OPERATION_ADD_TO_WATCHLIST", - 6: "WATCH_SYNC_OPERATION_REMOVE_FROM_WATCHLIST", - 7: "WATCH_SYNC_OPERATION_SCROBBLE_START", - 8: "WATCH_SYNC_OPERATION_SCROBBLE_PAUSE", - 9: "WATCH_SYNC_OPERATION_SCROBBLE_STOP", + 0: "WATCH_SYNC_OPERATION_UNSPECIFIED", + 1: "WATCH_SYNC_OPERATION_MARK_WATCHED", + 2: "WATCH_SYNC_OPERATION_MARK_UNWATCHED", + 3: "WATCH_SYNC_OPERATION_ADD_FAVORITE", + 4: "WATCH_SYNC_OPERATION_REMOVE_FAVORITE", + 5: "WATCH_SYNC_OPERATION_ADD_TO_WATCHLIST", + 6: "WATCH_SYNC_OPERATION_REMOVE_FROM_WATCHLIST", + 7: "WATCH_SYNC_OPERATION_SCROBBLE_START", + 8: "WATCH_SYNC_OPERATION_SCROBBLE_PAUSE", + 9: "WATCH_SYNC_OPERATION_SCROBBLE_STOP", + 10: "WATCH_SYNC_OPERATION_SET_RATING", + 11: "WATCH_SYNC_OPERATION_REMOVE_RATING", } WatchSyncOperation_value = map[string]int32{ "WATCH_SYNC_OPERATION_UNSPECIFIED": 0, @@ -165,6 +177,8 @@ var ( "WATCH_SYNC_OPERATION_SCROBBLE_START": 7, "WATCH_SYNC_OPERATION_SCROBBLE_PAUSE": 8, "WATCH_SYNC_OPERATION_SCROBBLE_STOP": 9, + "WATCH_SYNC_OPERATION_SET_RATING": 10, + "WATCH_SYNC_OPERATION_REMOVE_RATING": 11, } ) @@ -203,6 +217,7 @@ const ( WatchSyncRemoteStateKind_WATCH_SYNC_REMOTE_STATE_KIND_PROGRESS WatchSyncRemoteStateKind = 2 WatchSyncRemoteStateKind_WATCH_SYNC_REMOTE_STATE_KIND_FAVORITE WatchSyncRemoteStateKind = 3 WatchSyncRemoteStateKind_WATCH_SYNC_REMOTE_STATE_KIND_WATCHLIST WatchSyncRemoteStateKind = 4 + WatchSyncRemoteStateKind_WATCH_SYNC_REMOTE_STATE_KIND_RATING WatchSyncRemoteStateKind = 5 ) // Enum value maps for WatchSyncRemoteStateKind. @@ -213,6 +228,7 @@ var ( 2: "WATCH_SYNC_REMOTE_STATE_KIND_PROGRESS", 3: "WATCH_SYNC_REMOTE_STATE_KIND_FAVORITE", 4: "WATCH_SYNC_REMOTE_STATE_KIND_WATCHLIST", + 5: "WATCH_SYNC_REMOTE_STATE_KIND_RATING", } WatchSyncRemoteStateKind_value = map[string]int32{ "WATCH_SYNC_REMOTE_STATE_KIND_UNSPECIFIED": 0, @@ -220,6 +236,7 @@ var ( "WATCH_SYNC_REMOTE_STATE_KIND_PROGRESS": 2, "WATCH_SYNC_REMOTE_STATE_KIND_FAVORITE": 3, "WATCH_SYNC_REMOTE_STATE_KIND_WATCHLIST": 4, + "WATCH_SYNC_REMOTE_STATE_KIND_RATING": 5, } ) @@ -494,8 +511,19 @@ type WatchSyncProviderDescriptor struct { // an incremental subset cannot define positions relative to omitted items. ProvidesWatchlistOrder bool `protobuf:"varint,15,opt,name=provides_watchlist_order,json=providesWatchlistOrder,proto3" json:"provides_watchlist_order,omitempty"` ScrobblePlayback bool `protobuf:"varint,16,opt,name=scrobble_playback,json=scrobblePlayback,proto3" json:"scrobble_playback,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Ratings are integers from 1 to 10. Plugins convert between the provider's + // native scale and 1..10 by rounding half up and clamping to the valid range. + // The host sends rating events only for media types listed in + // supported_media_types, which must include MOVIE or SERIES when either + // ratings flag is set. + // + // When import_ratings is set, ListRemoteState returns RATING states. + ImportRatings bool `protobuf:"varint,17,opt,name=import_ratings,json=importRatings,proto3" json:"import_ratings,omitempty"` + // When export_ratings is set, ApplyEvents handles both SET_RATING and + // REMOVE_RATING; there is no separate flag for rating removal. + ExportRatings bool `protobuf:"varint,18,opt,name=export_ratings,json=exportRatings,proto3" json:"export_ratings,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *WatchSyncProviderDescriptor) Reset() { @@ -640,6 +668,20 @@ func (x *WatchSyncProviderDescriptor) GetScrobblePlayback() bool { return false } +func (x *WatchSyncProviderDescriptor) GetImportRatings() bool { + if x != nil { + return x.ImportRatings + } + return false +} + +func (x *WatchSyncProviderDescriptor) GetExportRatings() bool { + if x != nil { + return x.ExportRatings + } + return false +} + type WatchSyncProviderConfig struct { state protoimpl.MessageState `protogen:"open.v1"` Values map[string]string `protobuf:"bytes,1,rep,name=values,proto3" json:"values,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` @@ -1953,7 +1995,13 @@ type WatchSyncEvent struct { // Authoritative host completion state for playback events. A history ID can // exist for incomplete playback, and completion_percent may be below 100 // when the host's configured watched threshold has been reached. - Completed bool `protobuf:"varint,14,opt,name=completed,proto3" json:"completed,omitempty"` + Completed bool `protobuf:"varint,14,opt,name=completed,proto3" json:"completed,omitempty"` + // Desired rating for SET_RATING, an integer from 1 to 10. Required for + // SET_RATING and ignored for every other operation; zero is never a valid + // rating. SET_RATING and REMOVE_RATING are convergent desired-state writes: + // resending the same value, or removing a rating that is already absent, + // must yield APPLIED or NO_CHANGE, never a fault. + Rating int32 `protobuf:"varint,15,opt,name=rating,proto3" json:"rating,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -2086,6 +2134,13 @@ func (x *WatchSyncEvent) GetCompleted() bool { return false } +func (x *WatchSyncEvent) GetRating() int32 { + if x != nil { + return x.Rating + } + return 0 +} + type WatchSyncApplyEventsRequest struct { state protoimpl.MessageState `protogen:"open.v1"` Context *WatchSyncAuthenticatedContext `protobuf:"bytes,1,opt,name=context,proto3" json:"context,omitempty"` @@ -2517,11 +2572,77 @@ func (x *WatchSyncRemoteListState) GetRemoved() bool { return false } +type WatchSyncRemoteRatingState struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Integer rating from 1 to 10, converted from the provider's native scale. + // Required unless removed is true. + Rating int32 `protobuf:"varint,1,opt,name=rating,proto3" json:"rating,omitempty"` + // Provider timestamp for when the rating was last set, when available. + RatedAt *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=rated_at,json=ratedAt,proto3" json:"rated_at,omitempty"` + // True is an explicit tombstone for a rating cleared upstream. It is + // meaningful for incremental traversals, where omitting an item is not a + // deletion. + Removed bool `protobuf:"varint,3,opt,name=removed,proto3" json:"removed,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *WatchSyncRemoteRatingState) Reset() { + *x = WatchSyncRemoteRatingState{} + mi := &file_prairie_plugin_v1_watch_sync_provider_proto_msgTypes[27] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *WatchSyncRemoteRatingState) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WatchSyncRemoteRatingState) ProtoMessage() {} + +func (x *WatchSyncRemoteRatingState) ProtoReflect() protoreflect.Message { + mi := &file_prairie_plugin_v1_watch_sync_provider_proto_msgTypes[27] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WatchSyncRemoteRatingState.ProtoReflect.Descriptor instead. +func (*WatchSyncRemoteRatingState) Descriptor() ([]byte, []int) { + return file_prairie_plugin_v1_watch_sync_provider_proto_rawDescGZIP(), []int{27} +} + +func (x *WatchSyncRemoteRatingState) GetRating() int32 { + if x != nil { + return x.Rating + } + return 0 +} + +func (x *WatchSyncRemoteRatingState) GetRatedAt() *timestamppb.Timestamp { + if x != nil { + return x.RatedAt + } + return nil +} + +func (x *WatchSyncRemoteRatingState) GetRemoved() bool { + if x != nil { + return x.Removed + } + return false +} + type WatchSyncRemoteState struct { state protoimpl.MessageState `protogen:"open.v1"` ProviderItemKey string `protobuf:"bytes,1,opt,name=provider_item_key,json=providerItemKey,proto3" json:"provider_item_key,omitempty"` - // Required except on an explicit favorite/watchlist tombstone whose - // provider_item_key identifies a record returned previously. + // Required except on an explicit favorite, watchlist, or rating tombstone + // whose provider_item_key identifies a record returned previously. Media *WatchSyncMedia `protobuf:"bytes,2,opt,name=media,proto3" json:"media,omitempty"` // At least one typed state must be present. Both may be present when the // provider reports a completed play and a separate resume point. @@ -2529,13 +2650,14 @@ type WatchSyncRemoteState struct { Progress *WatchSyncRemoteProgressState `protobuf:"bytes,4,opt,name=progress,proto3" json:"progress,omitempty"` Favorite *WatchSyncRemoteListState `protobuf:"bytes,5,opt,name=favorite,proto3" json:"favorite,omitempty"` Watchlist *WatchSyncRemoteListState `protobuf:"bytes,6,opt,name=watchlist,proto3" json:"watchlist,omitempty"` + Rating *WatchSyncRemoteRatingState `protobuf:"bytes,7,opt,name=rating,proto3" json:"rating,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *WatchSyncRemoteState) Reset() { *x = WatchSyncRemoteState{} - mi := &file_prairie_plugin_v1_watch_sync_provider_proto_msgTypes[27] + mi := &file_prairie_plugin_v1_watch_sync_provider_proto_msgTypes[28] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2547,7 +2669,7 @@ func (x *WatchSyncRemoteState) String() string { func (*WatchSyncRemoteState) ProtoMessage() {} func (x *WatchSyncRemoteState) ProtoReflect() protoreflect.Message { - mi := &file_prairie_plugin_v1_watch_sync_provider_proto_msgTypes[27] + mi := &file_prairie_plugin_v1_watch_sync_provider_proto_msgTypes[28] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2560,7 +2682,7 @@ func (x *WatchSyncRemoteState) ProtoReflect() protoreflect.Message { // Deprecated: Use WatchSyncRemoteState.ProtoReflect.Descriptor instead. func (*WatchSyncRemoteState) Descriptor() ([]byte, []int) { - return file_prairie_plugin_v1_watch_sync_provider_proto_rawDescGZIP(), []int{27} + return file_prairie_plugin_v1_watch_sync_provider_proto_rawDescGZIP(), []int{28} } func (x *WatchSyncRemoteState) GetProviderItemKey() string { @@ -2605,6 +2727,13 @@ func (x *WatchSyncRemoteState) GetWatchlist() *WatchSyncRemoteListState { return nil } +func (x *WatchSyncRemoteState) GetRating() *WatchSyncRemoteRatingState { + if x != nil { + return x.Rating + } + return nil +} + type WatchSyncListRemoteStateResponse struct { state protoimpl.MessageState `protogen:"open.v1"` Items []*WatchSyncRemoteState `protobuf:"bytes,1,rep,name=items,proto3" json:"items,omitempty"` @@ -2618,7 +2747,8 @@ type WatchSyncListRemoteStateResponse struct { // incremental delta, so an absent item must not be interpreted as deletion. // The value must remain stable across every page in one traversal. A provider // advertising provides_watchlist_order must set this true for WATCHLIST - // traversals so the returned order is unambiguous. + // traversals so the returned order is unambiguous. In a complete RATING + // traversal, an item absent from the snapshot is unrated. CompleteSnapshot bool `protobuf:"varint,4,opt,name=complete_snapshot,json=completeSnapshot,proto3" json:"complete_snapshot,omitempty"` // Complete authoritative replacement. The host persists it before consuming // this page or fault; a persistence failure discards the page and cursor. @@ -2633,7 +2763,7 @@ type WatchSyncListRemoteStateResponse struct { func (x *WatchSyncListRemoteStateResponse) Reset() { *x = WatchSyncListRemoteStateResponse{} - mi := &file_prairie_plugin_v1_watch_sync_provider_proto_msgTypes[28] + mi := &file_prairie_plugin_v1_watch_sync_provider_proto_msgTypes[29] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2645,7 +2775,7 @@ func (x *WatchSyncListRemoteStateResponse) String() string { func (*WatchSyncListRemoteStateResponse) ProtoMessage() {} func (x *WatchSyncListRemoteStateResponse) ProtoReflect() protoreflect.Message { - mi := &file_prairie_plugin_v1_watch_sync_provider_proto_msgTypes[28] + mi := &file_prairie_plugin_v1_watch_sync_provider_proto_msgTypes[29] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2658,7 +2788,7 @@ func (x *WatchSyncListRemoteStateResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use WatchSyncListRemoteStateResponse.ProtoReflect.Descriptor instead. func (*WatchSyncListRemoteStateResponse) Descriptor() ([]byte, []int) { - return file_prairie_plugin_v1_watch_sync_provider_proto_rawDescGZIP(), []int{28} + return file_prairie_plugin_v1_watch_sync_provider_proto_rawDescGZIP(), []int{29} } func (x *WatchSyncListRemoteStateResponse) GetItems() []*WatchSyncRemoteState { @@ -2707,7 +2837,7 @@ var File_prairie_plugin_v1_watch_sync_provider_proto protoreflect.FileDescriptor const file_prairie_plugin_v1_watch_sync_provider_proto_rawDesc = "" + "\n" + - "+prairie/plugin/v1/watch_sync_provider.proto\x12\x11prairie.plugin.v1\x1a\x1egoogle/protobuf/duration.proto\x1a\x1cgoogle/protobuf/struct.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\xaa\x06\n" + + "+prairie/plugin/v1/watch_sync_provider.proto\x12\x11prairie.plugin.v1\x1a\x1egoogle/protobuf/duration.proto\x1a\x1cgoogle/protobuf/struct.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\xf8\x06\n" + "\x1bWatchSyncProviderDescriptor\x12I\n" + "\fauth_methods\x18\x01 \x03(\x0e2&.prairie.plugin.v1.WatchSyncAuthMethodR\vauthMethods\x12%\n" + "\x0eexport_watched\x18\x02 \x01(\bR\rexportWatched\x12)\n" + @@ -2725,7 +2855,9 @@ const file_prairie_plugin_v1_watch_sync_provider_proto_rawDesc = "" + "\x10export_watchlist\x18\r \x01(\bR\x0fexportWatchlist\x12)\n" + "\x10remove_watchlist\x18\x0e \x01(\bR\x0fremoveWatchlist\x128\n" + "\x18provides_watchlist_order\x18\x0f \x01(\bR\x16providesWatchlistOrder\x12+\n" + - "\x11scrobble_playback\x18\x10 \x01(\bR\x10scrobblePlayback\"\xc8\x02\n" + + "\x11scrobble_playback\x18\x10 \x01(\bR\x10scrobblePlayback\x12%\n" + + "\x0eimport_ratings\x18\x11 \x01(\bR\rimportRatings\x12%\n" + + "\x0eexport_ratings\x18\x12 \x01(\bR\rexportRatings\"\xc8\x02\n" + "\x17WatchSyncProviderConfig\x12N\n" + "\x06values\x18\x01 \x03(\v26.prairie.plugin.v1.WatchSyncProviderConfig.ValuesEntryR\x06values\x12a\n" + "\rsecret_values\x18\x02 \x03(\v2<.prairie.plugin.v1.WatchSyncProviderConfig.SecretValuesEntryR\fsecretValues\x1a9\n" + @@ -2844,7 +2976,7 @@ const file_prairie_plugin_v1_watch_sync_provider_proto_rawDesc = "" + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\x1aD\n" + "\x16SeriesExternalIdsEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + - "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xa6\x05\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xbe\x05\n" + "\x0eWatchSyncEvent\x12\x19\n" + "\bevent_id\x18\x01 \x01(\tR\aeventId\x12C\n" + "\toperation\x18\x02 \x01(\x0e2%.prairie.plugin.v1.WatchSyncOperationR\toperation\x12:\n" + @@ -2862,7 +2994,8 @@ const file_prairie_plugin_v1_watch_sync_provider_proto_rawDesc = "" + "play_count\x18\v \x01(\x05R\tplayCount\x12(\n" + "\rlist_position\x18\f \x01(\x05H\x00R\flistPosition\x88\x01\x01\x12*\n" + "\x11provider_item_key\x18\r \x01(\tR\x0fproviderItemKey\x12\x1c\n" + - "\tcompleted\x18\x0e \x01(\bR\tcompletedB\x10\n" + + "\tcompleted\x18\x0e \x01(\bR\tcompleted\x12\x16\n" + + "\x06rating\x18\x0f \x01(\x05R\x06ratingB\x10\n" + "\x0e_list_position\"\xa4\x01\n" + "\x1bWatchSyncApplyEventsRequest\x12J\n" + "\acontext\x18\x01 \x01(\v20.prairie.plugin.v1.WatchSyncAuthenticatedContextR\acontext\x129\n" + @@ -2892,14 +3025,19 @@ const file_prairie_plugin_v1_watch_sync_provider_proto_rawDesc = "" + "\tpaused_at\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\bpausedAt\"m\n" + "\x18WatchSyncRemoteListState\x127\n" + "\tlisted_at\x18\x01 \x01(\v2\x1a.google.protobuf.TimestampR\blistedAt\x12\x18\n" + - "\aremoved\x18\x02 \x01(\bR\aremoved\"\xa6\x03\n" + + "\aremoved\x18\x02 \x01(\bR\aremoved\"\x85\x01\n" + + "\x1aWatchSyncRemoteRatingState\x12\x16\n" + + "\x06rating\x18\x01 \x01(\x05R\x06rating\x125\n" + + "\brated_at\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\aratedAt\x12\x18\n" + + "\aremoved\x18\x03 \x01(\bR\aremoved\"\xed\x03\n" + "\x14WatchSyncRemoteState\x12*\n" + "\x11provider_item_key\x18\x01 \x01(\tR\x0fproviderItemKey\x127\n" + "\x05media\x18\x02 \x01(\v2!.prairie.plugin.v1.WatchSyncMediaR\x05media\x12H\n" + "\awatched\x18\x03 \x01(\v2..prairie.plugin.v1.WatchSyncRemoteWatchedStateR\awatched\x12K\n" + "\bprogress\x18\x04 \x01(\v2/.prairie.plugin.v1.WatchSyncRemoteProgressStateR\bprogress\x12G\n" + "\bfavorite\x18\x05 \x01(\v2+.prairie.plugin.v1.WatchSyncRemoteListStateR\bfavorite\x12I\n" + - "\twatchlist\x18\x06 \x01(\v2+.prairie.plugin.v1.WatchSyncRemoteListStateR\twatchlist\"\xea\x02\n" + + "\twatchlist\x18\x06 \x01(\v2+.prairie.plugin.v1.WatchSyncRemoteListStateR\twatchlist\x12E\n" + + "\x06rating\x18\a \x01(\v2-.prairie.plugin.v1.WatchSyncRemoteRatingStateR\x06rating\"\xea\x02\n" + " WatchSyncListRemoteStateResponse\x12=\n" + "\x05items\x18\x01 \x03(\v2'.prairie.plugin.v1.WatchSyncRemoteStateR\x05items\x12&\n" + "\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken\x12\x1f\n" + @@ -2912,11 +3050,12 @@ const file_prairie_plugin_v1_watch_sync_provider_proto_rawDesc = "" + "\"WATCH_SYNC_AUTH_METHOD_UNSPECIFIED\x10\x00\x12-\n" + ")WATCH_SYNC_AUTH_METHOD_AUTHORIZATION_CODE\x10\x01\x12\"\n" + "\x1eWATCH_SYNC_AUTH_METHOD_API_KEY\x10\x02\x12&\n" + - "\"WATCH_SYNC_AUTH_METHOD_DEVICE_CODE\x10\x03*\x7f\n" + + "\"WATCH_SYNC_AUTH_METHOD_DEVICE_CODE\x10\x03*\xa1\x01\n" + "\x12WatchSyncMediaType\x12%\n" + "!WATCH_SYNC_MEDIA_TYPE_UNSPECIFIED\x10\x00\x12\x1f\n" + "\x1bWATCH_SYNC_MEDIA_TYPE_MOVIE\x10\x01\x12!\n" + - "\x1dWATCH_SYNC_MEDIA_TYPE_EPISODE\x10\x02*\xb0\x03\n" + + "\x1dWATCH_SYNC_MEDIA_TYPE_EPISODE\x10\x02\x12 \n" + + "\x1cWATCH_SYNC_MEDIA_TYPE_SERIES\x10\x03*\xfd\x03\n" + "\x12WatchSyncOperation\x12$\n" + " WATCH_SYNC_OPERATION_UNSPECIFIED\x10\x00\x12%\n" + "!WATCH_SYNC_OPERATION_MARK_WATCHED\x10\x01\x12'\n" + @@ -2927,13 +3066,17 @@ const file_prairie_plugin_v1_watch_sync_provider_proto_rawDesc = "" + "*WATCH_SYNC_OPERATION_REMOVE_FROM_WATCHLIST\x10\x06\x12'\n" + "#WATCH_SYNC_OPERATION_SCROBBLE_START\x10\a\x12'\n" + "#WATCH_SYNC_OPERATION_SCROBBLE_PAUSE\x10\b\x12&\n" + - "\"WATCH_SYNC_OPERATION_SCROBBLE_STOP\x10\t*\xf4\x01\n" + + "\"WATCH_SYNC_OPERATION_SCROBBLE_STOP\x10\t\x12#\n" + + "\x1fWATCH_SYNC_OPERATION_SET_RATING\x10\n" + + "\x12&\n" + + "\"WATCH_SYNC_OPERATION_REMOVE_RATING\x10\v*\x9d\x02\n" + "\x18WatchSyncRemoteStateKind\x12,\n" + "(WATCH_SYNC_REMOTE_STATE_KIND_UNSPECIFIED\x10\x00\x12(\n" + "$WATCH_SYNC_REMOTE_STATE_KIND_WATCHED\x10\x01\x12)\n" + "%WATCH_SYNC_REMOTE_STATE_KIND_PROGRESS\x10\x02\x12)\n" + "%WATCH_SYNC_REMOTE_STATE_KIND_FAVORITE\x10\x03\x12*\n" + - "&WATCH_SYNC_REMOTE_STATE_KIND_WATCHLIST\x10\x04*\xae\x02\n" + + "&WATCH_SYNC_REMOTE_STATE_KIND_WATCHLIST\x10\x04\x12'\n" + + "#WATCH_SYNC_REMOTE_STATE_KIND_RATING\x10\x05*\xae\x02\n" + "\"WatchSyncDeviceAuthorizationStatus\x126\n" + "2WATCH_SYNC_DEVICE_AUTHORIZATION_STATUS_UNSPECIFIED\x10\x00\x122\n" + ".WATCH_SYNC_DEVICE_AUTHORIZATION_STATUS_PENDING\x10\x01\x125\n" + @@ -2985,7 +3128,7 @@ func file_prairie_plugin_v1_watch_sync_provider_proto_rawDescGZIP() []byte { } var file_prairie_plugin_v1_watch_sync_provider_proto_enumTypes = make([]protoimpl.EnumInfo, 8) -var file_prairie_plugin_v1_watch_sync_provider_proto_msgTypes = make([]protoimpl.MessageInfo, 34) +var file_prairie_plugin_v1_watch_sync_provider_proto_msgTypes = make([]protoimpl.MessageInfo, 35) var file_prairie_plugin_v1_watch_sync_provider_proto_goTypes = []any{ (WatchSyncAuthMethod)(0), // 0: prairie.plugin.v1.WatchSyncAuthMethod (WatchSyncMediaType)(0), // 1: prairie.plugin.v1.WatchSyncMediaType @@ -3022,43 +3165,44 @@ var file_prairie_plugin_v1_watch_sync_provider_proto_goTypes = []any{ (*WatchSyncRemoteWatchedState)(nil), // 32: prairie.plugin.v1.WatchSyncRemoteWatchedState (*WatchSyncRemoteProgressState)(nil), // 33: prairie.plugin.v1.WatchSyncRemoteProgressState (*WatchSyncRemoteListState)(nil), // 34: prairie.plugin.v1.WatchSyncRemoteListState - (*WatchSyncRemoteState)(nil), // 35: prairie.plugin.v1.WatchSyncRemoteState - (*WatchSyncListRemoteStateResponse)(nil), // 36: prairie.plugin.v1.WatchSyncListRemoteStateResponse - nil, // 37: prairie.plugin.v1.WatchSyncProviderConfig.ValuesEntry - nil, // 38: prairie.plugin.v1.WatchSyncProviderConfig.SecretValuesEntry - nil, // 39: prairie.plugin.v1.WatchSyncCredentials.SecretAttributesEntry - nil, // 40: prairie.plugin.v1.WatchSyncMedia.ExternalIdsEntry - nil, // 41: prairie.plugin.v1.WatchSyncMedia.SeriesExternalIdsEntry - (*timestamppb.Timestamp)(nil), // 42: google.protobuf.Timestamp - (*durationpb.Duration)(nil), // 43: google.protobuf.Duration - (*structpb.Struct)(nil), // 44: google.protobuf.Struct + (*WatchSyncRemoteRatingState)(nil), // 35: prairie.plugin.v1.WatchSyncRemoteRatingState + (*WatchSyncRemoteState)(nil), // 36: prairie.plugin.v1.WatchSyncRemoteState + (*WatchSyncListRemoteStateResponse)(nil), // 37: prairie.plugin.v1.WatchSyncListRemoteStateResponse + nil, // 38: prairie.plugin.v1.WatchSyncProviderConfig.ValuesEntry + nil, // 39: prairie.plugin.v1.WatchSyncProviderConfig.SecretValuesEntry + nil, // 40: prairie.plugin.v1.WatchSyncCredentials.SecretAttributesEntry + nil, // 41: prairie.plugin.v1.WatchSyncMedia.ExternalIdsEntry + nil, // 42: prairie.plugin.v1.WatchSyncMedia.SeriesExternalIdsEntry + (*timestamppb.Timestamp)(nil), // 43: google.protobuf.Timestamp + (*durationpb.Duration)(nil), // 44: google.protobuf.Duration + (*structpb.Struct)(nil), // 45: google.protobuf.Struct } var file_prairie_plugin_v1_watch_sync_provider_proto_depIdxs = []int32{ 0, // 0: prairie.plugin.v1.WatchSyncProviderDescriptor.auth_methods:type_name -> prairie.plugin.v1.WatchSyncAuthMethod 1, // 1: prairie.plugin.v1.WatchSyncProviderDescriptor.supported_media_types:type_name -> prairie.plugin.v1.WatchSyncMediaType - 37, // 2: prairie.plugin.v1.WatchSyncProviderConfig.values:type_name -> prairie.plugin.v1.WatchSyncProviderConfig.ValuesEntry - 38, // 3: prairie.plugin.v1.WatchSyncProviderConfig.secret_values:type_name -> prairie.plugin.v1.WatchSyncProviderConfig.SecretValuesEntry - 42, // 4: prairie.plugin.v1.WatchSyncCredentials.expires_at:type_name -> google.protobuf.Timestamp - 39, // 5: prairie.plugin.v1.WatchSyncCredentials.secret_attributes:type_name -> prairie.plugin.v1.WatchSyncCredentials.SecretAttributesEntry + 38, // 2: prairie.plugin.v1.WatchSyncProviderConfig.values:type_name -> prairie.plugin.v1.WatchSyncProviderConfig.ValuesEntry + 39, // 3: prairie.plugin.v1.WatchSyncProviderConfig.secret_values:type_name -> prairie.plugin.v1.WatchSyncProviderConfig.SecretValuesEntry + 43, // 4: prairie.plugin.v1.WatchSyncCredentials.expires_at:type_name -> google.protobuf.Timestamp + 40, // 5: prairie.plugin.v1.WatchSyncCredentials.secret_attributes:type_name -> prairie.plugin.v1.WatchSyncCredentials.SecretAttributesEntry 9, // 6: prairie.plugin.v1.WatchSyncAuthenticatedContext.provider_config:type_name -> prairie.plugin.v1.WatchSyncProviderConfig 10, // 7: prairie.plugin.v1.WatchSyncAuthenticatedContext.credentials:type_name -> prairie.plugin.v1.WatchSyncCredentials 7, // 8: prairie.plugin.v1.WatchSyncFault.code:type_name -> prairie.plugin.v1.WatchSyncFaultCode - 43, // 9: prairie.plugin.v1.WatchSyncFault.retry_after:type_name -> google.protobuf.Duration + 44, // 9: prairie.plugin.v1.WatchSyncFault.retry_after:type_name -> google.protobuf.Duration 9, // 10: prairie.plugin.v1.WatchSyncInitAuthorizeRequest.provider_config:type_name -> prairie.plugin.v1.WatchSyncProviderConfig 13, // 11: prairie.plugin.v1.WatchSyncInitAuthorizeResponse.fault:type_name -> prairie.plugin.v1.WatchSyncFault 9, // 12: prairie.plugin.v1.WatchSyncExchangeCodeRequest.provider_config:type_name -> prairie.plugin.v1.WatchSyncProviderConfig 9, // 13: prairie.plugin.v1.WatchSyncExchangeAPIKeyRequest.provider_config:type_name -> prairie.plugin.v1.WatchSyncProviderConfig 9, // 14: prairie.plugin.v1.WatchSyncDeviceAuthorizationServiceStartRequest.provider_config:type_name -> prairie.plugin.v1.WatchSyncProviderConfig - 43, // 15: prairie.plugin.v1.WatchSyncDeviceAuthorizationServiceStartResponse.polling_interval:type_name -> google.protobuf.Duration - 42, // 16: prairie.plugin.v1.WatchSyncDeviceAuthorizationServiceStartResponse.expires_at:type_name -> google.protobuf.Timestamp + 44, // 15: prairie.plugin.v1.WatchSyncDeviceAuthorizationServiceStartResponse.polling_interval:type_name -> google.protobuf.Duration + 43, // 16: prairie.plugin.v1.WatchSyncDeviceAuthorizationServiceStartResponse.expires_at:type_name -> google.protobuf.Timestamp 13, // 17: prairie.plugin.v1.WatchSyncDeviceAuthorizationServiceStartResponse.fault:type_name -> prairie.plugin.v1.WatchSyncFault 9, // 18: prairie.plugin.v1.WatchSyncDeviceAuthorizationServicePollRequest.provider_config:type_name -> prairie.plugin.v1.WatchSyncProviderConfig 4, // 19: prairie.plugin.v1.WatchSyncDeviceAuthorizationServicePollResponse.status:type_name -> prairie.plugin.v1.WatchSyncDeviceAuthorizationStatus 10, // 20: prairie.plugin.v1.WatchSyncDeviceAuthorizationServicePollResponse.credentials:type_name -> prairie.plugin.v1.WatchSyncCredentials 12, // 21: prairie.plugin.v1.WatchSyncDeviceAuthorizationServicePollResponse.account:type_name -> prairie.plugin.v1.WatchSyncAccount 13, // 22: prairie.plugin.v1.WatchSyncDeviceAuthorizationServicePollResponse.fault:type_name -> prairie.plugin.v1.WatchSyncFault - 43, // 23: prairie.plugin.v1.WatchSyncDeviceAuthorizationServicePollResponse.polling_interval:type_name -> google.protobuf.Duration - 42, // 24: prairie.plugin.v1.WatchSyncDeviceAuthorizationServicePollResponse.expires_at:type_name -> google.protobuf.Timestamp + 44, // 23: prairie.plugin.v1.WatchSyncDeviceAuthorizationServicePollResponse.polling_interval:type_name -> google.protobuf.Duration + 43, // 24: prairie.plugin.v1.WatchSyncDeviceAuthorizationServicePollResponse.expires_at:type_name -> google.protobuf.Timestamp 11, // 25: prairie.plugin.v1.WatchSyncRefreshCredentialsRequest.context:type_name -> prairie.plugin.v1.WatchSyncAuthenticatedContext 10, // 26: prairie.plugin.v1.WatchSyncCredentialResponse.credentials:type_name -> prairie.plugin.v1.WatchSyncCredentials 12, // 27: prairie.plugin.v1.WatchSyncCredentialResponse.account:type_name -> prairie.plugin.v1.WatchSyncAccount @@ -3067,12 +3211,12 @@ var file_prairie_plugin_v1_watch_sync_provider_proto_depIdxs = []int32{ 12, // 30: prairie.plugin.v1.WatchSyncGetAccountResponse.account:type_name -> prairie.plugin.v1.WatchSyncAccount 13, // 31: prairie.plugin.v1.WatchSyncGetAccountResponse.fault:type_name -> prairie.plugin.v1.WatchSyncFault 1, // 32: prairie.plugin.v1.WatchSyncMedia.media_type:type_name -> prairie.plugin.v1.WatchSyncMediaType - 40, // 33: prairie.plugin.v1.WatchSyncMedia.external_ids:type_name -> prairie.plugin.v1.WatchSyncMedia.ExternalIdsEntry - 41, // 34: prairie.plugin.v1.WatchSyncMedia.series_external_ids:type_name -> prairie.plugin.v1.WatchSyncMedia.SeriesExternalIdsEntry - 44, // 35: prairie.plugin.v1.WatchSyncMedia.metadata:type_name -> google.protobuf.Struct + 41, // 33: prairie.plugin.v1.WatchSyncMedia.external_ids:type_name -> prairie.plugin.v1.WatchSyncMedia.ExternalIdsEntry + 42, // 34: prairie.plugin.v1.WatchSyncMedia.series_external_ids:type_name -> prairie.plugin.v1.WatchSyncMedia.SeriesExternalIdsEntry + 45, // 35: prairie.plugin.v1.WatchSyncMedia.metadata:type_name -> google.protobuf.Struct 2, // 36: prairie.plugin.v1.WatchSyncEvent.operation:type_name -> prairie.plugin.v1.WatchSyncOperation 5, // 37: prairie.plugin.v1.WatchSyncEvent.origin:type_name -> prairie.plugin.v1.WatchSyncOrigin - 42, // 38: prairie.plugin.v1.WatchSyncEvent.occurred_at:type_name -> google.protobuf.Timestamp + 43, // 38: prairie.plugin.v1.WatchSyncEvent.occurred_at:type_name -> google.protobuf.Timestamp 26, // 39: prairie.plugin.v1.WatchSyncEvent.media:type_name -> prairie.plugin.v1.WatchSyncMedia 11, // 40: prairie.plugin.v1.WatchSyncApplyEventsRequest.context:type_name -> prairie.plugin.v1.WatchSyncAuthenticatedContext 27, // 41: prairie.plugin.v1.WatchSyncApplyEventsRequest.events:type_name -> prairie.plugin.v1.WatchSyncEvent @@ -3083,40 +3227,42 @@ var file_prairie_plugin_v1_watch_sync_provider_proto_depIdxs = []int32{ 13, // 46: prairie.plugin.v1.WatchSyncApplyEventsResponse.fault:type_name -> prairie.plugin.v1.WatchSyncFault 11, // 47: prairie.plugin.v1.WatchSyncListRemoteStateRequest.context:type_name -> prairie.plugin.v1.WatchSyncAuthenticatedContext 3, // 48: prairie.plugin.v1.WatchSyncListRemoteStateRequest.state_kinds:type_name -> prairie.plugin.v1.WatchSyncRemoteStateKind - 42, // 49: prairie.plugin.v1.WatchSyncRemoteWatchedState.last_watched_at:type_name -> google.protobuf.Timestamp - 42, // 50: prairie.plugin.v1.WatchSyncRemoteProgressState.paused_at:type_name -> google.protobuf.Timestamp - 42, // 51: prairie.plugin.v1.WatchSyncRemoteListState.listed_at:type_name -> google.protobuf.Timestamp - 26, // 52: prairie.plugin.v1.WatchSyncRemoteState.media:type_name -> prairie.plugin.v1.WatchSyncMedia - 32, // 53: prairie.plugin.v1.WatchSyncRemoteState.watched:type_name -> prairie.plugin.v1.WatchSyncRemoteWatchedState - 33, // 54: prairie.plugin.v1.WatchSyncRemoteState.progress:type_name -> prairie.plugin.v1.WatchSyncRemoteProgressState - 34, // 55: prairie.plugin.v1.WatchSyncRemoteState.favorite:type_name -> prairie.plugin.v1.WatchSyncRemoteListState - 34, // 56: prairie.plugin.v1.WatchSyncRemoteState.watchlist:type_name -> prairie.plugin.v1.WatchSyncRemoteListState - 35, // 57: prairie.plugin.v1.WatchSyncListRemoteStateResponse.items:type_name -> prairie.plugin.v1.WatchSyncRemoteState - 10, // 58: prairie.plugin.v1.WatchSyncListRemoteStateResponse.updated_credentials:type_name -> prairie.plugin.v1.WatchSyncCredentials - 13, // 59: prairie.plugin.v1.WatchSyncListRemoteStateResponse.fault:type_name -> prairie.plugin.v1.WatchSyncFault - 14, // 60: prairie.plugin.v1.WatchSyncProvider.InitAuthorize:input_type -> prairie.plugin.v1.WatchSyncInitAuthorizeRequest - 16, // 61: prairie.plugin.v1.WatchSyncProvider.ExchangeCode:input_type -> prairie.plugin.v1.WatchSyncExchangeCodeRequest - 17, // 62: prairie.plugin.v1.WatchSyncProvider.ExchangeAPIKey:input_type -> prairie.plugin.v1.WatchSyncExchangeAPIKeyRequest - 22, // 63: prairie.plugin.v1.WatchSyncProvider.RefreshCredentials:input_type -> prairie.plugin.v1.WatchSyncRefreshCredentialsRequest - 24, // 64: prairie.plugin.v1.WatchSyncProvider.GetAccount:input_type -> prairie.plugin.v1.WatchSyncGetAccountRequest - 28, // 65: prairie.plugin.v1.WatchSyncProvider.ApplyEvents:input_type -> prairie.plugin.v1.WatchSyncApplyEventsRequest - 31, // 66: prairie.plugin.v1.WatchSyncProvider.ListRemoteState:input_type -> prairie.plugin.v1.WatchSyncListRemoteStateRequest - 18, // 67: prairie.plugin.v1.WatchSyncDeviceAuthorizationService.Start:input_type -> prairie.plugin.v1.WatchSyncDeviceAuthorizationServiceStartRequest - 20, // 68: prairie.plugin.v1.WatchSyncDeviceAuthorizationService.Poll:input_type -> prairie.plugin.v1.WatchSyncDeviceAuthorizationServicePollRequest - 15, // 69: prairie.plugin.v1.WatchSyncProvider.InitAuthorize:output_type -> prairie.plugin.v1.WatchSyncInitAuthorizeResponse - 23, // 70: prairie.plugin.v1.WatchSyncProvider.ExchangeCode:output_type -> prairie.plugin.v1.WatchSyncCredentialResponse - 23, // 71: prairie.plugin.v1.WatchSyncProvider.ExchangeAPIKey:output_type -> prairie.plugin.v1.WatchSyncCredentialResponse - 23, // 72: prairie.plugin.v1.WatchSyncProvider.RefreshCredentials:output_type -> prairie.plugin.v1.WatchSyncCredentialResponse - 25, // 73: prairie.plugin.v1.WatchSyncProvider.GetAccount:output_type -> prairie.plugin.v1.WatchSyncGetAccountResponse - 30, // 74: prairie.plugin.v1.WatchSyncProvider.ApplyEvents:output_type -> prairie.plugin.v1.WatchSyncApplyEventsResponse - 36, // 75: prairie.plugin.v1.WatchSyncProvider.ListRemoteState:output_type -> prairie.plugin.v1.WatchSyncListRemoteStateResponse - 19, // 76: prairie.plugin.v1.WatchSyncDeviceAuthorizationService.Start:output_type -> prairie.plugin.v1.WatchSyncDeviceAuthorizationServiceStartResponse - 21, // 77: prairie.plugin.v1.WatchSyncDeviceAuthorizationService.Poll:output_type -> prairie.plugin.v1.WatchSyncDeviceAuthorizationServicePollResponse - 69, // [69:78] is the sub-list for method output_type - 60, // [60:69] is the sub-list for method input_type - 60, // [60:60] is the sub-list for extension type_name - 60, // [60:60] is the sub-list for extension extendee - 0, // [0:60] is the sub-list for field type_name + 43, // 49: prairie.plugin.v1.WatchSyncRemoteWatchedState.last_watched_at:type_name -> google.protobuf.Timestamp + 43, // 50: prairie.plugin.v1.WatchSyncRemoteProgressState.paused_at:type_name -> google.protobuf.Timestamp + 43, // 51: prairie.plugin.v1.WatchSyncRemoteListState.listed_at:type_name -> google.protobuf.Timestamp + 43, // 52: prairie.plugin.v1.WatchSyncRemoteRatingState.rated_at:type_name -> google.protobuf.Timestamp + 26, // 53: prairie.plugin.v1.WatchSyncRemoteState.media:type_name -> prairie.plugin.v1.WatchSyncMedia + 32, // 54: prairie.plugin.v1.WatchSyncRemoteState.watched:type_name -> prairie.plugin.v1.WatchSyncRemoteWatchedState + 33, // 55: prairie.plugin.v1.WatchSyncRemoteState.progress:type_name -> prairie.plugin.v1.WatchSyncRemoteProgressState + 34, // 56: prairie.plugin.v1.WatchSyncRemoteState.favorite:type_name -> prairie.plugin.v1.WatchSyncRemoteListState + 34, // 57: prairie.plugin.v1.WatchSyncRemoteState.watchlist:type_name -> prairie.plugin.v1.WatchSyncRemoteListState + 35, // 58: prairie.plugin.v1.WatchSyncRemoteState.rating:type_name -> prairie.plugin.v1.WatchSyncRemoteRatingState + 36, // 59: prairie.plugin.v1.WatchSyncListRemoteStateResponse.items:type_name -> prairie.plugin.v1.WatchSyncRemoteState + 10, // 60: prairie.plugin.v1.WatchSyncListRemoteStateResponse.updated_credentials:type_name -> prairie.plugin.v1.WatchSyncCredentials + 13, // 61: prairie.plugin.v1.WatchSyncListRemoteStateResponse.fault:type_name -> prairie.plugin.v1.WatchSyncFault + 14, // 62: prairie.plugin.v1.WatchSyncProvider.InitAuthorize:input_type -> prairie.plugin.v1.WatchSyncInitAuthorizeRequest + 16, // 63: prairie.plugin.v1.WatchSyncProvider.ExchangeCode:input_type -> prairie.plugin.v1.WatchSyncExchangeCodeRequest + 17, // 64: prairie.plugin.v1.WatchSyncProvider.ExchangeAPIKey:input_type -> prairie.plugin.v1.WatchSyncExchangeAPIKeyRequest + 22, // 65: prairie.plugin.v1.WatchSyncProvider.RefreshCredentials:input_type -> prairie.plugin.v1.WatchSyncRefreshCredentialsRequest + 24, // 66: prairie.plugin.v1.WatchSyncProvider.GetAccount:input_type -> prairie.plugin.v1.WatchSyncGetAccountRequest + 28, // 67: prairie.plugin.v1.WatchSyncProvider.ApplyEvents:input_type -> prairie.plugin.v1.WatchSyncApplyEventsRequest + 31, // 68: prairie.plugin.v1.WatchSyncProvider.ListRemoteState:input_type -> prairie.plugin.v1.WatchSyncListRemoteStateRequest + 18, // 69: prairie.plugin.v1.WatchSyncDeviceAuthorizationService.Start:input_type -> prairie.plugin.v1.WatchSyncDeviceAuthorizationServiceStartRequest + 20, // 70: prairie.plugin.v1.WatchSyncDeviceAuthorizationService.Poll:input_type -> prairie.plugin.v1.WatchSyncDeviceAuthorizationServicePollRequest + 15, // 71: prairie.plugin.v1.WatchSyncProvider.InitAuthorize:output_type -> prairie.plugin.v1.WatchSyncInitAuthorizeResponse + 23, // 72: prairie.plugin.v1.WatchSyncProvider.ExchangeCode:output_type -> prairie.plugin.v1.WatchSyncCredentialResponse + 23, // 73: prairie.plugin.v1.WatchSyncProvider.ExchangeAPIKey:output_type -> prairie.plugin.v1.WatchSyncCredentialResponse + 23, // 74: prairie.plugin.v1.WatchSyncProvider.RefreshCredentials:output_type -> prairie.plugin.v1.WatchSyncCredentialResponse + 25, // 75: prairie.plugin.v1.WatchSyncProvider.GetAccount:output_type -> prairie.plugin.v1.WatchSyncGetAccountResponse + 30, // 76: prairie.plugin.v1.WatchSyncProvider.ApplyEvents:output_type -> prairie.plugin.v1.WatchSyncApplyEventsResponse + 37, // 77: prairie.plugin.v1.WatchSyncProvider.ListRemoteState:output_type -> prairie.plugin.v1.WatchSyncListRemoteStateResponse + 19, // 78: prairie.plugin.v1.WatchSyncDeviceAuthorizationService.Start:output_type -> prairie.plugin.v1.WatchSyncDeviceAuthorizationServiceStartResponse + 21, // 79: prairie.plugin.v1.WatchSyncDeviceAuthorizationService.Poll:output_type -> prairie.plugin.v1.WatchSyncDeviceAuthorizationServicePollResponse + 71, // [71:80] is the sub-list for method output_type + 62, // [62:71] is the sub-list for method input_type + 62, // [62:62] is the sub-list for extension type_name + 62, // [62:62] is the sub-list for extension extendee + 0, // [0:62] is the sub-list for field type_name } func init() { file_prairie_plugin_v1_watch_sync_provider_proto_init() } @@ -3132,7 +3278,7 @@ func file_prairie_plugin_v1_watch_sync_provider_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_prairie_plugin_v1_watch_sync_provider_proto_rawDesc), len(file_prairie_plugin_v1_watch_sync_provider_proto_rawDesc)), NumEnums: 8, - NumMessages: 34, + NumMessages: 35, NumExtensions: 0, NumServices: 2, }, diff --git a/pkg/pluginproto/prairie/plugin/v1/watch_sync_provider_compat_test.go b/pkg/pluginproto/prairie/plugin/v1/watch_sync_provider_compat_test.go new file mode 100644 index 0000000..e415a1e --- /dev/null +++ b/pkg/pluginproto/prairie/plugin/v1/watch_sync_provider_compat_test.go @@ -0,0 +1,39 @@ +package pluginv1 + +import "context" + +// legacyDirectWatchSyncProvider is deliberately implemented without embedding +// UnimplementedWatchSyncProviderServer. Keep this compile-time assertion so a +// future additive RPC cannot silently break source compatibility for v0.12 +// plugins generated with require_unimplemented_servers=false. +type legacyDirectWatchSyncProvider struct{} + +func (legacyDirectWatchSyncProvider) InitAuthorize(context.Context, *WatchSyncInitAuthorizeRequest) (*WatchSyncInitAuthorizeResponse, error) { + return nil, nil +} + +func (legacyDirectWatchSyncProvider) ExchangeCode(context.Context, *WatchSyncExchangeCodeRequest) (*WatchSyncCredentialResponse, error) { + return nil, nil +} + +func (legacyDirectWatchSyncProvider) ExchangeAPIKey(context.Context, *WatchSyncExchangeAPIKeyRequest) (*WatchSyncCredentialResponse, error) { + return nil, nil +} + +func (legacyDirectWatchSyncProvider) RefreshCredentials(context.Context, *WatchSyncRefreshCredentialsRequest) (*WatchSyncCredentialResponse, error) { + return nil, nil +} + +func (legacyDirectWatchSyncProvider) GetAccount(context.Context, *WatchSyncGetAccountRequest) (*WatchSyncGetAccountResponse, error) { + return nil, nil +} + +func (legacyDirectWatchSyncProvider) ApplyEvents(context.Context, *WatchSyncApplyEventsRequest) (*WatchSyncApplyEventsResponse, error) { + return nil, nil +} + +func (legacyDirectWatchSyncProvider) ListRemoteState(context.Context, *WatchSyncListRemoteStateRequest) (*WatchSyncListRemoteStateResponse, error) { + return nil, nil +} + +var _ WatchSyncProviderServer = legacyDirectWatchSyncProvider{} diff --git a/pkg/pluginproto/prairie/plugin/v1/watch_sync_provider_test.go b/pkg/pluginproto/prairie/plugin/v1/watch_sync_provider_test.go index dff73a3..5898990 100644 --- a/pkg/pluginproto/prairie/plugin/v1/watch_sync_provider_test.go +++ b/pkg/pluginproto/prairie/plugin/v1/watch_sync_provider_test.go @@ -26,6 +26,7 @@ func TestWatchSyncTypedRemoteStateRoundTrip(t *testing.T) { ProgressPercent: 42.5, PausedAt: timestamppb.New(pausedAt), }, + Watchlist: &WatchSyncRemoteListState{ListedAt: timestamppb.New(pausedAt.Add(-2 * time.Hour)), Removed: true}, }}, NextCursor: "checkpoint-2", CompleteSnapshot: true, @@ -44,11 +45,205 @@ func TestWatchSyncTypedRemoteStateRoundTrip(t *testing.T) { if item.GetMedia().GetMediaType() != WatchSyncMediaType_WATCH_SYNC_MEDIA_TYPE_EPISODE || item.GetWatched().GetPlayCount() != 2 || item.GetProgress().GetProgressPercent() != 42.5 || - !item.GetProgress().GetPausedAt().AsTime().Equal(pausedAt) { + !item.GetProgress().GetPausedAt().AsTime().Equal(pausedAt) || + !item.GetWatchlist().GetListedAt().AsTime().Equal(pausedAt.Add(-2*time.Hour)) || + !item.GetWatchlist().GetRemoved() { t.Fatalf("remote state = %#v", item) } } +func TestWatchSyncDeviceAuthorizationRoundTrip(t *testing.T) { + expiresAt := time.Unix(1_800_000_000, 0).UTC() + input := &WatchSyncDeviceAuthorizationServiceStartResponse{ + UserCode: "ABCD-1234", + VerificationUrl: "https://provider.example/activate", + VerificationUrlComplete: "https://provider.example/activate?code=ABCD-1234", + ProviderState: []byte(`{"device_code":"secret"}`), + PollingInterval: durationpb.New(5 * time.Second), + ExpiresAt: timestamppb.New(expiresAt), + } + data, err := proto.Marshal(input) + if err != nil { + t.Fatal(err) + } + var output WatchSyncDeviceAuthorizationServiceStartResponse + if err := proto.Unmarshal(data, &output); err != nil { + t.Fatal(err) + } + if output.GetUserCode() != input.GetUserCode() || + output.GetVerificationUrl() != input.GetVerificationUrl() || + output.GetVerificationUrlComplete() != input.GetVerificationUrlComplete() || + !proto.Equal(&output, input) || + string(output.GetProviderState()) != string(input.GetProviderState()) || + output.GetPollingInterval().AsDuration() != 5*time.Second || + !output.GetExpiresAt().AsTime().Equal(expiresAt) { + t.Fatalf("device authorization = %#v", &output) + } +} + +func TestWatchSyncDeviceAuthorizationPendingStateRoundTrip(t *testing.T) { + expiresAt := time.Unix(1_800_000_100, 0).UTC() + input := &WatchSyncDeviceAuthorizationServicePollResponse{ + Status: WatchSyncDeviceAuthorizationStatus_WATCH_SYNC_DEVICE_AUTHORIZATION_STATUS_PENDING, + ProviderState: []byte("rotated-state"), + PollingInterval: durationpb.New(10 * time.Second), + ExpiresAt: timestamppb.New(expiresAt), + } + data, err := proto.Marshal(input) + if err != nil { + t.Fatal(err) + } + var output WatchSyncDeviceAuthorizationServicePollResponse + if err := proto.Unmarshal(data, &output); err != nil { + t.Fatal(err) + } + if !proto.Equal(input, &output) { + t.Fatalf("pending device authorization = %#v", &output) + } +} + +func TestWatchSyncDeviceAuthorizationPendingStatePreservesPresence(t *testing.T) { + explicitEmpty := &WatchSyncDeviceAuthorizationServicePollResponse{ProviderState: []byte{}} + data, err := proto.Marshal(explicitEmpty) + if err != nil { + t.Fatal(err) + } + var output WatchSyncDeviceAuthorizationServicePollResponse + if err := proto.Unmarshal(data, &output); err != nil { + t.Fatal(err) + } + if output.ProviderState == nil || len(output.GetProviderState()) != 0 { + t.Fatalf("explicit empty provider state = %#v", output.ProviderState) + } + + data, err = proto.Marshal(&WatchSyncDeviceAuthorizationServicePollResponse{}) + if err != nil { + t.Fatal(err) + } + output.Reset() + if err := proto.Unmarshal(data, &output); err != nil { + t.Fatal(err) + } + if output.ProviderState != nil { + t.Fatalf("omitted provider state = %#v, want nil", output.ProviderState) + } +} + +func TestWatchSyncListPositionPreservesPresence(t *testing.T) { + zero := int32(0) + assertOptionalInt32Presence( + t, + &WatchSyncEvent{ListPosition: &zero}, + &WatchSyncEvent{}, + func(message proto.Message) *int32 { + return message.(*WatchSyncEvent).ListPosition + }, + ) +} + +func TestWatchSyncListTombstoneDoesNotRequireMedia(t *testing.T) { + input := &WatchSyncRemoteState{ + ProviderItemKey: "remote-1", + Favorite: &WatchSyncRemoteListState{Removed: true}, + } + data, err := proto.Marshal(input) + if err != nil { + t.Fatal(err) + } + var output WatchSyncRemoteState + if err := proto.Unmarshal(data, &output); err != nil { + t.Fatal(err) + } + if !proto.Equal(input, &output) || output.GetMedia() != nil || !output.GetFavorite().GetRemoved() { + t.Fatalf("list tombstone = %#v", &output) + } +} + +func TestWatchSyncRatingRemoteStateRoundTrip(t *testing.T) { + ratedAt := time.Unix(1_750_000_000, 0).UTC() + input := &WatchSyncListRemoteStateResponse{ + Items: []*WatchSyncRemoteState{{ + ProviderItemKey: "show:7", + Media: &WatchSyncMedia{ + MediaItemId: "local-series-7", + MediaType: WatchSyncMediaType_WATCH_SYNC_MEDIA_TYPE_SERIES, + Title: "Example Series", + Year: 2019, + ExternalIds: map[string]string{"tvdb": "12345"}, + }, + Rating: &WatchSyncRemoteRatingState{ + Rating: 8, + RatedAt: timestamppb.New(ratedAt), + }, + }}, + NextCursor: "ratings-1", + CompleteSnapshot: true, + } + + data, err := proto.Marshal(input) + if err != nil { + t.Fatal(err) + } + var output WatchSyncListRemoteStateResponse + if err := proto.Unmarshal(data, &output); err != nil { + t.Fatal(err) + } + + item := output.GetItems()[0] + if !proto.Equal(input, &output) || + item.GetMedia().GetMediaType() != WatchSyncMediaType_WATCH_SYNC_MEDIA_TYPE_SERIES || + item.GetMedia().GetExternalIds()["tvdb"] != "12345" || + item.GetRating().GetRating() != 8 || + !item.GetRating().GetRatedAt().AsTime().Equal(ratedAt) || + item.GetRating().GetRemoved() { + t.Fatalf("rating remote state = %#v", item) + } +} + +func TestWatchSyncRatingTombstoneDoesNotRequireMedia(t *testing.T) { + input := &WatchSyncRemoteState{ + ProviderItemKey: "movie:9", + Rating: &WatchSyncRemoteRatingState{Removed: true}, + } + data, err := proto.Marshal(input) + if err != nil { + t.Fatal(err) + } + var output WatchSyncRemoteState + if err := proto.Unmarshal(data, &output); err != nil { + t.Fatal(err) + } + if !proto.Equal(input, &output) || output.GetMedia() != nil || + !output.GetRating().GetRemoved() || output.GetRating().GetRating() != 0 { + t.Fatalf("rating tombstone = %#v", &output) + } +} + +func TestWatchSyncEventCarriesRating(t *testing.T) { + input := &WatchSyncEvent{ + EventId: "rating-1", + Operation: WatchSyncOperation_WATCH_SYNC_OPERATION_SET_RATING, + Media: &WatchSyncMedia{ + MediaItemId: "local-movie-9", + MediaType: WatchSyncMediaType_WATCH_SYNC_MEDIA_TYPE_MOVIE, + }, + Rating: 10, + } + data, err := proto.Marshal(input) + if err != nil { + t.Fatal(err) + } + var output WatchSyncEvent + if err := proto.Unmarshal(data, &output); err != nil { + t.Fatal(err) + } + if !proto.Equal(input, &output) || + output.GetOperation() != WatchSyncOperation_WATCH_SYNC_OPERATION_SET_RATING || + output.GetRating() != 10 { + t.Fatalf("rating event = %#v", &output) + } +} + func TestWatchSyncApplyResultCarriesTypedRateLimit(t *testing.T) { retryAfter := 45 * time.Second request := &WatchSyncApplyEventsRequest{ @@ -87,3 +282,35 @@ func TestWatchSyncApplyResultCarriesTypedRateLimit(t *testing.T) { t.Fatalf("request=%#v result=%#v", request, &output) } } + +func TestWatchSyncEventCarriesAuthoritativeCompletionState(t *testing.T) { + input := &WatchSyncEvent{ + EventId: "playback-stop-1", + WatchHistoryId: "incomplete-history-row", + CompletionPercent: 5.5, + Completed: false, + } + data, err := proto.Marshal(input) + if err != nil { + t.Fatal(err) + } + var output WatchSyncEvent + if err := proto.Unmarshal(data, &output); err != nil { + t.Fatal(err) + } + if output.GetCompleted() { + t.Fatal("completed = true, want false") + } + + input.Completed = true + data, err = proto.Marshal(input) + if err != nil { + t.Fatal(err) + } + if err := proto.Unmarshal(data, &output); err != nil { + t.Fatal(err) + } + if !output.GetCompleted() { + t.Fatal("completed = false, want true") + } +} diff --git a/pkg/pluginsdk/capability/capability.go b/pkg/pluginsdk/capability/capability.go index faa4eb0..241ea40 100644 --- a/pkg/pluginsdk/capability/capability.go +++ b/pkg/pluginsdk/capability/capability.go @@ -15,6 +15,9 @@ const ( EbookBackend = "ebook_backend.v1" ScanSource = "scan_source.v1" WatchSyncProvider = "watch_sync_provider.v1" + // NetworkAccessProvider marks a resident plugin that gives the deployment + // an overlay-network identity (Tailscale, NetBird, ...). Added in v0.16.0. + NetworkAccessProvider = "network_access_provider.v1" ) // KnownTypes lists every capability type recognized by this SDK version. @@ -32,4 +35,5 @@ var KnownTypes = []string{ EbookBackend, ScanSource, WatchSyncProvider, + NetworkAccessProvider, } diff --git a/pkg/pluginsdk/capability/capability_test.go b/pkg/pluginsdk/capability/capability_test.go index bd13191..1bd6a64 100644 --- a/pkg/pluginsdk/capability/capability_test.go +++ b/pkg/pluginsdk/capability/capability_test.go @@ -49,3 +49,19 @@ func TestImageResolverIsKnownType(t *testing.T) { t.Fatalf("ImageResolver (%q) missing from KnownTypes %v", ImageResolver, KnownTypes) } } + +func TestNetworkAccessProviderIsKnownType(t *testing.T) { + if NetworkAccessProvider != "network_access_provider.v1" { + t.Fatalf("NetworkAccessProvider const = %q, want %q", NetworkAccessProvider, "network_access_provider.v1") + } + found := false + for _, k := range KnownTypes { + if k == NetworkAccessProvider { + found = true + break + } + } + if !found { + t.Fatalf("NetworkAccessProvider (%q) missing from KnownTypes %v", NetworkAccessProvider, KnownTypes) + } +} diff --git a/pkg/pluginsdk/convert/convert.go b/pkg/pluginsdk/convert/convert.go index fdc8981..a61ede5 100644 --- a/pkg/pluginsdk/convert/convert.go +++ b/pkg/pluginsdk/convert/convert.go @@ -10,6 +10,12 @@ import ( pluginv1 "github.com/prairie-server/prairie-plugin-sdk/pkg/pluginproto/prairie/plugin/v1" ) +// capabilityUnmarshal decodes stored capability metadata tolerantly. Unknown +// fields and enum names are dropped rather than rejected, so a node on an older +// SDK, in a mixed-version cluster sharing one database, can still load +// descriptors written by a newer one. +var capabilityUnmarshal = protojson.UnmarshalOptions{DiscardUnknown: true} + type CapabilityRecord struct { Type string ID string @@ -70,11 +76,22 @@ func DecodeCapability(record CapabilityRecord) (*pluginv1.CapabilityDescriptor, return nil, fmt.Errorf("encode watch sync provider descriptor: %w", err) } var typed pluginv1.WatchSyncProviderDescriptor - if err := protojson.Unmarshal(data, &typed); err != nil { + if err := capabilityUnmarshal.Unmarshal(data, &typed); err != nil { return nil, fmt.Errorf("decode watch sync provider descriptor: %w", err) } descriptor.WatchSyncProvider = &typed } + if requestRouter, ok := record.Metadata["request_router"]; ok { + data, err := json.Marshal(requestRouter) + if err != nil { + return nil, fmt.Errorf("encode request router descriptor: %w", err) + } + var typed pluginv1.RequestRouterDescriptor + if err := capabilityUnmarshal.Unmarshal(data, &typed); err != nil { + return nil, fmt.Errorf("decode request router descriptor: %w", err) + } + descriptor.RequestRouter = &typed + } if configSchema, ok := record.Metadata["config_schema"]; ok { schemas, err := decodeConfigSchemas(configSchema) if err != nil { @@ -122,16 +139,42 @@ func capabilityMetadata(descriptor *pluginv1.CapabilityDescriptor) (map[string]a } metadata["watch_sync_provider"] = value } + if descriptor.GetRequestRouter() != nil { + data, err := protojson.MarshalOptions{UseProtoNames: true}.Marshal(descriptor.GetRequestRouter()) + if err != nil { + return nil, fmt.Errorf("encode request router descriptor: %w", err) + } + var value map[string]any + if err := json.Unmarshal(data, &value); err != nil { + return nil, fmt.Errorf("decode request router descriptor JSON: %w", err) + } + metadata["request_router"] = value + } if len(descriptor.GetConfigSchema()) > 0 { schemas := make([]map[string]any, 0, len(descriptor.GetConfigSchema())) for _, schema := range descriptor.GetConfigSchema() { - schemas = append(schemas, map[string]any{ - "key": schema.GetKey(), - "title": schema.GetTitle(), - "description": schema.GetDescription(), - "json_schema": schema.GetJsonSchema(), - "required": schema.GetRequired(), - }) + if schema == nil { + schemas = append(schemas, map[string]any{}) + continue + } + data, err := protojson.MarshalOptions{UseProtoNames: true}.Marshal(schema) + if err != nil { + return nil, fmt.Errorf("encode capability config schema: %w", err) + } + var value map[string]any + if err := json.Unmarshal(data, &value); err != nil { + return nil, fmt.Errorf("decode capability config schema JSON: %w", err) + } + // CapabilityRecord.Metadata is a public compatibility boundary. The + // original scalar-only encoding always emitted these keys, including + // required=false; protojson omits default-valued fields unless they are + // restored explicitly. + value["key"] = schema.GetKey() + value["title"] = schema.GetTitle() + value["description"] = schema.GetDescription() + value["json_schema"] = schema.GetJsonSchema() + value["required"] = schema.GetRequired() + schemas = append(schemas, value) } metadata["config_schema"] = schemas } @@ -157,19 +200,13 @@ func decodeConfigSchemas(value any) ([]*pluginv1.ConfigSchema, error) { schemas := make([]*pluginv1.ConfigSchema, 0, len(entries)) for _, entry := range entries { rawSchema := toStringAnyMap(entry) - jsonData, err := protojson.MarshalOptions{UseProtoNames: true}.Marshal(&pluginv1.ConfigSchema{ - Key: stringValue(rawSchema["key"]), - Title: stringValue(rawSchema["title"]), - Description: stringValue(rawSchema["description"]), - JsonSchema: stringValue(rawSchema["json_schema"]), - Required: boolValue(rawSchema["required"]), - }) + jsonData, err := json.Marshal(rawSchema) if err != nil { - return nil, fmt.Errorf("encode config schema: %w", err) + return nil, fmt.Errorf("encode capability config schema: %w", err) } var schema pluginv1.ConfigSchema - if err := protojson.Unmarshal(jsonData, &schema); err != nil { - return nil, fmt.Errorf("decode config schema: %w", err) + if err := capabilityUnmarshal.Unmarshal(jsonData, &schema); err != nil { + return nil, fmt.Errorf("decode capability config schema: %w", err) } schemas = append(schemas, &schema) } @@ -206,13 +243,3 @@ func toStringAnyMap(value any) map[string]any { return map[string]any{} } } - -func stringValue(value any) string { - text, _ := value.(string) - return text -} - -func boolValue(value any) bool { - flag, _ := value.(bool) - return flag -} diff --git a/pkg/pluginsdk/convert/convert_test.go b/pkg/pluginsdk/convert/convert_test.go index ee1ca85..a38db17 100644 --- a/pkg/pluginsdk/convert/convert_test.go +++ b/pkg/pluginsdk/convert/convert_test.go @@ -3,6 +3,7 @@ package convert_test import ( "testing" + "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/structpb" pluginv1 "github.com/prairie-server/prairie-plugin-sdk/pkg/pluginproto/prairie/plugin/v1" @@ -34,7 +35,15 @@ func TestCapabilityRecordsFromManifestRoundTrips(t *testing.T) { Title: "Connection", Description: "API key", JsonSchema: `{"type":"object"}`, - Required: true, + Required: false, + AdminForm: &pluginv1.AdminFormDescriptor{ + SubmitLabel: "Connect", + Fields: []*pluginv1.AdminFormField{{ + Key: "base_url", Label: "Server URL", + Control: pluginv1.AdminFormControl_ADMIN_FORM_CONTROL_TEXT, + Required: true, + }}, + }, }, }, Metadata: metadata, @@ -49,6 +58,19 @@ func TestCapabilityRecordsFromManifestRoundTrips(t *testing.T) { if len(records) != 1 { t.Fatalf("record count = %d, want 1", len(records)) } + recordedSchema := records[0].Metadata["config_schema"].([]map[string]any)[0] + for key, want := range map[string]any{ + "key": "connection", + "title": "Connection", + "description": "API key", + "json_schema": `{"type":"object"}`, + "required": false, + } { + if got, present := recordedSchema[key]; !present || got != want { + t.Fatalf("config_schema[%q] = %#v, present=%v, want %#v", key, got, present, want) + } + } + recordedSchema["future_field"] = true decoded, err := convert.DecodeCapability(records[0]) if err != nil { @@ -60,6 +82,9 @@ func TestCapabilityRecordsFromManifestRoundTrips(t *testing.T) { if got := decoded.GetConfigSchema()[0].GetKey(); got != "connection" { t.Fatalf("config_schema key = %q, want connection", got) } + if !proto.Equal(decoded.GetConfigSchema()[0], manifest.GetCapabilities()[0].GetConfigSchema()[0]) { + t.Fatalf("config_schema round trip = %#v, want %#v", decoded.GetConfigSchema()[0], manifest.GetCapabilities()[0].GetConfigSchema()[0]) + } if got := decoded.GetMetadata().AsMap()["provider"]; got != "example" { t.Fatalf("metadata provider = %#v, want example", got) } diff --git a/pkg/pluginsdk/convert/request_router_test.go b/pkg/pluginsdk/convert/request_router_test.go new file mode 100644 index 0000000..9350326 --- /dev/null +++ b/pkg/pluginsdk/convert/request_router_test.go @@ -0,0 +1,61 @@ +package convert_test + +import ( + "testing" + + pluginv1 "github.com/prairie-server/prairie-plugin-sdk/pkg/pluginproto/prairie/plugin/v1" + "github.com/prairie-server/prairie-plugin-sdk/pkg/pluginsdk/convert" +) + +func TestRequestRouterDescriptorRoundTrip(t *testing.T) { + manifest := &pluginv1.PluginManifest{Capabilities: []*pluginv1.CapabilityDescriptor{{ + Type: "request_router.v1", + Id: "arr", + RequestRouter: &pluginv1.RequestRouterDescriptor{SupportsSeasons: true}, + }}} + records, err := convert.CapabilityRecordsFromManifest(manifest) + if err != nil { + t.Fatal(err) + } + decoded, err := convert.DecodeCapability(records[0]) + if err != nil { + t.Fatal(err) + } + if !decoded.GetRequestRouter().GetSupportsSeasons() { + t.Fatalf("decoded descriptor = %#v, want supports_seasons", decoded.GetRequestRouter()) + } +} + +// Metadata stored for a request router built on an older SDK has no +// request_router key; it must decode as declaring no season support. +func TestDecodeCapabilityWithoutRequestRouterDeclaresNoSeasonSupport(t *testing.T) { + record := convert.CapabilityRecord{ + Type: "request_router.v1", + ID: "arr", + Metadata: map[string]any{"display_name": "Arr"}, + } + decoded, err := convert.DecodeCapability(record) + if err != nil { + t.Fatal(err) + } + if decoded.GetRequestRouter() != nil { + t.Fatalf("decoded descriptor = %#v, want absent", decoded.GetRequestRouter()) + } +} + +func TestDecodeCapabilityDiscardsUnknownRequestRouterData(t *testing.T) { + record := convert.CapabilityRecord{ + Type: "request_router.v1", + ID: "arr", + Metadata: map[string]any{ + "request_router": map[string]any{"supports_seasons": true, "supports_future_thing": true}, + }, + } + decoded, err := convert.DecodeCapability(record) + if err != nil { + t.Fatalf("DecodeCapability() error = %v", err) + } + if !decoded.GetRequestRouter().GetSupportsSeasons() { + t.Fatalf("decoded descriptor = %#v, want supports_seasons", decoded.GetRequestRouter()) + } +} diff --git a/pkg/pluginsdk/convert/watch_sync_provider_test.go b/pkg/pluginsdk/convert/watch_sync_provider_test.go index f6c5660..18587b4 100644 --- a/pkg/pluginsdk/convert/watch_sync_provider_test.go +++ b/pkg/pluginsdk/convert/watch_sync_provider_test.go @@ -12,11 +12,16 @@ func TestWatchSyncProviderDescriptorRoundTrip(t *testing.T) { Type: "watch_sync_provider.v1", Id: "anilist", WatchSyncProvider: &pluginv1.WatchSyncProviderDescriptor{ - AuthMethods: []pluginv1.WatchSyncAuthMethod{pluginv1.WatchSyncAuthMethod_WATCH_SYNC_AUTH_METHOD_AUTHORIZATION_CODE}, - ExportWatched: true, + AuthMethods: []pluginv1.WatchSyncAuthMethod{pluginv1.WatchSyncAuthMethod_WATCH_SYNC_AUTH_METHOD_DEVICE_CODE}, + ExportWatched: true, + ImportWatchlist: true, + ScrobblePlayback: true, + ImportRatings: true, + ExportRatings: true, SupportedMediaTypes: []pluginv1.WatchSyncMediaType{ pluginv1.WatchSyncMediaType_WATCH_SYNC_MEDIA_TYPE_MOVIE, pluginv1.WatchSyncMediaType_WATCH_SYNC_MEDIA_TYPE_EPISODE, + pluginv1.WatchSyncMediaType_WATCH_SYNC_MEDIA_TYPE_SERIES, }, MaxBatchSize: 25, }, @@ -30,9 +35,52 @@ func TestWatchSyncProviderDescriptorRoundTrip(t *testing.T) { t.Fatal(err) } got := decoded.GetWatchSyncProvider() + if got == nil || !got.GetExportWatched() || !got.GetImportWatchlist() || !got.GetScrobblePlayback() || got.GetMaxBatchSize() != 25 || + !got.GetImportRatings() || !got.GetExportRatings() || + len(got.GetAuthMethods()) != 1 || got.GetAuthMethods()[0] != pluginv1.WatchSyncAuthMethod_WATCH_SYNC_AUTH_METHOD_DEVICE_CODE || + len(got.GetSupportedMediaTypes()) != 3 || + got.GetSupportedMediaTypes()[1] != pluginv1.WatchSyncMediaType_WATCH_SYNC_MEDIA_TYPE_EPISODE || + got.GetSupportedMediaTypes()[2] != pluginv1.WatchSyncMediaType_WATCH_SYNC_MEDIA_TYPE_SERIES { + t.Fatalf("decoded descriptor = %#v", got) + } +} + +// A node on an older SDK must still load a descriptor that a newer SDK wrote +// to the shared database: unknown fields and enum names are dropped. +func TestDecodeCapabilityDiscardsUnknownWatchSyncData(t *testing.T) { + record := convert.CapabilityRecord{ + Type: "watch_sync_provider.v1", + ID: "anilist", + Metadata: map[string]any{ + "watch_sync_provider": map[string]any{ + "auth_methods": []any{"WATCH_SYNC_AUTH_METHOD_DEVICE_CODE"}, + "export_watched": true, + "max_batch_size": float64(25), + "supported_media_types": []any{"WATCH_SYNC_MEDIA_TYPE_MOVIE", "WATCH_SYNC_MEDIA_TYPE_FUTURE_KIND", "WATCH_SYNC_MEDIA_TYPE_EPISODE"}, + "import_future_thing": true, + }, + "config_schema": []any{map[string]any{"key": "token", "future_option": "x"}}, + }, + } + decoded, err := convert.DecodeCapability(record) + if err != nil { + t.Fatalf("DecodeCapability() error = %v", err) + } + got := decoded.GetWatchSyncProvider() if got == nil || !got.GetExportWatched() || got.GetMaxBatchSize() != 25 || - len(got.GetAuthMethods()) != 1 || len(got.GetSupportedMediaTypes()) != 2 || - got.GetSupportedMediaTypes()[1] != pluginv1.WatchSyncMediaType_WATCH_SYNC_MEDIA_TYPE_EPISODE { + len(got.GetAuthMethods()) != 1 || got.GetAuthMethods()[0] != pluginv1.WatchSyncAuthMethod_WATCH_SYNC_AUTH_METHOD_DEVICE_CODE { t.Fatalf("decoded descriptor = %#v", got) } + types := got.GetSupportedMediaTypes() + if len(types) != 2 || + types[0] != pluginv1.WatchSyncMediaType_WATCH_SYNC_MEDIA_TYPE_MOVIE || + types[1] != pluginv1.WatchSyncMediaType_WATCH_SYNC_MEDIA_TYPE_EPISODE { + t.Fatalf("supported media types = %v, want the known values only", types) + } + if len(got.ProtoReflect().GetUnknown()) != 0 { + t.Fatalf("unknown data retained: %x", got.ProtoReflect().GetUnknown()) + } + if schemas := decoded.GetConfigSchema(); len(schemas) != 1 || schemas[0].GetKey() != "token" { + t.Fatalf("config schema = %#v", schemas) + } } diff --git a/pkg/pluginsdk/imagevariant/imagevariant.go b/pkg/pluginsdk/imagevariant/imagevariant.go new file mode 100644 index 0000000..8c53f1b --- /dev/null +++ b/pkg/pluginsdk/imagevariant/imagevariant.go @@ -0,0 +1,52 @@ +// Package imagevariant exposes the canonical image-variant hints Prairie sends to +// plugins that resolve image paths into URLs. +// +// A variant is a semantic size hint, not an exact pixel contract: the sender +// says roughly how large the image will be rendered and the resolver picks the +// closest size its upstream offers. Variants travel as plain strings in +// ResolveImageURLRequest.variant, ResolveImageURLsRequest.variant, and +// ResolveCatalogImageURLsRequest.variant. An empty variant uses the receiver's +// default: the plugin default for the metadata-provider requests and the host +// default for the RuntimeHost request. +// +// Two contract rules govern the vocabulary: +// +// 1. The set is open and may grow additively. New variants are added between or +// beyond the existing bands as Prairie's clients gain new rendering surfaces; +// adding one is not a breaking change, so a plugin must never assume the +// constants in this package are exhaustive. +// 2. A plugin receiving an unknown variant MUST degrade gracefully to its +// nearest supported size (or its default) and MUST NOT return an error. A +// slightly wrong image size is always preferable to a failed resolve, which +// surfaces to users as a missing poster. +// +// The idiomatic implementation is a switch with a default arm that returns the +// plugin's fallback size, so unrecognized values fall through instead of failing. +package imagevariant + +const ( + // Card is the smallest band: dense grid and list thumbnails, search + // results, and cast rows. Targets roughly 300px-wide posters. + Card = "card" + + // Featured is the mid band: hero artwork, detail-page posters, and section + // headers. Targets roughly 500px-wide posters and logos and 1280px-wide + // backdrops. + Featured = "featured" + + // Large sits between Featured and Full for high-density displays and + // client-selected larger image sizes. Targets roughly 780px-wide posters + // and stills and 1280px-wide logos and backdrops. + Large = "large" + + // Full is the largest sized band, for full-screen and near-source + // rendering. Targets the largest sized rendition the upstream offers, + // falling back to the original asset when no sized rendition is large + // enough. + Full = "full" + + // Original requests the unresized source asset, with no host or plugin + // downscaling. It is also the conventional fallback for an empty or + // unrecognized variant. + Original = "original" +) diff --git a/pkg/pluginsdk/imagevariant/imagevariant_test.go b/pkg/pluginsdk/imagevariant/imagevariant_test.go new file mode 100644 index 0000000..ff2aab8 --- /dev/null +++ b/pkg/pluginsdk/imagevariant/imagevariant_test.go @@ -0,0 +1,21 @@ +package imagevariant + +import "testing" + +func TestCanonicalValues(t *testing.T) { + for _, tc := range []struct { + name string + got string + want string + }{ + {"Card", Card, "card"}, + {"Featured", Featured, "featured"}, + {"Large", Large, "large"}, + {"Full", Full, "full"}, + {"Original", Original, "original"}, + } { + if tc.got != tc.want { + t.Errorf("%s = %q, want %q", tc.name, tc.got, tc.want) + } + } +} diff --git a/pkg/pluginsdk/manifest/manifest.go b/pkg/pluginsdk/manifest/manifest.go index db39975..82aa28a 100644 --- a/pkg/pluginsdk/manifest/manifest.go +++ b/pkg/pluginsdk/manifest/manifest.go @@ -113,6 +113,12 @@ func Validate(manifest *pluginv1.PluginManifest) error { if err := validateWatchSyncCapability(capability); err != nil { return err } + if err := validateNetworkAccessCapability(capability); err != nil { + return err + } + if err := validateRequestRouterCapability(capability); err != nil { + return err + } } for _, schema := range manifest.GlobalConfigSchema { if err := validateConfigSchema(schema); err != nil { @@ -156,9 +162,18 @@ func validateWatchSyncCapability(descriptor *pluginv1.CapabilityDescriptor) erro return fmt.Errorf("plugin capability %q: watch sync auth method cannot be unspecified", descriptor.GetId()) } } - if !watchSync.GetExportWatched() && !watchSync.GetExportUnwatched() && !watchSync.GetImportWatched() && !watchSync.GetImportProgress() { + if !watchSync.GetExportWatched() && !watchSync.GetExportUnwatched() && + !watchSync.GetImportWatched() && !watchSync.GetImportProgress() && + !watchSync.GetImportFavorites() && !watchSync.GetExportFavorites() && + !watchSync.GetRemoveFavorites() && !watchSync.GetImportWatchlist() && + !watchSync.GetExportWatchlist() && !watchSync.GetRemoveWatchlist() && + !watchSync.GetScrobblePlayback() && + !watchSync.GetImportRatings() && !watchSync.GetExportRatings() { return fmt.Errorf("plugin capability %q: at least one watch sync operation is required", descriptor.GetId()) } + if watchSync.GetProvidesWatchlistOrder() && !watchSync.GetImportWatchlist() { + return fmt.Errorf("plugin capability %q: watchlist order requires watchlist import", descriptor.GetId()) + } if watchSync.GetMaxBatchSize() < 1 || watchSync.GetMaxBatchSize() > 100 { return fmt.Errorf("plugin capability %q: watch sync max_batch_size must be between 1 and 100", descriptor.GetId()) } @@ -170,6 +185,11 @@ func validateWatchSyncCapability(descriptor *pluginv1.CapabilityDescriptor) erro return fmt.Errorf("plugin capability %q: watch sync media type cannot be unspecified", descriptor.GetId()) } } + // supported_media_types is non-empty here; an empty list is rejected above. + if (watchSync.GetImportRatings() || watchSync.GetExportRatings()) && + !watchSyncSupportsRatedMediaType(watchSync.GetSupportedMediaTypes()) { + return fmt.Errorf("plugin capability %q: watch sync ratings require a MOVIE or SERIES supported media type", descriptor.GetId()) + } for _, namespace := range watchSync.GetExternalIdNamespaces() { if !watchSyncSlugPattern.MatchString(namespace) { return fmt.Errorf("plugin capability %q: invalid external id namespace %q", descriptor.GetId(), namespace) @@ -178,6 +198,49 @@ func validateWatchSyncCapability(descriptor *pluginv1.CapabilityDescriptor) erro return nil } +// watchSyncSupportsRatedMediaType reports whether a ratings-capable provider +// lists a media type the host can rate. A value this SDK does not define counts +// as rateable so that a descriptor built against a newer SDK stays valid here. +func watchSyncSupportsRatedMediaType(mediaTypes []pluginv1.WatchSyncMediaType) bool { + for _, mediaType := range mediaTypes { + switch mediaType { + case pluginv1.WatchSyncMediaType_WATCH_SYNC_MEDIA_TYPE_MOVIE, + pluginv1.WatchSyncMediaType_WATCH_SYNC_MEDIA_TYPE_SERIES: + return true + } + if _, known := pluginv1.WatchSyncMediaType_name[int32(mediaType)]; !known { + return true + } + } + return false +} + +func validateNetworkAccessCapability(descriptor *pluginv1.CapabilityDescriptor) error { + networkAccess := descriptor.GetNetworkAccessProvider() + if descriptor.GetType() != capability.NetworkAccessProvider { + if networkAccess != nil { + return fmt.Errorf("plugin capability %q: network_access_provider descriptor requires type %q", descriptor.GetId(), capability.NetworkAccessProvider) + } + return nil + } + if networkAccess == nil { + return fmt.Errorf("plugin capability %q: network_access_provider descriptor is required", descriptor.GetId()) + } + if !watchSyncSlugPattern.MatchString(networkAccess.GetProvider()) { + return fmt.Errorf("plugin capability %q: network access provider must be a path-safe lowercase slug", descriptor.GetId()) + } + return nil +} + +// validateRequestRouterCapability keeps the descriptor optional so request +// routers built before it existed stay valid. +func validateRequestRouterCapability(descriptor *pluginv1.CapabilityDescriptor) error { + if descriptor.GetType() != capability.RequestRouter && descriptor.GetRequestRouter() != nil { + return fmt.Errorf("plugin capability %q: request_router descriptor requires type %q", descriptor.GetId(), capability.RequestRouter) + } + return nil +} + // ValidateCatalogPresentation applies the stricter presentation contract used // by curated catalogs. The general manifest validator keeps presentation // optional so older and directly uploaded plugins remain compatible. diff --git a/pkg/pluginsdk/manifest/network_access_provider_test.go b/pkg/pluginsdk/manifest/network_access_provider_test.go new file mode 100644 index 0000000..bcda2de --- /dev/null +++ b/pkg/pluginsdk/manifest/network_access_provider_test.go @@ -0,0 +1,62 @@ +package manifest_test + +import ( + "testing" + + pluginv1 "github.com/prairie-server/prairie-plugin-sdk/pkg/pluginproto/prairie/plugin/v1" + publicmanifest "github.com/prairie-server/prairie-plugin-sdk/pkg/pluginsdk/manifest" +) + +func TestLoadNetworkAccessProvider(t *testing.T) { + raw := []byte(`{ + "plugin_id":"prairie.tailscale", "version":"1.0.0", "prairie_api_version":"v1", + "capabilities":[{ + "type":"network_access_provider.v1", "id":"tailscale", "display_name":"Tailscale", + "network_access_provider":{"provider":"tailscale","display_name":"Tailscale"} + }] + }`) + manifest, err := publicmanifest.Load(raw) + if err != nil { + t.Fatal(err) + } + descriptor := manifest.GetCapabilities()[0].GetNetworkAccessProvider() + if descriptor.GetProvider() != "tailscale" || descriptor.GetDisplayName() != "Tailscale" { + t.Fatalf("network access descriptor = %#v", descriptor) + } +} + +func TestValidateNetworkAccessProviderRejectsMissingDescriptor(t *testing.T) { + manifest := &pluginv1.PluginManifest{ + PluginId: "prairie.invalid", Version: "1.0.0", + Capabilities: []*pluginv1.CapabilityDescriptor{{Type: "network_access_provider.v1", Id: "overlay"}}, + } + if err := publicmanifest.Validate(manifest); err == nil { + t.Fatal("expected missing network access descriptor to fail") + } +} + +func TestValidateNetworkAccessProviderRejectsBadSlug(t *testing.T) { + manifest := &pluginv1.PluginManifest{ + PluginId: "prairie.invalid", Version: "1.0.0", + Capabilities: []*pluginv1.CapabilityDescriptor{{ + Type: "network_access_provider.v1", Id: "overlay", + NetworkAccessProvider: &pluginv1.NetworkAccessProviderDescriptor{Provider: "Tail Scale"}, + }}, + } + if err := publicmanifest.Validate(manifest); err == nil { + t.Fatal("expected provider slug with spaces and capitals to fail") + } +} + +func TestValidateNetworkAccessProviderRejectsDescriptorOnOtherType(t *testing.T) { + manifest := &pluginv1.PluginManifest{ + PluginId: "prairie.invalid", Version: "1.0.0", + Capabilities: []*pluginv1.CapabilityDescriptor{{ + Type: "scheduled_task.v1", Id: "task", + NetworkAccessProvider: &pluginv1.NetworkAccessProviderDescriptor{Provider: "tailscale"}, + }}, + } + if err := publicmanifest.Validate(manifest); err == nil { + t.Fatal("expected network access descriptor on a scheduled task to fail") + } +} diff --git a/pkg/pluginsdk/manifest/request_router_test.go b/pkg/pluginsdk/manifest/request_router_test.go new file mode 100644 index 0000000..8d8f19a --- /dev/null +++ b/pkg/pluginsdk/manifest/request_router_test.go @@ -0,0 +1,55 @@ +package manifest_test + +import ( + "testing" + + pluginv1 "github.com/prairie-server/prairie-plugin-sdk/pkg/pluginproto/prairie/plugin/v1" + publicmanifest "github.com/prairie-server/prairie-plugin-sdk/pkg/pluginsdk/manifest" +) + +func TestLoadRequestRouterDeclaresSeasonSupport(t *testing.T) { + raw := []byte(`{ + "plugin_id":"prairie.requests-arr", "version":"1.0.0", "prairie_api_version":"v1", + "capabilities":[{ + "type":"request_router.v1", "id":"arr", "display_name":"Arr", + "request_router":{"supports_seasons":true} + }] + }`) + manifest, err := publicmanifest.Load(raw) + if err != nil { + t.Fatal(err) + } + if !manifest.GetCapabilities()[0].GetRequestRouter().GetSupportsSeasons() { + t.Fatal("supports_seasons = false, want true") + } +} + +// A manifest written before the descriptor existed stays valid and declares no +// season support, so the host keeps today's whole-series behaviour. +func TestLoadRequestRouterWithoutDescriptorDeclaresNoSeasonSupport(t *testing.T) { + raw := []byte(`{ + "plugin_id":"prairie.requests-arr", "version":"1.0.0", "prairie_api_version":"v1", + "capabilities":[{"type":"request_router.v1", "id":"arr", "display_name":"Arr"}] + }`) + manifest, err := publicmanifest.Load(raw) + if err != nil { + t.Fatal(err) + } + capability := manifest.GetCapabilities()[0] + if capability.GetRequestRouter() != nil { + t.Fatalf("request router descriptor = %#v, want absent", capability.GetRequestRouter()) + } +} + +func TestValidateRequestRouterRejectsDescriptorOnOtherType(t *testing.T) { + manifest := &pluginv1.PluginManifest{ + PluginId: "prairie.invalid", Version: "1.0.0", + Capabilities: []*pluginv1.CapabilityDescriptor{{ + Type: "scheduled_task.v1", Id: "task", + RequestRouter: &pluginv1.RequestRouterDescriptor{SupportsSeasons: true}, + }}, + } + if err := publicmanifest.Validate(manifest); err == nil { + t.Fatal("expected request router descriptor on a scheduled task to fail") + } +} diff --git a/pkg/pluginsdk/manifest/watch_sync_provider_test.go b/pkg/pluginsdk/manifest/watch_sync_provider_test.go index 5123ad6..c2df05c 100644 --- a/pkg/pluginsdk/manifest/watch_sync_provider_test.go +++ b/pkg/pluginsdk/manifest/watch_sync_provider_test.go @@ -127,6 +127,143 @@ func TestValidateWatchSyncProviderAllowsUnknownFutureEnums(t *testing.T) { } } +func TestValidateWatchSyncProviderAllowsDeviceCodeAndListOnly(t *testing.T) { + manifest := validWatchSyncManifest() + descriptor := manifest.Capabilities[0].WatchSyncProvider + descriptor.AuthMethods = []pluginv1.WatchSyncAuthMethod{ + pluginv1.WatchSyncAuthMethod_WATCH_SYNC_AUTH_METHOD_DEVICE_CODE, + } + descriptor.ExportWatched = false + descriptor.ImportWatchlist = true + descriptor.ProvidesWatchlistOrder = true + if err := publicmanifest.Validate(manifest); err != nil { + t.Fatalf("device-code list provider should be valid: %v", err) + } +} + +func TestValidateWatchSyncProviderRejectsOrderWithoutImport(t *testing.T) { + manifest := validWatchSyncManifest() + manifest.Capabilities[0].WatchSyncProvider.ProvidesWatchlistOrder = true + if err := publicmanifest.Validate(manifest); err == nil { + t.Fatal("expected watchlist order without import to fail") + } +} + +func TestLoadWatchSyncProviderRatingsOnly(t *testing.T) { + raw := []byte(`{ + "plugin_id":"prairie.ratings", "version":"1.0.0", "prairie_api_version":"v1", + "capabilities":[{ + "type":"watch_sync_provider.v1", "id":"ratings", "display_name":"Ratings", + "watch_sync_provider":{ + "auth_methods":["WATCH_SYNC_AUTH_METHOD_API_KEY"], + "import_ratings":true, + "export_ratings":true, + "supported_media_types":["WATCH_SYNC_MEDIA_TYPE_MOVIE","WATCH_SYNC_MEDIA_TYPE_SERIES"], + "max_batch_size":10 + } + }] + }`) + manifest, err := publicmanifest.Load(raw) + if err != nil { + t.Fatalf("ratings-only provider should load: %v", err) + } + descriptor := manifest.GetCapabilities()[0].GetWatchSyncProvider() + if !descriptor.GetImportRatings() || !descriptor.GetExportRatings() || + len(descriptor.GetSupportedMediaTypes()) != 2 || + descriptor.GetSupportedMediaTypes()[1] != pluginv1.WatchSyncMediaType_WATCH_SYNC_MEDIA_TYPE_SERIES { + t.Fatalf("watch sync descriptor = %#v", descriptor) + } +} + +func TestValidateWatchSyncProviderAllowsRatingsOnly(t *testing.T) { + for _, tc := range []struct { + name string + importRatings bool + exportRatings bool + }{ + {name: "import", importRatings: true}, + {name: "export", exportRatings: true}, + } { + t.Run(tc.name, func(t *testing.T) { + manifest := validWatchSyncManifest() + descriptor := manifest.Capabilities[0].WatchSyncProvider + descriptor.ExportWatched = false + descriptor.ImportRatings = tc.importRatings + descriptor.ExportRatings = tc.exportRatings + if err := publicmanifest.Validate(manifest); err != nil { + t.Fatalf("ratings-only provider should be valid: %v", err) + } + }) + } +} + +func TestValidateWatchSyncProviderRejectsRatingsWithoutRateableMediaType(t *testing.T) { + for _, tc := range []struct { + name string + importRatings bool + exportRatings bool + }{ + {name: "import", importRatings: true}, + {name: "export", exportRatings: true}, + } { + t.Run(tc.name, func(t *testing.T) { + manifest := validWatchSyncManifest() + descriptor := manifest.Capabilities[0].WatchSyncProvider + descriptor.ImportRatings = tc.importRatings + descriptor.ExportRatings = tc.exportRatings + descriptor.SupportedMediaTypes = []pluginv1.WatchSyncMediaType{ + pluginv1.WatchSyncMediaType_WATCH_SYNC_MEDIA_TYPE_EPISODE, + } + if err := publicmanifest.Validate(manifest); err == nil { + t.Fatal("expected ratings with only EPISODE media to fail") + } + }) + } +} + +func TestValidateWatchSyncProviderAllowsEpisodeOnlyWithoutRatings(t *testing.T) { + manifest := validWatchSyncManifest() + manifest.Capabilities[0].WatchSyncProvider.SupportedMediaTypes = []pluginv1.WatchSyncMediaType{ + pluginv1.WatchSyncMediaType_WATCH_SYNC_MEDIA_TYPE_EPISODE, + } + if err := publicmanifest.Validate(manifest); err != nil { + t.Fatalf("episode-only provider without ratings should be valid: %v", err) + } +} + +func TestValidateWatchSyncProviderAllowsRatingsForRateableMediaTypes(t *testing.T) { + for _, tc := range []struct { + name string + mediaTypes []pluginv1.WatchSyncMediaType + }{ + {name: "series", mediaTypes: []pluginv1.WatchSyncMediaType{ + pluginv1.WatchSyncMediaType_WATCH_SYNC_MEDIA_TYPE_SERIES, + }}, + {name: "episode and series", mediaTypes: []pluginv1.WatchSyncMediaType{ + pluginv1.WatchSyncMediaType_WATCH_SYNC_MEDIA_TYPE_EPISODE, + pluginv1.WatchSyncMediaType_WATCH_SYNC_MEDIA_TYPE_SERIES, + }}, + {name: "episode and movie", mediaTypes: []pluginv1.WatchSyncMediaType{ + pluginv1.WatchSyncMediaType_WATCH_SYNC_MEDIA_TYPE_EPISODE, + pluginv1.WatchSyncMediaType_WATCH_SYNC_MEDIA_TYPE_MOVIE, + }}, + {name: "unknown future type", mediaTypes: []pluginv1.WatchSyncMediaType{ + pluginv1.WatchSyncMediaType(99), + }}, + } { + t.Run(tc.name, func(t *testing.T) { + manifest := validWatchSyncManifest() + descriptor := manifest.Capabilities[0].WatchSyncProvider + descriptor.ImportRatings = true + descriptor.ExportRatings = true + descriptor.SupportedMediaTypes = tc.mediaTypes + if err := publicmanifest.Validate(manifest); err != nil { + t.Fatalf("ratings provider should be valid: %v", err) + } + }) + } +} + func validWatchSyncManifest() *pluginv1.PluginManifest { return &pluginv1.PluginManifest{ PluginId: "prairie.valid", Version: "1.0.0", diff --git a/pkg/pluginsdk/runtime/capability_servers_compat_test.go b/pkg/pluginsdk/runtime/capability_servers_compat_test.go new file mode 100644 index 0000000..706fe09 --- /dev/null +++ b/pkg/pluginsdk/runtime/capability_servers_compat_test.go @@ -0,0 +1,22 @@ +package runtime + +// Compile-time guard for the CapabilityServers unkeyed composite literal +// shape. v0.12 through v0.15 had twelve fields; v0.16.0 appended +// NetworkAccessProvider as the thirteenth. Plugins should use keyed literals; +// any further optional servers belong behind additive registration APIs +// (see ServeManifestOption), not here. +var _ = CapabilityServers{ + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, +} diff --git a/pkg/pluginsdk/runtime/image_resolver_test.go b/pkg/pluginsdk/runtime/image_resolver_test.go index a10acf5..71226d9 100644 --- a/pkg/pluginsdk/runtime/image_resolver_test.go +++ b/pkg/pluginsdk/runtime/image_resolver_test.go @@ -4,9 +4,10 @@ import ( "context" "testing" + "google.golang.org/grpc" + pluginv1 "github.com/prairie-server/prairie-plugin-sdk/pkg/pluginproto/prairie/plugin/v1" runtime "github.com/prairie-server/prairie-plugin-sdk/pkg/pluginsdk/runtime" - "google.golang.org/grpc" ) type stubImageResolver struct{} diff --git a/pkg/pluginsdk/runtime/marker_provider_test.go b/pkg/pluginsdk/runtime/marker_provider_test.go index f7b3045..bf2d793 100644 --- a/pkg/pluginsdk/runtime/marker_provider_test.go +++ b/pkg/pluginsdk/runtime/marker_provider_test.go @@ -4,9 +4,10 @@ import ( "context" "testing" + "google.golang.org/grpc" + pluginv1 "github.com/prairie-server/prairie-plugin-sdk/pkg/pluginproto/prairie/plugin/v1" runtime "github.com/prairie-server/prairie-plugin-sdk/pkg/pluginsdk/runtime" - "google.golang.org/grpc" ) type stubMarkerProvider struct{} diff --git a/pkg/pluginsdk/runtime/network_access_provider_test.go b/pkg/pluginsdk/runtime/network_access_provider_test.go new file mode 100644 index 0000000..1cb86fb --- /dev/null +++ b/pkg/pluginsdk/runtime/network_access_provider_test.go @@ -0,0 +1,45 @@ +package runtime_test + +import ( + "testing" + + "github.com/hashicorp/go-plugin" + "google.golang.org/grpc" + + pluginv1 "github.com/prairie-server/prairie-plugin-sdk/pkg/pluginproto/prairie/plugin/v1" + runtime "github.com/prairie-server/prairie-plugin-sdk/pkg/pluginsdk/runtime" +) + +type stubNetworkAccessProvider struct { + pluginv1.UnimplementedNetworkAccessProviderServer +} + +func TestGRPCServerRegistersNetworkAccessProvider(t *testing.T) { + plugins := runtime.DefaultPluginSet(runtime.CapabilityServers{ + Runtime: stubRuntime{}, + NetworkAccessProvider: stubNetworkAccessProvider{}, + }) + p, ok := plugins[runtime.PluginSetName].(plugin.GRPCPlugin) + if !ok { + t.Fatalf("plugin type = %T, want plugin.GRPCPlugin", plugins[runtime.PluginSetName]) + } + srv := grpc.NewServer() + if err := p.GRPCServer(nil, srv); err != nil { + t.Fatalf("GRPCServer = %v, want nil", err) + } + if _, ok := srv.GetServiceInfo()["prairie.plugin.v1.NetworkAccessProvider"]; !ok { + t.Fatalf("NetworkAccessProvider service not registered; got %v", srv.GetServiceInfo()) + } +} + +func TestGRPCServerSkipsNilNetworkAccessProvider(t *testing.T) { + plugins := runtime.DefaultPluginSet(runtime.CapabilityServers{Runtime: stubRuntime{}}) + p := plugins[runtime.PluginSetName].(plugin.GRPCPlugin) + srv := grpc.NewServer() + if err := p.GRPCServer(nil, srv); err != nil { + t.Fatalf("GRPCServer = %v, want nil", err) + } + if _, ok := srv.GetServiceInfo()["prairie.plugin.v1.NetworkAccessProvider"]; ok { + t.Fatalf("NetworkAccessProvider registered without a server") + } +} diff --git a/pkg/pluginsdk/runtime/request_router_test.go b/pkg/pluginsdk/runtime/request_router_test.go new file mode 100644 index 0000000..a13bf00 --- /dev/null +++ b/pkg/pluginsdk/runtime/request_router_test.go @@ -0,0 +1,58 @@ +package runtime_test + +import ( + "context" + "net" + "slices" + "testing" + + "github.com/hashicorp/go-plugin" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/test/bufconn" + + pluginv1 "github.com/prairie-server/prairie-plugin-sdk/pkg/pluginproto/prairie/plugin/v1" + runtime "github.com/prairie-server/prairie-plugin-sdk/pkg/pluginsdk/runtime" +) + +type recordingRequestRouter struct { + pluginv1.UnimplementedRequestRouterServer + seasons chan []int32 +} + +func (r recordingRequestRouter) Fulfill(_ context.Context, req *pluginv1.FulfillRequest) (*pluginv1.FulfillResponse, error) { + r.seasons <- req.GetRequest().GetSeasons() + return &pluginv1.FulfillResponse{}, nil +} + +func TestRequestRouterReceivesRequestedSeasons(t *testing.T) { + router := recordingRequestRouter{seasons: make(chan []int32, 1)} + plugins := runtime.DefaultPluginSet(runtime.CapabilityServers{Runtime: stubRuntime{}, RequestRouter: router}) + p := plugins[runtime.PluginSetName].(plugin.GRPCPlugin) + srv := grpc.NewServer() + if err := p.GRPCServer(nil, srv); err != nil { + t.Fatalf("GRPCServer = %v, want nil", err) + } + listener := bufconn.Listen(1 << 20) + go func() { _ = srv.Serve(listener) }() + t.Cleanup(srv.Stop) + + conn, err := grpc.NewClient("passthrough:///bufnet", + grpc.WithContextDialer(func(ctx context.Context, _ string) (net.Conn, error) { return listener.DialContext(ctx) }), + grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + t.Fatalf("NewClient = %v", err) + } + t.Cleanup(func() { _ = conn.Close() }) + + _, err = pluginv1.NewRequestRouterClient(conn).Fulfill(context.Background(), &pluginv1.FulfillRequest{ + CapabilityId: "arr", + Request: &pluginv1.RequestDescriptor{MediaType: "series", Title: "Example", Seasons: []int32{0, 4}}, + }) + if err != nil { + t.Fatalf("Fulfill = %v", err) + } + if got := <-router.seasons; !slices.Equal(got, []int32{0, 4}) { + t.Fatalf("seasons = %v, want [0 4]", got) + } +} diff --git a/pkg/pluginsdk/runtime/runtime.go b/pkg/pluginsdk/runtime/runtime.go index 016cf7c..8388128 100644 --- a/pkg/pluginsdk/runtime/runtime.go +++ b/pkg/pluginsdk/runtime/runtime.go @@ -35,6 +35,10 @@ type CapabilityServers struct { AuthProvider pluginv1.AuthProviderServer HttpRoutes pluginv1.HttpRoutesServer WatchSyncProvider pluginv1.WatchSyncProviderServer + // NetworkAccessProvider was added in v0.16.0. Plugins declaring + // network_access_provider.v1 register it here; the host treats them as + // resident and starts them at boot. + NetworkAccessProvider pluginv1.NetworkAccessProviderServer } // Client wraps the gRPC connection to a plugin and provides typed accessors @@ -155,6 +159,10 @@ func (c *Client) WatchSyncDeviceAuthorization() pluginv1.WatchSyncDeviceAuthoriz return pluginv1.NewWatchSyncDeviceAuthorizationServiceClient(c.conn) } +func (c *Client) NetworkAccessProvider() pluginv1.NetworkAccessProviderClient { + return pluginv1.NewNetworkAccessProviderClient(c.conn) +} + type GRPCPlugin struct { plugin.Plugin Servers CapabilityServers @@ -216,6 +224,9 @@ func (p *GRPCPlugin) GRPCServer(broker *plugin.GRPCBroker, server *grpc.Server) if p.Servers.WatchSyncProvider != nil { pluginv1.RegisterWatchSyncProviderServer(server, p.Servers.WatchSyncProvider) } + if p.Servers.NetworkAccessProvider != nil { + pluginv1.RegisterNetworkAccessProviderServer(server, p.Servers.NetworkAccessProvider) + } return nil } @@ -251,28 +262,43 @@ func (s *pluginHostState) setBroker(b *plugin.GRPCBroker) { s.mu.Unlock() } +// setBrokerID records the host-assigned stream and dials it at once. +// +// The dial cannot wait for the first Host() call: go-plugin's broker keeps +// the connection info the host sent for a stream for only five seconds +// (GRPCBroker.timeoutWait), and the host sends it from its AcceptAndServe +// just before invoking BindHostBroker. A plugin whose first host call comes +// later than that, which is the normal case for a resident plugin that idles +// until an admin connects it, would find the stream expired and every +// Host() call would return nil for the life of the process. Dialing here +// pins the connection while the window is open; runtimehost calls then +// multiplex over it. func (s *pluginHostState) setBrokerID(id uint32) { s.mu.Lock() + defer s.mu.Unlock() s.brokerID = id // new stream id → drop any cached client s.client = nil - s.mu.Unlock() + s.dialLocked() } -func (s *pluginHostState) host() *runtimehost.Client { - s.mu.Lock() - defer s.mu.Unlock() - if s.client != nil { - return s.client - } - if s.broker == nil || s.brokerID == 0 { - return nil +// dialLocked connects to the bound stream if it has not been connected yet. +// The caller holds s.mu. +func (s *pluginHostState) dialLocked() { + if s.client != nil || s.broker == nil || s.brokerID == 0 { + return } conn, err := s.broker.Dial(s.brokerID) if err != nil { - return nil + return } s.client = runtimehost.NewClient(conn) +} + +func (s *pluginHostState) host() *runtimehost.Client { + s.mu.Lock() + defer s.mu.Unlock() + s.dialLocked() return s.client } @@ -284,8 +310,9 @@ func SetHostBrokerID(id uint32) { pluginHost.setBrokerID(id) } // Host returns a runtimehost.Client connected to the prairie host. Returns // nil before the host has invoked Runtime.BindHostBroker (i.e. very briefly -// during plugin startup) or if the broker dial fails. Capability handlers -// should treat nil as transient and either skip or surface a temporary error. +// during plugin startup) or if the broker dial failed at bind time. +// Capability handlers should treat nil as transient and either skip or +// surface a temporary error. // // The first successful call dials the host broker stream and caches the // *runtimehost.Client; later calls reuse the same client. diff --git a/pkg/pluginsdk/runtime/scan_source_test.go b/pkg/pluginsdk/runtime/scan_source_test.go index d644948..950616e 100644 --- a/pkg/pluginsdk/runtime/scan_source_test.go +++ b/pkg/pluginsdk/runtime/scan_source_test.go @@ -4,15 +4,18 @@ import ( "context" "testing" + "google.golang.org/grpc" + pluginv1 "github.com/prairie-server/prairie-plugin-sdk/pkg/pluginproto/prairie/plugin/v1" runtime "github.com/prairie-server/prairie-plugin-sdk/pkg/pluginsdk/runtime" - "google.golang.org/grpc" ) // stubRuntime satisfies the required Runtime server (GRPCServer rejects a nil // Runtime) via the generated forward-compatible stub, so no method holds a nil // embedded interface. -type stubRuntime struct{ pluginv1.UnimplementedRuntimeServer } +type stubRuntime struct { + pluginv1.UnimplementedRuntimeServer +} type stubScanSource struct{} diff --git a/pkg/pluginsdk/runtimehost/client_test.go b/pkg/pluginsdk/runtimehost/client_test.go index eaf8c7e..c7151b9 100644 --- a/pkg/pluginsdk/runtimehost/client_test.go +++ b/pkg/pluginsdk/runtimehost/client_test.go @@ -132,6 +132,11 @@ func (f *fakeServer) CallPluginHTTP(_ context.Context, r *pluginv1.CallPluginHTT } func dial(t *testing.T, srv *fakeServer) *grpc.ClientConn { + t.Helper() + return dialServer(t, srv) +} + +func dialServer(t *testing.T, srv pluginv1.RuntimeHostServer) *grpc.ClientConn { t.Helper() lis := bufconn.Listen(1024 * 1024) g := grpc.NewServer() @@ -231,6 +236,47 @@ func TestGetHostInfo_MapsResponse(t *testing.T) { if got.PublicBaseURL != "https://prairie.example" || got.PluginProxyBaseURL == "" { t.Fatalf("bad host info: %+v", got) } + if got.HostRole != "" || got.NodeID != 0 || got.IngressToken != "" || got.Listeners != nil { + t.Fatalf("pre-v0.16 host must map to zero values: %+v", got) + } + if got.Listener(runtimehost.ListenerAPI) != nil { + t.Fatal("Listener(api) on a host without listeners must be nil") + } +} + +func TestGetHostInfo_MapsNetworkAccessFields(t *testing.T) { + srv := &fakeServer{ + hostInfoResp: &pluginv1.GetHostInfoResponse{ + PublicBaseUrl: "https://prairie.example", + HostRole: "proxy", + HostName: "proxy-1", + NodeId: 3, + IngressToken: "tok", + Listeners: []*pluginv1.HostListener{ + {Name: "api", Address: "127.0.0.1:8090", DefaultPort: 443}, + }, + }, + } + conn := dial(t, srv) + c := runtimehost.NewClient(conn) + + got, err := c.GetHostInfo(context.Background()) + if err != nil { + t.Fatalf("GetHostInfo: %v", err) + } + if got.HostRole != runtimehost.HostRoleProxy || got.HostName != "proxy-1" || got.NodeID != 3 || got.IngressToken != "tok" { + t.Fatalf("bad host info: %+v", got) + } + want := []runtimehost.HostListener{{Name: "api", Address: "127.0.0.1:8090", DefaultPort: 443}} + if !reflect.DeepEqual(got.Listeners, want) { + t.Fatalf("listeners = %+v, want %+v", got.Listeners, want) + } + if l := got.Listener(runtimehost.ListenerAPI); l == nil || l.Address != "127.0.0.1:8090" { + t.Fatalf("Listener(api) = %+v", l) + } + if got.Listener(runtimehost.ListenerJellyfin) != nil { + t.Fatal("Listener(jellyfin) should be nil when not reported") + } } func TestListLibraries_PassesUserID(t *testing.T) { diff --git a/pkg/pluginsdk/runtimehost/host_info.go b/pkg/pluginsdk/runtimehost/host_info.go index f3ae329..a4c1f8c 100644 --- a/pkg/pluginsdk/runtimehost/host_info.go +++ b/pkg/pluginsdk/runtimehost/host_info.go @@ -6,10 +6,63 @@ import ( pluginv1 "github.com/prairie-server/prairie-plugin-sdk/pkg/pluginproto/prairie/plugin/v1" ) +// Host roles reported in HostInfo.HostRole. +const ( + HostRoleAPI = "api" + HostRoleProxy = "proxy" +) + +// Listener names reported in HostListener.Name. +const ( + ListenerAPI = "api" + ListenerJellyfin = "jellyfin" + ListenerABS = "abs" +) + +// HostInfo is the typed view of RuntimeHost.GetHostInfo. type HostInfo struct { PublicBaseURL string InternalBaseURL string PluginProxyBaseURL string + + // HostRole is "api" or "proxy". Empty on hosts that predate v0.16.0. + HostRole string + // HostName is the node name on proxies or the server name on the api host. + HostName string + // NodeID is stream_nodes.id on proxies and 0 on the api host. + NodeID int64 + // IngressToken is the per-process-start secret a network access provider + // stamps on proxied requests as X-Prairie-Ingress-Token. Never log or persist + // it; call GetHostInfo again after a restart. + IngressToken string + // Listeners are the local listeners a network access provider should + // expose. Always contains "api" on hosts that implement v0.16.0. + Listeners []HostListener +} + +// HostListener is one local listener the host asks a network access provider +// to expose on the overlay network. +type HostListener struct { + // Name is "api", "jellyfin", or "abs". + Name string + // Address is the loopback dial address, host:port. + Address string + // DefaultPort is the port the plugin should expose the listener on; zero + // means provider default. + DefaultPort int +} + +// Listener returns the named listener, or nil when the host did not report it. +func (h *HostInfo) Listener(name string) *HostListener { + if h == nil { + return nil + } + for i := range h.Listeners { + if h.Listeners[i].Name == name { + return &h.Listeners[i] + } + } + return nil } func (c *Client) GetHostInfo(ctx context.Context) (*HostInfo, error) { @@ -17,9 +70,24 @@ func (c *Client) GetHostInfo(ctx context.Context) (*HostInfo, error) { if err != nil { return nil, err } - return &HostInfo{ + info := &HostInfo{ PublicBaseURL: resp.GetPublicBaseUrl(), InternalBaseURL: resp.GetInternalBaseUrl(), PluginProxyBaseURL: resp.GetPluginProxyBaseUrl(), - }, nil + HostRole: resp.GetHostRole(), + HostName: resp.GetHostName(), + NodeID: resp.GetNodeId(), + IngressToken: resp.GetIngressToken(), + } + if listeners := resp.GetListeners(); len(listeners) > 0 { + info.Listeners = make([]HostListener, 0, len(listeners)) + for _, l := range listeners { + info.Listeners = append(info.Listeners, HostListener{ + Name: l.GetName(), + Address: l.GetAddress(), + DefaultPort: int(l.GetDefaultPort()), + }) + } + } + return info, nil } diff --git a/pkg/pluginsdk/runtimehost/instance_state.go b/pkg/pluginsdk/runtimehost/instance_state.go new file mode 100644 index 0000000..7e06674 --- /dev/null +++ b/pkg/pluginsdk/runtimehost/instance_state.go @@ -0,0 +1,130 @@ +package runtimehost + +import ( + "context" + "errors" + "fmt" + "time" + + pluginv1 "github.com/prairie-server/prairie-plugin-sdk/pkg/pluginproto/prairie/plugin/v1" +) + +// Instance state limits enforced by the host. The client checks them before +// the round trip so a plugin gets a clear error instead of a gRPC status. +const ( + MaxInstanceStateKeyBytes = 256 + MaxInstanceStateValueBytes = 256 << 10 + MaxInstanceStateKeys = 256 +) + +// ErrStateNotExist is returned by ReadState when no value is stored under the +// key. It plays the role of tailscale's ipn.ErrStateNotExist: callers compare +// with errors.Is. +var ErrStateNotExist = errors.New("runtimehost: instance state key does not exist") + +// DefaultInstanceStateTimeout bounds each InstanceStateStore round trip when +// the caller does not supply a context (ipn.StateStore has none). +const DefaultInstanceStateTimeout = 30 * time.Second + +// ReadInstanceState reads one key of the calling plugin instance's private +// state. It returns (nil, false, nil) when the key is absent. +func (c *Client) ReadInstanceState(ctx context.Context, key string) ([]byte, bool, error) { + if err := validateInstanceStateKey(key); err != nil { + return nil, false, err + } + resp, err := c.rpc.ReadInstanceState(ctx, &pluginv1.ReadInstanceStateRequest{Key: key}) + if err != nil { + return nil, false, err + } + if !resp.GetFound() { + return nil, false, nil + } + return append([]byte(nil), resp.GetValue()...), true, nil +} + +// WriteInstanceState stores value under key in the calling plugin instance's +// private state. The host encrypts the value and never returns it through any +// API. +func (c *Client) WriteInstanceState(ctx context.Context, key string, value []byte) error { + if err := validateInstanceStateKey(key); err != nil { + return err + } + if len(value) > MaxInstanceStateValueBytes { + return fmt.Errorf("runtimehost: instance state value for %q is %d bytes, limit %d", key, len(value), MaxInstanceStateValueBytes) + } + _, err := c.rpc.WriteInstanceState(ctx, &pluginv1.WriteInstanceStateRequest{Key: key, Value: value}) + return err +} + +func validateInstanceStateKey(key string) error { + if key == "" { + return fmt.Errorf("runtimehost: instance state key is required") + } + if len(key) > MaxInstanceStateKeyBytes { + return fmt.Errorf("runtimehost: instance state key is %d bytes, limit %d", len(key), MaxInstanceStateKeyBytes) + } + return nil +} + +// StateStore is the two-method shape of tailscale's ipn.StateStore, declared +// here so the SDK does not depend on tailscale. ReadState returns +// ErrStateNotExist for an absent key. +// +// ipn.StateStore takes ipn.StateKey (a named string type) rather than string, +// so Go will not accept an InstanceStateStore as an ipn.StateStore directly. +// A tsnet plugin wraps it with a few lines; see docs/network-access-provider.md. +type StateStore interface { + ReadState(key string) ([]byte, error) + WriteState(key string, value []byte) error +} + +// InstanceStateStore implements StateStore over the ReadInstanceState and +// WriteInstanceState RPCs. Every value lives in the host's encrypted +// per-instance scope (installation plus host), so each proxy node keeps its +// own node key without the plugin managing files. +type InstanceStateStore struct { + client *Client + timeout time.Duration +} + +// InstanceStateStore returns a store that scopes every round trip with +// DefaultInstanceStateTimeout. +func (c *Client) InstanceStateStore() *InstanceStateStore { + return &InstanceStateStore{client: c, timeout: DefaultInstanceStateTimeout} +} + +// WithTimeout returns a copy of the store using the given per-call timeout. +// A non-positive timeout disables the deadline. +func (s *InstanceStateStore) WithTimeout(timeout time.Duration) *InstanceStateStore { + return &InstanceStateStore{client: s.client, timeout: timeout} +} + +// ReadState returns the value stored under key or ErrStateNotExist. +func (s *InstanceStateStore) ReadState(key string) ([]byte, error) { + ctx, cancel := s.context() + defer cancel() + value, found, err := s.client.ReadInstanceState(ctx, key) + if err != nil { + return nil, err + } + if !found { + return nil, ErrStateNotExist + } + return value, nil +} + +// WriteState stores value under key. +func (s *InstanceStateStore) WriteState(key string, value []byte) error { + ctx, cancel := s.context() + defer cancel() + return s.client.WriteInstanceState(ctx, key, value) +} + +var _ StateStore = (*InstanceStateStore)(nil) + +func (s *InstanceStateStore) context() (context.Context, context.CancelFunc) { + if s.timeout <= 0 { + return context.WithCancel(context.Background()) + } + return context.WithTimeout(context.Background(), s.timeout) +} diff --git a/pkg/pluginsdk/runtimehost/instance_state_test.go b/pkg/pluginsdk/runtimehost/instance_state_test.go new file mode 100644 index 0000000..87c3981 --- /dev/null +++ b/pkg/pluginsdk/runtimehost/instance_state_test.go @@ -0,0 +1,140 @@ +package runtimehost_test + +import ( + "bytes" + "context" + "errors" + "strings" + "testing" + + pluginv1 "github.com/prairie-server/prairie-plugin-sdk/pkg/pluginproto/prairie/plugin/v1" + "github.com/prairie-server/prairie-plugin-sdk/pkg/pluginsdk/runtimehost" +) + +type instanceStateServer struct { + pluginv1.UnimplementedRuntimeHostServer + values map[string][]byte + statuses []*pluginv1.NetworkAccessStatus +} + +func newInstanceStateServer() *instanceStateServer { + return &instanceStateServer{values: map[string][]byte{}} +} + +func (s *instanceStateServer) ReadInstanceState(_ context.Context, req *pluginv1.ReadInstanceStateRequest) (*pluginv1.ReadInstanceStateResponse, error) { + value, ok := s.values[req.GetKey()] + if !ok { + return &pluginv1.ReadInstanceStateResponse{}, nil + } + return &pluginv1.ReadInstanceStateResponse{Value: value, Found: true}, nil +} + +func (s *instanceStateServer) WriteInstanceState(_ context.Context, req *pluginv1.WriteInstanceStateRequest) (*pluginv1.WriteInstanceStateResponse, error) { + s.values[req.GetKey()] = append([]byte(nil), req.GetValue()...) + return &pluginv1.WriteInstanceStateResponse{}, nil +} + +func (s *instanceStateServer) ReportNetworkAccessStatus(_ context.Context, req *pluginv1.ReportNetworkAccessStatusRequest) (*pluginv1.ReportNetworkAccessStatusResponse, error) { + s.statuses = append(s.statuses, req.GetStatus()) + return &pluginv1.ReportNetworkAccessStatusResponse{}, nil +} + +func dialInstanceState(t *testing.T, srv *instanceStateServer) *runtimehost.Client { + t.Helper() + return runtimehost.NewClient(dialServer(t, srv)) +} + +func TestInstanceState_RoundTrip(t *testing.T) { + srv := newInstanceStateServer() + c := dialInstanceState(t, srv) + ctx := context.Background() + + _, found, err := c.ReadInstanceState(ctx, "_machinekey") + if err != nil || found { + t.Fatalf("ReadInstanceState before write = found %v, err %v; want absent", found, err) + } + if err := c.WriteInstanceState(ctx, "_machinekey", []byte("secret")); err != nil { + t.Fatalf("WriteInstanceState: %v", err) + } + value, found, err := c.ReadInstanceState(ctx, "_machinekey") + if err != nil || !found || string(value) != "secret" { + t.Fatalf("ReadInstanceState after write = %q found %v err %v", value, found, err) + } + // An empty stored value is still found. + if err := c.WriteInstanceState(ctx, "empty", nil); err != nil { + t.Fatalf("WriteInstanceState empty: %v", err) + } + if _, found, err := c.ReadInstanceState(ctx, "empty"); err != nil || !found { + t.Fatalf("empty value: found %v err %v, want found", found, err) + } +} + +func TestInstanceState_EnforcesLimitsClientSide(t *testing.T) { + srv := newInstanceStateServer() + c := dialInstanceState(t, srv) + ctx := context.Background() + + if _, _, err := c.ReadInstanceState(ctx, ""); err == nil { + t.Fatal("empty key accepted") + } + longKey := strings.Repeat("k", runtimehost.MaxInstanceStateKeyBytes+1) + if err := c.WriteInstanceState(ctx, longKey, []byte("x")); err == nil { + t.Fatal("oversized key accepted") + } + big := bytes.Repeat([]byte("v"), runtimehost.MaxInstanceStateValueBytes+1) + if err := c.WriteInstanceState(ctx, "big", big); err == nil { + t.Fatal("oversized value accepted") + } + if len(srv.values) != 0 { + t.Fatalf("rejected writes reached the host: %v", srv.values) + } + limit := bytes.Repeat([]byte("v"), runtimehost.MaxInstanceStateValueBytes) + if err := c.WriteInstanceState(ctx, "limit", limit); err != nil { + t.Fatalf("value at the limit rejected: %v", err) + } +} + +func TestInstanceStateStore_ImplementsStateStoreShape(t *testing.T) { + srv := newInstanceStateServer() + c := dialInstanceState(t, srv) + var store runtimehost.StateStore = c.InstanceStateStore() + + if _, err := store.ReadState("_profiles"); !errors.Is(err, runtimehost.ErrStateNotExist) { + t.Fatalf("ReadState missing = %v, want ErrStateNotExist", err) + } + if err := store.WriteState("_profiles", []byte(`{}`)); err != nil { + t.Fatalf("WriteState: %v", err) + } + got, err := store.ReadState("_profiles") + if err != nil || string(got) != `{}` { + t.Fatalf("ReadState = %q, %v", got, err) + } + if err := c.InstanceStateStore().WithTimeout(0).WriteState("k", []byte("v")); err != nil { + t.Fatalf("WriteState without deadline: %v", err) + } +} + +func TestReportNetworkAccessStatus(t *testing.T) { + srv := newInstanceStateServer() + c := dialInstanceState(t, srv) + ctx := context.Background() + + if err := c.ReportNetworkAccessStatus(ctx, nil); err == nil { + t.Fatal("nil status accepted") + } + if err := c.ReportNetworkAccessStatus(ctx, &pluginv1.NetworkAccessStatus{}); err == nil { + t.Fatal("status without state accepted") + } + status := &pluginv1.NetworkAccessStatus{ + State: runtimehost.NetworkAccessStateConnected, + Hostname: "prairie.tail1234.ts.net", + Origin: "https://prairie.tail1234.ts.net", + DesiredConnected: true, + } + if err := c.ReportNetworkAccessStatus(ctx, status); err != nil { + t.Fatalf("ReportNetworkAccessStatus: %v", err) + } + if len(srv.statuses) != 1 || srv.statuses[0].GetHostname() != "prairie.tail1234.ts.net" || !srv.statuses[0].GetDesiredConnected() { + t.Fatalf("host received %v", srv.statuses) + } +} diff --git a/pkg/pluginsdk/runtimehost/network_access.go b/pkg/pluginsdk/runtimehost/network_access.go new file mode 100644 index 0000000..fc10d0b --- /dev/null +++ b/pkg/pluginsdk/runtimehost/network_access.go @@ -0,0 +1,32 @@ +package runtimehost + +import ( + "context" + "fmt" + + pluginv1 "github.com/prairie-server/prairie-plugin-sdk/pkg/pluginproto/prairie/plugin/v1" +) + +// NetworkAccessStatus state values. The vocabulary is open; hosts tolerate +// values they do not recognize. +const ( + NetworkAccessStateDisconnected = "disconnected" + NetworkAccessStateAwaitingAuthorization = "awaiting_authorization" + NetworkAccessStateConnecting = "connecting" + NetworkAccessStateConnected = "connected" + NetworkAccessStateError = "error" +) + +// ReportNetworkAccessStatus pushes a network access status change to the +// host. Call it on every state transition so the host does not have to poll. +// The host logs state transitions only; auth_url is never logged. +func (c *Client) ReportNetworkAccessStatus(ctx context.Context, status *pluginv1.NetworkAccessStatus) error { + if status == nil { + return fmt.Errorf("runtimehost: network access status is required") + } + if status.GetState() == "" { + return fmt.Errorf("runtimehost: network access status state is required") + } + _, err := c.rpc.ReportNetworkAccessStatus(ctx, &pluginv1.ReportNetworkAccessStatusRequest{Status: status}) + return err +} diff --git a/proto/prairie/plugin/v1/common.proto b/proto/prairie/plugin/v1/common.proto index ad6f358..515261a 100644 --- a/proto/prairie/plugin/v1/common.proto +++ b/proto/prairie/plugin/v1/common.proto @@ -5,6 +5,8 @@ package prairie.plugin.v1; option go_package = "github.com/prairie-server/prairie-plugin-sdk/pkg/pluginproto/prairie/plugin/v1;pluginv1"; import "google/protobuf/struct.proto"; +import "prairie/plugin/v1/network_access_provider.proto"; +import "prairie/plugin/v1/request_router.proto"; import "prairie/plugin/v1/watch_sync_provider.proto"; message SupportedPlatform { @@ -142,6 +144,16 @@ message CapabilityDescriptor { // of type "watch_sync_provider.v1". Keeping this typed lets the host build // connection and authorization UI without launching the plugin. WatchSyncProviderDescriptor watch_sync_provider = 10; + + // Typed network-access contract metadata. Only meaningful for capabilities + // of type "network_access_provider.v1". The host lists available overlay + // providers from this descriptor without launching the plugin. + NetworkAccessProviderDescriptor network_access_provider = 11; + + // Typed request-router contract metadata. Only meaningful for capabilities + // of type "request_router.v1". Optional: an absent descriptor declares no + // optional request-router features. + RequestRouterDescriptor request_router = 12; } // PluginPresentation describes a plugin for server operators and links them to diff --git a/proto/prairie/plugin/v1/metadata_provider.proto b/proto/prairie/plugin/v1/metadata_provider.proto index 726cc60..e5f0b07 100644 --- a/proto/prairie/plugin/v1/metadata_provider.proto +++ b/proto/prairie/plugin/v1/metadata_provider.proto @@ -255,6 +255,12 @@ message GetImagesResponse { message ResolveImageURLRequest { string path = 1; + // Semantic size hint, not an exact pixel contract. Canonical values, smallest + // to largest: "card", "featured", "large", "full", "original". Empty means + // the plugin default. The vocabulary is open and grows additively, so a + // plugin receiving an unknown variant must degrade gracefully to its nearest + // supported size and must not return an error. See + // pkg/pluginsdk/imagevariant for constants and the full rules. string variant = 2; } @@ -264,6 +270,12 @@ message ResolveImageURLResponse { message ResolveImageURLsRequest { repeated string paths = 1; + // Semantic size hint applied to every path in this request. Canonical values, + // smallest to largest: "card", "featured", "large", "full", "original". Empty + // means the plugin default. The vocabulary is open and grows additively, so a + // plugin receiving an unknown variant must degrade gracefully to its nearest + // supported size and must not return an error. See + // pkg/pluginsdk/imagevariant for constants and the full rules. string variant = 2; } diff --git a/proto/prairie/plugin/v1/network_access_provider.proto b/proto/prairie/plugin/v1/network_access_provider.proto new file mode 100644 index 0000000..7962547 --- /dev/null +++ b/proto/prairie/plugin/v1/network_access_provider.proto @@ -0,0 +1,72 @@ +syntax = "proto3"; + +package prairie.plugin.v1; + +option go_package = "github.com/prairie-server/prairie-plugin-sdk/pkg/pluginproto/prairie/plugin/v1;pluginv1"; + +// NetworkAccessProvider gives a Prairie deployment an overlay-network identity +// (Tailscale, NetBird, ...). The plugin owns the overlay client and a reverse +// proxy in front of the host listeners it learns from RuntimeHost.GetHostInfo; +// the host owns supervision, per-instance state storage, status aggregation, +// and the admin API. Plugins declaring this capability are resident: the host +// starts them at boot, restarts them on crash, and stops them last. +service NetworkAccessProvider { + // Connect brings the overlay identity up and starts proxying. It returns the + // status reached so far; enrollment may continue in the background and be + // reported through RuntimeHost.ReportNetworkAccessStatus. + rpc Connect(NetworkAccessConnectRequest) returns (NetworkAccessStatus); + // Disconnect tears the overlay listener down and clears desired_connected. + rpc Disconnect(NetworkAccessDisconnectRequest) returns (NetworkAccessStatus); + // GetStatus returns the current status without changing it. + rpc GetStatus(NetworkAccessGetStatusRequest) returns (NetworkAccessStatus); +} + +message NetworkAccessConnectRequest {} + +message NetworkAccessDisconnectRequest {} + +message NetworkAccessGetStatusRequest {} + +// NetworkAccessStatus is the provider's view of one plugin instance. Every +// host (API server, each proxy node) runs its own instance and reports its own +// status; the host aggregates them. +message NetworkAccessStatus { + // One of: disconnected | awaiting_authorization | connecting | connected | error. + string state = 1; + // Overlay DNS name, e.g. prairie.tail1234.ts.net. + string hostname = 2; + // scheme://host[:port] clients should use for the api listener. + string origin = 3; + // Overlay IP addresses assigned to this instance. + repeated string addresses = 4; + // One entry per exposed host listener (see RuntimeHost.GetHostInfo). + repeated NetworkAccessListener listeners = 5; + // Interactive enrollment URL. Only set while state is awaiting_authorization. + // Admin-only; the plugin must never log it. + string auth_url = 6; + // Human-readable failure detail when state is error. + string error = 7; + // Provider client version shown to admins, e.g. "tsnet 1.102.4". + string provider_version = 8; + // Plugin-owned intent that survives restarts. The plugin persists it in + // instance state and reconnects on start when true. + bool desired_connected = 9; +} + +// NetworkAccessListener maps one host listener onto its overlay origin. +message NetworkAccessListener { + // Listener name from RuntimeHost.GetHostInfo: api | jellyfin | abs. + string name = 1; + // Overlay origin serving that listener, scheme://host[:port]. + string origin = 2; +} + +// NetworkAccessProviderDescriptor is the typed manifest metadata for a +// network_access_provider.v1 capability. The host lists providers from it +// without launching the plugin. +message NetworkAccessProviderDescriptor { + // Stable provider slug, e.g. "tailscale", "netbird". Lowercase, path-safe. + string provider = 1; + // Operator-facing name, e.g. "Tailscale". + string display_name = 2; +} diff --git a/proto/prairie/plugin/v1/request_router.proto b/proto/prairie/plugin/v1/request_router.proto index 694074f..124e519 100644 --- a/proto/prairie/plugin/v1/request_router.proto +++ b/proto/prairie/plugin/v1/request_router.proto @@ -31,6 +31,27 @@ message RequestDescriptor { // on the downstream service (e.g. Seerr). Empty when the host cannot resolve it. string requester_email = 8; string requester_username = 9; + // seasons lists the requested season numbers of a series; 0 is Specials. + // Empty means the whole series, which is what every request meant before + // this field existed. Only set when media_type is "series". Plugins that + // declare RequestRouterDescriptor.supports_seasons must fulfil only these + // seasons; see that field for the full contract. + repeated int32 seasons = 10; +} + +// RequestRouterDescriptor is typed request_router.v1 contract metadata that the +// host reads from the manifest without launching the plugin. An absent +// descriptor means every flag is false, which is how the host treats plugins +// built before a flag existed. +message RequestRouterDescriptor { + // supports_seasons declares that Fulfill and CheckStatus honour + // RequestDescriptor.seasons. A declaring plugin acquires only the listed + // seasons; when the series already exists upstream it adds those seasons to + // what is already tracked and leaves other seasons unchanged. Repeating a + // request converges rather than duplicating work. The host sends a request + // for only the missing seasons of a series it already has only to plugins + // that set this flag, because other plugins would add the whole series. + bool supports_seasons = 1; } message RouterConnection { diff --git a/proto/prairie/plugin/v1/runtime_host.proto b/proto/prairie/plugin/v1/runtime_host.proto index 924ef6e..e99c2bb 100644 --- a/proto/prairie/plugin/v1/runtime_host.proto +++ b/proto/prairie/plugin/v1/runtime_host.proto @@ -6,6 +6,7 @@ option go_package = "github.com/prairie-server/prairie-plugin-sdk/pkg/pluginprot import "google/protobuf/struct.proto"; import "prairie/plugin/v1/common.proto"; +import "prairie/plugin/v1/network_access_provider.proto"; // RuntimeHost is implemented by the prairie host. Plugins dial it via the // go-plugin gRPC broker. See pkg/pluginsdk/runtimehost for a typed client. @@ -64,6 +65,21 @@ service RuntimeHost { // through the host control plane. Calls are treated as authenticated // service-to-service traffic, but never as admin traffic. rpc CallPluginHTTP(CallPluginHTTPRequest) returns (CallPluginHTTPResponse); + + // ReadInstanceState reads one key of the calling plugin instance's private + // state. The scope (installation plus host) is derived by the host and never + // supplied by the plugin. See docs/network-access-provider.md. + rpc ReadInstanceState(ReadInstanceStateRequest) returns (ReadInstanceStateResponse); + + // WriteInstanceState writes one key of the calling plugin instance's private + // state. The host stores values encrypted and never returns them through + // any API. Limits: key <= 256 bytes, value <= 256 KiB, <= 256 keys per scope. + rpc WriteInstanceState(WriteInstanceStateRequest) returns (WriteInstanceStateResponse); + + // ReportNetworkAccessStatus pushes a network_access_provider.v1 status + // change to the host so it does not have to poll. Plugins call it on every + // state transition; the host may still call GetStatus on demand. + rpc ReportNetworkAccessStatus(ReportNetworkAccessStatusRequest) returns (ReportNetworkAccessStatusResponse); } message PublishEventRequest { @@ -102,6 +118,33 @@ message GetHostInfoResponse { string public_base_url = 1; string internal_base_url = 2; string plugin_proxy_base_url = 3; + // Role of the process hosting this plugin instance: api | proxy. + string host_role = 4; + // Node name on proxies, or the server name on the api host. + string host_name = 5; + // stream_nodes.id on proxies; 0 on the api host. + int64 node_id = 6; + // Per-process-start secret that a network access provider stamps on + // proxied requests as X-Prairie-Ingress-Token. Treat as a credential: keep it + // in memory, never log or persist it, and re-read GetHostInfo after a + // restart because the host rotates it on every start. + string ingress_token = 7; + // Host listeners a network access provider should expose. Always contains + // "api"; "jellyfin" and "abs" appear when those listeners are enabled. + // Proxies expose only "api". + repeated HostListener listeners = 8; +} + +// HostListener is one local listener the host asks a network access provider +// to expose on the overlay network. +message HostListener { + // Listener name: api | jellyfin | abs. + string name = 1; + // Loopback dial address for the listener, host:port. + string address = 2; + // Port the plugin should expose this listener on (e.g. 443 for api under + // HTTPS, 8096 for jellyfin, 13378 for abs). Zero means provider default. + int32 default_port = 3; } message ListLibrariesRequest { @@ -232,9 +275,13 @@ message ResolveCatalogImageURLsRequest { // backdrop_url values returned by ListLibraryMedia. Paths are host-owned // opaque strings; plugins should pass them back unchanged. repeated string paths = 1; - // Optional image variant name understood by the host resolver. Empty uses the - // host default. Common values include "thumbnail" and "original"; unsupported - // variants are handled host-side. + // Optional semantic size hint understood by the host resolver, not an exact + // pixel contract. Canonical values, smallest to largest: "card", "featured", + // "large", "full", "original". Empty uses the host default. The vocabulary is + // open and grows additively; unsupported variants are degraded host-side to + // the nearest available size rather than erroring, and the same graceful + // degradation is required of plugins that resolve variants themselves. See + // pkg/pluginsdk/imagevariant for constants and the full rules. string variant = 2; } @@ -280,3 +327,30 @@ message CallPluginHTTPResponse { map headers = 2; bytes body = 3; } + +message ReadInstanceStateRequest { + // State key, <= 256 bytes. + string key = 1; +} + +message ReadInstanceStateResponse { + // Stored value; empty when found is false. + bytes value = 1; + // False when no value is stored under key in this instance's scope. + bool found = 2; +} + +message WriteInstanceStateRequest { + // State key, <= 256 bytes. + string key = 1; + // Value to store, <= 256 KiB. + bytes value = 2; +} + +message WriteInstanceStateResponse {} + +message ReportNetworkAccessStatusRequest { + NetworkAccessStatus status = 1; +} + +message ReportNetworkAccessStatusResponse {} diff --git a/proto/prairie/plugin/v1/watch_sync_provider.proto b/proto/prairie/plugin/v1/watch_sync_provider.proto index be43519..874d684 100644 --- a/proto/prairie/plugin/v1/watch_sync_provider.proto +++ b/proto/prairie/plugin/v1/watch_sync_provider.proto @@ -41,6 +41,10 @@ enum WatchSyncMediaType { WATCH_SYNC_MEDIA_TYPE_UNSPECIFIED = 0; WATCH_SYNC_MEDIA_TYPE_MOVIE = 1; WATCH_SYNC_MEDIA_TYPE_EPISODE = 2; + // A series-level item. In WatchSyncMedia, external_ids, title, and year + // describe the series itself; the series_* fields, season_number, + // episode_number, and absolute_episode_number are unused. + WATCH_SYNC_MEDIA_TYPE_SERIES = 3; } enum WatchSyncOperation { @@ -54,6 +58,10 @@ enum WatchSyncOperation { WATCH_SYNC_OPERATION_SCROBBLE_START = 7; WATCH_SYNC_OPERATION_SCROBBLE_PAUSE = 8; WATCH_SYNC_OPERATION_SCROBBLE_STOP = 9; + // Rating writes carry the desired value in WatchSyncEvent.rating. Both are + // convergent desired-state operations; see WatchSyncEvent.rating. + WATCH_SYNC_OPERATION_SET_RATING = 10; + WATCH_SYNC_OPERATION_REMOVE_RATING = 11; } enum WatchSyncRemoteStateKind { @@ -62,6 +70,7 @@ enum WatchSyncRemoteStateKind { WATCH_SYNC_REMOTE_STATE_KIND_PROGRESS = 2; WATCH_SYNC_REMOTE_STATE_KIND_FAVORITE = 3; WATCH_SYNC_REMOTE_STATE_KIND_WATCHLIST = 4; + WATCH_SYNC_REMOTE_STATE_KIND_RATING = 5; } enum WatchSyncDeviceAuthorizationStatus { @@ -117,6 +126,17 @@ message WatchSyncProviderDescriptor { // an incremental subset cannot define positions relative to omitted items. bool provides_watchlist_order = 15; bool scrobble_playback = 16; + // Ratings are integers from 1 to 10. Plugins convert between the provider's + // native scale and 1..10 by rounding half up and clamping to the valid range. + // The host sends rating events only for media types listed in + // supported_media_types, which must include MOVIE or SERIES when either + // ratings flag is set. + // + // When import_ratings is set, ListRemoteState returns RATING states. + bool import_ratings = 17; + // When export_ratings is set, ApplyEvents handles both SET_RATING and + // REMOVE_RATING; there is no separate flag for rating removal. + bool export_ratings = 18; } message WatchSyncProviderConfig { @@ -313,6 +333,12 @@ message WatchSyncEvent { // exist for incomplete playback, and completion_percent may be below 100 // when the host's configured watched threshold has been reached. bool completed = 14; + // Desired rating for SET_RATING, an integer from 1 to 10. Required for + // SET_RATING and ignored for every other operation; zero is never a valid + // rating. SET_RATING and REMOVE_RATING are convergent desired-state writes: + // resending the same value, or removing a rating that is already absent, + // must yield APPLIED or NO_CHANGE, never a fault. + int32 rating = 15; } message WatchSyncApplyEventsRequest { @@ -382,10 +408,22 @@ message WatchSyncRemoteListState { bool removed = 2; } +message WatchSyncRemoteRatingState { + // Integer rating from 1 to 10, converted from the provider's native scale. + // Required unless removed is true. + int32 rating = 1; + // Provider timestamp for when the rating was last set, when available. + google.protobuf.Timestamp rated_at = 2; + // True is an explicit tombstone for a rating cleared upstream. It is + // meaningful for incremental traversals, where omitting an item is not a + // deletion. + bool removed = 3; +} + message WatchSyncRemoteState { string provider_item_key = 1; - // Required except on an explicit favorite/watchlist tombstone whose - // provider_item_key identifies a record returned previously. + // Required except on an explicit favorite, watchlist, or rating tombstone + // whose provider_item_key identifies a record returned previously. WatchSyncMedia media = 2; // At least one typed state must be present. Both may be present when the // provider reports a completed play and a separate resume point. @@ -393,6 +431,7 @@ message WatchSyncRemoteState { WatchSyncRemoteProgressState progress = 4; WatchSyncRemoteListState favorite = 5; WatchSyncRemoteListState watchlist = 6; + WatchSyncRemoteRatingState rating = 7; } message WatchSyncListRemoteStateResponse { @@ -407,7 +446,8 @@ message WatchSyncListRemoteStateResponse { // incremental delta, so an absent item must not be interpreted as deletion. // The value must remain stable across every page in one traversal. A provider // advertising provides_watchlist_order must set this true for WATCHLIST - // traversals so the returned order is unambiguous. + // traversals so the returned order is unambiguous. In a complete RATING + // traversal, an item absent from the snapshot is unrated. bool complete_snapshot = 4; // Complete authoritative replacement. The host persists it before consuming // this page or fault; a persistence failure discards the page and cursor.