syntax = "proto3"; package orchestrator.v1; import "google/protobuf/timestamp.proto"; option go_package = "git.dev.alexdunmow.com/block/cms/orchestrator/internal/api/orchestrator/v1;orchestratorv1"; // PluginScopeService manages @scope namespaces. service PluginScopeService { rpc CreateScope(CreateScopeRequest) returns (CreateScopeResponse); rpc ListMyScopes(ListMyScopesRequest) returns (ListMyScopesResponse); rpc GetScope(GetScopeRequest) returns (GetScopeResponse); rpc ListMyPlugins(ListMyPluginsRequest) returns (ListMyPluginsResponse); } // PluginRegistryService is the read-side public API plus CreatePlugin. service PluginRegistryService { rpc CreatePlugin(CreatePluginRequest) returns (CreatePluginResponse); rpc GetPlugin(GetPluginRequest) returns (GetPluginResponse); rpc ListPlugins(ListPluginsRequest) returns (ListPluginsResponse); rpc GetVersion(GetVersionRequest) returns (GetVersionResponse); rpc ResolveInstall(ResolveInstallRequest) returns (ResolveInstallResponse); rpc ListCategories(ListCategoriesRequest) returns (ListCategoriesResponse); rpc ListTags(ListTagsRequest) returns (ListTagsResponse); rpc SubmitForReview(SubmitForReviewRequest) returns (SubmitForReviewResponse); // Private-plugin RPCs. All require the caller to be a member of the target // account; the server resolves account membership from the bearer token. rpc ListPrivatePlugins(ListPrivatePluginsRequest) returns (ListPrivatePluginsResponse); rpc DeletePrivatePlugin(DeletePrivatePluginRequest) returns (DeletePrivatePluginResponse); rpc DeletePrivatePluginVersion(DeletePrivatePluginVersionRequest) returns (DeletePrivatePluginVersionResponse); rpc ListPrivatePluginInstallSites(ListPrivatePluginInstallSitesRequest) returns (ListPrivatePluginInstallSitesResponse); // Instance-authenticated variants used by the CMS install/update flow. A CMS // instance calls these with its per-account X-CMS-Secret header plus its // instance_id; the server resolves the owning account from the instance and // authorises private plugins owned by that account. No end-user JWT is // required (the calling instance, not a user, is the trust unit). rpc ListPrivatePluginsForInstance(ListPrivatePluginsForInstanceRequest) returns (ListPrivatePluginsResponse); rpc ResolveInstallForInstance(ResolveInstallForInstanceRequest) returns (ResolveInstallResponse); } // PluginModerationService is the superadmin-only side: lists pending submissions // and decides them. The lifecycle is: scope member CreatePlugin → PublishVersion(s) // → SubmitForReview → admin Approve / Reject / RequestChanges. service PluginModerationService { rpc ListPendingReviews(ListPendingReviewsRequest) returns (ListPendingReviewsResponse); rpc ApproveSubmission(ApproveSubmissionRequest) returns (ApproveSubmissionResponse); rpc RejectSubmission(RejectSubmissionRequest) returns (RejectSubmissionResponse); rpc RequestChanges(RequestChangesRequest) returns (RequestChangesResponse); } // PluginPublishService is called by the ninja CLI to publish a version. service PluginPublishService { rpc PublishVersion(PublishVersionRequest) returns (PublishVersionResponse); } // PluginAuthService handles OAuth device flow used by ninja CLI. service PluginAuthService { rpc StartDevice(StartDeviceRequest) returns (StartDeviceResponse); rpc PollDevice(PollDeviceRequest) returns (PollDeviceResponse); rpc ApproveDevice(ApproveDeviceRequest) returns (ApproveDeviceResponse); rpc DenyDevice(DenyDeviceRequest) returns (DenyDeviceResponse); rpc GetDeviceStatus(GetDeviceStatusRequest) returns (GetDeviceStatusResponse); rpc Whoami(WhoamiRequest) returns (WhoamiResponse); // ListMyAccounts is the CLI-facing account picker, returning the bare // minimum needed for `ninja login` / `ninja account list`. The CLI in // core/cmd/ninja consumes only this proto file; pulling in accounts.proto // and AccountService would force core to also generate bindings for the // full Account message and collide with the orchestrator's own copy at // proto-registration time. rpc ListMyAccountsForCLI(ListMyAccountsForCLIRequest) returns (ListMyAccountsForCLIResponse); } // --- Shared messages --- message Scope { string id = 1; string slug = 2; string display_name = 3; google.protobuf.Timestamp created_at = 4; } // PluginVisibility is the lifecycle/access state of a plugin in the registry. // PRIVATE plugins are scoped to a single account; PUBLIC plugins are visible // to all. UNDER_REVIEW / REJECTED / TAKEN_DOWN are public-registry moderation // states and do not apply to private plugins. enum PluginVisibility { PLUGIN_VISIBILITY_UNSPECIFIED = 0; PLUGIN_VISIBILITY_PRIVATE = 1; PLUGIN_VISIBILITY_UNDER_REVIEW = 2; PLUGIN_VISIBILITY_PUBLIC = 3; PLUGIN_VISIBILITY_REJECTED = 4; PLUGIN_VISIBILITY_TAKEN_DOWN = 5; } message Plugin { string id = 1; string scope_slug = 2; string name = 3; PluginVisibility visibility = 4; bool premium = 5; string description = 6; string homepage_url = 7; repeated string categories = 8; google.protobuf.Timestamp updated_at = 9; string kind = 10; string display_name = 11; // owner_account_id is set for private plugins and identifies the account // that owns the plugin. Empty for public plugins. string owner_account_id = 12; // latest_version is the most recent non-yanked version string published // for this plugin (e.g. "0.2.3"). Empty when the plugin has no versions // yet. Computed server-side as a LATERAL lookup on registry_versions so // listing endpoints don't N+1. string latest_version = 13; repeated string tags = 14; // preview_image_url is the PUBLIC, unsigned, long-lived image URL for this // plugin's latest non-yanked version preview (a PNG screenshot), usable // directly as an . Empty when no preview has been published yet — // the wizard card then degrades to display_name + tags (spec §5.2). string preview_image_url = 15; } message Category { string slug = 1; string display_name = 2; string description = 3; int32 sort_order = 4; } message Version { string id = 1; string plugin_id = 2; string version = 3; google.protobuf.Timestamp published_at = 6; bool yanked = 7; string sdk_constraint = 8; string source_archive_sha256 = 9; int64 source_archive_size = 10; // preview_image_url is the public unsigned URL of this specific version's // preview PNG, or empty when this version shipped no preview.png. string preview_image_url = 11; // abi_version is the wasm ABI major version this artifact was built against // (WO-WZ-011). 0 means a legacy source archive (compiled in-container), not a // .bnp. Instances filter incompatible artifacts on this before downloading. uint32 abi_version = 12; } message Requirement { string scope_slug = 1; string name = 2; string constraint_expr = 3; } // --- Scope service --- message CreateScopeRequest { string slug = 1; string display_name = 2; } message CreateScopeResponse { Scope scope = 1; } message ListMyScopesRequest {} message ListMyScopesResponse { repeated Scope scopes = 1; } message GetScopeRequest { string slug = 1; } message GetScopeResponse { Scope scope = 1; repeated Plugin plugins = 2; } message ListMyPluginsRequest {} message ListMyPluginsResponse { repeated Plugin plugins = 1; } // --- Registry service --- message CreatePluginRequest { string scope_slug = 1; string name = 2; string description = 3; string kind = 4; repeated string categories = 5; string display_name = 6; // visibility is the requested visibility of the plugin. UNSPECIFIED defers // to the registry's default (public for explicit scopes, private for the // "@private" sentinel). PluginVisibility visibility = 7; // active_account_id is required when visibility = PRIVATE; the server // verifies the caller is a member of this account before creating the // private plugin under it. string active_account_id = 8; } message CreatePluginResponse { Plugin plugin = 1; } message GetPluginRequest { string scope_slug = 1; string name = 2; // active_account_id is required when scope_slug = "private" so the server // can resolve (account_id, name) instead of (scope_id, name). Ignored // otherwise. string active_account_id = 3; } message GetPluginResponse { Plugin plugin = 1; repeated Version versions = 2; map channels = 3; } message ListPluginsRequest { int32 limit = 1; int32 offset = 2; string query = 3; string kind = 4; repeated string categories = 5; repeated string tags = 6; } message ListPluginsResponse { repeated Plugin plugins = 1; } message ListCategoriesRequest {} message ListCategoriesResponse { repeated Category categories = 1; } message ListTagsRequest { // Optional kind filter: "plugin", "theme", or "" for both. Lets the browse // UI show theme-specific tags when the user filters by kind=theme. string kind = 1; // Cap the response. 0 → server default (100). int32 limit = 2; } message ListTagsResponse { repeated TagCount tags = 1; } message TagCount { string tag = 1; int32 count = 2; } // --- Private plugin RPC messages --- message ListPrivatePluginsRequest { // account_id selects which account's private plugins to list. The caller // must be a member of this account. string account_id = 1; } message ListPrivatePluginsResponse { repeated PrivatePluginSummary plugins = 1; } // PrivatePluginSummary is the row shape used by the publisher dashboard and // by the CMS "Private" installer tab. It bundles a Plugin with the latest // version per channel and an installation count. message PrivatePluginSummary { Plugin plugin = 1; // channel_versions maps channel name -> latest version string published on // that channel (e.g. "latest" -> "0.3.0", "beta" -> "0.4.0-beta.1"). map channel_versions = 2; // installed_site_count is the number of CMS sites in the owning account // that currently have any version of this plugin installed. Used to gate // delete-plugin in the dashboard. int32 installed_site_count = 3; } message DeletePrivatePluginRequest { string account_id = 1; string plugin_name = 2; } message DeletePrivatePluginResponse {} message DeletePrivatePluginVersionRequest { string account_id = 1; string plugin_name = 2; string version = 3; } message DeletePrivatePluginVersionResponse {} message ListPrivatePluginInstallSitesRequest { string account_id = 1; string plugin_name = 2; } message ListPrivatePluginInstallSitesResponse { repeated PrivatePluginInstallSite sites = 1; } message PrivatePluginInstallSite { string instance_id = 1; string site_display_name = 2; string installed_version = 3; string pinned_channel = 4; } message GetVersionRequest { string scope_slug = 1; string plugin_name = 2; string version = 3; } message GetVersionResponse { Version version = 1; repeated Requirement requires = 2; string readme_md = 3; string changelog_md = 4; } message ResolveInstallRequest { string scope_slug = 1; string plugin_name = 2; string version_or_channel = 3; // active_account_id is required when scope_slug = "private" so the server // resolves (account_id, name) and verifies the caller's account // membership. Ignored for public scopes. string active_account_id = 4; } message ResolveInstallResponse { string version_id = 1; string scope_slug = 2; string plugin_name = 3; string version = 4; string channel_pinned = 5; string archive_url = 6; string archive_sha256 = 7; string sdk_constraint = 8; repeated Requirement requires = 9; bool yanked = 10; repeated string warnings = 11; // abi_version is the wasm ABI major of the resolved artifact (WO-WZ-011); 0 // for a legacy source archive. The installing instance rejects an artifact // whose abi_version its loader cannot run. uint32 abi_version = 12; } // --- Instance-authenticated request shapes --- // The instance proves its identity with the per-account X-CMS-Secret header; // instance_id selects the instance whose owning account scopes the call. No // active_account_id / user JWT is needed. These cover only private plugins — // public plugins use the anonymous ListPlugins / ResolveInstall. message ListPrivatePluginsForInstanceRequest { string instance_id = 1; } message ResolveInstallForInstanceRequest { string instance_id = 1; string plugin_name = 2; string version_or_channel = 3; } // --- Review / moderation --- message PendingReview { string review_id = 1; string plugin_id = 2; string scope_slug = 3; string plugin_name = 4; string plugin_display_name = 5; string submitted_by_user_id = 6; google.protobuf.Timestamp submitted_at = 7; } message SubmitForReviewRequest { string plugin_id = 1; } message SubmitForReviewResponse { // visibility is "under_review" for non-admin authors, "public" when a superadmin // submits (they bypass the queue entirely). String form is preserved here for // backwards compatibility with existing handlers; the canonical enum is // PluginVisibility on the Plugin message. string visibility = 1; } message ListPendingReviewsRequest { int32 limit = 1; int32 offset = 2; } message ListPendingReviewsResponse { repeated PendingReview reviews = 1; } message ApproveSubmissionRequest { string plugin_id = 1; string reason = 2; // optional note for audit log } message ApproveSubmissionResponse {} message RejectSubmissionRequest { string plugin_id = 1; string reason = 2; // required } message RejectSubmissionResponse {} message RequestChangesRequest { string plugin_id = 1; string reason = 2; // required — author sees this verbatim } message RequestChangesResponse {} // --- Publish service --- message PublishVersionRequest { string plugin_id = 1; string version = 2; string channel = 3; bytes archive = 4; string readme_md = 5; string changelog_md = 6; } message PublishVersionResponse { Version version = 1; string archive_url = 2; repeated string warnings = 3; } // --- Auth service (device flow) --- message StartDeviceRequest { repeated string scopes = 1; } message StartDeviceResponse { string device_code = 1; string user_code = 2; string verification_uri = 3; int32 interval_seconds = 4; int32 expires_in_seconds = 5; } message PollDeviceRequest { string device_code = 1; } message PollDeviceResponse { string access_token = 1; string status = 2; } message ApproveDeviceRequest { string user_code = 1; } message ApproveDeviceResponse {} message DenyDeviceRequest { string user_code = 1; } message DenyDeviceResponse {} message GetDeviceStatusRequest { string user_code = 1; } message GetDeviceStatusResponse { bool valid = 1; bool authorized = 2; bool denied = 3; string client_id = 4; } message WhoamiRequest {} message WhoamiResponse { string user_id = 1; string email = 2; string display_name = 3; } // MyAccount is the slim account row returned to the CLI by // PluginAuthService.ListMyAccounts. The orchestrator's full Account message // lives in accounts.proto and carries billing, plan, status, etc.; CLI // account-switching needs none of that, so this shape stays minimal and // avoids forcing core to generate bindings for accounts.proto. message MyAccount { string id = 1; string slug = 2; string name = 3; } message ListMyAccountsForCLIRequest {} message ListMyAccountsForCLIResponse { repeated MyAccount accounts = 1; }