From ada314b264b7a95ea9e75f1d35ed3992bb8b77c7 Mon Sep 17 00:00:00 2001 From: Julian Coy Date: Tue, 23 Jun 2026 16:50:19 -0400 Subject: [PATCH] Regenerate registry API snapshot with organizations, impersonation, migration batches, private facets, and admin event-health endpoints --- packages/engine/src/registry/fixtures.ts | 4 + .../src/registry/generated/registry-api.ts | 1215 ++++++++++++++++- .../engine/src/registry/openapi.snapshot.yaml | 1020 +++++++++++++- 3 files changed, 2136 insertions(+), 103 deletions(-) diff --git a/packages/engine/src/registry/fixtures.ts b/packages/engine/src/registry/fixtures.ts index 56ef57f0..391dc9bd 100644 --- a/packages/engine/src/registry/fixtures.ts +++ b/packages/engine/src/registry/fixtures.ts @@ -35,6 +35,8 @@ export function versionMetadata(overrides?: Partial): Wire manifest_json: '{}', published_at: '2026-05-01T00:00:00Z', publisher: 'test-publisher', + owner: { kind: 'user', username: 'test-publisher' }, + visibility: 'public', size_bytes: 100, asset_counts: assetCounts({ commands: 1 }), ...overrides, @@ -51,6 +53,8 @@ export function facetSummary(overrides?: Partial): WirePack latest_version: '0.1.0', published_at: '2026-05-01T00:00:00Z', publisher: 'test-publisher', + owner: { kind: 'user', username: 'test-publisher' }, + visibility: 'public', asset_counts: assetCounts(), ...overrides, } diff --git a/packages/engine/src/registry/generated/registry-api.ts b/packages/engine/src/registry/generated/registry-api.ts index 2c9d62a0..da8e39ff 100644 --- a/packages/engine/src/registry/generated/registry-api.ts +++ b/packages/engine/src/registry/generated/registry-api.ts @@ -41,6 +41,26 @@ export interface paths { patch?: never; trace?: never; }; + "/v0/facets/count": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Public facet count + * @description Returns the projected count of live public facets. The count is event-backed and eventually consistent; it is never derived from facet search and never reveals private facet counts. + */ + get: operations["getV0FacetsCount"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/v0/facets/{scope}/{name}/latest-version": { parameters: { query?: never; @@ -170,7 +190,7 @@ export interface paths { }; /** * Get the latest version pointer for a facet - * @description Returns the current latest version string for a facet. Short-cached (s-maxage=30) so CloudFront absorbs traffic; all downstream reads key off the concrete version it returns. + * @description Returns the caller-relative latest version string for a facet — the highest version the caller is authorized to read. Downstream reads key off the concrete version it returns. */ get: operations["getV0FacetsByNameLatestVersion"]; put?: never; @@ -190,7 +210,7 @@ export interface paths { }; /** * Get the full version list for a facet - * @description Returns the complete sorted version list. The :version in the path is the cache key (expected to be the current latest version). A concrete version gets immutable caching + ETag; the literal `latest` resolves server-side and is not cached. + * @description Returns the sorted list of versions the caller is authorized to see. Responses are `no-store`: the visible set is caller-relative, so a list cached for one caller must not be served to another. */ get: operations["getV0FacetsByNameVersionsByVersion"]; put?: never; @@ -210,7 +230,7 @@ export interface paths { }; /** * Get metadata for a specific version - * @description Returns the version row's metadata including the verbatim manifestJson string and the verified publisher username. version may be `latest` to resolve through FACET META's `latest_version` attribute. Because (name, version) is immutable, a concrete-version response is cacheable for a year with a strong ETag derived from the verified content fingerprint; conditional If-None-Match requests get a 304. `latest` resolves server-side and is not cached. + * @description Returns the version row's metadata including the verbatim manifestJson string and the verified publisher username. `version` may be `latest`, which resolves to the highest version the caller is authorized to read. A concrete public version is immutable and identical for every caller, so it is long-cacheable; a private version is `no-store` because each read is authorized at request time, as is a `latest` resolution. */ get: operations["getV0FacetsByNameByVersion"]; put?: never; @@ -250,7 +270,7 @@ export interface paths { }; /** * Get the verified bodies of a version's resources - * @description Returns the verified body of each skill, agent, and command in the version. Because (name, version) is immutable, a concrete-version response is cacheable for a year with a strong ETag derived from the verified content fingerprint; conditional If-None-Match requests get a 304. `latest` resolves server-side and is not cached. + * @description Returns the verified body of each skill, agent, and command in the version. A concrete public version is immutable and identical for every caller, so it is long-cacheable; a private version is `no-store` because each read is authorized at request time, as is a `latest` resolution. */ get: operations["getV0FacetsByNameByVersionContents"]; put?: never; @@ -405,6 +425,26 @@ export interface paths { patch?: never; trace?: never; }; + "/v0/auth/impersonation": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + /** + * Stop impersonating + * @description Stateless no-op that returns 204. Impersonation holds no server state; the client stops by dropping the impersonation header and stored target metadata. + */ + delete: operations["deleteV0AuthImpersonation"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/v0/onboarding/username": { parameters: { query?: never; @@ -465,6 +505,66 @@ export interface paths { patch?: never; trace?: never; }; + "/v0/admin/migrations/batches/latest-once": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Start a "migrate to latest" batch (admin) + * @description Runs every pending one-time migration as a dry run followed by a real run, in dependency order, stopping on the first failure. Refuses to start if a migration or batch is already in progress. + */ + post: operations["postV0AdminMigrationsBatchesLatestOnce"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v0/admin/migrations/batches/{batchId}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get a migration batch (admin) + * @description Returns a batch by id for polling: status, progress, and references to the current/failed run. + */ + get: operations["getV0AdminMigrationsBatchesByBatchId"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v0/admin/migrations/batches/{batchId}/cancel": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Cancel a running migration batch (admin) + * @description Marks the batch failed (cancelled), stops the orchestrator execution (live stages), and force-releases the global migration lease so a stopped run does not block migrations until its lease expires. A non-running batch returns its current state unchanged. + */ + post: operations["postV0AdminMigrationsBatchesByBatchIdCancel"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/v0/admin/migrations/{id}/runs": { parameters: { query?: never; @@ -577,6 +677,46 @@ export interface paths { patch?: never; trace?: never; }; + "/v0/admin/users": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Search users by username prefix + * @description Bounded username-prefix search over the GSI3 USER_ALL# partition. Admin-gated, so the response carries email, tier, and suspension status. An empty `q` returns no users. + */ + get: operations["getV0AdminUsers"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v0/admin/users/{id}/impersonation": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Start impersonating a user + * @description Validates that the target exists and is not an admin, then returns the non-secret metadata the UI stores to drive impersonation. No token is minted — every subsequent request re-authorizes from the admin's live JWT plus the X-Facet-Impersonate-User-Id header. + */ + post: operations["postV0AdminUsersByIdImpersonation"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/v0/admin/users/{id}/suspension": { parameters: { query?: never; @@ -601,6 +741,26 @@ export interface paths { patch?: never; trace?: never; }; + "/v0/admin/event-health": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Event delivery health + * @description Product-event delivery health: pending backlog size, oldest-pending age, and the full unresolved dead-letter set. Admin only. + */ + get: operations["getV0AdminEventHealth"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/v0/settings/review-queue": { parameters: { query?: never; @@ -641,6 +801,167 @@ export interface paths { patch?: never; trace?: never; }; + "/v0/organizations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** List the caller's organizations and invitations */ + get: operations["getV0Organizations"]; + put?: never; + /** + * Create or claim an organization + * @description Auto-creates the organization when the slug is unprotected and within the per-user claim budget (1/24h, 5/30d); otherwise queues the claim for admin review. Reserves the matching @ scope and makes the caller the founding Admin. + */ + post: operations["postV0Organizations"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v0/organizations/{slug}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get an organization and the caller role */ + get: operations["getV0OrganizationsBySlug"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v0/organizations/{slug}/lookup-user": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Search users by username prefix for an organization invite + * @description Returns up to 10 users whose username begins with the query, across all users. The caller must be an Admin of the organization in the URL. + */ + get: operations["getV0OrganizationsBySlugLookupUser"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v0/organizations/{slug}/members": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** List organization members */ + get: operations["getV0OrganizationsBySlugMembers"]; + put?: never; + /** Invite a user to the organization */ + post: operations["postV0OrganizationsBySlugMembers"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v0/organizations/{slug}/members/{userId}/role": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Change a member's role */ + post: operations["postV0OrganizationsBySlugMembersByUserIdRole"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v0/organizations/{slug}/members/{userId}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + /** Remove a member */ + delete: operations["deleteV0OrganizationsBySlugMembersByUserId"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v0/organizations/{slug}/profile": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Edit organization profile/settings */ + post: operations["postV0OrganizationsBySlugProfile"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v0/organizations/{slug}/invitation/accept": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Accept an organization invitation */ + post: operations["postV0OrganizationsBySlugInvitationAccept"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v0/organizations/{slug}/invitation/decline": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Decline an organization invitation */ + post: operations["postV0OrganizationsBySlugInvitationDecline"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; } export type webhooks = Record; export interface components { @@ -660,21 +981,35 @@ export interface components { asset_counts: components["schemas"]["AssetCounts"]; latest_version: string; name: string; + owner: { + /** @constant */ + kind: "org"; + slug: string; + } | { + /** @constant */ + kind: "user"; + username: string; + }; published_at: string; publisher: string; + /** @enum {unknown} */ + visibility: "private" | "public"; author?: string; description?: string; }; SearchResponse: { facets: components["schemas"]["FacetSummary"][]; }; + PublicFacetCountResponse: { + count: number; + }; LatestVersionResponse: { latest: string; name: string; }; ApiErrorBody: { /** @enum {unknown} */ - code: "E_ACCOUNT_SUSPENDED" | "E_ADMIN_REQUIRED" | "E_ALREADY_ONBOARDED" | "E_API_KEY_MISSING" | "E_ARCHIVE_DECOMPRESSED_TOO_LARGE" | "E_ARCHIVE_MALFORMED" | "E_CLAIM_ALREADY_PENDING" | "E_CLAIM_PENDING_ELSEWHERE" | "E_CONTENT_INTEGRITY_MISMATCH" | "E_DRY_RUN_REQUIRED" | "E_FACET_NOT_FOUND" | "E_FACET_NOT_OWNED" | "E_INTERACTIVE_SESSION_REQUIRED" | "E_INVALID_NAME" | "E_INVALID_VERSION" | "E_LOGOUT_REQUIRES_JWT" | "E_MANIFEST_CONTENT_MISMATCH" | "E_MIGRATION_ALREADY_COMPLETED" | "E_MIGRATION_DEPENDENCY_UNMET" | "E_MIGRATION_NOT_FOUND" | "E_MIGRATION_RUNNING" | "E_NAME_BLOCKED" | "E_ONBOARDING_REQUIRED" | "E_PREFIX_COLLISION_RETRY_EXHAUSTED" | "E_PROFILE_CORRUPT" | "E_QUEUE_FULL" | "E_QUEUE_ITEM_NOT_FOUND" | "E_QUEUE_ITEM_NOT_PENDING" | "E_REGISTRY_UNAVAILABLE" | "E_RESERVATION_EXISTS" | "E_RESERVATION_NOT_FOUND" | "E_REVIEW_ARTIFACT_MISSING" | "E_RUN_NOT_FOUND" | "E_SCOPE_NOT_FOUND" | "E_SCOPE_NOT_OWNED" | "E_TARBALL_CORRUPTED" | "E_TARBALL_TOO_LARGE" | "E_TOKEN_EXPIRED" | "E_TOKEN_NOT_FOUND" | "E_TOKEN_REVOKED" | "E_UNAUTHENTICATED" | "E_UNDECLARED_CONTENT" | "E_USERNAME_TAKEN" | "E_USER_NOT_FOUND" | "E_VERSION_EXISTS"; + code: "E_ACCOUNT_SUSPENDED" | "E_ADMIN_REQUIRED" | "E_ALREADY_MEMBER" | "E_ALREADY_ONBOARDED" | "E_API_KEY_MISSING" | "E_ARCHIVE_DECOMPRESSED_TOO_LARGE" | "E_ARCHIVE_MALFORMED" | "E_CLAIM_ALREADY_PENDING" | "E_CLAIM_PENDING_ELSEWHERE" | "E_CONTENT_INTEGRITY_MISMATCH" | "E_DRY_RUN_REQUIRED" | "E_FACET_NOT_FOUND" | "E_FACET_NOT_OWNED" | "E_FORBIDDEN" | "E_GLOBAL_FACET_MUST_BE_PUBLIC" | "E_IMPERSONATION_FORBIDDEN" | "E_INTERACTIVE_SESSION_REQUIRED" | "E_INVALID_NAME" | "E_INVALID_VERSION" | "E_INVITATION_NOT_FOUND" | "E_LOGOUT_REQUIRES_JWT" | "E_MANIFEST_CONTENT_MISMATCH" | "E_MEMBER_NOT_FOUND" | "E_MIGRATION_ALREADY_COMPLETED" | "E_MIGRATION_BATCH_NOT_FOUND" | "E_MIGRATION_BATCH_RUNNING" | "E_MIGRATION_DEPENDENCY_UNMET" | "E_MIGRATION_NOT_FOUND" | "E_MIGRATION_RUNNING" | "E_NAME_BLOCKED" | "E_ONBOARDING_REQUIRED" | "E_ORG_FORBIDDEN" | "E_ORG_LAST_ADMIN" | "E_ORG_NAME_RESERVED" | "E_ORG_NAME_TAKEN" | "E_ORG_NOT_FOUND" | "E_PREFIX_COLLISION_RETRY_EXHAUSTED" | "E_PRIVATE_FACET_ENTITLEMENT_REQUIRED" | "E_PROFILE_CORRUPT" | "E_QUEUE_FULL" | "E_QUEUE_ITEM_NOT_FOUND" | "E_QUEUE_ITEM_NOT_PENDING" | "E_REGISTRY_UNAVAILABLE" | "E_RESERVATION_EXISTS" | "E_RESERVATION_NOT_FOUND" | "E_REVIEW_ARTIFACT_MISSING" | "E_RUN_NOT_FOUND" | "E_SCOPE_NOT_FOUND" | "E_SCOPE_NOT_OWNED" | "E_TARBALL_CORRUPTED" | "E_TARBALL_TOO_LARGE" | "E_TOKEN_EXPIRED" | "E_TOKEN_NOT_FOUND" | "E_TOKEN_REVOKED" | "E_UNAUTHENTICATED" | "E_UNDECLARED_CONTENT" | "E_USERNAME_TAKEN" | "E_USER_NOT_FOUND" | "E_VERSION_EXISTS"; docs_url: string; error: string; fix: string; @@ -690,10 +1025,21 @@ export interface components { content_integrity: string; manifest_json: string; name: string; + owner: { + /** @constant */ + kind: "org"; + slug: string; + } | { + /** @constant */ + kind: "user"; + username: string; + }; published_at: string; publisher: string; size_bytes: number; version: string; + /** @enum {unknown} */ + visibility: "private" | "public"; author?: string; description?: string; }; @@ -705,16 +1051,19 @@ export interface components { }; version: string; }; - OwnerRef: { - /** @constant */ - kind: "user"; - username: string; - }; ScopeRootResponse: { created_at: string; facets: components["schemas"]["FacetSummary"][]; name: string; - owner: components["schemas"]["OwnerRef"]; + owner: { + /** @constant */ + kind: "org"; + slug: string; + } | { + /** @constant */ + kind: "user"; + username: string; + }; }; PublishResponse: { content_hash: string; @@ -781,6 +1130,29 @@ export interface components { }; unmet_dependencies?: string[]; }[]; + latest_batch?: { + actor_username: string; + batch_id: string; + completed_migration_ids: string[]; + started_at: string; + /** @enum {unknown} */ + status: "failed" | "running" | "succeeded"; + total_pending: number; + current?: { + migration_id: string; + /** @enum {unknown} */ + phase: "dry_run" | "real_run"; + run_id?: string; + }; + failed?: { + /** @enum {unknown} */ + phase: "dry_run" | "real_run"; + error?: string; + migration_id?: string; + run_id?: string; + }; + finished_at?: string; + }; }; MigrationRunListResponse: { runs: { @@ -802,6 +1174,29 @@ export interface components { finished_at?: string; }[]; }; + MigrationBatchResponse: { + actor_username: string; + batch_id: string; + completed_migration_ids: string[]; + started_at: string; + /** @enum {unknown} */ + status: "failed" | "running" | "succeeded"; + total_pending: number; + current?: { + migration_id: string; + /** @enum {unknown} */ + phase: "dry_run" | "real_run"; + run_id?: string; + }; + failed?: { + /** @enum {unknown} */ + phase: "dry_run" | "real_run"; + error?: string; + migration_id?: string; + run_id?: string; + }; + finished_at?: string; + }; MigrationRunResponse: { actor_username: string; created: number; @@ -838,8 +1233,8 @@ export interface components { items: { created_at: string; id: string; - /** @constant */ - queue_type: "global-facet"; + /** @enum {unknown} */ + queue_type: "global-facet" | "org-claim"; /** @enum {unknown} */ reason: "pending" | "rate-limit" | "reserved"; /** @enum {unknown} */ @@ -853,14 +1248,48 @@ export interface components { decided_by?: string; decision_reason?: string; justification?: string; + requested_display_name?: string; + requested_slug?: string; + }[]; + }; + AdminUserListResponse: { + users: { + email: string; + suspended: boolean; + /** @enum {unknown} */ + tier: "admin" | "enterprise" | "free" | "pro"; + user_id: string; + username: string; }[]; }; + StartImpersonationResponse: { + target: { + user_id: string; + username: string; + }; + }; + EventDeliveryHealthResponse: { + dead_lettered_count: number; + dead_letters: { + attempt_count: number; + event_id: string; + event_type: string; + failed_at: string; + occurred_at: string; + producer: string; + subject: string; + subject_sequence: number; + last_error?: string; + }[]; + oldest_pending_age_ms: number | null; + pending_count: number; + }; ReviewQueueListResponse: { items: { created_at: string; id: string; - /** @constant */ - queue_type: "global-facet"; + /** @enum {unknown} */ + queue_type: "global-facet" | "org-claim"; /** @enum {unknown} */ reason: "pending" | "rate-limit" | "reserved"; /** @enum {unknown} */ @@ -870,6 +1299,49 @@ export interface components { decided_by?: string; decision_reason?: string; justification?: string; + requested_display_name?: string; + requested_slug?: string; + }[]; + }; + OrgDetailResponse: { + created_at: string; + display_name: string; + slug: string; + /** @enum {unknown} */ + viewer_role: "admin" | "none" | "publisher" | "viewer"; + description?: string; + homepage_url?: string; + }; + OrgListResponse: { + invitations: { + display_name: string; + invited_by_user_id: string; + /** @enum {unknown} */ + role: "admin" | "publisher" | "viewer"; + slug: string; + }[]; + organizations: { + display_name: string; + /** @enum {unknown} */ + role: "admin" | "publisher" | "viewer"; + slug: string; + }[]; + }; + UserSearchResponse: { + users: { + user_id: string; + username: string; + }[]; + }; + OrgMemberListResponse: { + members: { + email: string; + /** @enum {unknown} */ + role: "admin" | "publisher" | "viewer"; + /** @enum {unknown} */ + status: "active" | "invited"; + user_id: string; + username: string; }[]; }; }; @@ -921,6 +1393,26 @@ export interface operations { }; }; }; + getV0FacetsCount: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Aggregate public facet count */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PublicFacetCountResponse"]; + }; + }; + }; + }; getV0FacetsByScopeByNameLatestVersion: { parameters: { query?: never; @@ -1242,13 +1734,6 @@ export interface operations { "application/json": components["schemas"]["VersionMetadata"]; }; }; - /** @description Not modified (If-None-Match matched the ETag) */ - 304: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; /** @description Facet or version not found */ 404: { headers: { @@ -1316,13 +1801,6 @@ export interface operations { "application/json": components["schemas"]["ContentsResponse"]; }; }; - /** @description Not modified (If-None-Match matched the ETag) */ - 304: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; /** @description Facet or version not found */ 404: { headers: { @@ -1749,6 +2227,24 @@ export interface operations { }; }; }; + deleteV0AuthImpersonation: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Impersonation stopped */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; postV0OnboardingUsername: { parameters: { query?: never; @@ -1845,6 +2341,97 @@ export interface operations { }; }; }; + postV0AdminMigrationsBatchesLatestOnce: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Batch accepted */ + 202: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["MigrationBatchResponse"]; + }; + }; + /** @description A migration or batch is already in progress */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ApiErrorBody"]; + }; + }; + }; + }; + getV0AdminMigrationsBatchesByBatchId: { + parameters: { + query?: never; + header?: never; + path: { + batchId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Batch status */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["MigrationBatchResponse"]; + }; + }; + /** @description No such batch */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ApiErrorBody"]; + }; + }; + }; + }; + postV0AdminMigrationsBatchesByBatchIdCancel: { + parameters: { + query?: never; + header?: never; + path: { + batchId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Batch cancelled (or already terminal) */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["MigrationBatchResponse"]; + }; + }; + /** @description No such batch */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ApiErrorBody"]; + }; + }; + }; + }; getV0AdminMigrationsByIdRuns: { parameters: { query?: never; @@ -2197,23 +2784,23 @@ export interface operations { }; }; }; - postV0AdminUsersByIdSuspension: { + getV0AdminUsers: { parameters: { query?: never; header?: never; - path: { - id: string; - }; + path?: never; cookie?: never; }; requestBody?: never; responses: { - /** @description User suspended */ + /** @description Matching users */ 200: { headers: { [name: string]: unknown; }; - content?: never; + content: { + "application/json": components["schemas"]["AdminUserListResponse"]; + }; }; /** @description Missing or invalid credentials */ 401: { @@ -2233,18 +2820,9 @@ export interface operations { "application/json": components["schemas"]["ApiErrorBody"]; }; }; - /** @description Target user does not exist */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["ApiErrorBody"]; - }; - }; }; }; - deleteV0AdminUsersByIdSuspension: { + postV0AdminUsersByIdImpersonation: { parameters: { query?: never; header?: never; @@ -2255,12 +2833,14 @@ export interface operations { }; requestBody?: never; responses: { - /** @description Suspension lifted */ + /** @description Impersonation target validated */ 200: { headers: { [name: string]: unknown; }; - content?: never; + content: { + "application/json": components["schemas"]["StartImpersonationResponse"]; + }; }; /** @description Missing or invalid credentials */ 401: { @@ -2271,7 +2851,7 @@ export interface operations { "application/json": components["schemas"]["ApiErrorBody"]; }; }; - /** @description Caller is not an admin (or caller is suspended) */ + /** @description Caller is not an admin, or the target is an admin (E_IMPERSONATION_FORBIDDEN) */ 403: { headers: { [name: string]: unknown; @@ -2291,26 +2871,35 @@ export interface operations { }; }; }; - getV0SettingsReviewQueue: { + postV0AdminUsersByIdSuspension: { parameters: { query?: never; header?: never; - path?: never; + path: { + id: string; + }; cookie?: never; }; requestBody?: never; responses: { - /** @description Items in the requested bucket */ + /** @description User suspended */ 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Missing or invalid credentials */ + 401: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["ReviewQueueListResponse"]; + "application/json": components["schemas"]["ApiErrorBody"]; }; }; - /** @description Invalid status query param */ - 400: { + /** @description Caller is not an admin (or caller is suspended) */ + 403: { headers: { [name: string]: unknown; }; @@ -2318,8 +2907,8 @@ export interface operations { "application/json": components["schemas"]["ApiErrorBody"]; }; }; - /** @description Missing or invalid credentials */ - 401: { + /** @description Target user does not exist */ + 404: { headers: { [name: string]: unknown; }; @@ -2329,7 +2918,7 @@ export interface operations { }; }; }; - deleteV0SettingsReviewQueueById: { + deleteV0AdminUsersByIdSuspension: { parameters: { query?: never; header?: never; @@ -2340,7 +2929,7 @@ export interface operations { }; requestBody?: never; responses: { - /** @description Withdrawn */ + /** @description Suspension lifted */ 200: { headers: { [name: string]: unknown; @@ -2356,8 +2945,8 @@ export interface operations { "application/json": components["schemas"]["ApiErrorBody"]; }; }; - /** @description No such queue item for this user */ - 404: { + /** @description Caller is not an admin (or caller is suspended) */ + 403: { headers: { [name: string]: unknown; }; @@ -2365,8 +2954,510 @@ export interface operations { "application/json": components["schemas"]["ApiErrorBody"]; }; }; - /** @description Item is no longer pending */ - 409: { + /** @description Target user does not exist */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ApiErrorBody"]; + }; + }; + }; + }; + getV0AdminEventHealth: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Delivery-health surface */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["EventDeliveryHealthResponse"]; + }; + }; + }; + }; + getV0SettingsReviewQueue: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Items in the requested bucket */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ReviewQueueListResponse"]; + }; + }; + /** @description Invalid status query param */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ApiErrorBody"]; + }; + }; + /** @description Missing or invalid credentials */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ApiErrorBody"]; + }; + }; + }; + }; + deleteV0SettingsReviewQueueById: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Withdrawn */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Missing or invalid credentials */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ApiErrorBody"]; + }; + }; + /** @description No such queue item for this user */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ApiErrorBody"]; + }; + }; + /** @description Item is no longer pending */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ApiErrorBody"]; + }; + }; + }; + }; + getV0Organizations: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Organizations + invitations */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["OrgListResponse"]; + }; + }; + }; + }; + postV0Organizations: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Organization created */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["OrgDetailResponse"]; + }; + }; + /** @description Claim queued for admin review */ + 202: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Blocked or reserved name */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ApiErrorBody"]; + }; + }; + /** @description Slug taken or a pending claim exists */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ApiErrorBody"]; + }; + }; + }; + }; + getV0OrganizationsBySlug: { + parameters: { + query?: never; + header?: never; + path: { + slug: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Organization detail */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["OrgDetailResponse"]; + }; + }; + /** @description No such organization */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ApiErrorBody"]; + }; + }; + }; + }; + getV0OrganizationsBySlugLookupUser: { + parameters: { + query?: never; + header?: never; + path: { + slug: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Matching users */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["UserSearchResponse"]; + }; + }; + /** @description Caller is not an Admin */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ApiErrorBody"]; + }; + }; + /** @description No such organization */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ApiErrorBody"]; + }; + }; + }; + }; + getV0OrganizationsBySlugMembers: { + parameters: { + query?: never; + header?: never; + path: { + slug: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Member list */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["OrgMemberListResponse"]; + }; + }; + /** @description Not a member */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ApiErrorBody"]; + }; + }; + /** @description No such organization */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ApiErrorBody"]; + }; + }; + }; + }; + postV0OrganizationsBySlugMembers: { + parameters: { + query?: never; + header?: never; + path: { + slug: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Invitation created */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Caller is not an Admin */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ApiErrorBody"]; + }; + }; + /** @description No such organization or user */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ApiErrorBody"]; + }; + }; + /** @description User is already a member or invited */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ApiErrorBody"]; + }; + }; + }; + }; + postV0OrganizationsBySlugMembersByUserIdRole: { + parameters: { + query?: never; + header?: never; + path: { + slug: string; + userId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Role changed */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Caller is not an Admin */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ApiErrorBody"]; + }; + }; + /** @description No such organization or member */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ApiErrorBody"]; + }; + }; + }; + }; + deleteV0OrganizationsBySlugMembersByUserId: { + parameters: { + query?: never; + header?: never; + path: { + slug: string; + userId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Member removed */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Caller is not an Admin */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ApiErrorBody"]; + }; + }; + /** @description No such organization or member */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ApiErrorBody"]; + }; + }; + }; + }; + postV0OrganizationsBySlugProfile: { + parameters: { + query?: never; + header?: never; + path: { + slug: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Profile updated */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Caller is not an Admin */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ApiErrorBody"]; + }; + }; + /** @description No such organization */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ApiErrorBody"]; + }; + }; + }; + }; + postV0OrganizationsBySlugInvitationAccept: { + parameters: { + query?: never; + header?: never; + path: { + slug: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Invitation accepted */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description No such organization or pending invitation */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ApiErrorBody"]; + }; + }; + }; + }; + postV0OrganizationsBySlugInvitationDecline: { + parameters: { + query?: never; + header?: never; + path: { + slug: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Invitation declined */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description No such organization or pending invitation */ + 404: { headers: { [name: string]: unknown; }; diff --git a/packages/engine/src/registry/openapi.snapshot.yaml b/packages/engine/src/registry/openapi.snapshot.yaml index 8ebe4f9b..f8cb4d26 100644 --- a/packages/engine/src/registry/openapi.snapshot.yaml +++ b/packages/engine/src/registry/openapi.snapshot.yaml @@ -1,6 +1,6 @@ # Generated by: bun run codegen:registry # Source: https://api.facet.cafe/v0/openapi.yaml -# Generated-At: 2026-06-15T03:29:38.004Z +# Generated-At: 2026-06-23T20:50:08.809Z # Do not edit by hand. Run `bun run codegen:registry` from packages/engine to refresh. openapi: 3.1.0 info: @@ -53,10 +53,34 @@ components: type: string name: type: string + owner: + anyOf: + - type: object + properties: + kind: + const: org + slug: + type: string + required: + - kind + - slug + - type: object + properties: + kind: + const: user + username: + type: string + required: + - kind + - username published_at: type: string publisher: type: string + visibility: + enum: + - private + - public author: type: string description: @@ -65,8 +89,10 @@ components: - asset_counts - latest_version - name + - owner - published_at - publisher + - visibility SearchResponse: type: object properties: @@ -76,6 +102,13 @@ components: $ref: "#/components/schemas/FacetSummary" required: - facets + PublicFacetCountResponse: + type: object + properties: + count: + type: number + required: + - count LatestVersionResponse: type: object properties: @@ -93,6 +126,7 @@ components: enum: - E_ACCOUNT_SUSPENDED - E_ADMIN_REQUIRED + - E_ALREADY_MEMBER - E_ALREADY_ONBOARDED - E_API_KEY_MISSING - E_ARCHIVE_DECOMPRESSED_TOO_LARGE @@ -103,18 +137,31 @@ components: - E_DRY_RUN_REQUIRED - E_FACET_NOT_FOUND - E_FACET_NOT_OWNED + - E_FORBIDDEN + - E_GLOBAL_FACET_MUST_BE_PUBLIC + - E_IMPERSONATION_FORBIDDEN - E_INTERACTIVE_SESSION_REQUIRED - E_INVALID_NAME - E_INVALID_VERSION + - E_INVITATION_NOT_FOUND - E_LOGOUT_REQUIRES_JWT - E_MANIFEST_CONTENT_MISMATCH + - E_MEMBER_NOT_FOUND - E_MIGRATION_ALREADY_COMPLETED + - E_MIGRATION_BATCH_NOT_FOUND + - E_MIGRATION_BATCH_RUNNING - E_MIGRATION_DEPENDENCY_UNMET - E_MIGRATION_NOT_FOUND - E_MIGRATION_RUNNING - E_NAME_BLOCKED - E_ONBOARDING_REQUIRED + - E_ORG_FORBIDDEN + - E_ORG_LAST_ADMIN + - E_ORG_NAME_RESERVED + - E_ORG_NAME_TAKEN + - E_ORG_NOT_FOUND - E_PREFIX_COLLISION_RETRY_EXHAUSTED + - E_PRIVATE_FACET_ENTITLEMENT_REQUIRED - E_PROFILE_CORRUPT - E_QUEUE_FULL - E_QUEUE_ITEM_NOT_FOUND @@ -175,6 +222,26 @@ components: type: string name: type: string + owner: + anyOf: + - type: object + properties: + kind: + const: org + slug: + type: string + required: + - kind + - slug + - type: object + properties: + kind: + const: user + username: + type: string + required: + - kind + - username published_at: type: string publisher: @@ -183,6 +250,10 @@ components: type: number version: type: string + visibility: + enum: + - private + - public author: type: string description: @@ -193,10 +264,12 @@ components: - content_integrity - manifest_json - name + - owner - published_at - publisher - size_bytes - version + - visibility ContentsResponse: type: object properties: @@ -215,16 +288,6 @@ components: - name - resources - version - OwnerRef: - type: object - properties: - kind: - const: user - username: - type: string - required: - - kind - - username ScopeRootResponse: type: object properties: @@ -237,7 +300,25 @@ components: name: type: string owner: - $ref: "#/components/schemas/OwnerRef" + anyOf: + - type: object + properties: + kind: + const: org + slug: + type: string + required: + - kind + - slug + - type: object + properties: + kind: + const: user + username: + type: string + required: + - kind + - username required: - created_at - facets @@ -425,6 +506,64 @@ components: - description - id - kind + latest_batch: + type: object + properties: + actor_username: + type: string + batch_id: + type: string + completed_migration_ids: + type: array + items: + type: string + started_at: + type: string + status: + enum: + - failed + - running + - succeeded + total_pending: + type: number + current: + type: object + properties: + migration_id: + type: string + phase: + enum: + - dry_run + - real_run + run_id: + type: string + required: + - migration_id + - phase + failed: + type: object + properties: + phase: + enum: + - dry_run + - real_run + error: + type: string + migration_id: + type: string + run_id: + type: string + required: + - phase + finished_at: + type: string + required: + - actor_username + - batch_id + - completed_migration_ids + - started_at + - status + - total_pending required: - migrations MigrationRunListResponse: @@ -484,6 +623,64 @@ components: - status required: - runs + MigrationBatchResponse: + type: object + properties: + actor_username: + type: string + batch_id: + type: string + completed_migration_ids: + type: array + items: + type: string + started_at: + type: string + status: + enum: + - failed + - running + - succeeded + total_pending: + type: number + current: + type: object + properties: + migration_id: + type: string + phase: + enum: + - dry_run + - real_run + run_id: + type: string + required: + - migration_id + - phase + failed: + type: object + properties: + phase: + enum: + - dry_run + - real_run + error: + type: string + migration_id: + type: string + run_id: + type: string + required: + - phase + finished_at: + type: string + required: + - actor_username + - batch_id + - completed_migration_ids + - started_at + - status + - total_pending MigrationRunResponse: type: object properties: @@ -587,7 +784,9 @@ components: id: type: string queue_type: - const: global-facet + enum: + - global-facet + - org-claim reason: enum: - pending @@ -619,6 +818,10 @@ components: type: string justification: type: string + requested_display_name: + type: string + requested_slug: + type: string required: - created_at - id @@ -631,6 +834,99 @@ components: - username required: - items + AdminUserListResponse: + type: object + properties: + users: + type: array + items: + type: object + properties: + email: + type: string + suspended: + type: boolean + tier: + enum: + - admin + - enterprise + - free + - pro + user_id: + type: string + username: + type: string + required: + - email + - suspended + - tier + - user_id + - username + required: + - users + StartImpersonationResponse: + type: object + properties: + target: + type: object + properties: + user_id: + type: string + username: + type: string + required: + - user_id + - username + required: + - target + EventDeliveryHealthResponse: + type: object + properties: + dead_lettered_count: + type: number + dead_letters: + type: array + items: + type: object + properties: + attempt_count: + type: number + event_id: + type: string + event_type: + type: string + failed_at: + type: string + occurred_at: + type: string + producer: + type: string + subject: + type: string + subject_sequence: + type: number + last_error: + type: string + required: + - attempt_count + - event_id + - event_type + - failed_at + - occurred_at + - producer + - subject + - subject_sequence + oldest_pending_age_ms: + anyOf: + - type: number + - type: "null" + pending_count: + type: number + required: + - dead_lettered_count + - dead_letters + - oldest_pending_age_ms + - pending_count ReviewQueueListResponse: type: object properties: @@ -644,7 +940,9 @@ components: id: type: string queue_type: - const: global-facet + enum: + - global-facet + - org-claim reason: enum: - pending @@ -667,6 +965,10 @@ components: type: string justification: type: string + requested_display_name: + type: string + requested_slug: + type: string required: - created_at - id @@ -676,6 +978,123 @@ components: - target required: - items + OrgDetailResponse: + type: object + properties: + created_at: + type: string + display_name: + type: string + slug: + type: string + viewer_role: + enum: + - admin + - none + - publisher + - viewer + description: + type: string + homepage_url: + type: string + required: + - created_at + - display_name + - slug + - viewer_role + OrgListResponse: + type: object + properties: + invitations: + type: array + items: + type: object + properties: + display_name: + type: string + invited_by_user_id: + type: string + role: + enum: + - admin + - publisher + - viewer + slug: + type: string + required: + - display_name + - invited_by_user_id + - role + - slug + organizations: + type: array + items: + type: object + properties: + display_name: + type: string + role: + enum: + - admin + - publisher + - viewer + slug: + type: string + required: + - display_name + - role + - slug + required: + - invitations + - organizations + UserSearchResponse: + type: object + properties: + users: + type: array + items: + type: object + properties: + user_id: + type: string + username: + type: string + required: + - user_id + - username + required: + - users + OrgMemberListResponse: + type: object + properties: + members: + type: array + items: + type: object + properties: + email: + type: string + role: + enum: + - admin + - publisher + - viewer + status: + enum: + - active + - invited + user_id: + type: string + username: + type: string + required: + - email + - role + - status + - user_id + - username + required: + - members paths: /v0/health: get: @@ -706,6 +1125,22 @@ paths: application/json: schema: $ref: "#/components/schemas/SearchResponse" + /v0/facets/count: + get: + operationId: getV0FacetsCount + summary: Public facet count + description: Returns the projected count of live public facets. The count is + event-backed and eventually consistent; it is never derived from facet + search and never reveals private facet counts. + tags: + - facets + responses: + "200": + description: Aggregate public facet count + content: + application/json: + schema: + $ref: "#/components/schemas/PublicFacetCountResponse" /v0/facets/{scope}/{name}/latest-version: get: operationId: getV0FacetsByScopeByNameLatestVersion @@ -888,8 +1323,8 @@ paths: get: operationId: getV0FacetsByNameLatestVersion summary: Get the latest version pointer for a facet - description: Returns the current latest version string for a facet. Short-cached - (s-maxage=30) so CloudFront absorbs traffic; all downstream reads key + description: Returns the caller-relative latest version string for a facet — the + highest version the caller is authorized to read. Downstream reads key off the concrete version it returns. tags: - facets @@ -920,10 +1355,9 @@ paths: get: operationId: getV0FacetsByNameVersionsByVersion summary: Get the full version list for a facet - description: Returns the complete sorted version list. The :version in the path - is the cache key (expected to be the current latest version). A concrete - version gets immutable caching + ETag; the literal `latest` resolves - server-side and is not cached. + description: "Returns the sorted list of versions the caller is authorized to + see. Responses are `no-store`: the visible set is caller-relative, so a + list cached for one caller must not be served to another." tags: - facets parameters: @@ -947,12 +1381,12 @@ paths: operationId: getV0FacetsByNameByVersion summary: Get metadata for a specific version description: Returns the version row's metadata including the verbatim - manifestJson string and the verified publisher username. version may be - `latest` to resolve through FACET META's `latest_version` attribute. - Because (name, version) is immutable, a concrete-version response is - cacheable for a year with a strong ETag derived from the verified - content fingerprint; conditional If-None-Match requests get a 304. - `latest` resolves server-side and is not cached. + manifestJson string and the verified publisher username. `version` may + be `latest`, which resolves to the highest version the caller is + authorized to read. A concrete public version is immutable and identical + for every caller, so it is long-cacheable; a private version is + `no-store` because each read is authorized at request time, as is a + `latest` resolution. tags: - facets parameters: @@ -965,8 +1399,6 @@ paths: application/json: schema: $ref: "#/components/schemas/VersionMetadata" - "304": - description: Not modified (If-None-Match matched the ETag) "404": description: Facet or version not found content: @@ -1003,10 +1435,9 @@ paths: operationId: getV0FacetsByNameByVersionContents summary: Get the verified bodies of a version's resources description: Returns the verified body of each skill, agent, and command in the - version. Because (name, version) is immutable, a concrete-version - response is cacheable for a year with a strong ETag derived from the - verified content fingerprint; conditional If-None-Match requests get a - 304. `latest` resolves server-side and is not cached. + version. A concrete public version is immutable and identical for every + caller, so it is long-cacheable; a private version is `no-store` because + each read is authorized at request time, as is a `latest` resolution. tags: - facets parameters: @@ -1019,8 +1450,6 @@ paths: application/json: schema: $ref: "#/components/schemas/ContentsResponse" - "304": - description: Not modified (If-None-Match matched the ETag) "404": description: Facet or version not found content: @@ -1322,6 +1751,20 @@ paths: application/json: schema: $ref: "#/components/schemas/ApiErrorBody" + /v0/auth/impersonation: + delete: + operationId: deleteV0AuthImpersonation + summary: Stop impersonating + description: Stateless no-op that returns 204. Impersonation holds no server + state; the client stops by dropping the impersonation header and stored + target metadata. + tags: + - auth + security: + - BearerAuth: [] + responses: + "204": + description: Impersonation stopped /v0/onboarding/username: post: operationId: postV0OnboardingUsername @@ -1403,6 +1846,90 @@ paths: application/json: schema: $ref: "#/components/schemas/MigrationRunListResponse" + /v0/admin/migrations/batches/latest-once: + post: + operationId: postV0AdminMigrationsBatchesLatestOnce + summary: Start a "migrate to latest" batch (admin) + description: Runs every pending one-time migration as a dry run followed by a + real run, in dependency order, stopping on the first failure. Refuses to + start if a migration or batch is already in progress. + tags: + - admin + security: + - BearerAuth: [] + responses: + "202": + description: Batch accepted + content: + application/json: + schema: + $ref: "#/components/schemas/MigrationBatchResponse" + "409": + description: A migration or batch is already in progress + content: + application/json: + schema: + $ref: "#/components/schemas/ApiErrorBody" + /v0/admin/migrations/batches/{batchId}: + get: + operationId: getV0AdminMigrationsBatchesByBatchId + summary: Get a migration batch (admin) + description: "Returns a batch by id for polling: status, progress, and + references to the current/failed run." + tags: + - admin + security: + - BearerAuth: [] + responses: + "200": + description: Batch status + content: + application/json: + schema: + $ref: "#/components/schemas/MigrationBatchResponse" + "404": + description: No such batch + content: + application/json: + schema: + $ref: "#/components/schemas/ApiErrorBody" + parameters: + - schema: + type: string + in: path + name: batchId + required: true + /v0/admin/migrations/batches/{batchId}/cancel: + post: + operationId: postV0AdminMigrationsBatchesByBatchIdCancel + summary: Cancel a running migration batch (admin) + description: Marks the batch failed (cancelled), stops the orchestrator + execution (live stages), and force-releases the global migration lease + so a stopped run does not block migrations until its lease expires. A + non-running batch returns its current state unchanged. + tags: + - admin + security: + - BearerAuth: [] + responses: + "200": + description: Batch cancelled (or already terminal) + content: + application/json: + schema: + $ref: "#/components/schemas/MigrationBatchResponse" + "404": + description: No such batch + content: + application/json: + schema: + $ref: "#/components/schemas/ApiErrorBody" + parameters: + - schema: + type: string + in: path + name: batchId + required: true /v0/admin/migrations/{id}/runs: post: operationId: postV0AdminMigrationsByIdRuns @@ -1694,13 +2221,87 @@ paths: in: path name: id required: true - /v0/admin/users/{id}/suspension: - post: - operationId: postV0AdminUsersByIdSuspension - summary: Suspend a user - description: "Authoritative step: UpdateItem on the target user's PROFILE sets - suspended_at, suspended_reason, and suspended_by_user_id. Best-effort - cleanup follows sequentially: Cognito AdminUserGlobalSignOut, then + /v0/admin/users: + get: + operationId: getV0AdminUsers + summary: Search users by username prefix + description: Bounded username-prefix search over the GSI3 + USER_ALL# partition. Admin-gated, so the response carries + email, tier, and suspension status. An empty `q` returns no users. + tags: + - admin + security: + - BearerAuth: [] + responses: + "200": + description: Matching users + content: + application/json: + schema: + $ref: "#/components/schemas/AdminUserListResponse" + "401": + description: Missing or invalid credentials + content: + application/json: + schema: + $ref: "#/components/schemas/ApiErrorBody" + "403": + description: Caller is not an admin (or caller is suspended) + content: + application/json: + schema: + $ref: "#/components/schemas/ApiErrorBody" + /v0/admin/users/{id}/impersonation: + post: + operationId: postV0AdminUsersByIdImpersonation + summary: Start impersonating a user + description: Validates that the target exists and is not an admin, then returns + the non-secret metadata the UI stores to drive impersonation. No token + is minted — every subsequent request re-authorizes from the admin's live + JWT plus the X-Facet-Impersonate-User-Id header. + tags: + - admin + security: + - BearerAuth: [] + responses: + "200": + description: Impersonation target validated + content: + application/json: + schema: + $ref: "#/components/schemas/StartImpersonationResponse" + "401": + description: Missing or invalid credentials + content: + application/json: + schema: + $ref: "#/components/schemas/ApiErrorBody" + "403": + description: Caller is not an admin, or the target is an admin + (E_IMPERSONATION_FORBIDDEN) + content: + application/json: + schema: + $ref: "#/components/schemas/ApiErrorBody" + "404": + description: Target user does not exist + content: + application/json: + schema: + $ref: "#/components/schemas/ApiErrorBody" + parameters: + - schema: + type: string + in: path + name: id + required: true + /v0/admin/users/{id}/suspension: + post: + operationId: postV0AdminUsersByIdSuspension + summary: Suspend a user + description: "Authoritative step: UpdateItem on the target user's PROFILE sets + suspended_at, suspended_reason, and suspended_by_user_id. Best-effort + cleanup follows sequentially: Cognito AdminUserGlobalSignOut, then per-PAT revoke. Failures in the cleanup phase don't roll back the suspension — the PROFILE gate is the source of truth." tags: @@ -1772,6 +2373,24 @@ paths: in: path name: id required: true + /v0/admin/event-health: + get: + operationId: getV0AdminEventHealth + summary: Event delivery health + description: "Product-event delivery health: pending backlog size, + oldest-pending age, and the full unresolved dead-letter set. Admin + only." + tags: + - admin + security: + - BearerAuth: [] + responses: + "200": + description: Delivery-health surface + content: + application/json: + schema: + $ref: "#/components/schemas/EventDeliveryHealthResponse" /v0/settings/review-queue: get: operationId: getV0SettingsReviewQueue @@ -1842,3 +2461,322 @@ paths: in: path name: id required: true + /v0/organizations: + post: + operationId: postV0Organizations + summary: Create or claim an organization + description: Auto-creates the organization when the slug is unprotected and + within the per-user claim budget (1/24h, 5/30d); otherwise queues the + claim for admin review. Reserves the matching @ scope and makes + the caller the founding Admin. + tags: + - organizations + security: + - BearerAuth: [] + responses: + "201": + description: Organization created + content: + application/json: + schema: + $ref: "#/components/schemas/OrgDetailResponse" + "202": + description: Claim queued for admin review + "403": + description: Blocked or reserved name + content: + application/json: + schema: + $ref: "#/components/schemas/ApiErrorBody" + "409": + description: Slug taken or a pending claim exists + content: + application/json: + schema: + $ref: "#/components/schemas/ApiErrorBody" + get: + operationId: getV0Organizations + summary: List the caller's organizations and invitations + tags: + - organizations + security: + - BearerAuth: [] + responses: + "200": + description: Organizations + invitations + content: + application/json: + schema: + $ref: "#/components/schemas/OrgListResponse" + /v0/organizations/{slug}: + get: + operationId: getV0OrganizationsBySlug + summary: Get an organization and the caller role + tags: + - organizations + security: + - BearerAuth: [] + responses: + "200": + description: Organization detail + content: + application/json: + schema: + $ref: "#/components/schemas/OrgDetailResponse" + "404": + description: No such organization + content: + application/json: + schema: + $ref: "#/components/schemas/ApiErrorBody" + parameters: + - schema: + type: string + in: path + name: slug + required: true + /v0/organizations/{slug}/lookup-user: + get: + operationId: getV0OrganizationsBySlugLookupUser + summary: Search users by username prefix for an organization invite + description: Returns up to 10 users whose username begins with the query, across + all users. The caller must be an Admin of the organization in the URL. + tags: + - organizations + security: + - BearerAuth: [] + responses: + "200": + description: Matching users + content: + application/json: + schema: + $ref: "#/components/schemas/UserSearchResponse" + "403": + description: Caller is not an Admin + content: + application/json: + schema: + $ref: "#/components/schemas/ApiErrorBody" + "404": + description: No such organization + content: + application/json: + schema: + $ref: "#/components/schemas/ApiErrorBody" + parameters: + - schema: + type: string + in: path + name: slug + required: true + /v0/organizations/{slug}/members: + get: + operationId: getV0OrganizationsBySlugMembers + summary: List organization members + tags: + - organizations + security: + - BearerAuth: [] + responses: + "200": + description: Member list + content: + application/json: + schema: + $ref: "#/components/schemas/OrgMemberListResponse" + "403": + description: Not a member + content: + application/json: + schema: + $ref: "#/components/schemas/ApiErrorBody" + "404": + description: No such organization + content: + application/json: + schema: + $ref: "#/components/schemas/ApiErrorBody" + parameters: + - schema: + type: string + in: path + name: slug + required: true + post: + operationId: postV0OrganizationsBySlugMembers + summary: Invite a user to the organization + tags: + - organizations + security: + - BearerAuth: [] + responses: + "200": + description: Invitation created + "403": + description: Caller is not an Admin + content: + application/json: + schema: + $ref: "#/components/schemas/ApiErrorBody" + "404": + description: No such organization or user + content: + application/json: + schema: + $ref: "#/components/schemas/ApiErrorBody" + "409": + description: User is already a member or invited + content: + application/json: + schema: + $ref: "#/components/schemas/ApiErrorBody" + parameters: + - schema: + type: string + in: path + name: slug + required: true + /v0/organizations/{slug}/members/{userId}/role: + post: + operationId: postV0OrganizationsBySlugMembersByUserIdRole + summary: Change a member's role + tags: + - organizations + security: + - BearerAuth: [] + responses: + "200": + description: Role changed + "403": + description: Caller is not an Admin + content: + application/json: + schema: + $ref: "#/components/schemas/ApiErrorBody" + "404": + description: No such organization or member + content: + application/json: + schema: + $ref: "#/components/schemas/ApiErrorBody" + parameters: + - schema: + type: string + in: path + name: slug + required: true + - schema: + type: string + in: path + name: userId + required: true + /v0/organizations/{slug}/members/{userId}: + delete: + operationId: deleteV0OrganizationsBySlugMembersByUserId + summary: Remove a member + tags: + - organizations + security: + - BearerAuth: [] + responses: + "200": + description: Member removed + "403": + description: Caller is not an Admin + content: + application/json: + schema: + $ref: "#/components/schemas/ApiErrorBody" + "404": + description: No such organization or member + content: + application/json: + schema: + $ref: "#/components/schemas/ApiErrorBody" + parameters: + - schema: + type: string + in: path + name: slug + required: true + - schema: + type: string + in: path + name: userId + required: true + /v0/organizations/{slug}/profile: + post: + operationId: postV0OrganizationsBySlugProfile + summary: Edit organization profile/settings + tags: + - organizations + security: + - BearerAuth: [] + responses: + "200": + description: Profile updated + "403": + description: Caller is not an Admin + content: + application/json: + schema: + $ref: "#/components/schemas/ApiErrorBody" + "404": + description: No such organization + content: + application/json: + schema: + $ref: "#/components/schemas/ApiErrorBody" + parameters: + - schema: + type: string + in: path + name: slug + required: true + /v0/organizations/{slug}/invitation/accept: + post: + operationId: postV0OrganizationsBySlugInvitationAccept + summary: Accept an organization invitation + tags: + - organizations + security: + - BearerAuth: [] + responses: + "200": + description: Invitation accepted + "404": + description: No such organization or pending invitation + content: + application/json: + schema: + $ref: "#/components/schemas/ApiErrorBody" + parameters: + - schema: + type: string + in: path + name: slug + required: true + /v0/organizations/{slug}/invitation/decline: + post: + operationId: postV0OrganizationsBySlugInvitationDecline + summary: Decline an organization invitation + tags: + - organizations + security: + - BearerAuth: [] + responses: + "200": + description: Invitation declined + "404": + description: No such organization or pending invitation + content: + application/json: + schema: + $ref: "#/components/schemas/ApiErrorBody" + parameters: + - schema: + type: string + in: path + name: slug + required: true