# Hikyo HTTP API — the single source of truth for every client. # # Hand-written and reviewed like code (api-cli-surface ADR § Version promise): # the behavioural half of the freeze — authorization formula, side effects, # idempotency, error semantics — is recorded here per operation and cannot be # policed by schema diffing alone. # # OpenAPI 3.1 per the 2026-08-07 amendment banner in system-architecture.md. # The bound 3.1 semantic profile, asserted by internal/api's profile tests: # * nullability is `type: [T, "null"]` — legacy `nullable` is prohibited; # * `jsonSchemaDialect` is the OAS 3.1 default dialect, alternates fail closed; # * top-level `webhooks` is prohibited in v1. # # Hikyo extensions, each cross-checked against the Go registries in CI so the # document cannot drift from the code it describes: # x-hikyo-class probe classification (tenant-isolation ADR invariant 1); # must equal internal/authz's wire-registry entry. # x-hikyo-operation the registered authz operation this route reaches; # absent for routes that reach none. # x-hikyo-formula the authorization formula, spelled `cap@level`; must # equal the operation registry's formula pin. # x-hikyo-artifacts the authentication-artifact eligibility set # (api-cli-surface ADR § verb × artifact matrix). # x-hikyo-min-revision the API revision an operation first appeared in; the # capability registry a new client checks against an old # server's advertised revision. # x-extensible-enum an enum declared OPEN: it may gain values additively and # every generated consumer must tolerate unknown ones. The # `enum` keyword is deliberately absent on open enums so # runtime validation tolerates growth; a closed enum uses # `enum` and never grows. openapi: 3.1.0 jsonSchemaDialect: https://spec.openapis.org/oas/3.1/dialect/base info: title: Hikyo API version: "1" summary: Self-hosted environment and secret management. description: | Every response may gain fields additively within `/api/v1`; clients MUST ignore fields they do not know. Open enums (`x-extensible-enum`) may gain values; closed enums never do. Unauthorized is indistinguishable from nonexistent on every tenant-scoped route: status, body and shape are identical (permission model ADR). Error bodies carry a fixed message per code — nothing derived from the request — except `bad_request`, which is decided before any tenant resolution and so may carry a `detail` that leaks nothing about tenancy. license: name: MPL-2.0 identifier: MPL-2.0 servers: - url: / description: The instance this document was served from. tags: - name: meta description: Unauthenticated instance discovery. - name: auth description: | Identity-protocol endpoints — the closed, enumerated exception to the one-surface rule (api-cli-surface ADR). Client-specific by nature and parity-exempt; never domain endpoints. - name: orgs description: Organisation administration — instance-scoped operator work. - name: hierarchy description: | The domain hierarchy: Organization, Project, Environment, Folder. Every by-id route is tenant-scoped, so unauthorized is indistinguishable from nonexistent at every level. - name: keys description: | The key catalogue and its groups - the project's schema. There is no separate schema object: every constraint is declared inline on the Key, so the catalogue revision IS the schema revision. Tenant-scoped throughout, at project depth. - name: definitions description: | Canonical project-definition export, drift checks, immutable impact plans, atomic apply, and Git-governance settings. - name: identities description: | Machine identities: project-owned service accounts and the credentials that authenticate them. A service account is a principal in the grant table; a credential is an authenticator for it, and every credential of one confers identical authority. - name: delivery description: | The machine fetch surface. Fetches are CONDITIONAL, and the cursor covers authorization rather than merely content: a caller presenting a current cursor is told it is current and receives no plaintext, while any authorization movement invalidates the cursor and produces a full authorized delivery. - name: values description: | The flat value model: a value attaches to a `(key, environment)` and there are no other layers. Presence is two-state - a `set` entry delivers, `absent` delivers nothing, and no fallback source exists. The deleted `masked` state appears nowhere in this contract. Plaintext of a `secret` key flows only through the reveal-gated shapes, each of which emits one audit event per disclosed key. - name: approvals description: | Secret-change approvals (#151). Policy-bound, multi-person review-and-merge for secret and configuration changes in sensitive scopes. An approval authorises one exact reviewed change set and is never a second mutation path: merge and emergency bypass ride the ordinary publish endpoint. paths: /api/v1/runtime/status: get: operationId: getRuntimeStatus tags: [meta] summary: Public runtime availability for maintenance and reconnection. description: | Reports only this instance's availability and upgrade phase. No tenant data, identity, configuration, paths, or failure details are exposed. Polling does not extend sessions. Storage failures return unavailable, never a guessed ready or maintenance state. security: [] x-hikyo-class: unauthenticated x-hikyo-artifacts: [none] x-hikyo-min-revision: 4 responses: "200": description: Current runtime status. Responses must not be cached. content: application/json: schema: $ref: "#/components/schemas/RuntimeStatus" "429": $ref: "#/components/responses/TooManyRequests" "503": $ref: "#/components/responses/ServiceUnavailable" /api/v1/meta: get: operationId: getMeta tags: [meta] summary: Instance discovery, unauthenticated. description: | The response is an exact closed allowlist: server version, API revision, and which authentication protocol flows this instance serves. Nothing tenant- or configuration-shaped — capability discovery beyond protocol selection happens after authentication. Sits under the instance-wide pre-auth admission limits like every other pre-auth path. security: [] x-hikyo-class: unauthenticated x-hikyo-artifacts: [none] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: Instance metadata. content: application/json: schema: $ref: "#/components/schemas/Meta" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/auth/credential/establish: post: operationId: establishCredential tags: [auth] summary: Consume a credential-establishment authority and set an initial credential. description: | The named — and only — exception to "a new credential may never authorize its own enrolment" (human-auth ADR § Credential-establishment authority). The authority is target-bound, purpose-bound, single-use, expiring, and issued only by the bootstrap path, `credential-reset`, a member invitation, local break-glass, or recovery. Consuming it establishes exactly one initial credential, atomically, and nothing more: **no session is created, no assurance is carried, no reauthentication window opens.** The holder authenticates afterwards with the credential they just set, like anyone else. Every refusal — unknown authority, expired, already consumed, wrong epoch, weak password — answers the same `401` so presentation reveals nothing about which authorities exist. security: [] x-hikyo-class: unauthenticated x-hikyo-artifacts: [none] x-hikyo-min-revision: 1 requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/EstablishCredentialRequest" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "204": description: Credential established. No session, no assurance. "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/auth/local/login: post: operationId: localLogin tags: [auth] summary: Local password login; mints a CLI or browser session artifact. description: | The local floor (human-auth ADR § Lockout and the local floor), which an installation may never remove. Verifies an Argon2id password verifier inside the instance-wide admission budget; an unknown account traverses a bounded dummy-verifier path so timing is comparable and no pre-authentication path distinguishes an existing account from a missing one. Mints the artifact the request asks for. Both are distinct types with their own storage, lifetime, audit identity and revocation surface, and neither value is ever re-retrievable: - **CLI session** (default): a replayable bearer credential returned in the response body, presented on `Authorization: Bearer`. - **Browser session**: delivered ONLY on the `__Host-hikyo` HttpOnly cookie, never in the body, so injected same-origin script cannot read it. Its synchronizer token rides the `__Host-hikyo-csrf` companion cookie and must be echoed on the `X-Hikyo-CSRF` header for every state-changing cookie-authenticated request. See `/api/v1/meta` for which protocol flows an instance actually offers. security: [] x-hikyo-class: unauthenticated x-hikyo-artifacts: [none] x-hikyo-min-revision: 1 requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/LocalLoginRequest" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: Session minted. The token is displayed once and never again. content: application/json: schema: $ref: "#/components/schemas/LoginResult" "202": description: | A second factor stands on this account and a browser session was requested. No session is minted and no cookie is set; the response carries a single-use, expiring login challenge that must be satisfied at `/api/v1/auth/login/challenge/{id}/...` before a session exists. The factor is never skippable (human-auth ADR § Assurance). Only a `browser` artifact request receives this; a `cli` request mints a `password`-assurance session and steps up on demand at the authorization chokepoint. content: application/json: schema: $ref: "#/components/schemas/LoginChallenge" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/auth/login/challenge/{challenge}/totp: post: operationId: loginChallengeTotp tags: [auth] summary: Satisfy a login challenge with a TOTP code; mints the session. description: | Consumes the single-use login challenge (single-use, expiring, bound to the account) and mints the browser session the login asked for, with `totp` recorded alongside `password` in the assurance. Refusals mirror the step-up path: `401` for a wrong code, `409` for a code already used for its time step, `429` for the factor budget. security: [] x-hikyo-class: unauthenticated x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 parameters: - $ref: "#/components/parameters/ChallengeId" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/TotpCodeRequest" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The minted browser session. content: application/json: schema: $ref: "#/components/schemas/LoginResult" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/Conflict" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/auth/login/challenge/{challenge}/webauthn/start: post: operationId: loginChallengeWebauthnStart tags: [auth] summary: Begin a passkey assertion against a login challenge. description: | Resolves the login challenge to its account and opens a WebAuthn assertion ceremony for that account's registered passkeys. Mints nothing; the ceremony is completed at `.../webauthn/finish`. security: [] x-hikyo-class: unauthenticated x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 parameters: - $ref: "#/components/parameters/ChallengeId" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: Opaque WebAuthn request options for the browser. content: application/json: schema: $ref: "#/components/schemas/WebauthnOptions" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/auth/login/challenge/{challenge}/webauthn/finish: post: operationId: loginChallengeWebauthnFinish tags: [auth] summary: Complete a passkey assertion against a login challenge; mints the session. description: | Validates the assertion against the live ceremony, consumes the login challenge, and mints the browser session with `webauthn` recorded alongside `password` in the assurance. A signature-count regression disables the cloned credential (audited) and refuses. security: [] x-hikyo-class: unauthenticated x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 parameters: - $ref: "#/components/parameters/ChallengeId" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/WebauthnResponse" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The minted browser session. content: application/json: schema: $ref: "#/components/schemas/LoginResult" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/auth/logout: post: operationId: logout tags: [auth] summary: Revoke the presented session. description: | Deletes the presented session row. Idempotent in effect: a session that no longer resolves answers `401` like any other unauthenticated request, because there is nothing to revoke and nothing to report. x-hikyo-class: unauthenticated x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "204": description: Session revoked. "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/auth/whoami: get: operationId: whoami tags: [auth] summary: Describe the presented session. description: | Reports the acting principal and how **this session** authenticated — the assurance record the chokepoint consults, never what the account could have presented (human-auth ADR § Assurance). x-hikyo-class: unauthenticated x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The acting session. content: application/json: schema: $ref: "#/components/schemas/WhoAmI" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/auth/totp/enrol/start: post: operationId: enrolTotpStart tags: [auth] summary: Begin TOTP enrolment; returns the otpauth URI once. description: | An account-security proof up front (the pre-existing password, since no possession factor stands yet — human-auth ADR § Account-security mutations). Stages a sealed seed as a pending, inert enrolment and returns the `otpauth://` URI exactly once. It confirms nothing and mutates no session: `enrol/confirm` completes the enrolment. A confirmed factor already standing is refused (`400`). x-hikyo-class: unauthenticated x-hikyo-artifacts: [human-session] x-hikyo-reauth: {class: account-security, proof: [password], presence: required} x-hikyo-min-revision: 1 requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/TotpEnrolStartRequest" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The provisioning URI, returned once. content: application/json: schema: $ref: "#/components/schemas/TotpEnrolStartResult" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/auth/totp/enrol/confirm: post: operationId: enrolTotpConfirm tags: [auth] summary: Confirm TOTP enrolment with a code; reissues the session. description: | Consumes a valid code against the pending seed (single-use) and completes the enrolment as an account-security mutation: the generation advances, every session is deleted, and the acting session is reissued carrying ONLY the proof class — NOT `totp`. The user steps up separately to present the factor they just enrolled. The reissued token is returned and MUST replace the old one. x-hikyo-class: unauthenticated x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/TotpCodeRequest" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: Enrolment confirmed. The reissued session token replaces the old one. content: application/json: schema: $ref: "#/components/schemas/LoginResult" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/Conflict" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/auth/totp/step-up: post: operationId: stepUpTotp tags: [auth] summary: Elevate the acting session by presenting a TOTP code. description: | A valid code consumes its step (single-use) and appends `totp` to the acting session's factor set, rotating its token — not an account-security mutation, so no generation advance and no session deletion, and the original authenticated_at is preserved (human-auth ADR § Assurance). The rotated token is returned and MUST replace the old one. x-hikyo-class: unauthenticated x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/TotpCodeRequest" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: Session elevated. The rotated token replaces the old one. content: application/json: schema: $ref: "#/components/schemas/LoginResult" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/Conflict" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/auth/reauth/totp: post: operationId: reauthTotp tags: [auth] summary: Open a disclosure reauthentication window with a TOTP code. description: | The possession-factor half of the reveal ceremony (#58, permission-model ADR § The reveal guard). It is DISCLOSURE reauthentication, not account step-up: it opens a sliding window over one environment and changes nothing about the account. The environment is RESOLVED AND AUTHORIZED under `read(E)` before its reauthentication policy is looked at. An environment the caller cannot reach answers exactly as a nonexistent one does — the uniform `401` this route gives every other refusal — so the policy-shaped `409` below is never an oracle for which environment ids exist or which are protected. TOTP cannot bind its challenge to an enumerated unit, so it cannot honour a per-disclosure gate. Where the environment's effective window is `0` — which every protected environment is capped at — this route REFUSES with `409`, naming the passkey ceremony as the remedy, and `/auth/webauthn/reauth/*` is the only path that opens a window there. The reauth rotates the acting session; a browser session's rotated token is delivered on the `__Host-hikyo` cookie. x-hikyo-class: unauthenticated x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/TotpReauthRequest" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The window this reauthentication opened. content: application/json: schema: $ref: "#/components/schemas/ReauthResult" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/Conflict" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/auth/cli-reauth/start: post: operationId: startCLIReauth tags: [auth] summary: Bind an adapter reauthentication handoff to the acting CLI session and PKCE challenge. x-hikyo-class: unauthenticated x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 requestBody: {required: true, content: {application/json: {schema: {$ref: "#/components/schemas/CLIReauthStartRequest"}}}} responses: "503": $ref: "#/components/responses/ServiceUnavailable" "201": {description: Opaque state for the browser handoff., content: {application/json: {schema: {$ref: "#/components/schemas/CLIReauthStart"}}}} "400": {$ref: "#/components/responses/BadRequest"} "401": {$ref: "#/components/responses/Unauthenticated"} "404": {$ref: "#/components/responses/NotFound"} "429": {$ref: "#/components/responses/TooManyRequests"} "500": {$ref: "#/components/responses/Internal"} /api/v1/auth/cli-reauth/transactions/{state}: parameters: - {name: state, in: path, required: true, schema: {type: string, minLength: 1, maxLength: 512}} get: operationId: showCLIReauthTransaction tags: [auth] summary: Load display-only policy and exact callback metadata for a live CLI handoff. x-hikyo-class: unauthenticated x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": {description: "Live transaction metadata without ids, bearers, or verifiers.", content: {application/json: {schema: {$ref: "#/components/schemas/CLIReauthTransaction"}}}} "401": {$ref: "#/components/responses/Unauthenticated"} "404": {$ref: "#/components/responses/NotFound"} "409": {$ref: "#/components/responses/Conflict"} "500": {$ref: "#/components/responses/Internal"} /api/v1/auth/cli-reauth/approve: post: operationId: approveCLIReauth tags: [auth] summary: Approve a CLI adapter ceremony with the browser session's exact bound windows. x-hikyo-class: unauthenticated x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 requestBody: {required: true, content: {application/json: {schema: {$ref: "#/components/schemas/CLIReauthApproveRequest"}}}} responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": {description: Single-use authorization code., content: {application/json: {schema: {$ref: "#/components/schemas/CLIReauthApproved"}}}} "400": {$ref: "#/components/responses/BadRequest"} "401": {$ref: "#/components/responses/Unauthenticated"} "404": {$ref: "#/components/responses/NotFound"} "409": {$ref: "#/components/responses/Conflict"} "500": {$ref: "#/components/responses/Internal"} /api/v1/auth/cli-reauth/redeem: post: operationId: redeemCLIReauth tags: [auth] summary: Atomically exchange code and PKCE verifier for the rotated CLI bearer. x-hikyo-class: unauthenticated x-hikyo-artifacts: [none] x-hikyo-min-revision: 1 security: [] requestBody: {required: true, content: {application/json: {schema: {$ref: "#/components/schemas/CLIReauthRedeemRequest"}}}} responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": {description: Rotated bearer disclosed once to the initiating shell., content: {application/json: {schema: {$ref: "#/components/schemas/CLIReauthRedeemed"}}}} "400": {$ref: "#/components/responses/BadRequest"} "401": {$ref: "#/components/responses/Unauthenticated"} "409": {$ref: "#/components/responses/Conflict"} "500": {$ref: "#/components/responses/Internal"} /api/v1/auth/totp: get: operationId: getTotpStatus tags: [auth] summary: Report whether the caller has a TOTP factor. description: | Reads the caller's OWN authenticator state: whether a confirmed factor stands, and whether an enrolment is mid-flight (a staged seed awaiting its first confirming code). It discloses nothing a caller could not already learn by attempting an enrolment — a second start is refused by name — so the account surface can state the fact rather than disclaim knowledge of it. x-hikyo-class: unauthenticated x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The caller's TOTP factor state. content: application/json: schema: $ref: "#/components/schemas/TotpStatus" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" delete: operationId: removeTotp tags: [auth] summary: Remove the confirmed TOTP factor. description: | An account-security mutation authorized by the password: the factor being removed is excluded from the proof set, so a stolen phone alone cannot drop the very factor it is (human-auth ADR § Account-security mutations, finding B7). Reissues the acting session carrying only the password class; the reissued token is returned. x-hikyo-class: unauthenticated x-hikyo-artifacts: [human-session] x-hikyo-reauth: {class: account-security, proof: [password], presence: required} x-hikyo-min-revision: 1 requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/TotpProofRequest" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: Factor removed. The reissued session token replaces the old one. content: application/json: schema: $ref: "#/components/schemas/LoginResult" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/auth/recovery-codes/regenerate: post: operationId: regenerateRecoveryCodes tags: [auth] summary: Replace the recovery-code batch; returns the codes once. description: | An account-security mutation proved by the confirmed TOTP code where one stands, else the password (recovery codes never authorize their own regeneration). Replaces the batch atomically and returns the plaintext codes exactly once. Reissues the acting session; the reissued token is returned and MUST replace the old one. x-hikyo-class: unauthenticated x-hikyo-artifacts: [human-session] x-hikyo-reauth: {class: account-security, proof: [proof], presence: required} x-hikyo-min-revision: 1 requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/RecoveryProofRequest" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: New batch. The codes are displayed once and never again. content: application/json: schema: $ref: "#/components/schemas/RecoveryCodesResult" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/Conflict" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/auth/recovery/begin: post: operationId: beginRecovery tags: [auth] summary: Consume a recovery code for a credential-establishment authority. description: | The pre-auth break-in-glass path. Consumes a single-use recovery code and mints a credential-establishment authority that may only ever establish a password (human-auth ADR § Recovery). It creates **no session, no assurance, no reauthentication window** — the holder establishes a new password with the authority and then logs in. Rides the instance-wide admission budget; an unknown username, an absent batch, a stale epoch and a non-matching code all answer the same `401`. security: [] x-hikyo-class: unauthenticated x-hikyo-artifacts: [none] x-hikyo-min-revision: 1 requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/RecoveryBeginRequest" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: A session-less credential-establishment authority, returned once. content: application/json: schema: $ref: "#/components/schemas/RecoveryBeginResult" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/orgs: get: operationId: listOrgs tags: [orgs] summary: List organisations. description: | An instance-scoped operator read of cross-tenant metadata, and therefore itself audited — the audit model's default-deny rule refuses `audited: none` to instance-class operations. The event commits with the read. x-hikyo-class: instance x-hikyo-operation: org.list x-hikyo-formula: [instance-config@instance] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 parameters: - name: origin in: query required: false description: | Only orgs of this origin: the operator's filter for self-served (`registration`) orgs, pruned with the ordinary delete (#585 d9). schema: type: string enum: [manual, registration] responses: "503": $ref: "#/components/responses/ServiceUnavailable" "400": $ref: "#/components/responses/BadRequest" "200": description: The organisations this principal may administer. content: application/json: schema: $ref: "#/components/schemas/OrgList" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "403": $ref: "#/components/responses/Forbidden" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" post: operationId: createOrg tags: [orgs] summary: Create an organisation. description: | Authorization is evaluated at the chokepoint inside the request's own transaction. The organisation, `org.created` event, and creator's org `admin` template grants commit atomically. The privilege increase ends the creator's sessions; the new membership is usable after login. Not idempotent: each call creates a distinct organisation. The name must be unused among live organisations; a collision is a `conflict`. x-hikyo-class: instance x-hikyo-operation: org.create x-hikyo-formula: [instance-config@instance, manage-members@instance] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CreateOrgRequest" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "201": description: The created organisation. content: application/json: schema: $ref: "#/components/schemas/Org" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "403": $ref: "#/components/responses/Forbidden" "409": $ref: "#/components/responses/Conflict" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/orgs/{org}: parameters: - $ref: "#/components/parameters/OrgID" get: operationId: getOrg tags: [hierarchy] summary: Read one organisation. description: | Tenant-scoped at org depth, unlike the create and list operations above: an organisation the caller may not reach answers exactly like one that does not exist, which is what "unauthorized access at each level returns the uniform nonexistent shape" requires of the top level too. There is therefore no `403` on this route — a refusal is a `404`. x-hikyo-class: tenant x-hikyo-operation: org.get x-hikyo-formula: [read@org] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The organisation. content: application/json: schema: $ref: "#/components/schemas/Org" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" patch: operationId: renameOrg tags: [hierarchy] summary: Rename an organisation. description: | Requires manage-members at the addressed organisation. Organisation administrators can rename their own organisation; instance-level manage-members grants satisfy this by inheritance. A bare instance-config grant does not authorize renaming. Refusal is the uniform 404. Deleting remains an instance-config operation. x-hikyo-class: tenant x-hikyo-operation: org.rename x-hikyo-formula: [manage-members@org] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/RenameRequest" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The renamed organisation. content: application/json: schema: $ref: "#/components/schemas/Org" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/Conflict" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" delete: operationId: deleteOrg tags: [hierarchy] summary: Delete an organisation. description: | Never cascades. An organisation that still holds projects, or grants pointing into it, is refused with `conflict` — emptying a tenant is explicit, separately audited work and not a side effect of one call. x-hikyo-class: tenant x-hikyo-operation: org.delete x-hikyo-formula: [instance-config@instance] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "204": description: Deleted. "401": $ref: "#/components/responses/Unauthenticated" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/Conflict" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/orgs/{org}/projects: parameters: - $ref: "#/components/parameters/OrgID" get: operationId: listProjects tags: [hierarchy] summary: List the organisation's projects. description: | A proof-scoped read at org depth. Unpaged: the hierarchy is bounded by the ops spec's own caps (50 environments per project) and an organisation's project list is administrative, not a data feed. x-hikyo-class: tenant x-hikyo-operation: project.list x-hikyo-formula: [read@org] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The projects in this organisation, by name. content: application/json: schema: $ref: "#/components/schemas/ProjectList" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" post: operationId: createProject tags: [hierarchy] summary: Create a project. description: | Not idempotent: each call creates a distinct project. The name must be unused among the organisation's live projects; a collision is a `conflict`, decided after authorization so it discloses nothing a caller could not already read. x-hikyo-class: tenant x-hikyo-operation: project.create x-hikyo-formula: [manage-projects@org] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CreateProjectRequest" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "201": description: The created project. content: application/json: schema: $ref: "#/components/schemas/Project" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/Conflict" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/orgs/{org}/retention: parameters: - $ref: "#/components/parameters/OrgID" get: operationId: getOrgRetention tags: [hierarchy] summary: Read the organisation retention cap. description: | The default is keep-if-either: age at most 90 days OR among the last 10 revisions of an environment. Unlimited is explicit org state, never a missing-value default. x-hikyo-class: tenant x-hikyo-operation: retention.org-read x-hikyo-formula: [read@org] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The organisation retention cap. content: application/json: schema: $ref: "#/components/schemas/RetentionPolicy" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" put: operationId: setOrgRetention tags: [hierarchy] summary: Set the organisation retention cap. x-hikyo-class: tenant x-hikyo-operation: retention.org-update x-hikyo-formula: [project-settings@org] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/RetentionPolicy" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The stored organisation retention cap. content: application/json: schema: $ref: "#/components/schemas/RetentionPolicy" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/orgs/{org}/projects/{project}: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ProjectID" get: operationId: getProject tags: [hierarchy] summary: Read one project. x-hikyo-class: tenant x-hikyo-operation: project.get x-hikyo-formula: [read@project] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The project. content: application/json: schema: $ref: "#/components/schemas/Project" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" patch: operationId: renameProject tags: [hierarchy] summary: Rename a project. description: | Changes the mutable name only. Identity is the immutable id, so a rename never breaks a reference. Idempotent: renaming to the current name succeeds and changes nothing. x-hikyo-class: tenant x-hikyo-operation: project.rename x-hikyo-formula: [manage-projects@org] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/RenameRequest" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The renamed project. content: application/json: schema: $ref: "#/components/schemas/Project" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/Conflict" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" delete: operationId: deleteProject tags: [hierarchy] summary: Delete a project. description: | Never cascades. A project that still holds environments or folders is refused with `conflict`; emptying it is the caller's explicit, separately audited work. x-hikyo-class: tenant x-hikyo-operation: project.delete x-hikyo-formula: [manage-projects@org] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "204": description: Deleted. "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/Conflict" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/orgs/{org}/projects/{project}/environments: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ProjectID" get: operationId: listEnvironments tags: [hierarchy] summary: List the project's environments, in display order. description: | Ordered by `display_order`, ties broken by name so the sequence is deterministic before any reorder has happened. x-hikyo-class: tenant x-hikyo-operation: environment.list x-hikyo-formula: [read@project] x-hikyo-artifacts: [human-session, machine-credential] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The project's environments, in display order. content: application/json: schema: $ref: "#/components/schemas/EnvironmentList" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" post: operationId: createEnvironment tags: [hierarchy] summary: Create an environment. description: | Environments are user-defined per project and appended at the end of the display order; `PUT .../environments/order` moves them. There is no `base` pointer and no defaults layer to inherit from — every value is explicit per environment (flat-model ADR). A project holds at most 50 environments; the 51st is refused by name with `limit_exceeded`, decided against a count read inside the creating transaction. x-hikyo-class: tenant x-hikyo-operation: environment.create x-hikyo-formula: [definitions-edit@project] x-hikyo-artifacts: [human-session, machine-credential] x-hikyo-min-revision: 1 requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CreateEnvironmentRequest" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "201": description: The created environment. content: application/json: schema: $ref: "#/components/schemas/Environment" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/Conflict" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/orgs/{org}/projects/{project}/environments/clone: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ProjectID" post: operationId: cloneEnvironment tags: [hierarchy] summary: Create an environment holding a copy of another one's values. description: | Clone-at-creation is ONE act: the environment row, every copied value and the audit records for both commit together or not at all - the atomicity rule admits no partially-valid creation. The copy is preflighted. `config` values copy freely (freely meaning without the reveal gate, not without authorization); `secret` values copy only where the source-material gate passes, and a caller who cannot read them is NOT refused the creation - what could not be copied comes back enumerated by name. The one exception is the abort: a `secret` that would be ABSENT in the new environment - the gate blocked it, or the source never held it - and is `required_in` it under a `mode: all` rule ABORTS the creation, naming the keys, because an environment born invalid is exactly what the atomicity rule forbids. It is a separate route rather than a flag on `createEnvironment` because its RESPONSE is different: a clone has to report what it could not take. x-hikyo-class: tenant x-hikyo-operation: environment.create x-hikyo-formula: [definitions-edit@project] x-hikyo-artifacts: [human-session, machine-credential] x-hikyo-min-revision: 1 requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CloneEnvironmentRequest" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "201": description: The created environment and what the clone carried. content: application/json: schema: $ref: "#/components/schemas/ClonedEnvironment" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/Conflict" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/orgs/{org}/projects/{project}/environments/order: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ProjectID" put: operationId: reorderEnvironments tags: [hierarchy] summary: Rewrite the project's environment display order. description: | Takes the WHOLE ordered set, not one position: a single transaction then cannot leave two environments sharing a position or a gap in the sequence, and a concurrent reorder serializes behind it. The list must name exactly the project's environments, once each. Anything else — a short list, a duplicate, an id from elsewhere — is one `bad_request` with one fixed message, so a foreign id discloses nothing. Idempotent: applying the same order twice changes nothing. x-hikyo-class: tenant x-hikyo-operation: environment.reorder x-hikyo-formula: [definitions-edit@project] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/EnvironmentOrderRequest" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The project's environments in their new display order. content: application/json: schema: $ref: "#/components/schemas/EnvironmentList" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/orgs/{org}/projects/{project}/environments/{environment}: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ProjectID" - $ref: "#/components/parameters/EnvironmentID" get: operationId: getEnvironment tags: [hierarchy] summary: Read one environment. x-hikyo-class: tenant x-hikyo-operation: environment.read x-hikyo-formula: [read@environment] x-hikyo-artifacts: [human-session, machine-credential] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The environment. content: application/json: schema: $ref: "#/components/schemas/Environment" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" patch: operationId: renameEnvironment tags: [hierarchy] summary: Rename an environment. description: | Environment topology, so it carries `definitions-edit` rather than `edit`: renaming is a change to the project's shape, not to a value. x-hikyo-class: tenant x-hikyo-operation: environment.rename x-hikyo-formula: [definitions-edit@project] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/RenameRequest" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The renamed environment. content: application/json: schema: $ref: "#/components/schemas/Environment" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/Conflict" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" delete: operationId: deleteEnvironment tags: [hierarchy] summary: Delete an environment. description: | The remaining environments keep their positions: display order is a property of each row, not an invariant over the set, so a gap is not a defect and closing it would be an unrequested write. Automation requires project-scoped definitions-edit and cannot delete a protected environment. Deletion removes values, drafts, pins and revision payloads; audit events remain subject to audit retention. x-hikyo-class: tenant x-hikyo-operation: environment.delete x-hikyo-formula: [definitions-edit@project] x-hikyo-artifacts: [human-session, machine-credential] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "204": description: Deleted. "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/Conflict" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/orgs/{org}/projects/{project}/retention: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ProjectID" get: operationId: getProjectRetention tags: [hierarchy] summary: Read the project's effective retention policy. x-hikyo-class: tenant x-hikyo-operation: retention.project-read x-hikyo-formula: [read@project] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: Effective policy and whether it is inherited. content: application/json: schema: $ref: "#/components/schemas/ProjectRetentionPolicy" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" put: operationId: setProjectRetention tags: [hierarchy] summary: Set or clear the project's bounded retention override. description: | inherited=true clears the override. A stored override must be at or below the organisation cap in both age and revision count; exceeding either is a named bad-request refusal. x-hikyo-class: tenant x-hikyo-operation: retention.project-update x-hikyo-formula: [project-settings@project] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/SetProjectRetentionRequest" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: Effective policy and whether it is inherited. content: application/json: schema: $ref: "#/components/schemas/ProjectRetentionPolicy" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/orgs/{org}/projects/{project}/folders: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ProjectID" get: operationId: listFolders tags: [hierarchy] summary: List the project's folders. x-hikyo-class: tenant x-hikyo-operation: folder.list x-hikyo-formula: [read@project] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The project's folders, by path. content: application/json: schema: $ref: "#/components/schemas/FolderList" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" post: operationId: createFolder tags: [hierarchy] summary: Create a folder. description: | A folder is organizational only in v1: namespace and display grouping. No grant is ever scoped to one and no value ever attaches to one. x-hikyo-class: tenant x-hikyo-operation: folder.create x-hikyo-formula: [definitions-edit@project] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CreateFolderRequest" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "201": description: The created folder. content: application/json: schema: $ref: "#/components/schemas/Folder" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/Conflict" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/orgs/{org}/projects/{project}/folders/{folder}: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ProjectID" - $ref: "#/components/parameters/FolderID" get: operationId: getFolder tags: [hierarchy] summary: Read one folder. x-hikyo-class: tenant x-hikyo-operation: folder.get x-hikyo-formula: [read@project] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The folder. content: application/json: schema: $ref: "#/components/schemas/Folder" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" patch: operationId: renameFolder tags: [hierarchy] summary: Move a folder to a new path. description: | Renames exactly the folder named. A v1 folder is a flat namespace label, not a tree node with children to carry along, so there is no cascade. x-hikyo-class: tenant x-hikyo-operation: folder.rename x-hikyo-formula: [definitions-edit@project] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/RenameFolderRequest" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The moved folder. content: application/json: schema: $ref: "#/components/schemas/Folder" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/Conflict" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" delete: operationId: deleteFolder tags: [hierarchy] summary: Delete a folder. x-hikyo-class: tenant x-hikyo-operation: folder.delete x-hikyo-formula: [definitions-edit@project] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "204": description: Deleted. "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" # The access surface (#55, permission-model ADR). One path per addressed # scope depth, because the authorization formula differs per depth and the # ADR forbids "the capability for this endpoint" as a statement: a grant # addressed at environment depth is authorized by `manage-members(project)`, # since `manage-members` is not grantable at environment scope at all. # # The grant's scope IS the addressed path. There is no scope in any body: # a body-supplied scope would let a caller authorized at one depth write a # grant at another, which is the whole formula defeated by a JSON field. /api/v1/instance/grants: get: operationId: listInstanceGrants tags: [access] summary: List the instance-scope membership surface. description: | One line per capability per principal, with the origins holding it. Grants inside an org are NOT listed here even though instance-scope grants reach into every org: membership of a scope is what was granted AT it. x-hikyo-class: instance x-hikyo-operation: grant.list-instance x-hikyo-formula: [manage-members@instance] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The instance-scope grant lines. content: application/json: schema: $ref: "#/components/schemas/GrantList" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "403": $ref: "#/components/responses/Forbidden" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" post: operationId: createInstanceGrant tags: [access] summary: Grant one capability at instance scope. x-hikyo-class: instance x-hikyo-operation: grant.create-instance x-hikyo-formula: [manage-members@instance] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CreateGrantRequest" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The grant mutation result, including an idempotent repeat. content: application/json: schema: $ref: "#/components/schemas/GrantResult" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "403": $ref: "#/components/responses/Forbidden" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" delete: operationId: revokeInstanceGrant tags: [access] summary: Revoke one capability at instance scope. description: | Releases the origins this surface owns and deletes the grant row when the last one is gone, advancing the target's session generation and deleting their sessions in the same transaction. Refused when it would remove the last instance `manage-members` holder. x-hikyo-class: instance x-hikyo-operation: grant.revoke-instance x-hikyo-formula: [manage-members@instance] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 parameters: - $ref: "#/components/parameters/GrantPrincipal" - $ref: "#/components/parameters/GrantCapability" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "204": description: Revoked. "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "403": $ref: "#/components/responses/Forbidden" "409": $ref: "#/components/responses/Conflict" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/instance/grants/template: post: operationId: applyInstanceTemplate tags: [access] summary: Apply a role template at instance scope. description: | The template expands AT GRANT TIME into individual capability grants, each separately visible and separately revocable. Nothing stores the template name. x-hikyo-class: instance x-hikyo-operation: grant.template-instance x-hikyo-formula: [manage-members@instance] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/ApplyTemplateRequest" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The grants the expansion produced. content: application/json: schema: $ref: "#/components/schemas/GrantResultList" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "403": $ref: "#/components/responses/Forbidden" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/instance/invitations: post: operationId: inviteInstanceMember tags: [access] summary: Invite a human at instance scope with a local credential. description: | The instance-scope form of `inviteOrgMember`: the same one-transaction account creation and authority mint, authorized by `manage-members` held at instance scope. The optional template is expanded at instance scope (`operator`), so this is how a second instance operator comes to exist without host access. Without a template the account can sign in and see nothing until someone grants it something. A username already in use is a `conflict`. x-hikyo-class: instance x-hikyo-operation: member.invite-instance x-hikyo-formula: [manage-members@instance] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/InviteMemberRequest" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "201": description: The invited principal and its single-use authority. content: application/json: schema: $ref: "#/components/schemas/InvitationResult" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "403": $ref: "#/components/responses/Forbidden" "409": $ref: "#/components/responses/Conflict" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/instance/registration-policy: get: operationId: getInstanceRegistrationPolicy tags: [access] summary: Read the instance registration policy. description: | The instance's open-registration policy (#579), or `404` when the scope has none: registration is closed. The standing delegation and the preconditions are re-checked on this read, so `state` is live: an authority principal who no longer holds the landing's grants, or a failing precondition, reads `inactive` with its cause. Recorded as a membership-surface read. x-hikyo-class: instance x-hikyo-operation: registration-policy.get-instance x-hikyo-formula: [manage-members@instance] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The policy with its live state. content: application/json: schema: $ref: "#/components/schemas/RegistrationPolicy" "401": $ref: "#/components/responses/Unauthenticated" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" put: operationId: putInstanceRegistrationPolicy tags: [access] summary: Create or replace the instance registration policy. description: | A full replacement; the caller becomes the authority principal (any edit reassigns it, #579 d4). Reauthentication-gated: `proof` carries a TOTP code, or the password where no factor is enrolled, consumed in the write transaction. No session is purged. Write-time preconditions refuse `400` naming the failing item in `detail`: `no-public-origin`, `mailer-unconfigured`, `provider-disabled`, `provider-kind-unsupported`, `provider-missing-email-scope`, `cap-zero`, `template-not-org-applicable`, with the provider as `:` where one is involved. `email` is never an allowlist claim. x-hikyo-class: instance x-hikyo-operation: registration-policy.put-instance x-hikyo-formula: [manage-members@instance] x-hikyo-artifacts: [human-session] x-hikyo-reauth: {class: account-security, proof: [proof], presence: required} x-hikyo-min-revision: 1 requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/RegistrationPolicyPutRequest" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The saved policy with its live state. content: application/json: schema: $ref: "#/components/schemas/RegistrationPolicy" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/Conflict" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" delete: operationId: deleteInstanceRegistrationPolicy tags: [access] summary: Close registration at this scope. description: | Deletes the policy and, in the same transaction, the pending local sign-ups it admitted (each audited `registration.signup_expired`, cause `policy-deleted`). Orgs the policy minted keep their origin and policy pointer. Reauthentication-gated like `put`; no session is purged. x-hikyo-class: instance x-hikyo-operation: registration-policy.delete-instance x-hikyo-formula: [manage-members@instance] x-hikyo-artifacts: [human-session] x-hikyo-reauth: {class: account-security, proof: [proof], presence: required} x-hikyo-min-revision: 1 requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/RegistrationPolicyDeleteRequest" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "204": description: Registration is closed at this scope. "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/Conflict" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/orgs/{org}/grants: parameters: - $ref: "#/components/parameters/OrgID" get: operationId: listOrgGrants tags: [access] summary: List the organisation's membership surface. description: | Every grant line scoped inside the org — org, project and environment scoped alike — with its origin chips. Instance-scope grants that reach the org by inheritance are deliberately absent: listing them here would invite revoking an instance operator from a page with no authority over one. x-hikyo-class: tenant x-hikyo-operation: grant.list-org x-hikyo-formula: [manage-members@org] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The organisation's grant lines. content: application/json: schema: $ref: "#/components/schemas/GrantList" "401": $ref: "#/components/responses/Unauthenticated" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" post: operationId: createOrgGrant tags: [access] summary: Grant one capability at organisation scope. description: | `manage-members` held at org scope may grant capabilities the grantor does not hold — the escalation path the threat model accepts, and the one that keeps a fresh installation bootstrappable. x-hikyo-class: tenant x-hikyo-operation: grant.create-org x-hikyo-formula: [manage-members@org] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CreateGrantRequest" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The grant mutation result, including an idempotent repeat. content: application/json: schema: $ref: "#/components/schemas/GrantResult" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" delete: operationId: revokeOrgGrant tags: [access] summary: Revoke one capability at organisation scope. description: | Refused when it would remove the last org `manage-members` holder: an unadministrable organisation is a support incident with no in-product recovery. x-hikyo-class: tenant x-hikyo-operation: grant.revoke-org x-hikyo-formula: [manage-members@org] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 parameters: - $ref: "#/components/parameters/GrantPrincipal" - $ref: "#/components/parameters/GrantCapability" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "204": description: Revoked. "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/Conflict" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/orgs/{org}/grants/template: parameters: - $ref: "#/components/parameters/OrgID" post: operationId: applyOrgTemplate tags: [access] summary: Apply a role template at organisation scope. x-hikyo-class: tenant x-hikyo-operation: grant.template-org x-hikyo-formula: [manage-members@org] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/ApplyTemplateRequest" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The grants the expansion produced. content: application/json: schema: $ref: "#/components/schemas/GrantResultList" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/orgs/{org}/invitations: parameters: - $ref: "#/components/parameters/OrgID" post: operationId: inviteOrgMember tags: [access] summary: Invite a human into this organisation with a local credential. description: | The account-creation path the human-auth ADR names ("accounts are created by invitation from a `manage-members` holder"). In ONE transaction: a human principal and an account with the given login handle are created, the optional role template is expanded into grants at organisation scope exactly as `applyOrgTemplate` would write them, and a credential-establishment authority is minted for the new account with issuer `invitation` and the credential-reset lifetime. The authority is returned once and never re-displayed: hand it to the invitee out of band, who consumes it at `establishCredential` and then signs in like anyone else. No session, no assurance and no identity link are created. An invitation carries no email and no delivery channel of its own. A username already in use is a `conflict`. x-hikyo-class: tenant x-hikyo-operation: member.invite-org x-hikyo-formula: [manage-members@org] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/InviteMemberRequest" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "201": description: The invited principal and its single-use authority. content: application/json: schema: $ref: "#/components/schemas/InvitationResult" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "403": $ref: "#/components/responses/Forbidden" "409": $ref: "#/components/responses/Conflict" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/orgs/{org}/registration-policy: parameters: - $ref: "#/components/parameters/OrgID" get: operationId: getOrgRegistrationPolicy tags: [access] summary: Read the organisation registration policy. description: | The organisation's open-registration policy (#579), or `404` when the scope has none: registration is closed. The standing delegation and the preconditions are re-checked on this read, so `state` is live: an authority principal who no longer holds the landing's grants, or a failing precondition, reads `inactive` with its cause. Recorded as a membership-surface read. x-hikyo-class: tenant x-hikyo-operation: registration-policy.get-org x-hikyo-formula: [manage-members@org] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The policy with its live state. content: application/json: schema: $ref: "#/components/schemas/RegistrationPolicy" "401": $ref: "#/components/responses/Unauthenticated" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" put: operationId: putOrgRegistrationPolicy tags: [access] summary: Create or replace the organisation registration policy. description: | A full replacement; the caller becomes the authority principal (any edit reassigns it, #579 d4). Reauthentication-gated: `proof` carries a TOTP code, or the password where no factor is enrolled, consumed in the write transaction. No session is purged. Write-time preconditions refuse `400` naming the failing item in `detail`: `no-public-origin`, `mailer-unconfigured`, `provider-disabled`, `provider-kind-unsupported`, `provider-missing-email-scope`, `cap-zero`, `template-not-org-applicable`, with the provider as `:` where one is involved. `email` is never an allowlist claim. x-hikyo-class: tenant x-hikyo-operation: registration-policy.put-org x-hikyo-formula: [manage-members@org] x-hikyo-artifacts: [human-session] x-hikyo-reauth: {class: account-security, proof: [proof], presence: required} x-hikyo-min-revision: 1 requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/RegistrationPolicyPutRequest" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The saved policy with its live state. content: application/json: schema: $ref: "#/components/schemas/RegistrationPolicy" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/Conflict" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" delete: operationId: deleteOrgRegistrationPolicy tags: [access] summary: Close registration at this scope. description: | Deletes the policy and, in the same transaction, the pending local sign-ups it admitted (each audited `registration.signup_expired`, cause `policy-deleted`). Orgs the policy minted keep their origin and policy pointer. Reauthentication-gated like `put`; no session is purged. x-hikyo-class: tenant x-hikyo-operation: registration-policy.delete-org x-hikyo-formula: [manage-members@org] x-hikyo-artifacts: [human-session] x-hikyo-reauth: {class: account-security, proof: [proof], presence: required} x-hikyo-min-revision: 1 requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/RegistrationPolicyDeleteRequest" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "204": description: Registration is closed at this scope. "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/Conflict" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/orgs/{org}/projects/{project}/grants: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ProjectID" get: operationId: listProjectGrants tags: [access] summary: List the project's membership surface. x-hikyo-class: tenant x-hikyo-operation: grant.list-project x-hikyo-formula: [manage-members@project] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The project's grant lines. content: application/json: schema: $ref: "#/components/schemas/GrantList" "401": $ref: "#/components/responses/Unauthenticated" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" post: operationId: createProjectGrant tags: [access] summary: Grant one capability at project scope. description: | `manage-members` held at PROJECT scope may grant only capabilities the grantor currently holds at or above the target scope, so a stolen project-admin account is exactly the authority it was given and not automatic compromise of the project's secrets. x-hikyo-class: tenant x-hikyo-operation: grant.create-project x-hikyo-formula: [manage-members@project] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CreateGrantRequest" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The grant mutation result, including an idempotent repeat. content: application/json: schema: $ref: "#/components/schemas/GrantResult" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" delete: operationId: revokeProjectGrant tags: [access] summary: Revoke one capability at project scope. x-hikyo-class: tenant x-hikyo-operation: grant.revoke-project x-hikyo-formula: [manage-members@project] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 parameters: - $ref: "#/components/parameters/GrantPrincipal" - $ref: "#/components/parameters/GrantCapability" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "204": description: Revoked. "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/Conflict" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/orgs/{org}/projects/{project}/grants/template: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ProjectID" post: operationId: applyProjectTemplate tags: [access] summary: Apply a role template at project scope. x-hikyo-class: tenant x-hikyo-operation: grant.template-project x-hikyo-formula: [manage-members@project] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/ApplyTemplateRequest" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The grants the expansion produced. content: application/json: schema: $ref: "#/components/schemas/GrantResultList" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/orgs/{org}/projects/{project}/environments/{environment}/grants: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ProjectID" - $ref: "#/components/parameters/EnvironmentID" post: operationId: createEnvGrant tags: [access] summary: Grant one capability on one environment. description: | Authorized by `manage-members(project)`, not by anything at environment scope: `manage-members` is an org/project atom, so the formula truncates the resolved chain to the project. There is no `grant.list-env` — "who can reach this environment" must include the org- and project-scoped grants that reach it, which an environment-only listing would silently omit; list at org or project scope instead. x-hikyo-class: tenant x-hikyo-operation: grant.create-env x-hikyo-formula: [manage-members@project] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CreateGrantRequest" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The grant mutation result, including an idempotent repeat. content: application/json: schema: $ref: "#/components/schemas/GrantResult" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" delete: operationId: revokeEnvGrant tags: [access] summary: Revoke one capability on one environment. x-hikyo-class: tenant x-hikyo-operation: grant.revoke-env x-hikyo-formula: [manage-members@project] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 parameters: - $ref: "#/components/parameters/GrantPrincipal" - $ref: "#/components/parameters/GrantCapability" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "204": description: Revoked. "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/Conflict" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/orgs/{org}/projects/{project}/environments/{environment}/grants/template: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ProjectID" - $ref: "#/components/parameters/EnvironmentID" post: operationId: applyEnvTemplate tags: [access] summary: Apply a role template on one environment. x-hikyo-class: tenant x-hikyo-operation: grant.template-env x-hikyo-formula: [manage-members@project] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/ApplyTemplateRequest" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The grants the expansion produced. content: application/json: schema: $ref: "#/components/schemas/GrantResultList" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/orgs/{org}/projects/{project}/environments/{environment}/settings: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ProjectID" - $ref: "#/components/parameters/EnvironmentID" get: operationId: getEnvironmentSettings tags: [access] summary: Read an environment's protection state and reauthentication window. description: | Bare `read(E)`: an environment's protection state is part of its public shape, and hiding it from a reader would make the reveal ceremony inexplicable to the person subject to it. x-hikyo-class: tenant x-hikyo-operation: environment.settings-read x-hikyo-formula: [read@environment] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The environment's settings. content: application/json: schema: $ref: "#/components/schemas/EnvironmentSettings" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" put: operationId: setEnvironmentSettings tags: [access] summary: Set an environment's protection state and reauthentication window. description: | `project-settings`, deliberately split out of `definitions-edit`: these knobs exist to restrain the definitions editor, and a guard whose off-switch sits in the hand it restrains is not a guard. Both knobs are written as ONE fact, because marking an environment protected CAPS its window at the protected default. Raising a protected environment's window above that cap is refused, never silently clamped. Omitting `reauth_window_seconds` means "inherit the instance default"; a null is that same statement written out. x-hikyo-class: tenant x-hikyo-operation: environment.settings-update x-hikyo-formula: [project-settings@project] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/EnvironmentSettings" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The settings as stored. content: application/json: schema: $ref: "#/components/schemas/EnvironmentSettings" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/orgs/{org}/projects/{project}/definitions/export: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ProjectID" get: operationId: exportDefinitions tags: [definitions] summary: Export the project's canonical definitions bundle. x-hikyo-class: tenant x-hikyo-operation: definitions.export x-hikyo-formula: [read@project] x-hikyo-artifacts: [human-session, machine-credential] x-hikyo-min-revision: 2 parameters: - name: portable in: query schema: {type: boolean, default: false} responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: Canonical definitions bundle bytes. content: application/json: schema: {$ref: "#/components/schemas/DefinitionsBundle"} "401": {$ref: "#/components/responses/Unauthenticated"} "404": {$ref: "#/components/responses/NotFound"} "429": {$ref: "#/components/responses/TooManyRequests"} "500": {$ref: "#/components/responses/Internal"} /api/v1/orgs/{org}/projects/{project}/definitions/check: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ProjectID" post: operationId: checkDefinitions tags: [definitions] summary: Compare a definitions bundle with current project state. x-hikyo-class: tenant x-hikyo-operation: definitions.check x-hikyo-formula: [read@project] x-hikyo-artifacts: [human-session, machine-credential] x-hikyo-min-revision: 2 requestBody: required: true content: application/json: schema: {$ref: "#/components/schemas/DefinitionsBundle"} responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: Drift classification and structural differences. content: application/json: schema: {$ref: "#/components/schemas/DefinitionsCheckResult"} "400": {$ref: "#/components/responses/BadRequest"} "401": {$ref: "#/components/responses/Unauthenticated"} "404": {$ref: "#/components/responses/NotFound"} "429": {$ref: "#/components/responses/TooManyRequests"} "500": {$ref: "#/components/responses/Internal"} /api/v1/orgs/{org}/projects/{project}/definitions/plans: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ProjectID" - $ref: "#/components/parameters/DefinitionsAcknowledgements" post: operationId: createDefinitionsPlan tags: [definitions] summary: Persist an immutable definitions impact plan. x-hikyo-class: tenant x-hikyo-operation: definitions.plan x-hikyo-formula: [definitions-edit@project] x-hikyo-artifacts: [human-session, machine-credential] x-hikyo-min-revision: 2 requestBody: required: true content: application/json: schema: {$ref: "#/components/schemas/DefinitionsBundle"} responses: "503": $ref: "#/components/responses/ServiceUnavailable" "201": description: The immutable impact plan. content: application/json: schema: {$ref: "#/components/schemas/DefinitionsPlanResponse"} "400": {$ref: "#/components/responses/BadRequest"} "401": {$ref: "#/components/responses/Unauthenticated"} "404": {$ref: "#/components/responses/NotFound"} "409": {$ref: "#/components/responses/Conflict"} "429": {$ref: "#/components/responses/TooManyRequests"} "500": {$ref: "#/components/responses/Internal"} /api/v1/orgs/{org}/projects/{project}/definitions/plans/{plan}: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ProjectID" - $ref: "#/components/parameters/DefinitionsPlanID" get: operationId: getDefinitionsPlan tags: [definitions] summary: Read an immutable definitions impact plan. x-hikyo-class: tenant x-hikyo-operation: definitions.plan.get x-hikyo-formula: [definitions-edit@project] x-hikyo-artifacts: [human-session, machine-credential] x-hikyo-min-revision: 2 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The immutable impact plan. content: application/json: schema: {$ref: "#/components/schemas/DefinitionsPlanResponse"} "401": {$ref: "#/components/responses/Unauthenticated"} "404": {$ref: "#/components/responses/NotFound"} "429": {$ref: "#/components/responses/TooManyRequests"} "500": {$ref: "#/components/responses/Internal"} /api/v1/orgs/{org}/projects/{project}/definitions/plans/{plan}/apply: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ProjectID" - $ref: "#/components/parameters/DefinitionsPlanID" post: operationId: applyDefinitionsPlan tags: [definitions] summary: Atomically apply a pinned definitions plan. x-hikyo-class: tenant x-hikyo-operation: definitions.apply x-hikyo-formula: [definitions-edit@project] x-hikyo-dynamic-formula: [publish@each-environment] x-hikyo-artifacts: [human-session, machine-credential] x-hikyo-min-revision: 2 requestBody: required: true content: application/json: schema: {$ref: "#/components/schemas/ApplyDefinitionsPlanRequest"} responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The committed schema revision and published environments. content: application/json: schema: {$ref: "#/components/schemas/ApplyDefinitionsPlanResult"} "400": {$ref: "#/components/responses/BadRequest"} "401": {$ref: "#/components/responses/Unauthenticated"} "404": {$ref: "#/components/responses/NotFound"} "409": {$ref: "#/components/responses/Conflict"} "429": {$ref: "#/components/responses/TooManyRequests"} "500": {$ref: "#/components/responses/Internal"} /api/v1/orgs/{org}/projects/{project}/definitions/settings: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ProjectID" get: operationId: getDefinitionsSettings tags: [definitions] summary: Read definitions governance settings and last-apply provenance. x-hikyo-class: tenant x-hikyo-operation: definitions.settings.get x-hikyo-formula: [read@project] x-hikyo-artifacts: [human-session, machine-credential] x-hikyo-min-revision: 2 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: Definitions governance settings. content: application/json: schema: {$ref: "#/components/schemas/DefinitionsSettings"} "401": {$ref: "#/components/responses/Unauthenticated"} "404": {$ref: "#/components/responses/NotFound"} "429": {$ref: "#/components/responses/TooManyRequests"} "500": {$ref: "#/components/responses/Internal"} put: operationId: setDefinitionsSettings tags: [definitions] summary: Select database- or Git-governed definitions. x-hikyo-class: tenant x-hikyo-operation: definitions.settings.set x-hikyo-formula: [project-settings@project] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 2 requestBody: required: true content: application/json: schema: {$ref: "#/components/schemas/SetDefinitionsSettingsRequest"} responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: Updated definitions governance settings. content: application/json: schema: {$ref: "#/components/schemas/DefinitionsSettings"} "400": {$ref: "#/components/responses/BadRequest"} "401": {$ref: "#/components/responses/Unauthenticated"} "404": {$ref: "#/components/responses/NotFound"} "429": {$ref: "#/components/responses/TooManyRequests"} "500": {$ref: "#/components/responses/Internal"} /api/v1/orgs/{org}/projects/{project}/machine-reveal: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ProjectID" get: operationId: getMachineReveal tags: [access] summary: Read the per-project machine-reveal opt-in. description: | The source-of-truth ADR's explicit, documented, per-project operator opt-in. While it is off no machine principal may be granted `reveal` in this project and every machine fetch delivers secret presence only, whatever grant rows exist. x-hikyo-class: tenant x-hikyo-operation: project.machine-reveal.get x-hikyo-formula: [read@project] x-hikyo-artifacts: [human-session, machine-credential] x-hikyo-min-revision: 2 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The opt-in state. content: application/json: schema: {$ref: "#/components/schemas/MachineRevealSettings"} "401": {$ref: "#/components/responses/Unauthenticated"} "404": {$ref: "#/components/responses/NotFound"} "429": {$ref: "#/components/responses/TooManyRequests"} "500": {$ref: "#/components/responses/Internal"} put: operationId: setMachineReveal tags: [access] summary: Enable or withdraw the per-project machine-reveal opt-in. description: | Enabling admits `reveal` grants onto workload and automation principals in this project - a standing decryption capability, and on a CI runner that capability in the most-attacked box in the system (machine-identities ADR). Withdrawing it stops every machine secret delivery on the next fetch without touching any grant and moves every machine cursor. The formula carries `reveal` because nobody may confer plaintext reach they do not hold themselves, and `reveal` is MFA-mandatory. x-hikyo-class: tenant x-hikyo-operation: project.machine-reveal.set x-hikyo-formula: [project-settings@project, reveal@project] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 2 requestBody: required: true content: application/json: schema: {$ref: "#/components/schemas/MachineRevealSettings"} responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The opt-in state after the write. content: application/json: schema: {$ref: "#/components/schemas/MachineRevealSettings"} "400": {$ref: "#/components/responses/BadRequest"} "401": {$ref: "#/components/responses/Unauthenticated"} "403": {$ref: "#/components/responses/Forbidden"} "404": {$ref: "#/components/responses/NotFound"} "429": {$ref: "#/components/responses/TooManyRequests"} "500": {$ref: "#/components/responses/Internal"} /api/v1/orgs/{org}/projects/{project}/keys: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ProjectID" get: operationId: listKeys tags: [keys] summary: List the project's key catalogue. description: | The catalogue IS the project's schema, so the response carries the project's monotonic schema revision beside the keys: a reader that acts on this list can say which schema it saw. x-hikyo-class: tenant x-hikyo-operation: key.list x-hikyo-formula: [read@project] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The project's keys, by name, and the schema revision. content: application/json: schema: $ref: "#/components/schemas/KeyList" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" post: operationId: createKey tags: [keys] summary: Declare a key. description: | The schema is closed: typing a key name that does not exist is a key CREATION, an explicit act, never a silent value write. Declaration well-formedness BLOCKS the save — a value may be wrong, a rule may not be meaningless — so an unsupported RE2 construct, an out-of-profile JSON Schema keyword, a reference cycle or a bound violation is refused here rather than discovered at publish. x-hikyo-class: tenant x-hikyo-operation: key.create x-hikyo-formula: [definitions-edit@project] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CreateKeyRequest" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "201": description: The declared key. content: application/json: schema: $ref: "#/components/schemas/Key" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/Conflict" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/orgs/{org}/projects/{project}/keys/{key}: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ProjectID" - $ref: "#/components/parameters/KeyID" get: operationId: getKey tags: [keys] summary: Read one key declaration. x-hikyo-class: tenant x-hikyo-operation: key.get x-hikyo-formula: [read@project] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The key. content: application/json: schema: $ref: "#/components/schemas/Key" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" patch: operationId: updateKeyMetadata tags: [keys] summary: Update a key's non-semantic metadata. description: | Folder path, description, deprecation flag and deprecation note only. These cannot change what any environment delivers or whether it validates, so they need `definitions-edit` alone, take no reveal gate, and move no schema revision. A body carrying `classification` is REFUSED — classification changes only through the reclassification ceremony. The field is declared here so the refusal names the ceremony rather than reading as an unknown member. x-hikyo-class: tenant x-hikyo-operation: key.update-metadata x-hikyo-formula: [definitions-edit@project] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/UpdateKeyMetadataRequest" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The updated key. content: application/json: schema: $ref: "#/components/schemas/Key" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/Conflict" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" delete: operationId: deleteKey tags: [keys] summary: Delete a key. description: | Removes the declaration, its explicit presence rows and its group membership. The group survives, possibly inert. x-hikyo-class: tenant x-hikyo-operation: key.delete x-hikyo-formula: [definitions-edit@project] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "204": description: Deleted. "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/Conflict" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/orgs/{org}/projects/{project}/keys/{key}/name: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ProjectID" - $ref: "#/components/parameters/KeyID" put: operationId: renameKey tags: [keys] summary: Rename a key. description: | Identity is the immutable id, so a rename never breaks a reference — but it DOES change the delivered payload's key set, which makes it a content-affecting schema change and advances the schema revision. x-hikyo-class: tenant x-hikyo-operation: key.rename x-hikyo-formula: [definitions-edit@project] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/RenameKeyRequest" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The renamed key. content: application/json: schema: $ref: "#/components/schemas/Key" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/Conflict" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/orgs/{org}/projects/{project}/keys/{key}/declaration: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ProjectID" - $ref: "#/components/parameters/KeyID" put: operationId: updateKeyDeclaration tags: [keys] summary: Replace a key's value-dependent rules and presence rules. description: | Changing a VALUE-DEPENDENT rule on a `secret`-classified key is a disclosure: the result bit itself leaks the value, so such a change additionally requires current `reveal` on that key and is rejected WITHOUT EVALUATING the new declaration when the caller lacks it. The refusal is the uniform nonexistent shape, because a distinguishable one would itself be the oracle the gate exists to close. Presence rules are not value-dependent and need no reveal: they report only whether an entry is set or absent. x-hikyo-class: tenant x-hikyo-operation: key.update-declaration x-hikyo-formula: [definitions-edit@project] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/UpdateKeyDeclarationRequest" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The updated key. content: application/json: schema: $ref: "#/components/schemas/Key" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/Conflict" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/orgs/{org}/projects/{project}/keys/{key}/classification: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ProjectID" - $ref: "#/components/parameters/KeyID" put: operationId: reclassifyKey tags: [keys] summary: The reclassification ceremony. description: | The ONLY path that changes a key's classification. Declassifying (`secret` to `config`) additionally requires current `reveal`: after it the value is readable under ordinary environment read, and you must be able to see a secret in order to stop it being one. Tightening (`config` to `secret`) needs no reveal and cannot un-disclose what was already served as config. x-hikyo-class: tenant x-hikyo-operation: key.reclassify x-hikyo-formula: [definitions-edit@project] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/ReclassifyKeyRequest" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The reclassified key. content: application/json: schema: $ref: "#/components/schemas/Key" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/Conflict" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/orgs/{org}/projects/{project}/keys/{key}/group: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ProjectID" - $ref: "#/components/parameters/KeyID" put: operationId: setKeyGroup tags: [keys] summary: Set or clear a key's group membership. description: | A key belongs to at most one group; an empty `group_id` takes it out of every group. Membership is coupling, and coupling is a schema change. x-hikyo-class: tenant x-hikyo-operation: key.set-group x-hikyo-formula: [definitions-edit@project] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/SetKeyGroupRequest" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The updated key. content: application/json: schema: $ref: "#/components/schemas/Key" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/Conflict" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/orgs/{org}/projects/{project}/key-groups: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ProjectID" get: operationId: listKeyGroups tags: [keys] summary: List the project's key groups. description: | Each group reports its members and whether it is INERT — a group left with fewer than two members couples nothing, which is surfaced rather than silently repaired. x-hikyo-class: tenant x-hikyo-operation: key-group.list x-hikyo-formula: [read@project] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The project's key groups, by name. content: application/json: schema: $ref: "#/components/schemas/KeyGroupList" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" post: operationId: createKeyGroup tags: [keys] summary: Declare a key group. x-hikyo-class: tenant x-hikyo-operation: key-group.create x-hikyo-formula: [definitions-edit@project] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CreateKeyGroupRequest" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "201": description: The created group. content: application/json: schema: $ref: "#/components/schemas/KeyGroup" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/Conflict" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/orgs/{org}/projects/{project}/key-groups/{group}: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ProjectID" - $ref: "#/components/parameters/KeyGroupID" get: operationId: getKeyGroup tags: [keys] summary: Read one key group. x-hikyo-class: tenant x-hikyo-operation: key-group.get x-hikyo-formula: [read@project] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The group. content: application/json: schema: $ref: "#/components/schemas/KeyGroup" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" patch: operationId: renameKeyGroup tags: [keys] summary: Rename a key group. description: | A group name is never delivered and never validated against, so — unlike a key rename — it moves no schema revision. x-hikyo-class: tenant x-hikyo-operation: key-group.rename x-hikyo-formula: [definitions-edit@project] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/RenameKeyGroupRequest" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The renamed group. content: application/json: schema: $ref: "#/components/schemas/KeyGroup" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/Conflict" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" delete: operationId: deleteKeyGroup tags: [keys] summary: Delete a key group. description: | Dissolves the coupling and releases its members; it never deletes the keys it coupled. x-hikyo-class: tenant x-hikyo-operation: key-group.delete x-hikyo-formula: [definitions-edit@project] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "204": description: Deleted. "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/Conflict" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/orgs/{org}/projects/{project}/environments/{environment}/values: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ProjectID" - $ref: "#/components/parameters/EnvironmentID" get: operationId: listValues tags: [values] summary: The environment's resolved values. description: | One row per DECLARED key, each `set` or `absent`. In the flat model this IS the resolution - a lookup per key with nothing underneath - so an absent row carries no value from any source. x-hikyo-class: tenant x-hikyo-operation: value.list x-hikyo-formula: [read@environment] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The environment's resolved values. content: application/json: schema: $ref: "#/components/schemas/ValueList" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/orgs/{org}/projects/{project}/environments/{environment}/values/{key}: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ProjectID" - $ref: "#/components/parameters/EnvironmentID" - $ref: "#/components/parameters/ValueKeyName" get: operationId: getValue tags: [values] summary: Read one value cell. x-hikyo-class: tenant x-hikyo-operation: value.read x-hikyo-formula: [read@environment] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The cell, `set` or `absent`. content: application/json: schema: $ref: "#/components/schemas/ValueCell" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" put: operationId: setValue tags: [values] summary: Stage one value edit. description: | This STAGES; it does not publish. The edit lands in the caller's own working state as a pending change with an immutable version id, and the environment keeps delivering what it delivered until a publish names that id. The formula is `edit` ALONE: `edit` confers no delivery power and a draft is never a disclosure, so staging must not require `publish`. Edit-without-publish is the propose-and-review flow. SAVING IS FREE. The value is not validated here, and a key that is `forbidden_in` this environment may still be staged: a draft is the user's scratchpad, and blocking a save pushes work in progress into external notepads, which for secrets is exactly where it must not go. Every one of those refusals fires at publish instead, by name. A key that is not declared still refuses - typing a name that does not exist is a key CREATION, an explicit act elsewhere. x-hikyo-class: tenant x-hikyo-operation: value.stage x-hikyo-formula: [edit@environment] x-hikyo-artifacts: [human-session, machine-credential] x-hikyo-min-revision: 1 requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/SetValueRequest" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The staged pending change. It never echoes the value back. content: application/json: schema: $ref: "#/components/schemas/PendingChange" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/Conflict" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" delete: operationId: clearValue tags: [values] summary: Stage a clear to `absent`. description: | Stages the `set` to `absent` transition; it does not publish. With nothing to inherit from, this is the whole of "remove this value now": there is no layer underneath for it to fall back to. A key `required_in` this environment may be staged for clearing and the publish that would commit it is what refuses, naming key and environment. Saving is free; publish is the authority. x-hikyo-class: tenant x-hikyo-operation: value.stage x-hikyo-formula: [edit@environment] x-hikyo-artifacts: [human-session, machine-credential] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The staged pending change. content: application/json: schema: $ref: "#/components/schemas/PendingChange" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/orgs/{org}/projects/{project}/values/declare: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ProjectID" post: operationId: declareValues tags: [values] summary: Declare one supplied value into several environments. description: | Inheritance's legitimate job was "I do not want to type this four times"; this is the explicit operation that does it. One plaintext the caller supplies, several environments, one transaction - and no relationship between the copies: editing one later changes nothing elsewhere, by design. Authorized per DESTINATION. Holding the write formula on two of three environments writes into none of them. x-hikyo-class: tenant x-hikyo-operation: value.set x-hikyo-formula: [edit@environment, publish@environment] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/DeclareValuesRequest" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The cells as stored, one per environment. content: application/json: schema: $ref: "#/components/schemas/ValueList" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/Conflict" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/orgs/{org}/projects/{project}/values/copy: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ProjectID" post: operationId: copyValues tags: [values] summary: Copy stored values into other environments (copy-to / bulk-apply). description: | Server-side duplication of STORED material, which the flat model calls authorized duplication and never supply. The locked formula, evaluated per side and per classification: secret material: reveal(source) AND reveal(destination) AND publish(destination) config material: read(source) AND publish(destination) Every copy is an INDEPENDENT value: the ciphertext is never carried over, the plaintext is re-sealed under the destination row's own associated data, and editing the source later changes nothing downstream. A protected destination refuses the copy unless `confirm_protected` says so, by name. x-hikyo-class: tenant x-hikyo-operation: value.copy-destination x-hikyo-formula: [reveal@environment, publish@environment] x-hikyo-artifacts: [human-session, machine-credential] x-hikyo-min-revision: 1 requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CopyValuesRequest" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: What landed, one entry per (key, destination). content: application/json: schema: $ref: "#/components/schemas/CopyValuesResult" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/Conflict" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/orgs/{org}/projects/{project}/values/diff: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ProjectID" get: operationId: diffValues tags: [values] summary: Compare two environments' values. description: | An explicit, authorized, per-invocation comparison - NOT an ambient drift signal: divergence between environments is the point of the flat model, so equality is not a health signal and nothing watches it. Oracle rules: write-presence without the reveal gate, plaintext only with it. Where both sides are `set` and at least one is unreadable, `equal` is ABSENT rather than false - whether two secrets match is itself material. Authorized per side, so `read` on one environment says nothing about the other. x-hikyo-class: tenant x-hikyo-operation: value.list x-hikyo-formula: [read@environment] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 parameters: - name: left in: query required: true description: The left environment. schema: $ref: "#/components/schemas/ID" - name: right in: query required: true description: The right environment. schema: $ref: "#/components/schemas/ID" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: One row per declared key. content: application/json: schema: $ref: "#/components/schemas/ValueDiff" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/orgs/{org}/projects/{project}/environments/{environment}/reveal-window: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ProjectID" - $ref: "#/components/parameters/EnvironmentID" get: operationId: getRevealWindow tags: [values] summary: The reveal guard's state for this environment and session. description: | The window gates the PROMPT, never the authorization check (permission-model ADR § The reveal guard), so a client has to know whether the next disclosure will prompt and which factor that prompt may offer. This route answers exactly that and discloses no material, which is why its formula is `read` alone: the effective window and the protected flag are project settings, and the live window is the caller's own session state. `totp_offered` is false exactly where the effective window is `0`. Human sessions only: a machine identity never reauthenticates, has no window to report, and reaches secret material through its own fetch path rather than this one. x-hikyo-class: tenant x-hikyo-operation: reveal.window_read x-hikyo-formula: [read@environment] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The guard's state. content: application/json: schema: $ref: "#/components/schemas/RevealWindow" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/orgs/{org}/projects/{project}/environments/{environment}/values/occurrences: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ProjectID" - $ref: "#/components/parameters/EnvironmentID" post: operationId: listValueOccurrences tags: [values] summary: Import phase 1 - declared keys, presence, and occurrence tokens. description: | The ONLY server contact an `import` phase 1 makes. It returns, for one environment: the project's definitions revision, and per key its classification, its declared type, whether it is `set` or `absent`, and a SERVER-MINTED OPAQUE OCCURRENCE TOKEN naming the exact resolved state observed. It is a POST because it carries a body, not because it acts: `candidates` names the keys and exact declarations the run intends to propose. Every one of them gets a token, INCLUDING names the project does not declare yet - those are exactly the keys an import is about to create, and a manifest row without a server-minted token is the one row an edited manifest could forge freely. An undeclared candidate answers `declared: false`, `set: false` and a token naming that state. The token is what binds phase 1 to phase 2. A bucket label cannot do the job: `set` to `set` with a changed value preserves the bucket, so a bucket-checked "reviewed overwrite" would still clobber the newer value. The token is computed over the value row id, which is minted per write and never reused, so an edit advances it and the match fails. It is opaque by construction: there is nothing inside it to parse, and a caller cannot construct one for a state they have not been served. An unrecognized token is exactly as informative as a stale one. It never requires `reveal`, never compares values and never writes. Human-only: `import` joins the human-only verb list. x-hikyo-class: tenant x-hikyo-operation: import.presence x-hikyo-formula: [read@project] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/ValueOccurrencesRequest" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The environment's keys, presence and occurrence tokens. content: application/json: schema: $ref: "#/components/schemas/ValueOccurrenceList" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/orgs/{org}/projects/{project}/environments/{environment}/values/import: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ProjectID" - $ref: "#/components/parameters/EnvironmentID" post: operationId: importValues tags: [values] summary: Import phase 2 - a strict, human-only batch value write. description: | Consumes one environment's values file. Strict closed schema: an undeclared key rejects the run BY NAME rather than being created, and the whole run is one transaction. Collisions bucket on the target environment's state. A key already `set` is SKIPPED BY DEFAULT and listed by name; `overwrite` is an enumerated opt-in list, never a blanket flag. Skip-by-default is what makes a re-run naturally idempotent. `precondition` is the one declared additive input, consumed from an import run manifest. When present, the server re-evaluates phase 1's read formula - `read@project AND read@environment` for every environment the manifest names - inside this same transaction and ON TOP of this verb's own formula, and only then compares the definitions revision and each written key's occurrence token. A caller lacking either read atom receives the plain authorization failure and no precondition result: the precondition is not an oracle. Any movement rejects those keys by name and writes nothing. Human-only, per the locked human-only verb list. x-hikyo-class: tenant x-hikyo-operation: value.import x-hikyo-formula: [read@project, edit@environment, publish@environment] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/ImportValuesRequest" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: What landed and what was skipped by name. content: application/json: schema: $ref: "#/components/schemas/ImportValuesResult" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/Conflict" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/orgs/{org}/projects/{project}/environments/{environment}/values/reveal: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ProjectID" - $ref: "#/components/parameters/EnvironmentID" post: operationId: revealValues tags: [values] summary: The environment's values, with `secret` plaintext. description: | The locked disclosure formula for current material: `read(E)` AND `reveal(E)`, with reauthentication and ONE audit event per disclosed key - never one row saying "revealed 40 secrets". It is a POST, and a route of its own rather than a flag on the read, for two reasons that are the same reason: disclosure is an ACT (it writes the audit records that make it accountable), and the ADR's rule is one verb per disclosure path, one gate. x-hikyo-class: tenant x-hikyo-operation: value.reveal x-hikyo-formula: [read@environment, reveal@environment] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The environment's resolved values, `secret` plaintext included. content: application/json: schema: $ref: "#/components/schemas/ValueList" "401": $ref: "#/components/responses/Unauthenticated" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/orgs/{org}/projects/{project}/environments/{environment}/values/{key}/reveal: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ProjectID" - $ref: "#/components/parameters/EnvironmentID" - $ref: "#/components/parameters/ValueKeyName" post: operationId: revealValue tags: [values] summary: Read one cell with `secret` plaintext. x-hikyo-class: tenant x-hikyo-operation: value.reveal x-hikyo-formula: [read@environment, reveal@environment] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The cell, plaintext included. content: application/json: schema: $ref: "#/components/schemas/ValueCell" "401": $ref: "#/components/responses/Unauthenticated" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/orgs/{org}/projects/{project}/values/diff/reveal: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ProjectID" post: operationId: revealValueDiff tags: [values] summary: Compare two environments with `secret` plaintext. description: | The gated half of `values diff`. Authorized PER SIDE - `reveal` on staging says nothing about prod - and one disclosure event per key per side. x-hikyo-class: tenant x-hikyo-operation: value.reveal x-hikyo-formula: [read@environment, reveal@environment] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/RevealDiffRequest" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: One row per declared key, plaintext included. content: application/json: schema: $ref: "#/components/schemas/ValueDiff" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/auth/methods: get: operationId: authMethods tags: [auth] summary: Enabled login methods for this instance. description: | Public discovery: the enabled OIDC provider slugs and display names, and whether local login is enabled. Instance-level, never per-account, so it discloses nothing about which accounts or factors exist. Under the discovery admission bucket. security: [] x-hikyo-class: unauthenticated x-hikyo-artifacts: [none] x-hikyo-min-revision: 1 parameters: - name: org in: query required: false description: | The organisation whose sign-up door to render (`signup_open`, `signup_paused`, `signup_methods`); absent means the instance scope. An unknown organisation and one without a policy are the same closed door. schema: type: string maxLength: 128 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The enabled login methods. content: application/json: schema: $ref: "#/components/schemas/AuthMethods" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/auth/oidc/{provider}/start: parameters: - $ref: "#/components/parameters/ProviderSlug" post: operationId: oidcStart tags: [auth] summary: Begin an OIDC transaction (login, link or reauth). description: | Creates a single-use server-side transaction (PKCE S256 always) and returns the IdP authorization URL. `login` is anonymous and bound by a `__Host-` browser-binding cookie set on this response (A2/A16); `link` and `reauth` require an authenticated session and are session-bound, and `link` verifies the account-security proof up front (A6). A `login` records its `intent` (absent = `sign-in`) and, for a `sign-up`, the addressed scope (`signup_org`, absent = instance); the start reads no registration policy (#604 d5). x-hikyo-class: unauthenticated x-hikyo-artifacts: [human-session] x-hikyo-reauth: {class: account-security, proof: [proof], presence: when-value, member: purpose, values: [link]} x-hikyo-min-revision: 1 requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/OidcStartRequest" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The IdP authorization URL. content: application/json: schema: $ref: "#/components/schemas/OidcStartResult" # Every expected start refusal (unknown or disabled slug, bad purpose, # environment-less reauth, unauthenticated link/reauth, and a missing # body) collapses to one uniform 401 so a pre-auth prober cannot # enumerate provider config. 429/500 are the cross-cutting admission # and fault responses. There are two exceptions. First, a # request-shape refusal that depends on nothing but the body: # `environment_id` on a purpose other than `reauth` is a 400 naming # the field. Second, a `reauth` on a provider row with no assurance # policy is refused by name (409, the remedy in `detail`) before any # round-trip (#588 d4): the caller has already authenticated a session # through that very provider, so naming the row's policy state # discloses nothing to a prober. "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "409": $ref: "#/components/responses/Conflict" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/auth/oidc/{provider}/callback: parameters: - $ref: "#/components/parameters/ProviderSlug" get: operationId: oidcCallback tags: [auth] summary: Complete an OIDC transaction from the IdP redirect. description: | Per-provider distinct callback path (mix-up defence leg 1): the path provider must equal the transaction's provider, refused BEFORE any token is fetched (A1). The transaction is consumed single-use, the code is exchanged only at the recorded provider, the ID token validated completely, and the purpose wall is structural. Returns the minted or rotated session. x-hikyo-class: unauthenticated x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 parameters: - name: code in: query required: false schema: { type: string, maxLength: 4096 } - name: state in: query required: false schema: { type: string, maxLength: 512 } - name: iss in: query required: false schema: { type: string, maxLength: 2048 } - name: error in: query required: false schema: { type: string, maxLength: 256 } responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The minted or rotated session. content: application/json: schema: $ref: "#/components/schemas/LoginResult" "303": description: Browser transaction completed; continue on the SPA done page. headers: Location: required: true schema: { type: string } "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/auth/saml/{provider}/start: parameters: - $ref: "#/components/parameters/ProviderSlug" post: operationId: samlStart tags: [auth] summary: Begin an SP-initiated SAML transaction (login, link or reauth). description: | Mints the server-side request, RelayState and initiator binding before returning the IdP HTTP-Redirect URL. `login` is anonymous; `link` and `reauth` require and bind the acting human session. The initiator cookie is delivered only as a Secure, HttpOnly, SameSite=None cookie scoped to this provider's ACS path; it is never returned in JSON. x-hikyo-class: unauthenticated x-hikyo-artifacts: [none, human-session] x-hikyo-reauth: {class: account-security, proof: [proof], presence: when-value, member: purpose, values: [link]} x-hikyo-min-revision: 1 security: - {} - hikyoSession: [] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/SamlStartRequest" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The signed or unsigned IdP HTTP-Redirect URL. content: application/json: schema: $ref: "#/components/schemas/SamlStartResult" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/auth/saml/{provider}/acs: parameters: - $ref: "#/components/parameters/ProviderSlug" post: operationId: samlACS tags: [auth] summary: Complete an SP-initiated SAML transaction over HTTP-POST. description: | The only accepted response binding. The per-provider path, exact Destination, both Issuer legs, RelayState, InResponseTo and initiator binding must all resolve to the same live transaction before an ordinary session can be minted, linked or reauthenticated. x-hikyo-class: unauthenticated x-hikyo-artifacts: [none, human-session] x-hikyo-min-revision: 1 security: [] requestBody: required: true content: application/x-www-form-urlencoded: schema: $ref: "#/components/schemas/SamlACSRequest" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The minted or rotated ordinary Hikyo session. content: application/json: schema: $ref: "#/components/schemas/LoginResult" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/auth/saml/{provider}/metadata: parameters: - $ref: "#/components/parameters/ProviderSlug" get: operationId: samlMetadata tags: [auth] summary: Publish this provider's SAML SP metadata. description: | Documentation-class public material: entityID, this provider's ACS, the active and retiring SP signing certificates, and the signed-request policy. Served under pre-authentication admission controls. security: [] x-hikyo-class: unauthenticated x-hikyo-artifacts: [none] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: SAML 2.0 SP metadata XML. content: application/samlmetadata+xml: schema: type: string "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/me/profile: get: operationId: getMyProfile tags: [auth] summary: Read your own account profile. x-hikyo-class: unauthenticated x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: Your account profile. content: application/json: schema: $ref: "#/components/schemas/AccountProfile" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" patch: operationId: updateMyProfile tags: [auth] summary: Update your own account profile. description: Updates the username and display name. The email is not profile data and cannot be set here; only verified local sign-up writes a verified one. Provisioned names remain controlled by SCIM. Username changes require current account proof. x-hikyo-class: unauthenticated x-hikyo-artifacts: [human-session] x-hikyo-reauth: {class: account-security, proof: [proof], presence: when-changed, member: username} x-hikyo-min-revision: 1 requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/UpdateAccountProfileRequest" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: Your updated profile. Existing sessions remain valid. content: application/json: schema: $ref: "#/components/schemas/AccountProfile" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/Conflict" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/me/orgs: get: operationId: listMyOrgs tags: [auth] summary: The organisations the caller's own grants name. description: | The navigation surface: exactly the organisations in which the calling principal holds at least one grant, at org scope or below. This is **not** `listOrgs`. That one enumerates every organisation on the instance and is operator work under `instance-config`, which the human-auth ADR makes MFA-mandatory; using it for navigation would put a second-factor wall in front of a sidebar. This one is a projection of the caller's *own* grant rows — disclosure of one's own grants is already presupposed by the grant views — so it needs no capability and can leak nothing the caller does not already hold. A principal whose only grant is **instance-scoped** gets an EMPTY list, and that is correct rather than a gap. Instance scope inherits downward, so expanding it here would reproduce `listOrgs` without its gate. An operator who wants the whole instance asks for the whole instance, and steps up to do it. Identity only — id and name. An organisation's `metadata` is operator-set and is read through `getOrg`, which authorizes. x-hikyo-class: unauthenticated x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The caller's organisations, empty when their grants name none. content: application/json: schema: $ref: "#/components/schemas/MyOrgList" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/auth/identities: get: operationId: listIdentities tags: [auth] summary: List the caller's linked external identities. x-hikyo-class: unauthenticated x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The caller's linked identities. content: application/json: schema: $ref: "#/components/schemas/IdentityList" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/auth/identities/link: post: operationId: linkIdentity tags: [auth] summary: Begin linking an external identity (OIDC purpose=link). description: | An account-security mutation: verifies the pre-existing password up front and starts an OIDC link transaction bound to the acting session. Returns the IdP authorization URL; the link lands at the callback. x-hikyo-class: unauthenticated x-hikyo-artifacts: [human-session] x-hikyo-reauth: {class: account-security, proof: [proof], presence: required} x-hikyo-min-revision: 1 requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/IdentityLinkRequest" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The IdP authorization URL for the link. content: application/json: schema: $ref: "#/components/schemas/OidcStartResult" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/auth/identities/{id}: parameters: - $ref: "#/components/parameters/IdentityID" delete: operationId: unlinkIdentity tags: [auth] summary: Unlink an external identity. description: | An account-security mutation proved by the pre-existing password. Refuses removing the last remaining credential; deletes the identity's sessions; reissues the acting session. The reissued token replaces the old one. x-hikyo-class: unauthenticated x-hikyo-artifacts: [human-session] x-hikyo-reauth: {class: account-security, proof: [proof], presence: required} x-hikyo-min-revision: 1 requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/IdentityUnlinkRequest" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: Unlinked. The reissued session token replaces the old one. content: application/json: schema: $ref: "#/components/schemas/LoginResult" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/accounts/{principal}/credential-reset: parameters: - $ref: "#/components/parameters/ResetTargetPrincipal" post: operationId: resetCredential tags: [accounts] summary: Issue a credential-establishment authority for another account. description: | Administrator-issued credential reset (human-auth ADR § Recovery). A credential-reset holder mints a single-use, hashed, expiring credential-establishment authority for a target, returned once and transmitted out of band. The reset advances the target's session generation and revokes its sessions in the same transaction. Org-scoped credential-reset reaches only a target whose grants lie entirely within that org and who holds no instance capability; the org-bounded test is evaluated in one serializable transaction with the target's grant set (a concurrent grant landing conflicts with the in-flight reset). A target holding an instance capability has no network reset path at all — break-glass only. This route dispatches between the org-scoped and instance-scoped credential-reset operations by that classification. Every failure that could enumerate the target — an unknown or non-human principal, a caller lacking credential-reset, an org-P holder reaching an org-O target, and a target holding an instance capability (break-glass only) — answers a single uniform `401`; the true cause is durable in the audit trail, never on the wire. x-hikyo-class: unauthenticated x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: A session-less credential-establishment authority, returned once. content: application/json: schema: $ref: "#/components/schemas/CredentialResetResult" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/auth/webauthn/enrol/start: post: operationId: enrolPasskeyStart tags: [auth] summary: Begin passkey enrolment; returns opaque creation options. description: | Verifies the account-security proof up front (the pre-existing password or a confirmed TOTP code, where one stands) and opens a registration ceremony, returning the browser-generated creation options verbatim. `enrol/finish` completes the enrolment. x-hikyo-class: unauthenticated x-hikyo-artifacts: [human-session] x-hikyo-reauth: {class: account-security, proof: [password, code], presence: selected} x-hikyo-min-revision: 1 requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/WebauthnEnrolStartRequest" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: Opaque WebAuthn creation options for the browser. content: application/json: schema: $ref: "#/components/schemas/WebauthnOptions" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/auth/webauthn/enrol/finish: post: operationId: enrolPasskeyFinish tags: [auth] summary: Complete passkey enrolment from the attestation response. description: | Validates the opaque attestation response against the live ceremony, stores the credential, and reissues the acting session. A browser session's reissued token is delivered on the `__Host-hikyo` cookie. x-hikyo-class: unauthenticated x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/WebauthnResponse" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: Enrolled. The reissued session replaces the old one. content: application/json: schema: $ref: "#/components/schemas/LoginResult" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/auth/webauthn/login/start: post: operationId: passkeyLoginStart tags: [auth] summary: Begin a discoverable-credential passkey login. description: | Fully pre-auth: opens a discoverable-credential request ceremony and returns the opaque request options. Discloses nothing about which accounts or credentials exist. x-hikyo-class: unauthenticated x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: Opaque WebAuthn request options for the browser. content: application/json: schema: $ref: "#/components/schemas/WebauthnOptions" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/auth/webauthn/login/finish: post: operationId: passkeyLoginFinish tags: [auth] summary: Complete a passkey login from the assertion response. description: | Validates the opaque assertion against the live ceremony and mints a browser session carrying WebAuthn assurance, delivered on the `__Host-hikyo` cookie. A signature-count regression disables the cloned credential (audited) and refuses. x-hikyo-class: unauthenticated x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/WebauthnResponse" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The minted browser session. content: application/json: schema: $ref: "#/components/schemas/LoginResult" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/auth/webauthn/step-up/start: post: operationId: stepUpPasskeyStart tags: [auth] summary: Begin a passkey step-up for the acting session. description: | Opens a non-discoverable ceremony scoped to the acting account's credentials and returns opaque request options. `step-up/finish` elevates the session. x-hikyo-class: unauthenticated x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: Opaque WebAuthn request options for the browser. content: application/json: schema: $ref: "#/components/schemas/WebauthnOptions" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/auth/webauthn/step-up/finish: post: operationId: stepUpPasskeyFinish tags: [auth] summary: Elevate the acting session by presenting a passkey. description: | Validates the opaque assertion and appends `webauthn` to the acting session's factor set, rotating its token (delivered on the `__Host-hikyo` cookie for a browser session). The original authenticated_at is preserved. x-hikyo-class: unauthenticated x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/WebauthnResponse" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: Session elevated. The rotated token replaces the old one. content: application/json: schema: $ref: "#/components/schemas/LoginResult" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/auth/webauthn/reauth/start: post: operationId: reauthPasskeyStart tags: [auth] summary: Begin a passkey reauthentication bound to an operation unit. description: | Opens a ceremony whose challenge is bound to the enumerated unit (environment plus the sorted credential ids), so the assertion authorizes exactly that unit. `reauth/finish` opens the window. x-hikyo-class: unauthenticated x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/WebauthnReauthStartRequest" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: Opaque WebAuthn request options for the browser. content: application/json: schema: $ref: "#/components/schemas/WebauthnOptions" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/auth/webauthn/reauth/finish: post: operationId: reauthPasskeyFinish tags: [auth] summary: Complete a passkey reauthentication; opens the window. description: | Validates the opaque assertion and opens a reauthentication window over the bound environment (single-decision where the effective window is zero). The reauth rotates the acting session; a browser session's rotated token is delivered on the `__Host-hikyo` cookie. x-hikyo-class: unauthenticated x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/WebauthnResponse" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The window this reauthentication opened. content: application/json: schema: $ref: "#/components/schemas/ReauthResult" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/auth/webauthn/credentials: get: operationId: listPasskeys tags: [auth] summary: List the caller's enrolled passkeys. x-hikyo-class: unauthenticated x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The caller's enrolled credentials. content: application/json: schema: $ref: "#/components/schemas/PasskeyList" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/auth/webauthn/credentials/{id}: parameters: - $ref: "#/components/parameters/WebauthnCredentialID" delete: operationId: removePasskey tags: [auth] summary: Remove an enrolled passkey. description: | An account-security mutation proved by the pre-existing password or a TOTP code (never the credential being removed, B7). Refuses a removal that would drop a passwordless account below the passkey-only floor; reissues the acting session. A browser session's reissued token is delivered on the `__Host-hikyo` cookie. x-hikyo-class: unauthenticated x-hikyo-artifacts: [human-session] x-hikyo-reauth: {class: account-security, proof: [password, code], presence: selected} x-hikyo-min-revision: 1 requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/WebauthnCredentialProofRequest" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: Removed. The reissued session replaces the old one. content: application/json: schema: $ref: "#/components/schemas/LoginResult" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/instance/oidc-providers: get: operationId: listOidcProviders tags: [instance] summary: List configured OIDC providers. x-hikyo-class: instance x-hikyo-operation: oidc-provider.list x-hikyo-formula: [instance-config@instance] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The configured providers. content: application/json: schema: $ref: "#/components/schemas/OidcProviderList" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "403": $ref: "#/components/responses/Forbidden" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/instance/oidc-providers/{slug}: parameters: - $ref: "#/components/parameters/ProviderSlugPath" get: operationId: getOidcProvider tags: [instance] summary: Read one OIDC provider. x-hikyo-class: instance x-hikyo-operation: oidc-provider.get x-hikyo-formula: [instance-config@instance] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The provider. content: application/json: schema: $ref: "#/components/schemas/OidcProvider" "401": $ref: "#/components/responses/Unauthenticated" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" put: operationId: putOidcProvider tags: [instance] summary: Create or reconfigure an OIDC provider. description: | The issuer is immutable after create (A3): a changed issuer is refused. Discovery is re-run and the document's issuer must byte-equal the configured one. A reconfigure sweeps every session authenticated through the provider (A4). x-hikyo-class: instance x-hikyo-operation: oidc-provider.put x-hikyo-formula: [instance-config@instance] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/OidcProviderInput" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The created or reconfigured provider. content: application/json: schema: $ref: "#/components/schemas/OidcProvider" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "403": $ref: "#/components/responses/Forbidden" "409": $ref: "#/components/responses/Conflict" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" delete: operationId: deleteOidcProvider tags: [instance] summary: Delete an OIDC provider. description: | Sweeps every session authenticated through the provider (A4); its live transactions cascade (A14). x-hikyo-class: instance x-hikyo-operation: oidc-provider.delete x-hikyo-formula: [instance-config@instance] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "204": description: Deleted. "401": $ref: "#/components/responses/Unauthenticated" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/instance/retention-health: get: operationId: getRetentionHealth tags: [instance] summary: Read payload-pruner health. x-hikyo-class: instance x-hikyo-operation: retention.health-read x-hikyo-formula: [instance-config@instance] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: Persisted payload-pruner health. content: application/json: schema: $ref: "#/components/schemas/RetentionHealth" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "403": $ref: "#/components/responses/Forbidden" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/instance/update-status: get: operationId: getUpdateStatus tags: [instance] summary: Read the configured release channel and available update. x-hikyo-class: instance x-hikyo-operation: update.status-read x-hikyo-formula: [instance-config@instance] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "200": description: Current version and the newest release on the configured channel. content: application/json: schema: $ref: "#/components/schemas/UpdateStatus" "401": $ref: "#/components/responses/Unauthenticated" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "503": $ref: "#/components/responses/ServiceUnavailable" "500": $ref: "#/components/responses/Internal" /api/v1/instance/update: post: operationId: requestInstanceUpdate tags: [instance] summary: Refuse legacy remote apply while preserving authenticated audit evidence. description: | Remote apply is disabled because the legacy updater cannot prove safe rollback after schema writes. No job is submitted or resumed. An authorized request returns 409 with an explicit unsupported reason. The caller must still hold instance-config, have multi-factor assurance, and have authenticated within the last five minutes. This endpoint remains present for older clients; release metadata and manual verified downloads remain available. The 202 shape is retained for wire compatibility with historical helper responses, not current enablement. x-hikyo-class: instance x-hikyo-operation: update.request x-hikyo-formula: [instance-config@instance] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/InstanceUpdateRequest" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "202": description: Update accepted by the local helper. content: application/json: schema: $ref: "#/components/schemas/InstanceUpdateJob" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/Conflict" "500": $ref: "#/components/responses/Internal" /api/v1/instance/updates/{job}: parameters: - $ref: "#/components/parameters/InstanceUpdateJobID" get: operationId: getInstanceUpdateJob tags: [instance] summary: Read one local updater job and reconcile its terminal audit outcome. x-hikyo-class: instance x-hikyo-operation: update.job-read x-hikyo-formula: [instance-config@instance] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: Durable helper-owned update state. content: application/json: schema: $ref: "#/components/schemas/InstanceUpdateJob" "401": $ref: "#/components/responses/Unauthenticated" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "500": $ref: "#/components/responses/Internal" /api/v1/instance/saml-providers: get: operationId: listSamlProviders tags: [instance] summary: List configured SAML providers. x-hikyo-class: instance x-hikyo-operation: saml-provider.list x-hikyo-formula: [instance-config@instance] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The configured SAML providers. content: application/json: schema: $ref: "#/components/schemas/SamlProviderList" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "403": $ref: "#/components/responses/Forbidden" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/instance/saml-providers/{slug}: parameters: - $ref: "#/components/parameters/ProviderSlugPath" get: operationId: getSamlProvider tags: [instance] summary: Read one SAML provider. x-hikyo-class: instance x-hikyo-operation: saml-provider.get x-hikyo-formula: [instance-config@instance] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The provider. content: application/json: schema: $ref: "#/components/schemas/SamlProvider" "401": $ref: "#/components/responses/Unauthenticated" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" put: operationId: putSamlProvider tags: [instance] summary: Create or reconfigure a SAML provider from pinned metadata. description: | The configured entityID selects exactly one metadata descriptor and is immutable after create. File and one-shot URL sources enter the same diff-and-confirm ceremony; certificate, endpoint and assurance-policy changes sweep this provider's sessions. SAML never provisions accounts, so no provisioning member exists in this request schema. x-hikyo-class: instance x-hikyo-operation: saml-provider.put x-hikyo-formula: [instance-config@instance] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/SamlProviderInput" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: Applied provider or metadata diff requiring explicit confirmation. content: application/json: schema: $ref: "#/components/schemas/SamlProviderMutationResult" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "403": $ref: "#/components/responses/Forbidden" "409": $ref: "#/components/responses/Conflict" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" patch: operationId: patchSamlProvider tags: [instance] summary: Partially update SAML provider policy or disable the provider. description: | Omitted members preserve their stored values. Metadata, entityID and certificate changes cannot ride this path: they remain behind the metadata diff-and-confirm ceremony. Disabling sweeps this provider's sessions. This additive endpoint supports the locked `update` and `disable` CLI spellings without requiring unreadable metadata input. x-hikyo-class: instance x-hikyo-operation: saml-provider.patch x-hikyo-formula: [instance-config@instance] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/SamlProviderPatch" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The updated provider. content: application/json: schema: $ref: "#/components/schemas/SamlProvider" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/Conflict" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" delete: operationId: deleteSamlProvider tags: [instance] summary: Remove a SAML provider. description: Deletes every session authenticated through this provider. x-hikyo-class: instance x-hikyo-operation: saml-provider.delete x-hikyo-formula: [instance-config@instance] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "204": description: Removed. "401": $ref: "#/components/responses/Unauthenticated" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/instance/saml-providers/{slug}/refresh-metadata: parameters: - $ref: "#/components/parameters/ProviderSlugPath" post: operationId: refreshSamlProviderMetadata tags: [instance] summary: Refresh and explicitly confirm a SAML provider's metadata diff. description: | Performs one URL fetch for URL-backed providers. Any new endpoint, assertion-signing certificate or metadata-signing fingerprint remains refused until named in `confirmed_fingerprints` and committed through this action. File-backed providers supply the replacement document. x-hikyo-class: instance x-hikyo-operation: saml-provider.refresh-metadata x-hikyo-formula: [instance-config@instance] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/SamlMetadataRefreshRequest" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: Applied provider or metadata diff requiring explicit confirmation. content: application/json: schema: $ref: "#/components/schemas/SamlProviderMutationResult" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/Conflict" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/instance/saml-sp-keys: get: operationId: listSamlSpKeys tags: [instance] summary: List SAML SP signing-key lifecycle state. description: Returns fingerprints and public lifecycle metadata only; private key material never leaves the server. x-hikyo-class: instance x-hikyo-operation: saml-sp-key.list x-hikyo-formula: [instance-config@instance] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: Active and overlap-retiring SP keys. content: application/json: schema: $ref: "#/components/schemas/SamlSpKeyList" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "403": $ref: "#/components/responses/Forbidden" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/instance/saml-sp-keys/rotate: post: operationId: rotateSamlSpKey tags: [instance] summary: Rotate the active SAML SP signing key with an overlap window. description: Atomically marks the old active key retiring and publishes a new active key; both certificates remain in SP metadata until explicit retirement. x-hikyo-class: instance x-hikyo-operation: saml-sp-key.rotate x-hikyo-formula: [instance-config@instance] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The newly active key. content: application/json: schema: $ref: "#/components/schemas/SamlSpKey" "401": $ref: "#/components/responses/Unauthenticated" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/Conflict" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/instance/saml-sp-keys/{fingerprint}: parameters: - name: fingerprint in: path required: true description: Exact `sha256:` public-key fingerprint returned by the list operation. schema: type: string delete: operationId: retireSamlSpKey tags: [instance] summary: Retire and erase an overlap-retiring SAML SP key. description: Refuses the active key; rotate first so request-signing continuity is explicit. x-hikyo-class: instance x-hikyo-operation: saml-sp-key.retire x-hikyo-formula: [instance-config@instance] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "204": description: Retired key erased and removed from SP metadata. "401": $ref: "#/components/responses/Unauthenticated" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/Conflict" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/instance/saml-sp-keys/{fingerprint}/compromise-retire: parameters: - name: fingerprint in: path required: true description: Exact active-key fingerprint returned by the list operation. schema: type: string post: operationId: compromiseRetireSamlSpKey tags: [instance] summary: Immediately erase and replace a compromised active SAML SP key. description: Atomically removes the named active key and mints its replacement without publishing an overlap window. x-hikyo-class: instance x-hikyo-operation: saml-sp-key.compromise-retire x-hikyo-formula: [instance-config@instance] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The newly active replacement key. content: application/json: schema: $ref: "#/components/schemas/SamlSpKey" "401": $ref: "#/components/responses/Unauthenticated" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/Conflict" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/orgs/{org}/projects/{project}/service-accounts: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ProjectID" get: operationId: listServiceAccounts tags: [identities] summary: List the project's service accounts. description: | Metadata only, and listing is a narrowing-class operation: plain `manage-identities`, no disclosure gate, because incident response must never be gated on disclosure rights. x-hikyo-class: tenant x-hikyo-operation: identity.service-account-list x-hikyo-formula: [manage-identities@project] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The project's service accounts. content: application/json: schema: $ref: "#/components/schemas/ServiceAccountList" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" post: operationId: createServiceAccount tags: [identities] summary: Create a project-owned machine principal. description: | `kind` is declared here and is IMMUTABLE. A project that guessed wrong creates a second service account; there is no in-place widening of a credential class that N workloads already hold. x-hikyo-class: tenant x-hikyo-operation: identity.service-account-create x-hikyo-formula: [manage-identities@project] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CreateServiceAccountRequest" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "201": description: The created service account. content: application/json: schema: $ref: "#/components/schemas/ServiceAccount" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/Conflict" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/orgs/{org}/projects/{project}/service-accounts/{serviceAccount}: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ProjectID" - $ref: "#/components/parameters/ServiceAccountID" delete: operationId: deleteServiceAccount tags: [identities] summary: Delete a service account, revoking every credential and grant. description: | One transaction: every credential is revoked and every grant released. Plain `manage-identities`, no disclosure gate — requiring `reveal` to deprovision a compromised workload would be a self-inflicted incident-response delay. x-hikyo-class: tenant x-hikyo-operation: identity.service-account-delete x-hikyo-formula: [manage-identities@project] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "204": description: Deleted. "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/orgs/{org}/projects/{project}/service-accounts/{serviceAccount}/credentials: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ProjectID" - $ref: "#/components/parameters/ServiceAccountID" get: operationId: listMachineCredentials tags: [identities] summary: List a service account's credentials, metadata only. description: | Never the value. A credential value is displayed exactly once, at mint, and no route in this contract returns one afterwards. x-hikyo-class: tenant x-hikyo-operation: identity.credential-list x-hikyo-formula: [manage-identities@project] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The credentials' metadata. content: application/json: schema: $ref: "#/components/schemas/MachineCredentialList" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" post: operationId: mintMachineCredential tags: [identities] summary: Mint a bearer credential, returned exactly once. description: | The full authorization formula is `manage-identities(project)` AND a disclosure capability over EVERY environment reachable in the resulting post-state AND reauthentication. The post-state phrasing is load-bearing: a replacement credential adds no environment, so an "added environments" reading would let a principal holding `manage-identities` and no `reveal` rotate a production credential and walk away with a live production-reading token. The two disclosure classes are evaluated independently — `reveal` over newly reachable current plaintext, `reveal-history` over historical. Rotation is this call followed by a revoke of the predecessor: overlap is the intended steady state, so a mint that succeeds and a revoke that fails leaves two live credentials rather than none. x-hikyo-class: tenant x-hikyo-operation: identity.credential-mint x-hikyo-formula: [manage-identities@project] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/MintCredentialRequest" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: | The credential value, returned EXACTLY ONCE. Nothing in the system can return it again. content: application/json: schema: $ref: "#/components/schemas/MintCredentialResult" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/Conflict" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/orgs/{org}/projects/{project}/service-accounts/{serviceAccount}/credentials/{credential}: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ProjectID" - $ref: "#/components/parameters/ServiceAccountID" - $ref: "#/components/parameters/CredentialID" delete: operationId: revokeMachineCredential tags: [identities] summary: Revoke one credential. description: | Bites at the next request: the liveness predicate is read in the authenticating transaction, uncached. Revoking one credential is not deprovisioning — grants are untouched and siblings keep working. x-hikyo-class: tenant x-hikyo-operation: identity.credential-revoke x-hikyo-formula: [manage-identities@project] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "204": description: Revoked. "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/instance/config: get: operationId: getInstanceConfig tags: [instance] summary: Read this owner instance configuration status. description: | Human administration on the owning instance. Remote workspaces call the owner directly. Instance authority never substitutes for bound project grants. x-hikyo-class: instance x-hikyo-operation: self-config.status x-hikyo-formula: [instance-config@instance] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "200": description: Owner-local result. No configuration values are disclosed. content: application/json: schema: $ref: "#/components/schemas/InstanceConfigStatus" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/Conflict" "429": $ref: "#/components/responses/TooManyRequests" "503": $ref: "#/components/responses/ServiceUnavailable" "500": $ref: "#/components/responses/Internal" /api/v1/instance/config/adoption: get: operationId: previewInstanceConfigAdoption tags: [instance] summary: Preview one-time adoption without disclosing values. description: | Human administration on the owning instance. Remote workspaces call the owner directly. Instance authority never substitutes for bound project grants. x-hikyo-class: instance x-hikyo-operation: self-config.preview x-hikyo-formula: [instance-config@instance, manage-members@instance] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "200": description: Owner-local result. No configuration values are disclosed. content: application/json: schema: $ref: "#/components/schemas/InstanceConfigAdoptionPreview" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/Conflict" "429": $ref: "#/components/responses/TooManyRequests" "503": $ref: "#/components/responses/ServiceUnavailable" "500": $ref: "#/components/responses/Internal" post: operationId: adoptInstanceConfig tags: [instance] summary: Adopt the exact previewed effective configuration. description: | Human administration on the owning instance. Remote workspaces call the owner directly. Instance authority never substitutes for bound project grants. x-hikyo-class: instance x-hikyo-operation: self-config.adopt x-hikyo-formula: [instance-config@instance, manage-members@instance] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/InstanceConfigAdoptRequest" responses: "200": description: Owner-local result. No configuration values are disclosed. content: application/json: schema: $ref: "#/components/schemas/InstanceConfigStatus" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/Conflict" "429": $ref: "#/components/responses/TooManyRequests" "503": $ref: "#/components/responses/ServiceUnavailable" "500": $ref: "#/components/responses/Internal" /api/v1/instance/config/apply: post: operationId: applyInstanceConfig tags: [instance] summary: Apply one published revision on this logical instance. description: | Human administration on the owning instance. Remote workspaces call the owner directly. Instance authority never substitutes for bound project grants. x-hikyo-class: tenant x-hikyo-operation: self-config.apply x-hikyo-formula: [instance-config@instance, publish@environment, read@environment] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/InstanceConfigApplyRequest" responses: "202": description: Owner-local result. No configuration values are disclosed. content: application/json: schema: $ref: "#/components/schemas/InstanceConfigStatus" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/Conflict" "429": $ref: "#/components/responses/TooManyRequests" "503": $ref: "#/components/responses/ServiceUnavailable" "500": $ref: "#/components/responses/Internal" /api/v1/instance/config/mail/test: post: operationId: testInstanceConfigMail tags: [instance] summary: Send one test message using exactly the selected revision. description: | Human administration on the owning instance. Remote workspaces call the owner directly. Instance authority never substitutes for bound project grants. x-hikyo-class: tenant x-hikyo-operation: self-config.test x-hikyo-formula: [instance-config@instance, read@environment] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/InstanceConfigMailTestRequest" responses: "200": description: Owner-local result. No configuration values are disclosed. content: application/json: schema: $ref: "#/components/schemas/InstanceConfigMailTestResult" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/Conflict" "429": $ref: "#/components/responses/TooManyRequests" "503": $ref: "#/components/responses/ServiceUnavailable" "500": $ref: "#/components/responses/Internal" /api/v1/instance/credential-policy: get: operationId: getCredentialPolicy tags: [instance] summary: Read the instance credential lifetime controls. x-hikyo-class: instance x-hikyo-operation: identity.credential-policy-read x-hikyo-formula: [instance-config@instance] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The instance credential policy. content: application/json: schema: $ref: "#/components/schemas/CredentialPolicy" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "403": $ref: "#/components/responses/Forbidden" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" put: operationId: setCredentialPolicy tags: [instance] summary: Move the instance credential lifetime controls. description: | The two controls are separate on purpose: raising `max_finite_lifetime_seconds` can never manufacture an indefinite credential. A TIGHTENING enumerates every affected live credential and refuses with 409 until the caller re-sends with `confirm: true`, so a settings change never silently kills a live credential. x-hikyo-class: instance x-hikyo-operation: identity.credential-policy-update x-hikyo-formula: [instance-config@instance] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/SetCredentialPolicyRequest" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The policy as written, with what the clamp touched. content: application/json: schema: $ref: "#/components/schemas/CredentialPolicyResult" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "403": $ref: "#/components/responses/Forbidden" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/instance/federation-issuers: get: operationId: listFederationIssuers tags: [instance] summary: List the configured OIDC federation issuers. description: | Instance-scoped, never org- or project-scoped: an org-scoped issuer would let an org admin add a provider and mint identities authenticating into the instance. Audited rather than silent — reading which external authorities the instance trusts to name principals is not a bare tenant read. x-hikyo-class: instance x-hikyo-operation: federation.issuer-list x-hikyo-formula: [instance-config@instance] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The configured issuers. content: application/json: schema: $ref: "#/components/schemas/FederationIssuerList" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "403": $ref: "#/components/responses/Forbidden" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" post: operationId: createFederationIssuer tags: [instance] summary: Configure an OIDC federation issuer. description: | The issuer string is stored and matched BYTE-EXACT, with no canonicalization step. OpenID Connect defines `iss` as case-sensitive, so any normalization that folds case, resolves a URL or strips a trailing slash can merge two distinct external issuers into one configuration — and the uniqueness constraint would enforce the merge rather than catch it. x-hikyo-class: instance x-hikyo-operation: federation.issuer-create x-hikyo-formula: [instance-config@instance] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CreateFederationIssuerRequest" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "201": description: The configured issuer. content: application/json: schema: $ref: "#/components/schemas/FederationIssuer" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "403": $ref: "#/components/responses/Forbidden" "409": $ref: "#/components/responses/Conflict" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/instance/federation-issuers/{issuer}: parameters: - $ref: "#/components/parameters/FederationIssuerID" patch: operationId: updateFederationIssuer tags: [instance] summary: Move an issuer's JWKS source or refused audiences. description: | The mutable half only. The issuer string and the platform type cannot move: changing either would silently re-point every binding underneath at a different external authority, which is a replacement, not an edit. x-hikyo-class: instance x-hikyo-operation: federation.issuer-update x-hikyo-formula: [instance-config@instance] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/UpdateFederationIssuerRequest" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The issuer as written. content: application/json: schema: $ref: "#/components/schemas/FederationIssuer" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" delete: operationId: deleteFederationIssuer tags: [instance] summary: Remove an issuer configuration. description: | Refused with 409 while live bindings still name it. A cascade would deprovision N workloads under an operation whose name says "configuration", and an orphaning delete would leave bindings that silently stop authenticating. The operator revokes the bindings first, which is the same act stated honestly. x-hikyo-class: instance x-hikyo-operation: federation.issuer-delete x-hikyo-formula: [instance-config@instance] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "204": description: Deleted. "401": $ref: "#/components/responses/Unauthenticated" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/Conflict" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/orgs/{org}/projects/{project}/service-accounts/{serviceAccount}/bindings: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ProjectID" - $ref: "#/components/parameters/ServiceAccountID" post: operationId: createFederatedBinding tags: [identities] summary: Mint a federated `(issuer, subject)` binding. Nothing at rest. description: | The full authorization formula is `manage-identities(project)` AND a disclosure capability over EVERY environment reachable in the resulting post-state AND reauthentication — the same formula a bearer mint carries, and for the same reason: the actor decides which external identity may speak as this service account, and that identity reaches everything the account's grants reach. There is no PUT and no PATCH. Bindings are IMMUTABLE; a change is a replacement mint through this route naming `replaces`, so the predecessor is revoked and the successor inserted in one transaction. Editing in place would let a principal without `reveal` re-point a production binding at an identity they control while the recorded authorization stayed behind. Bindings are LISTED and DELETED through the credentials routes: a binding is a credential row, so a second pair of routes over the same rows would be two places for one formula to drift. x-hikyo-class: tenant x-hikyo-operation: identity.binding-create x-hikyo-formula: [manage-identities@project] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CreateBindingRequest" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "201": description: The created binding. content: application/json: schema: $ref: "#/components/schemas/FederatedBinding" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/Conflict" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/orgs/{org}/projects/{project}/environments/{environment}/parameters: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ProjectID" - $ref: "#/components/parameters/EnvironmentID" get: operationId: listEnvironmentParameters tags: [environments] summary: List declarations used by the next publication. x-hikyo-class: tenant x-hikyo-operation: environment.read x-hikyo-formula: [read@environment] x-hikyo-artifacts: [human-session, machine-credential] x-hikyo-min-revision: 3 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: Parameter names mapped to validation patterns. content: application/json: schema: $ref: "#/components/schemas/EnvironmentParameters" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" post: operationId: changeEnvironmentParameter tags: [environments] summary: Add or delete one parameter declaration for the next publication. x-hikyo-class: tenant x-hikyo-operation: environment.parameter-set x-hikyo-formula: [definitions-edit@project] x-hikyo-artifacts: [human-session, machine-credential] x-hikyo-min-revision: 3 requestBody: required: true content: application/json: schema: type: object additionalProperties: false required: [name, action] properties: name: type: string pattern: '^[A-Z][A-Z0-9_]{0,63}$' action: type: string enum: [add, delete] pattern: type: string maxLength: 512 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "204": description: Declaration changed; existing snapshots remain unchanged. "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/Conflict" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/orgs/{org}/projects/{project}/environments/{environment}/delivery: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ProjectID" - $ref: "#/components/parameters/EnvironmentID" - $ref: "#/components/parameters/DeliveryCursor" - $ref: "#/components/parameters/DeliveryProjection" - $ref: "#/components/parameters/DeliveryAcknowledgedKeys" - name: parameters in: query description: JSON object of public parameter names and string values. All declared parameters are required. Never supply secrets. schema: type: string maxLength: 16384 get: operationId: fetchDelivery tags: [delivery] summary: Fetch the authorized projection, conditionally. description: | The machine fetch surface. Presenting a current cursor answers `current: true` with NO content and no key names — only a fetch that actually delivers values is a disclosure — and either disposition emits exactly one immutable access record, plus one immutable disclosure record per delivered value. Authorization is evaluated on the conditional path EXACTLY as on the delivering path, so a caller who has lost `read` receives the uniform nonexistent response rather than "current". What a key delivers is decided server-side, per key, in-transaction: a `config` key carries its plaintext under `read`; a `secret` key carries its plaintext under `read ∧ reveal` (or `read ∧ reveal-history` for a pinned non-current revision), and otherwise arrives presence-only with no value. This is the surface the Compose and Kubernetes integrations consume to converge their delivery targets. Both a `hik_` bearer credential and an externally issued OIDC ID token authenticate here, with identical authority: a machine principal's authority is the union of the grants on its service account and nothing else, so there is no per-credential scope for either kind to widen. Human sessions are refused at artifact admission with the uniform nonexistent response. x-hikyo-class: tenant x-hikyo-operation: delivery.fetch x-hikyo-formula: [read@environment] x-hikyo-artifacts: [machine-credential] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The authorized projection, or the statement that the cursor is current. content: application/json: schema: $ref: "#/components/schemas/DeliveryResponse" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/Conflict" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/orgs/{org}/projects/{project}/environments/{environment}/delivery/offline-records: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ProjectID" - $ref: "#/components/parameters/EnvironmentID" post: operationId: reconcileOfflineRecords tags: [delivery] summary: Reconcile client-durable offline disclosure records. description: | Accepts records fsynced by a client before offline plaintext release. Idempotency is scoped to the presenting principal and record id. A live credential may reconcile records served by a credential since revoked. x-hikyo-class: tenant x-hikyo-operation: delivery.reconcile-offline x-hikyo-formula: [read@environment] x-hikyo-artifacts: [machine-credential] x-hikyo-min-revision: 1 requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/ReconcileOfflineRecordsRequest" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The idempotent batch outcome. content: application/json: schema: $ref: "#/components/schemas/ReconcileOfflineRecordsResponse" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/Conflict" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" # --------------------------------------------------------------------------- # Delivery-target condition reporting (#788, k8s-condition-reporting ADR). # # The report and tombstone are the workload's one value-free write # (`report-delivery-status`, D1), integration-neutral: any integration # presenting a machine credential with the atom may report. The list is the # human view under `read` on the environment (D7). Every request string is a # closed enum or a fixed grammar; nothing free-form crosses (D4). # --------------------------------------------------------------------------- /api/v1/orgs/{org}/projects/{project}/environments/{environment}/delivery-targets: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ProjectID" - $ref: "#/components/parameters/EnvironmentID" get: operationId: listDeliveryTargets tags: [delivery] summary: List the environment's delivery targets and their derived state. description: | Two labelled layers, never merged into one health bit (ADR D2): per reporting principal, the server's own observation (its last authenticated delivery fetch here) and its quota notice; per target, the conditions its controller asserted, with a state derived at read time. `reporter-revoked`, `refused` and `stale` take precedence over `reported`, in that order; nothing is stored as healthy. An environment the caller cannot read is the uniform nonexistent answer, never an empty or redacted list. x-hikyo-class: tenant x-hikyo-operation: delivery-target.list x-hikyo-formula: [read@environment] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 5 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The environment's delivery targets. content: application/json: schema: $ref: "#/components/schemas/DeliveryTargetList" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" post: operationId: reportDeliveryTarget tags: [delivery] summary: Report one delivery target's asserted conditions. description: | Upserts the caller's latest-state row for one target, keyed by the presenting principal (never the credential, so rotation keeps one row) and the target's cluster, instance and object UIDs. An accepted repeat report is not audited; a new row, and every refusal, is. Refusals: a caller without `report-delivery-status` on this environment is the uniform 404; a value outside the advertised vocabulary refuses the whole report with 422 naming the member and stores none of it (only the closed cause, on an existing row); a lower `observed_generation`, a not-later `reported_at` within one generation, or a `reported_at` more than 5 minutes ahead is a 409 that is audited and dropped; a principal already reporting 100 targets is the named 409 `limit_exceeded`; a body over 8 KiB is a 413 that is never tied to a row. Charged to a separate budget, 60/min per principal and 300/min per organisation, after authorization. x-hikyo-class: tenant x-hikyo-operation: delivery-target.report x-hikyo-formula: [report-delivery-status@environment] x-hikyo-artifacts: [machine-credential] x-hikyo-min-revision: 5 requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/DeliveryTargetReportRequest" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "204": description: Accepted. "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/Conflict" "413": $ref: "#/components/responses/PayloadTooLarge" "422": $ref: "#/components/responses/UnprocessableContent" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/orgs/{org}/projects/{project}/environments/{environment}/delivery-targets/tombstone: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ProjectID" - $ref: "#/components/parameters/EnvironmentID" post: operationId: tombstoneDeliveryTarget tags: [delivery] summary: Remove the caller's row for one deleted delivery target. description: | The best-effort tombstone a controller sends when its target is deleted (ADR D6): the row is removed right after the tombstone event. A target the caller has no row for is the uniform 404. x-hikyo-class: tenant x-hikyo-operation: delivery-target.tombstone x-hikyo-formula: [report-delivery-status@environment] x-hikyo-artifacts: [machine-credential] x-hikyo-min-revision: 5 requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/DeliveryTargetTombstoneRequest" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "204": description: The row is removed. "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" # --------------------------------------------------------------------------- # SCIM administration (#73, scim-provisioning ADR §8; spellings §1). # # Domain surface with full UI<->CLI parity: binding CRUD, mapping-table # administration, credential lifecycle, and the provisioned directory views. # Every operation is `manage-members` held at ORG SCOPE EXACTLY — a mapping # row causes grants its author need not hold, and unheld-capability granting # is an org/instance power, so a project-scope member manager must not reach # it through SCIM. The atom is MFA-mandatory, so every route declares 403. # --------------------------------------------------------------------------- /api/v1/orgs/{org}/scim-bindings: parameters: - $ref: "#/components/parameters/OrgID" get: operationId: listScimBindings tags: [scim] summary: List this organisation's SCIM bindings. description: | Each binding with its attention states — the org admin's answer to "what does SCIM think is wrong?". Provider availability and staleness are reconciled on read, in both directions, so the view is current rather than as of the last write. x-hikyo-class: tenant x-hikyo-operation: scim-binding.list x-hikyo-formula: [manage-members@org] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The organisation's bindings. content: application/json: schema: $ref: "#/components/schemas/ScimBindingList" "401": $ref: "#/components/responses/Unauthenticated" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" post: operationId: createScimBinding tags: [scim] summary: Create the organisation's SCIM binding for one identity provider. description: | At most one binding per (organisation, provider): the concurrent-create race resolves to one row and the loser is refused with `conflict`. The subject source and the NameID profile are immutable after creation, and the provider reference is read-only — creating a binding grants no authority over the provider it names. Creating the binding also creates its provisioning connection and that connection's structural `scim-provision` grant, atomically. x-hikyo-class: tenant x-hikyo-operation: scim-binding.create x-hikyo-formula: [manage-members@org] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CreateScimBindingRequest" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The created binding. content: application/json: schema: $ref: "#/components/schemas/ScimBinding" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/Conflict" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/orgs/{org}/scim-bindings/{binding}: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ScimBindingID" get: operationId: getScimBinding tags: [scim] summary: Read one SCIM binding and its attention states. description: | Deliberately keeps working while the referenced provider is disabled or removed: the whole wire surface fails closed in that state, and "state is preserved for repair" is not preserved by a surface that refuses to show the state. x-hikyo-class: tenant x-hikyo-operation: scim-binding.get x-hikyo-formula: [manage-members@org] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The binding. content: application/json: schema: $ref: "#/components/schemas/ScimBinding" "401": $ref: "#/components/responses/Unauthenticated" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" delete: operationId: deleteScimBinding tags: [scim] summary: Delete a SCIM binding, running its atomic teardown. description: | One transaction, in order: every credential is revoked so no new wire transaction can begin; every `scim` origin the binding holds is released (lockout conversion included, and any resulting retention origins survive the binding); the provisioning connection and its structural grant are retired; the directory, mapping table and binding row go. Identity links and accounts SURVIVE — they are account property, exactly as they would be had the user been invited. x-hikyo-class: tenant x-hikyo-operation: scim-binding.delete x-hikyo-formula: [manage-members@org] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "204": description: The binding and everything it owned are gone. "401": $ref: "#/components/responses/Unauthenticated" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/orgs/{org}/scim-bindings/{binding}/mappings: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ScimBindingID" get: operationId: listScimMappings tags: [scim] summary: List the binding's group-to-template mapping table. description: | Each row with the capabilities it expands into, so a reader does not have to know the template table by heart, and with its `inert` flag — a row whose group was deleted at the identity provider grants nothing until a human edits or deletes it. x-hikyo-class: tenant x-hikyo-operation: scim-mapping.list x-hikyo-formula: [manage-members@org] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The mapping table. content: application/json: schema: $ref: "#/components/schemas/ScimMappingList" "401": $ref: "#/components/responses/Unauthenticated" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" post: operationId: createScimMapping tags: [scim] summary: Add a mapping row, granting the group's current members immediately. description: | The blast-radius moment. The locked rule demands the org-scope warning "at grant time", when a human is present — under SCIM no human attends the sync, so the consequence language is returned HERE, in the same transaction that creates the origins for every member the group already has. There is no "waits for the next push" gap, and the `warnings` array is part of the response rather than an optional decoration. x-hikyo-class: tenant x-hikyo-operation: scim-mapping.create x-hikyo-formula: [manage-members@org] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/ScimMappingRequest" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The row, its consequence language, and what it granted. content: application/json: schema: $ref: "#/components/schemas/ScimMappingResult" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/Conflict" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" put: operationId: updateScimMapping tags: [scim] summary: Retarget an addressed mapping row's template. description: | The row is addressed by (group, scope) and keeps its id, which is what makes narrowing precise: origins key on the row, so a delete-and- recreate would release every origin it holds and immediately recreate most of them — momentarily revoking capabilities that never stopped being granted. Widening creates the newly covered origins now; narrowing releases the no-longer-covered part in this same transaction, with no round-trip to the identity provider. x-hikyo-class: tenant x-hikyo-operation: scim-mapping.update x-hikyo-formula: [manage-members@org] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/ScimMappingRequest" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The row, its consequence language, and what it changed. content: application/json: schema: $ref: "#/components/schemas/ScimMappingResult" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/Conflict" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" delete: operationId: deleteScimMapping tags: [scim] summary: Delete an addressed mapping row and release every origin it holds. description: | Reconciles in this transaction, with no round-trip to the identity provider: the org administrator is never stuck behind an IdP they do not control. The row is addressed by query parameters because the triple is an address, and query parameters are the shape every intermediary handles the same way. x-hikyo-class: tenant x-hikyo-operation: scim-mapping.delete x-hikyo-formula: [manage-members@org] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 parameters: - name: group in: query required: true description: The server-minted SCIM group id the row names. schema: $ref: "#/components/schemas/ID" - name: project in: query required: false description: The row's project scope, absent for an org-scoped row. schema: $ref: "#/components/schemas/ID" - name: environment in: query required: false description: The row's environment scope. schema: $ref: "#/components/schemas/ID" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The deleted row and the origin count it released. content: application/json: schema: $ref: "#/components/schemas/ScimMappingResult" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/Conflict" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/orgs/{org}/scim-bindings/{binding}/credentials: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ScimBindingID" get: operationId: listScimCredentials tags: [scim] summary: List the binding's provisioning credentials. description: | Ids and metadata, never token material — which does not exist to be listed, because only the verifier was ever stored. x-hikyo-class: tenant x-hikyo-operation: scim-credential.list x-hikyo-formula: [manage-members@org] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The binding's credentials. content: application/json: schema: $ref: "#/components/schemas/ScimCredentialList" "401": $ref: "#/components/responses/Unauthenticated" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" post: operationId: mintScimCredential tags: [scim] summary: Mint a NEW provisioning credential; several may be live at once. description: | Display-once: the token is in this response and nowhere else, ever. Overlap rotation is mint-new, update the identity provider, revoke-old, with identical authority throughout — so a mint that joins an already-live credential reports `rotated` and there is no third verb and no offline window. x-hikyo-class: tenant x-hikyo-operation: scim-credential.mint x-hikyo-formula: [manage-members@org] x-hikyo-artifacts: [human-session] x-hikyo-reauth: {class: account-security, proof: [proof], presence: session} x-hikyo-min-revision: 1 requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/MintScimCredentialRequest" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The credential and its display-once token. content: application/json: schema: $ref: "#/components/schemas/ScimMintResult" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/Conflict" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/orgs/{org}/scim-bindings/{binding}/credentials/{id}: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ScimBindingID" - $ref: "#/components/parameters/ScimResourceID" get: operationId: getScimCredential tags: [scim] summary: Read one provisioning credential's metadata. x-hikyo-class: tenant x-hikyo-operation: scim-credential.get x-hikyo-formula: [manage-members@org] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The credential. content: application/json: schema: $ref: "#/components/schemas/ScimCredential" "401": $ref: "#/components/responses/Unauthenticated" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" delete: operationId: revokeScimCredential tags: [scim] summary: Revoke one provisioning credential. description: | Bites at the next request. The row is marked rather than deleted, so the verifier stays occupied and the id keeps naming a real thing here. x-hikyo-class: tenant x-hikyo-operation: scim-credential.revoke x-hikyo-formula: [manage-members@org] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "204": description: The credential is dead. "401": $ref: "#/components/responses/Unauthenticated" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/orgs/{org}/scim-bindings/{binding}/directory/users: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ScimBindingID" get: operationId: listScimDirectoryUsers tags: [scim] summary: The provisioned user directory, with per-user attention flags. description: | Carries the loud per-user flag a deprovision raises: the identity provider declared this human gone, and grants made by hand in this org remain and are still usable. That flag exists precisely because a human must decide about the manual remainder. x-hikyo-class: tenant x-hikyo-operation: scim-directory.users x-hikyo-formula: [manage-members@org] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The provisioned users. content: application/json: schema: $ref: "#/components/schemas/ScimDirectoryUserList" "401": $ref: "#/components/responses/Unauthenticated" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/orgs/{org}/scim-bindings/{binding}/directory/groups: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ScimBindingID" get: operationId: listScimDirectoryGroups tags: [scim] summary: The provisioned group directory. x-hikyo-class: tenant x-hikyo-operation: scim-directory.groups x-hikyo-formula: [manage-members@org] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The provisioned groups. content: application/json: schema: $ref: "#/components/schemas/ScimDirectoryGroupList" "401": $ref: "#/components/responses/Unauthenticated" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" # --------------------------------------------------------------------------- # The SCIM wire (#73 §8) — identity-protocol endpoints. # # RFC 7644 fixes the relative path shapes and identity providers take a # configurable base URL, so the per-binding base composes cleanly. These are # the IdP's wire and are meaningless to the SPA and to a human at a CLI: they # join the closed parity-exemption list as protocol paths, exactly as the # SAML and OIDC ceremony endpoints do. # # The credential presented must match the binding IN THE PATH — a mismatch is # a named authentication failure, audited, and answered 401. There is no # ambient routing by credential. # # No route here declares 403: `scim-provision` is not MFA-mandatory (machines # do not reauthenticate), so the assurance leg a 403 exists for cannot arise. # --------------------------------------------------------------------------- /api/v1/orgs/{org}/scim/v2/{binding}/ServiceProviderConfig: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ScimBindingID" get: operationId: scimServiceProviderConfig tags: [scim-protocol] summary: Advertise what this SCIM service provider implements. description: | The closed truth of what is implemented: no Bulk, no filter beyond the four `eq` probes, no sorting, no ETag, no change password. Each absence is advertised here AND refused by name at its endpoint, never half-implemented. x-hikyo-class: tenant x-hikyo-operation: scim-discovery.read x-hikyo-formula: [scim-provision@org] x-hikyo-artifacts: [scim-credential] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ScimServiceUnavailable" "200": description: The ServiceProviderConfig resource. content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" "401": description: | Authentication failed. Unknown, malformed, revoked, expired, epoch-superseded and wrong-binding credentials all answer this, so presentation reveals nothing about which credentials exist. A credential that does not match the binding in the path is an authentication failure, NEVER a SCIM 400. content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" "404": description: | A resource this binding did not mint, or a binding this caller cannot reach. content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" "429": $ref: "#/components/responses/TooManyRequests" "500": description: | An internal error, in the RFC 7644 shape. The SCIM surface answers its own error dialect for every status: an identity provider that met one Hikyo envelope among SCIM bodies would have to parse two. content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" /api/v1/orgs/{org}/scim/v2/{binding}/ResourceTypes: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ScimBindingID" get: operationId: scimResourceTypes tags: [scim-protocol] summary: The resource types this service provider implements. description: The closed set — User and Group, and nothing else. x-hikyo-class: tenant x-hikyo-operation: scim-discovery.read x-hikyo-formula: [scim-provision@org] x-hikyo-artifacts: [scim-credential] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ScimServiceUnavailable" "200": description: The ResourceTypes list. content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" "401": description: | Authentication failed. Unknown, malformed, revoked, expired, epoch-superseded and wrong-binding credentials all answer this, so presentation reveals nothing about which credentials exist. A credential that does not match the binding in the path is an authentication failure, NEVER a SCIM 400. content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" "404": description: | A resource this binding did not mint, or a binding this caller cannot reach. content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" "429": $ref: "#/components/responses/TooManyRequests" "500": description: | An internal error, in the RFC 7644 shape. The SCIM surface answers its own error dialect for every status: an identity provider that met one Hikyo envelope among SCIM bodies would have to parse two. content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" /api/v1/orgs/{org}/scim/v2/{binding}/Schemas: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ScimBindingID" get: operationId: scimSchemas tags: [scim-protocol] summary: The schemas this service provider implements. x-hikyo-class: tenant x-hikyo-operation: scim-discovery.read x-hikyo-formula: [scim-provision@org] x-hikyo-artifacts: [scim-credential] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ScimServiceUnavailable" "200": description: The Schemas list. content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" "401": description: | Authentication failed. Unknown, malformed, revoked, expired, epoch-superseded and wrong-binding credentials all answer this, so presentation reveals nothing about which credentials exist. A credential that does not match the binding in the path is an authentication failure, NEVER a SCIM 400. content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" "404": description: | A resource this binding did not mint, or a binding this caller cannot reach. content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" "429": $ref: "#/components/responses/TooManyRequests" "500": description: | An internal error, in the RFC 7644 shape. The SCIM surface answers its own error dialect for every status: an identity provider that met one Hikyo envelope among SCIM bodies would have to parse two. content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" /api/v1/orgs/{org}/scim/v2/{binding}/Users: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ScimBindingID" get: operationId: scimListUsers tags: [scim-protocol] summary: List or filter provisioned users. description: | The filter grammar is closed to `userName eq "…"` and `externalId eq "…"`; anything else is `invalidFilter`. Paging is RFC-shaped and 1-based, `count` is clamped to the server's bound, and an out-of-range page returns an empty `Resources` with a truthful `totalResults`. `sortBy` is refused with 501: sorting is advertised absent and is not half-implemented here. x-hikyo-class: tenant x-hikyo-operation: scim-user.list x-hikyo-formula: [scim-provision@org] x-hikyo-artifacts: [scim-credential] x-hikyo-min-revision: 1 parameters: - $ref: "#/components/parameters/ScimFilter" - $ref: "#/components/parameters/ScimStartIndex" - $ref: "#/components/parameters/ScimCount" - $ref: "#/components/parameters/ScimSortBy" - $ref: "#/components/parameters/ScimSortOrder" responses: "503": $ref: "#/components/responses/ScimServiceUnavailable" "200": description: An RFC 7644 ListResponse. content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" "400": description: | An RFC 7644 error body. `scimType` carries the closed 400-class mapping: `invalidFilter`, `invalidPath`, `invalidValue`, `invalidSyntax`, `mutability`, `noTarget`. content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" "401": description: | Authentication failed. Unknown, malformed, revoked, expired, epoch-superseded and wrong-binding credentials all answer this, so presentation reveals nothing about which credentials exist. A credential that does not match the binding in the path is an authentication failure, NEVER a SCIM 400. content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" "404": description: | A resource this binding did not mint, or a binding this caller cannot reach. content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" "429": $ref: "#/components/responses/TooManyRequests" "500": description: | An internal error, in the RFC 7644 shape. The SCIM surface answers its own error dialect for every status: an identity provider that met one Hikyo envelope among SCIM bodies would have to parse two. content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" "501": description: | A feature this service provider advertises as absent. It carries NO `scimType`: that field is a 400-class discriminator, and putting one on a 501 would name a code RFC 7644 does not define. content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" post: operationId: scimCreateUser tags: [scim-protocol] summary: Provision a user, pre-linked and with zero grants. description: | Creates the account with its external identity ALREADY BOUND — no invitation token, no credential-establishment authority, no session, no assurance. If the asserted identity already exists instance-wide the account is ATTACHED instead, through the same query path and with a byte-shape-identical response, so a caller cannot tell the two apart. Grants come only from group mappings. The `password` attribute is refused by name: provisioning never establishes credentials. x-hikyo-class: tenant x-hikyo-operation: scim-user.create x-hikyo-formula: [scim-provision@org] x-hikyo-artifacts: [scim-credential] x-hikyo-min-revision: 1 requestBody: required: true content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" application/json: schema: $ref: "#/components/schemas/ScimResource" responses: "503": $ref: "#/components/responses/ScimServiceUnavailable" "201": description: The provisioned User resource. content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" "400": description: | An RFC 7644 error body. `scimType` carries the closed 400-class mapping: `invalidFilter`, `invalidPath`, `invalidValue`, `invalidSyntax`, `mutability`, `noTarget`. content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" "401": description: | Authentication failed. Unknown, malformed, revoked, expired, epoch-superseded and wrong-binding credentials all answer this, so presentation reveals nothing about which credentials exist. A credential that does not match the binding in the path is an authentication failure, NEVER a SCIM 400. content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" "404": description: | A resource this binding did not mint, or a binding this caller cannot reach. content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" "409": description: | A uniqueness violation - duplicate userName, displayName or subject. content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" "413": description: | The request body exceeds this service provider's bound. content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" "429": $ref: "#/components/responses/TooManyRequests" "500": description: | An internal error, in the RFC 7644 shape. The SCIM surface answers its own error dialect for every status: an identity provider that met one Hikyo envelope among SCIM bodies would have to parse two. content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" /api/v1/orgs/{org}/scim/v2/{binding}/Users/{id}: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ScimBindingID" - $ref: "#/components/parameters/ScimResourceID" get: operationId: scimGetUser tags: [scim-protocol] summary: Read one provisioned user. x-hikyo-class: tenant x-hikyo-operation: scim-user.get x-hikyo-formula: [scim-provision@org] x-hikyo-artifacts: [scim-credential] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ScimServiceUnavailable" "200": description: The User resource. content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" "401": description: | Authentication failed. Unknown, malformed, revoked, expired, epoch-superseded and wrong-binding credentials all answer this, so presentation reveals nothing about which credentials exist. A credential that does not match the binding in the path is an authentication failure, NEVER a SCIM 400. content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" "404": description: | A resource this binding did not mint, or a binding this caller cannot reach. content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" "429": $ref: "#/components/responses/TooManyRequests" "500": description: | An internal error, in the RFC 7644 shape. The SCIM surface answers its own error dialect for every status: an identity provider that met one Hikyo envelope among SCIM bodies would have to parse two. content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" put: operationId: scimReplaceUser tags: [scim-protocol] summary: Replace a provisioned user (RFC replacement). description: | Omitted mutable attributes clear to their defaults and `active` defaults TRUE, so an omitted `active` REACTIVATES. `userName` is required. The subject source is exempt from replacement: the subject is write-once per resource, and a change is refused with `mutability` — an identifier migration at the identity provider is the rebinding hazard the identity model exists to prevent. `groups` is ignored on input; membership is authored exclusively through Group operations. x-hikyo-class: tenant x-hikyo-operation: scim-user.replace x-hikyo-formula: [scim-provision@org] x-hikyo-artifacts: [scim-credential] x-hikyo-min-revision: 1 requestBody: required: true content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" application/json: schema: $ref: "#/components/schemas/ScimResource" responses: "503": $ref: "#/components/responses/ScimServiceUnavailable" "200": description: The replaced User resource. content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" "400": description: | An RFC 7644 error body. `scimType` carries the closed 400-class mapping: `invalidFilter`, `invalidPath`, `invalidValue`, `invalidSyntax`, `mutability`, `noTarget`. content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" "401": description: | Authentication failed. Unknown, malformed, revoked, expired, epoch-superseded and wrong-binding credentials all answer this, so presentation reveals nothing about which credentials exist. A credential that does not match the binding in the path is an authentication failure, NEVER a SCIM 400. content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" "404": description: | A resource this binding did not mint, or a binding this caller cannot reach. content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" "409": description: | A uniqueness violation - duplicate userName, displayName or subject. content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" "413": description: | The request body exceeds this service provider's bound. content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" "429": $ref: "#/components/responses/TooManyRequests" "500": description: | An internal error, in the RFC 7644 shape. The SCIM surface answers its own error dialect for every status: an identity provider that met one Hikyo envelope among SCIM bodies would have to parse two. content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" patch: operationId: scimPatchUser tags: [scim-protocol] summary: Patch a provisioned user through the closed operation matrix. description: | Accepted cells only — add/replace on the pathless value object, on a plain attribute path and on `active`; remove on a non-required plain attribute. Everything else is `invalidPath`, `members` on a User included. A PATCH is ATOMIC: one invalid operation fails the whole request with nothing committed. Stringified booleans for `active` are normalized on ingest, a named tolerance. x-hikyo-class: tenant x-hikyo-operation: scim-user.patch x-hikyo-formula: [scim-provision@org] x-hikyo-artifacts: [scim-credential] x-hikyo-min-revision: 1 requestBody: required: true content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" application/json: schema: $ref: "#/components/schemas/ScimResource" responses: "503": $ref: "#/components/responses/ScimServiceUnavailable" "200": description: The patched User resource. content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" "400": description: | An RFC 7644 error body. `scimType` carries the closed 400-class mapping: `invalidFilter`, `invalidPath`, `invalidValue`, `invalidSyntax`, `mutability`, `noTarget`. content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" "401": description: | Authentication failed. Unknown, malformed, revoked, expired, epoch-superseded and wrong-binding credentials all answer this, so presentation reveals nothing about which credentials exist. A credential that does not match the binding in the path is an authentication failure, NEVER a SCIM 400. content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" "404": description: | A resource this binding did not mint, or a binding this caller cannot reach. content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" "409": description: | A uniqueness violation - duplicate userName, displayName or subject. content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" "413": description: | The request body exceeds this service provider's bound. content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" "429": $ref: "#/components/responses/TooManyRequests" "500": description: | An internal error, in the RFC 7644 shape. The SCIM surface answers its own error dialect for every status: an identity provider that met one Hikyo envelope among SCIM bodies would have to parse two. content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" delete: operationId: scimDeleteUser tags: [scim-protocol] summary: Deprovision and remove a user from this binding's directory. description: | Releases every origin this binding holds for the user, removes every member reference to them from this binding's groups in the same transaction, and advances their session generation UNCONDITIONALLY — even when no grant row changed, because the identity provider has declared this human gone. The account and its identity links survive: other organisations' memberships and other providers are not this provider's to kill. x-hikyo-class: tenant x-hikyo-operation: scim-user.delete x-hikyo-formula: [scim-provision@org] x-hikyo-artifacts: [scim-credential] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ScimServiceUnavailable" "204": description: The directory entry is gone. "401": description: | Authentication failed. Unknown, malformed, revoked, expired, epoch-superseded and wrong-binding credentials all answer this, so presentation reveals nothing about which credentials exist. A credential that does not match the binding in the path is an authentication failure, NEVER a SCIM 400. content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" "404": description: | A resource this binding did not mint, or a binding this caller cannot reach. content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" "429": $ref: "#/components/responses/TooManyRequests" "500": description: | An internal error, in the RFC 7644 shape. The SCIM surface answers its own error dialect for every status: an identity provider that met one Hikyo envelope among SCIM bodies would have to parse two. content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" /api/v1/orgs/{org}/scim/v2/{binding}/Groups: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ScimBindingID" get: operationId: scimListGroups tags: [scim-protocol] summary: List or filter provisioned groups. description: | `displayName eq "…"` and `externalId eq "…"` only. The first is load-bearing rather than a convenience: Okta and Entra both discover a group by it before creating or updating one. x-hikyo-class: tenant x-hikyo-operation: scim-group.list x-hikyo-formula: [scim-provision@org] x-hikyo-artifacts: [scim-credential] x-hikyo-min-revision: 1 parameters: - $ref: "#/components/parameters/ScimFilter" - $ref: "#/components/parameters/ScimStartIndex" - $ref: "#/components/parameters/ScimCount" - $ref: "#/components/parameters/ScimSortBy" - $ref: "#/components/parameters/ScimSortOrder" responses: "503": $ref: "#/components/responses/ScimServiceUnavailable" "200": description: An RFC 7644 ListResponse. content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" "400": description: | An RFC 7644 error body. `scimType` carries the closed 400-class mapping: `invalidFilter`, `invalidPath`, `invalidValue`, `invalidSyntax`, `mutability`, `noTarget`. content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" "401": description: | Authentication failed. Unknown, malformed, revoked, expired, epoch-superseded and wrong-binding credentials all answer this, so presentation reveals nothing about which credentials exist. A credential that does not match the binding in the path is an authentication failure, NEVER a SCIM 400. content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" "404": description: | A resource this binding did not mint, or a binding this caller cannot reach. content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" "429": $ref: "#/components/responses/TooManyRequests" "500": description: | An internal error, in the RFC 7644 shape. The SCIM surface answers its own error dialect for every status: an identity provider that met one Hikyo envelope among SCIM bodies would have to parse two. content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" "501": description: | A feature this service provider advertises as absent. It carries NO `scimType`: that field is a 400-class discriminator, and putting one on a 501 would name a code RFC 7644 does not define. content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" post: operationId: scimCreateGroup tags: [scim-protocol] summary: Provision a group. description: | Members are references to THIS binding's provisioned users; one resolving to no such user is refused with `invalidValue`, because the identity provider can only reference ids this server minted. Group-typed members are refused by name: v1 is flat. x-hikyo-class: tenant x-hikyo-operation: scim-group.create x-hikyo-formula: [scim-provision@org] x-hikyo-artifacts: [scim-credential] x-hikyo-min-revision: 1 requestBody: required: true content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" application/json: schema: $ref: "#/components/schemas/ScimResource" responses: "503": $ref: "#/components/responses/ScimServiceUnavailable" "201": description: The provisioned Group resource. content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" "400": description: | An RFC 7644 error body. `scimType` carries the closed 400-class mapping: `invalidFilter`, `invalidPath`, `invalidValue`, `invalidSyntax`, `mutability`, `noTarget`. content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" "401": description: | Authentication failed. Unknown, malformed, revoked, expired, epoch-superseded and wrong-binding credentials all answer this, so presentation reveals nothing about which credentials exist. A credential that does not match the binding in the path is an authentication failure, NEVER a SCIM 400. content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" "404": description: | A resource this binding did not mint, or a binding this caller cannot reach. content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" "409": description: | A uniqueness violation - duplicate userName, displayName or subject. content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" "413": description: | The request body exceeds this service provider's bound. content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" "429": $ref: "#/components/responses/TooManyRequests" "500": description: | An internal error, in the RFC 7644 shape. The SCIM surface answers its own error dialect for every status: an identity provider that met one Hikyo envelope among SCIM bodies would have to parse two. content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" /api/v1/orgs/{org}/scim/v2/{binding}/Groups/{id}: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ScimBindingID" - $ref: "#/components/parameters/ScimResourceID" get: operationId: scimGetGroup tags: [scim-protocol] summary: Read one provisioned group. x-hikyo-class: tenant x-hikyo-operation: scim-group.get x-hikyo-formula: [scim-provision@org] x-hikyo-artifacts: [scim-credential] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ScimServiceUnavailable" "200": description: The Group resource. content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" "401": description: | Authentication failed. Unknown, malformed, revoked, expired, epoch-superseded and wrong-binding credentials all answer this, so presentation reveals nothing about which credentials exist. A credential that does not match the binding in the path is an authentication failure, NEVER a SCIM 400. content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" "404": description: | A resource this binding did not mint, or a binding this caller cannot reach. content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" "429": $ref: "#/components/responses/TooManyRequests" "500": description: | An internal error, in the RFC 7644 shape. The SCIM surface answers its own error dialect for every status: an identity provider that met one Hikyo envelope among SCIM bodies would have to parse two. content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" put: operationId: scimReplaceGroup tags: [scim-protocol] summary: Replace a provisioned group (RFC replacement). description: | `displayName` is required and the member set is replaced wholesale. Adding a member creates that user's origins from this group's mapping rows; removing one releases exactly this group's origins for them. x-hikyo-class: tenant x-hikyo-operation: scim-group.replace x-hikyo-formula: [scim-provision@org] x-hikyo-artifacts: [scim-credential] x-hikyo-min-revision: 1 requestBody: required: true content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" application/json: schema: $ref: "#/components/schemas/ScimResource" responses: "503": $ref: "#/components/responses/ScimServiceUnavailable" "200": description: The replaced Group resource. content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" "400": description: | An RFC 7644 error body. `scimType` carries the closed 400-class mapping: `invalidFilter`, `invalidPath`, `invalidValue`, `invalidSyntax`, `mutability`, `noTarget`. content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" "401": description: | Authentication failed. Unknown, malformed, revoked, expired, epoch-superseded and wrong-binding credentials all answer this, so presentation reveals nothing about which credentials exist. A credential that does not match the binding in the path is an authentication failure, NEVER a SCIM 400. content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" "404": description: | A resource this binding did not mint, or a binding this caller cannot reach. content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" "409": description: | A uniqueness violation - duplicate userName, displayName or subject. content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" "413": description: | The request body exceeds this service provider's bound. content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" "429": $ref: "#/components/responses/TooManyRequests" "500": description: | An internal error, in the RFC 7644 shape. The SCIM surface answers its own error dialect for every status: an identity provider that met one Hikyo envelope among SCIM bodies would have to parse two. content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" patch: operationId: scimPatchGroup tags: [scim-protocol] summary: Patch a provisioned group through the closed operation matrix. description: | Accepted cells only — add/replace on the pathless value object, on a plain attribute path and on `members`; remove on a non-required plain attribute, on `members` (clear) and on `members[value eq "…"]` (remove one reference). Everything else is `invalidPath`, `active` on a Group included. x-hikyo-class: tenant x-hikyo-operation: scim-group.patch x-hikyo-formula: [scim-provision@org] x-hikyo-artifacts: [scim-credential] x-hikyo-min-revision: 1 requestBody: required: true content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" application/json: schema: $ref: "#/components/schemas/ScimResource" responses: "503": $ref: "#/components/responses/ScimServiceUnavailable" "200": description: The patched Group resource. content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" "400": description: | An RFC 7644 error body. `scimType` carries the closed 400-class mapping: `invalidFilter`, `invalidPath`, `invalidValue`, `invalidSyntax`, `mutability`, `noTarget`. content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" "401": description: | Authentication failed. Unknown, malformed, revoked, expired, epoch-superseded and wrong-binding credentials all answer this, so presentation reveals nothing about which credentials exist. A credential that does not match the binding in the path is an authentication failure, NEVER a SCIM 400. content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" "404": description: | A resource this binding did not mint, or a binding this caller cannot reach. content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" "409": description: | A uniqueness violation - duplicate userName, displayName or subject. content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" "413": description: | The request body exceeds this service provider's bound. content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" "429": $ref: "#/components/responses/TooManyRequests" "500": description: | An internal error, in the RFC 7644 shape. The SCIM surface answers its own error dialect for every status: an identity provider that met one Hikyo envelope among SCIM bodies would have to parse two. content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" delete: operationId: scimDeleteGroup tags: [scim-protocol] summary: Delete a provisioned group. description: | Releases that group's origins for every member and flips every referencing mapping row to INERT with an attention state, rather than deleting rows behind the administrator's back: a row the server tidied away is a row nobody got to decide about. x-hikyo-class: tenant x-hikyo-operation: scim-group.delete x-hikyo-formula: [scim-provision@org] x-hikyo-artifacts: [scim-credential] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ScimServiceUnavailable" "204": description: The group is gone. "401": description: | Authentication failed. Unknown, malformed, revoked, expired, epoch-superseded and wrong-binding credentials all answer this, so presentation reveals nothing about which credentials exist. A credential that does not match the binding in the path is an authentication failure, NEVER a SCIM 400. content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" "404": description: | A resource this binding did not mint, or a binding this caller cannot reach. content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" "429": $ref: "#/components/responses/TooManyRequests" "500": description: | An internal error, in the RFC 7644 shape. The SCIM surface answers its own error dialect for every status: an identity provider that met one Hikyo envelope among SCIM bodies would have to parse two. content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" /api/v1/orgs/{org}/scim/v2/{binding}/Bulk: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ScimBindingID" post: operationId: scimBulk tags: [scim-protocol] summary: Refused — Bulk is not implemented. description: | Advertised absent in ServiceProviderConfig and refused HERE by name, with the RFC 7644 error body and HTTP 501. It carries no `scimType`: that field is a 400-class discriminator, and inventing one for a 501 would name a code the RFC does not define. The route exists so the refusal is the protocol's own shape rather than a generic 404. x-hikyo-class: tenant x-hikyo-operation: scim-unsupported.refuse x-hikyo-formula: [scim-provision@org] x-hikyo-artifacts: [scim-credential] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ScimServiceUnavailable" "401": description: | Authentication failed. Unknown, malformed, revoked, expired, epoch-superseded and wrong-binding credentials all answer this, so presentation reveals nothing about which credentials exist. A credential that does not match the binding in the path is an authentication failure, NEVER a SCIM 400. content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" "404": description: | A resource this binding did not mint, or a binding this caller cannot reach. content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" "429": $ref: "#/components/responses/TooManyRequests" "500": description: | An internal error, in the RFC 7644 shape. The SCIM surface answers its own error dialect for every status: an identity provider that met one Hikyo envelope among SCIM bodies would have to parse two. content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" "501": description: | A feature this service provider advertises as absent. It carries NO `scimType`: that field is a 400-class discriminator, and putting one on a 501 would name a code RFC 7644 does not define. content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" /api/v1/orgs/{org}/scim/v2/{binding}/Me: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ScimBindingID" get: operationId: scimMe tags: [scim-protocol] summary: Refused — /Me is not implemented. description: | A provisioning connection has no "me": it asserts identities, it does not have one. Advertised absent and refused by name with HTTP 501. x-hikyo-class: tenant x-hikyo-operation: scim-unsupported.refuse x-hikyo-formula: [scim-provision@org] x-hikyo-artifacts: [scim-credential] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ScimServiceUnavailable" "401": description: | Authentication failed. Unknown, malformed, revoked, expired, epoch-superseded and wrong-binding credentials all answer this, so presentation reveals nothing about which credentials exist. A credential that does not match the binding in the path is an authentication failure, NEVER a SCIM 400. content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" "404": description: | A resource this binding did not mint, or a binding this caller cannot reach. content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" "429": $ref: "#/components/responses/TooManyRequests" "500": description: | An internal error, in the RFC 7644 shape. The SCIM surface answers its own error dialect for every status: an identity provider that met one Hikyo envelope among SCIM bodies would have to parse two. content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" "501": description: | A feature this service provider advertises as absent. It carries NO `scimType`: that field is a 400-class discriminator, and putting one on a 501 would name a code RFC 7644 does not define. content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" /api/v1/orgs/{org}/scim/v2/{binding}/Users/.search: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ScimBindingID" post: operationId: scimSearchUsers tags: [scim-protocol] summary: Refused — the .search POST query is not implemented. description: | The filter grammar this provider answers is closed to four `eq` probes and is expressed as a query parameter. A POST query would be a second, wider filter surface; advertised absent and refused by name with 501. x-hikyo-class: tenant x-hikyo-operation: scim-unsupported.refuse x-hikyo-formula: [scim-provision@org] x-hikyo-artifacts: [scim-credential] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ScimServiceUnavailable" "401": description: | Authentication failed. Unknown, malformed, revoked, expired, epoch-superseded and wrong-binding credentials all answer this, so presentation reveals nothing about which credentials exist. A credential that does not match the binding in the path is an authentication failure, NEVER a SCIM 400. content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" "404": description: | A resource this binding did not mint, or a binding this caller cannot reach. content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" "429": $ref: "#/components/responses/TooManyRequests" "500": description: | An internal error, in the RFC 7644 shape. The SCIM surface answers its own error dialect for every status: an identity provider that met one Hikyo envelope among SCIM bodies would have to parse two. content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" "501": description: | A feature this service provider advertises as absent. It carries NO `scimType`: that field is a 400-class discriminator, and putting one on a 501 would name a code RFC 7644 does not define. content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" /api/v1/orgs/{org}/scim/v2/{binding}/Groups/.search: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ScimBindingID" post: operationId: scimSearchGroups tags: [scim-protocol] summary: Refused — the .search POST query is not implemented. x-hikyo-class: tenant x-hikyo-operation: scim-unsupported.refuse x-hikyo-formula: [scim-provision@org] x-hikyo-artifacts: [scim-credential] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ScimServiceUnavailable" "401": description: | Authentication failed. Unknown, malformed, revoked, expired, epoch-superseded and wrong-binding credentials all answer this, so presentation reveals nothing about which credentials exist. A credential that does not match the binding in the path is an authentication failure, NEVER a SCIM 400. content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" "404": description: | A resource this binding did not mint, or a binding this caller cannot reach. content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" "429": $ref: "#/components/responses/TooManyRequests" "500": description: | An internal error, in the RFC 7644 shape. The SCIM surface answers its own error dialect for every status: an identity provider that met one Hikyo envelope among SCIM bodies would have to parse two. content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" "501": description: | A feature this service provider advertises as absent. It carries NO `scimType`: that field is a 400-class discriminator, and putting one on a 501 would name a code RFC 7644 does not define. content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" # --------------------------------------------------------------------------- # Multi-instance (#71, multi-instance ADR). Two tiers, both additive. # # There is deliberately NO endpoint here that relays a request to another # instance. The directory tier reads foreign METADATA server-to-server under a # pinned connection; the workspace tier is the BROWSER talking to the remote # directly. A server-side proxy is the design the ADR rejected, and # api/noproxy_test.go is what keeps it rejected. # --------------------------------------------------------------------------- /api/v1/instance/directory: get: operationId: serveDirectory tags: [instance] summary: This instance's directory listing, as served to a connected instance. description: | The ONE endpoint an instance-connection credential may reach. Artifact eligibility caps the credential to this operation alone, so a future operation adopting the `instance-directory` formula does not widen any existing token. What it returns, exhaustively: this instance's opaque identity, its version, and the NAMES and counts of organisations and projects. No values, no keys, no environments, no membership, no settings, no audit — the response schema has no field that could carry one. The identity is returned ONLY here, never pre-authentication. That is what makes self-connection detectable at the fetch rather than guessable from outside. Human holders of `instance-directory` reach it too: it is the same listing, and hiding it from the humans the atom exists for would be arbitrary. x-hikyo-class: instance x-hikyo-operation: remote.directory-serve x-hikyo-formula: [instance-directory@instance] x-hikyo-artifacts: [human-session, instance-credential] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The served listing. content: application/json: schema: $ref: "#/components/schemas/DirectoryListing" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "403": $ref: "#/components/responses/Forbidden" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/instance/remotes: get: operationId: listRemotes tags: [instance] summary: The directory of connected instances. description: | Fetched ON VIEW. There is no background poller: a fetch happens because a holder of `instance-directory` is looking, and at no other moment. An unreachable entry serves its last-known snapshot MARKED STALE with its age, never silently as current. A credential rejection is its own loud state, distinct from unreachability, because the operator's fix differs. When the instance-wide or per-viewer trigger budget is exhausted no round runs and every entry is served from its snapshot, marked stale — the bounds limit fetch triggers, not views. x-hikyo-class: instance x-hikyo-operation: remote.list x-hikyo-formula: [instance-directory@instance] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: Every configured entry with its last-known state. content: application/json: schema: $ref: "#/components/schemas/RemoteList" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "403": $ref: "#/components/responses/Forbidden" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" post: operationId: addRemote tags: [instance] summary: Add a connection entry, verified by one authenticated fetch. description: | URL AND PIN ARE IMMUTABLE once stored, so there is no edit verb anywhere: re-pointing a stored credential at a different host is the credential-redirect attack, and re-pointing is remove + add, which re-runs the full ceremony including the human fingerprint confirmation. The caller must already have displayed the SPKI fingerprint and taken the human's confirmation — that is interactive and belongs to the client. This endpoint performs the authenticated directory fetch, which returns the remote's instance identity: self-connection (409) and duplicate-identity (409) fire there, BEFORE the entry is committed. A fetch that does not succeed commits nothing (422). The credential is write-only after storage: sealed under the instance keyring, never re-displayable through any surface, and leaving the process only inside TLS to the pinned remote. x-hikyo-class: instance x-hikyo-operation: remote.add x-hikyo-formula: [instance-config@instance] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/AddRemoteRequest" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "201": description: The entry, with the listing its verifying fetch returned. content: application/json: schema: $ref: "#/components/schemas/Remote" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "403": $ref: "#/components/responses/Forbidden" "409": $ref: "#/components/responses/Conflict" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/instance/remotes/{remote}: parameters: - $ref: "#/components/parameters/RemoteName" get: operationId: showRemote tags: [instance] summary: One connection entry, full detail, fetched on view. x-hikyo-class: instance x-hikyo-operation: remote.show x-hikyo-formula: [instance-directory@instance] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The entry with its last-known state. content: application/json: schema: $ref: "#/components/schemas/Remote" "401": $ref: "#/components/responses/Unauthenticated" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" patch: operationId: renameRemote tags: [instance] summary: Change an entry's display name. description: | The display name is the ONE mutable field. There is no URL or pin edit here or anywhere; see `addRemote` for why. API and UI only, deliberately: the API/CLI ADR's closed verb list for this family is `remote add|list|show|remove`, and parity holds because this is public surface. x-hikyo-class: instance x-hikyo-operation: remote.rename x-hikyo-formula: [instance-config@instance] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/RenameRemoteRequest" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The entry as renamed. content: application/json: schema: $ref: "#/components/schemas/Remote" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/Conflict" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" delete: operationId: removeRemote tags: [instance] summary: Destroy an entry, its stored credential and its snapshot. description: | THIS IS NOT REVOCATION. The serving side's credential remains valid until it is revoked THERE, and every surface says so: destroying the local copy is not revocation and this API does not pretend otherwise. x-hikyo-class: instance x-hikyo-operation: remote.remove x-hikyo-formula: [instance-config@instance] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "204": description: Removed. The serving-side credential is still live. "401": $ref: "#/components/responses/Unauthenticated" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/instance/connections: get: operationId: listInstanceConnections tags: [instance] summary: The connection credentials this instance has minted. description: | Metadata only, never the value: a credential is display-once at mint and write-only after, and no statement anywhere reads a verifier back out for display. x-hikyo-class: instance x-hikyo-operation: remote-credential.list x-hikyo-formula: [instance-config@instance] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: Every connection, live and revoked. content: application/json: schema: $ref: "#/components/schemas/InstanceConnectionList" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "403": $ref: "#/components/responses/Forbidden" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" post: operationId: mintInstanceConnection tags: [instance] summary: Mint a connection principal and its one credential. description: | Principal and credential are ONE UNIT with a stable immutable id, and there is one credential per principal EVER — rotation is a new mint plus a revoke of the predecessor, never a second credential on this one. The value is returned EXACTLY ONCE, here. It is not retrievable afterwards through any surface. The label names the intended peer for the audit trail. It is descriptive and NOT enforced: this instance cannot verify who holds the token, and does not pretend to. x-hikyo-class: instance x-hikyo-operation: remote-credential.create x-hikyo-formula: [instance-config@instance] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/MintInstanceConnectionRequest" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "201": description: The connection, with its value disclosed once. content: application/json: schema: $ref: "#/components/schemas/MintedInstanceConnection" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "403": $ref: "#/components/responses/Forbidden" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/instance/connections/{connection}: parameters: - $ref: "#/components/parameters/ConnectionID" get: operationId: showInstanceConnection tags: [instance] summary: One connection's metadata. x-hikyo-class: instance x-hikyo-operation: remote-credential.show x-hikyo-formula: [instance-config@instance] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The connection. content: application/json: schema: $ref: "#/components/schemas/InstanceConnection" "401": $ref: "#/components/responses/Unauthenticated" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" delete: operationId: revokeInstanceConnection tags: [instance] summary: Revoke the credential and retire its principal with it. description: | It bites at the NEXT presentation: the liveness predicate is read in the authenticating transaction, uncached, so there is no window in which a revoked connection still serves a listing. A double revoke is a 409, not a silent success — an act that did not happen must not put a second event on the trail. x-hikyo-class: instance x-hikyo-operation: remote-credential.revoke x-hikyo-formula: [instance-config@instance] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "204": description: Revoked. "401": $ref: "#/components/responses/Unauthenticated" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/Conflict" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/instance/workspace-origins: get: operationId: listWorkspaceOrigins tags: [instance] summary: The origin allowlist — which UI origins may run the handoff. description: | EXACT origins. No wildcards, no subdomain matching. It names UI ORIGINS, not instances, deliberately: what is trusted is precisely the code served at that origin, and that is what an admin must be able to point at when auditing why cross-origin access exists. x-hikyo-class: instance x-hikyo-operation: workspace-origin.list x-hikyo-formula: [instance-config@instance] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The allowlist. content: application/json: schema: $ref: "#/components/schemas/WorkspaceOriginList" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "403": $ref: "#/components/responses/Forbidden" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" post: operationId: addWorkspaceOrigin tags: [instance] summary: Consent to one exact UI origin. x-hikyo-class: instance x-hikyo-operation: workspace-origin.add x-hikyo-formula: [instance-config@instance] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/WorkspaceOriginRequest" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "201": description: Allowlisted. content: application/json: schema: $ref: "#/components/schemas/WorkspaceOrigin" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "403": $ref: "#/components/responses/Forbidden" "409": $ref: "#/components/responses/Conflict" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" delete: operationId: removeWorkspaceOrigin tags: [instance] summary: De-allowlist an origin and kill every session bound to it. description: | THE KILL SWITCH. Removing the entry and revoking every workspace session bound to that origin happen in ONE transaction, so there is no window in which the origin is de-allowlisted and its sessions still authenticate. New handoffs refuse at the transaction, not merely at CORS. The origin travels in the REQUEST BODY and never as a path parameter, deliberately: a path parameter naming an origin is one review lapse away from a path parameter naming a target, which is the proxy shape this contract refuses by construction. x-hikyo-class: instance x-hikyo-operation: workspace-origin.remove x-hikyo-formula: [instance-config@instance] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/WorkspaceOriginRequest" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: Removed, with the number of sessions the same transaction killed. content: application/json: schema: $ref: "#/components/schemas/WorkspaceOriginRemoved" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/auth/workspace/start: post: operationId: startWorkspaceHandoff tags: [auth] summary: Open a workspace handoff transaction. description: | The browser edition of the RFC 8252 shape the CLI already uses: the front channel carries code and state ONLY, never the artifact. Pre-authentication and CORS-readable for an allowlisted origin. A non-allowlisted origin is refused HERE, at the transaction — CORS controls what a browser may read, never what a token may do. The viewing shell opens the returned authorization URL in a popup with `noopener`, so the return path is not `window.opener`: the popup's final redirect lands on a same-origin callback page of the viewing UI, which hands the result to the shell over a nonce-named `BroadcastChannel`. x-hikyo-class: unauthenticated x-hikyo-artifacts: [none] x-hikyo-min-revision: 1 security: [] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/StartWorkspaceHandoffRequest" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "201": description: The transaction's opaque state and its expiry. content: application/json: schema: $ref: "#/components/schemas/WorkspaceHandoffStarted" "400": $ref: "#/components/responses/BadRequest" "403": $ref: "#/components/responses/Forbidden" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/auth/workspace/approve: post: operationId: approveWorkspaceHandoff tags: [auth] summary: Approve a handoff as the authenticated human, minting the code. description: | Runs INSIDE THE POPUP, on this instance's own origin, under this instance's own ceremonies — password + TOTP, passkey, OIDC, whatever this instance's human-auth configuration says. Authentication to a remote always happens on the remote's own origin; that is a design rule, not a platform limitation. It takes a HUMAN SESSION and refuses a workspace bearer by construction: a cross-origin credential living in another origin's JavaScript must not be able to authorize the issuance of another one. x-hikyo-class: unauthenticated x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/ApproveWorkspaceHandoffRequest" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The authorization code and the callback to deliver it to. content: application/json: schema: $ref: "#/components/schemas/WorkspaceHandoffApproved" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "403": $ref: "#/components/responses/Forbidden" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/auth/workspace/redeem: post: operationId: redeemWorkspaceHandoff tags: [auth] summary: Exchange code + PKCE verifier for a workspace session. description: | Single-use and atomically consumed: two concurrent redemptions of one code cannot both yield a session. The result is an opaque bearer whose server-side row here carries everything the locked session model requires, plus its two bound extras: the requesting origin and the issuing transaction. It differs from a browser session ONLY in transport — presented in an `Authorization` header, never a cookie — which is why CORS runs without credentials mode and this instance's CSRF posture is untouched. CLIENT-SIDE THE BEARER LIVES IN JS MEMORY ONLY: never a cookie, never localStorage or sessionStorage, gone on tab close. x-hikyo-class: unauthenticated x-hikyo-artifacts: [none] x-hikyo-min-revision: 1 security: [] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/RedeemWorkspaceHandoffRequest" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "201": description: The workspace session, disclosed once. content: application/json: schema: $ref: "#/components/schemas/WorkspaceSession" "400": $ref: "#/components/responses/BadRequest" "403": $ref: "#/components/responses/Forbidden" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/auth/workspace/transactions/{state}: parameters: - {name: state, in: path, required: true, schema: {type: string, minLength: 1, maxLength: 512}} get: operationId: showWorkspaceHandoff tags: [auth] summary: Load the authoritative shape of a live workspace handoff. description: | The approve page reads the operation, environment and enumerated key set the transaction was opened against, rather than trusting them from its own URL — the key set can be large, and a URL is a poor and length-capped place for the authoritative binding to live. Only the server-bound transaction answers, and only to the authenticated human on this instance; the numbers here are identifiers, never values or verifiers. x-hikyo-class: unauthenticated x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The live transaction's authoritative purpose and any bound step-up policy — identifiers only. content: application/json: schema: $ref: "#/components/schemas/WorkspaceHandoffTransaction" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "500": $ref: "#/components/responses/Internal" /api/v1/me/sessions: get: operationId: listMySessions tags: [auth] summary: The caller's own active sessions, workspace sessions included. description: | SELF-SCOPED: the caller's sessions and nothing else. It needs no capability for the reason `listMyOrgs` needs none — it is a projection of what the caller already holds — and requiring one would make incident response depend on an authority an attacker may have just removed. A WORKSPACE SESSION APPEARS HERE AS ITS OWN ARTIFACT TYPE, carrying the requesting origin. That is the ADR's requirement and the reason a workspace session is a session row at all. Metadata only. No verifier is returned here or anywhere. x-hikyo-class: unauthenticated x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The caller's sessions. content: application/json: schema: $ref: "#/components/schemas/SessionList" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/me/sessions/{session}: parameters: - $ref: "#/components/parameters/SessionID" delete: operationId: revokeMySession tags: [auth] summary: Revoke one of the caller's own sessions. description: | Kills it MID-FLIGHT: the next request re-resolves the row in its own transaction, so a revoked session stops working immediately rather than at some cache horizon. The principal conjunct lives in the SQL, so one caller is structurally unable to reach another's session by guessing an id — and an id that is not the caller's is a 404, not a 403. x-hikyo-class: unauthenticated x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "204": description: Revoked. "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/orgs/{org}/projects/{project}/environments/{environment}/publish: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ProjectID" - $ref: "#/components/parameters/EnvironmentID" post: operationId: publishPendingChanges tags: [revisions] summary: Publish a selection of your own pending changes. description: | SELECTIVE. The body names version ids, and only those - plus whatever key-group closure pulls in - are committed. Every other pending change, the caller's own unselected ones and every other principal's, is invisible to the computation, so the materialized snapshot always corresponds to a state the publisher previewed. A publish carries ONLY the caller's own pending changes. There is no cross-user hand-off of staged work: a key group whose members carry a pending change owned by another principal is refused, loud, naming the group and the key, never silently split. The whole operation is one serializable transaction holding the per-project lock. A version whose published cell has moved since it was staged is refused loud rather than rebased, and a `required_in` key left absent vetoes the publish naming key and environment. Publishing a version whose environment differs from the addressed one is permitted and each such environment is authorized for `publish` separately, immediately before commit. Every protected environment reached requires its bound publish confirmation. Machine callers name the exact protected set they reviewed; human callers consume the bound ceremony. A selection containing restore-authored drafts also requires the preview token returned by rollback; schema, published state, authorization, selection, or key-group closure drift invalidates that token and requires a fresh rollback preview. x-hikyo-class: tenant x-hikyo-operation: value.publish x-hikyo-formula: [publish@environment] x-hikyo-dynamic-formula: [publish@each-affected-environment, protected-confirmation@exact-reviewed-protected-set] x-hikyo-artifacts: [human-session, machine-credential] x-hikyo-min-revision: 1 requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/PublishRequest" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: What committed, and which environments advanced. content: application/json: schema: $ref: "#/components/schemas/PublishResult" "202": description: | Secret-change approvals (#151): the environment is covered by an approval policy and no completed approval was presented, so a request was staged for review instead of a revision published. content: application/json: schema: $ref: "#/components/schemas/ApprovalRequestSummary" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/Conflict" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/orgs/{org}/projects/{project}/approval-policies: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ProjectID" get: operationId: listApprovalPolicies tags: [approvals] summary: The project's change-approval policies. description: | Secret-change approvals (#151). The scoped policies governing publishes in this project: their approvers (users and SCIM groups), quorum, self-approval behaviour, request expiry and emergency bypassers. Reading the set is an administrative act and is itself audited. x-hikyo-class: tenant x-hikyo-operation: approval.policy-read x-hikyo-formula: [project-settings@project] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The project's approval policies. content: application/json: schema: $ref: "#/components/schemas/ApprovalPolicyList" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" post: operationId: createApprovalPolicy tags: [approvals] summary: Create a change-approval policy. description: | Bind an approval policy to the project or one of its environments (`environment_id` empty means every environment). Staging stays unrestricted; a covered PUBLISH then requires the policy's completed approval state. x-hikyo-class: tenant x-hikyo-operation: approval.policy-write x-hikyo-formula: [project-settings@project] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/ApprovalPolicyInput" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The created policy. content: application/json: schema: $ref: "#/components/schemas/ApprovalPolicy" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/Conflict" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/orgs/{org}/projects/{project}/approval-policies/{policy}: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ProjectID" - name: policy in: path required: true schema: {$ref: "#/components/schemas/ID"} put: operationId: updateApprovalPolicy tags: [approvals] summary: Update a change-approval policy. description: | Replace the policy's fields and member sets. The version increments, so every open request pinned to the previous version fails closed the next time it is voted on or merged. Disabling is `enabled: false`. x-hikyo-class: tenant x-hikyo-operation: approval.policy-write x-hikyo-formula: [project-settings@project] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/ApprovalPolicyInput" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The updated policy. content: application/json: schema: $ref: "#/components/schemas/ApprovalPolicy" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/Conflict" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" delete: operationId: deleteApprovalPolicy tags: [approvals] summary: Delete a change-approval policy. description: | Remove the policy and its member sets. Open requests pinned to it fail closed at merge; the audit trail of resolved requests is retained. x-hikyo-class: tenant x-hikyo-operation: approval.policy-write x-hikyo-formula: [project-settings@project] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "204": description: The policy was deleted. "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/orgs/{org}/projects/{project}/environments/{environment}/approval-requests: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ProjectID" - $ref: "#/components/parameters/EnvironmentID" get: operationId: listApprovalRequests tags: [approvals] summary: The environment's change-approval requests. description: | The review queue for one environment: each request's pinned change set size, purpose, state, quorum progress and votes. No value plaintext is carried. Gated by `read` on the environment. x-hikyo-class: tenant x-hikyo-operation: approval.request-read x-hikyo-formula: [read@environment] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The environment's approval requests. content: application/json: schema: $ref: "#/components/schemas/ApprovalRequestList" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/orgs/{org}/projects/{project}/environments/{environment}/approval-requests/{approvalRequest}/vote: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ProjectID" - $ref: "#/components/parameters/EnvironmentID" - name: approvalRequest in: path required: true schema: {$ref: "#/components/schemas/ID"} post: operationId: voteApprovalRequest tags: [approvals] summary: Approve or reject a change-approval request. description: | An eligible approver casts one decision. A repeated identical decision is idempotent; a conflicting one is refused. A reject resolves the request; approvals accumulate toward the policy's quorum. The requester cannot approve their own request unless the policy permits it. The vote consumes a purpose-bound reauthentication ceremony. x-hikyo-class: tenant x-hikyo-operation: approval.vote x-hikyo-formula: [publish@environment] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/ApprovalVoteRequest" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The request after the vote. content: application/json: schema: $ref: "#/components/schemas/ApprovalRequest" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/Conflict" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/orgs/{org}/projects/{project}/environments/{environment}/approval-requests/{approvalRequest}/ceremony: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ProjectID" - $ref: "#/components/parameters/EnvironmentID" - name: approvalRequest in: path required: true schema: {$ref: "#/components/schemas/ID"} get: operationId: getApprovalCeremony tags: [approvals] summary: Prepare a purpose-bound approval ceremony. description: | Returns the request's exact key unit and the acting session's current reauthentication-window state. It carries no value material and is authorized by `publish`, so a named approver does not need `read` or `reveal` merely to prepare an otherwise-authorized decision. x-hikyo-class: tenant x-hikyo-operation: approval.vote x-hikyo-formula: [publish@environment] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The exact approval ceremony binding. content: application/json: schema: $ref: "#/components/schemas/ApprovalCeremonyBinding" "401": $ref: "#/components/responses/Unauthenticated" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/orgs/{org}/projects/{project}/environments/{environment}/revisions/diff: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ProjectID" - $ref: "#/components/parameters/EnvironmentID" post: operationId: diffRevisions tags: [revisions] summary: Compare two revisions with secret write-presence only. description: | Both revisions must retain their payload. A secret on either side makes the entire row secret. Secret status uses write-presence only. Explicit per-key reveal checks current reveal or reveal-history for each side, runs one key-bound ceremony, and audits each disclosed revision. x-hikyo-class: tenant x-hikyo-operation: value.export x-hikyo-formula: [read@environment] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 requestBody: required: true content: application/json: schema: {$ref: "#/components/schemas/RevisionDiffRequest"} responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The requested comparison, without change tokens or digests. content: application/json: schema: {$ref: "#/components/schemas/RevisionDiff"} "400": {$ref: "#/components/responses/BadRequest"} "401": {$ref: "#/components/responses/Unauthenticated"} "404": {$ref: "#/components/responses/NotFound"} "409": {$ref: "#/components/responses/Conflict"} "429": {$ref: "#/components/responses/TooManyRequests"} "500": {$ref: "#/components/responses/Internal"} /api/v1/orgs/{org}/projects/{project}/environments/{environment}/revisions/diff/reveal: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ProjectID" - $ref: "#/components/parameters/EnvironmentID" post: operationId: revealRevisionDiff tags: [revisions] summary: Reveal one key across two revisions. description: | Both revisions must retain their payload. A secret on either side makes the entire row secret. Secret status uses write-presence only. Explicit per-key reveal checks current reveal or reveal-history for each side, runs one key-bound ceremony, and audits each disclosed revision. x-hikyo-class: tenant x-hikyo-operation: value.export-reveal x-hikyo-formula: [read@environment, reveal@environment] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 requestBody: required: true content: application/json: schema: {$ref: "#/components/schemas/RevealRevisionDiffRequest"} responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The requested comparison, without change tokens or digests. content: application/json: schema: {$ref: "#/components/schemas/RevisionDiff"} "400": {$ref: "#/components/responses/BadRequest"} "401": {$ref: "#/components/responses/Unauthenticated"} "403": {$ref: "#/components/responses/Forbidden"} "404": {$ref: "#/components/responses/NotFound"} "409": {$ref: "#/components/responses/Conflict"} "429": {$ref: "#/components/responses/TooManyRequests"} "500": {$ref: "#/components/responses/Internal"} /api/v1/orgs/{org}/projects/{project}/environments/{environment}/signals: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ProjectID" - $ref: "#/components/parameters/EnvironmentID" get: operationId: getEnvironmentSignals tags: [revisions] summary: The matrix signals for one environment. description: | The two "changed" signals the matrix renders, plus the environment's current revision. `pending_version_id` is the caller's OWN live draft for the cell - the id a selective publish names; `pending_by_others` is the quieter collision marker and carries no id, no owner and no operation, because another principal's draft is write-presence and nothing more. Both signals degrade to write-presence for `secret` keys: they report that someone WROTE here, never that the plaintext differs from the published one. A comparison status is itself an oracle. This is also the advisory stream's documented POLLING FALLBACK: the same facts, pulled under the caller's own authorization. x-hikyo-class: tenant x-hikyo-operation: revision.signals x-hikyo-formula: [read@environment] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The environment's signals. content: application/json: schema: $ref: "#/components/schemas/EnvironmentSignals" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/orgs/{org}/projects/{project}/environments/{environment}/pending: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ProjectID" - $ref: "#/components/parameters/EnvironmentID" get: operationId: listPendingDrafts tags: [revisions] summary: Preview the caller's pending drafts in one environment. description: | Returns only the caller's own drafts. Other principals' drafts are invisible here, including their presence; the matrix signals endpoint carries the permitted write-presence marker instead. A config `set` carries its plaintext under read authorization unless its staged material originated as secret. Secret-origin drafts never carry material here and remain hidden: offering a secret preview would require consuming a disclosure ceremony, which is outside this seam's scope. Unset drafts carry no material. `value` is present if and only if `revealed` is true. x-hikyo-class: tenant x-hikyo-operation: value.pending-list x-hikyo-formula: [read@environment] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The caller's pending drafts in the environment. content: application/json: schema: $ref: "#/components/schemas/PendingDraftList" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/orgs/{org}/projects/{project}/environments/{environment}/revisions/{revision}/rollback: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ProjectID" - $ref: "#/components/parameters/EnvironmentID" - name: revision in: path required: true schema: {type: integer, format: int64, minimum: 1} post: operationId: rollbackRevision tags: [revisions] summary: Stage an environment or key restore from one revision. description: | Computes a two-way set/unset diff and writes ordinary caller-owned pending changes. The normal publish verb commits them and validates against the current schema. Reading superseded secret material also requires read AND reveal-history; comparing current secret plaintext also requires read AND reveal. Both take reauthentication and one disclosure event per key and revision at this staging step. The response includes the full, reveal-safe impact preview and its exact-input publish token. x-hikyo-class: tenant x-hikyo-operation: revision.restore x-hikyo-formula: [edit@environment] x-hikyo-dynamic-formula: ["read@environment + reveal-history@environment when superseded secret material is read", "read@environment + reveal@environment when current secret material is compared"] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 requestBody: required: false content: application/json: schema: $ref: "#/components/schemas/RollbackRequest" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: Ordinary pending changes staged by the restore. content: application/json: schema: $ref: "#/components/schemas/RollbackResult" "400": {$ref: "#/components/responses/BadRequest"} "401": {$ref: "#/components/responses/Unauthenticated"} "404": {$ref: "#/components/responses/NotFound"} "409": {$ref: "#/components/responses/Conflict"} "429": {$ref: "#/components/responses/TooManyRequests"} "500": {$ref: "#/components/responses/Internal"} /api/v1/orgs/{org}/projects/{project}/environments/{environment}/pins: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ProjectID" - $ref: "#/components/parameters/EnvironmentID" get: operationId: listRevisionPins tags: [revisions] summary: List durable workload revision pins. x-hikyo-class: tenant x-hikyo-operation: pin.list x-hikyo-formula: [read@environment] x-hikyo-artifacts: [human-session, machine-credential] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: Pins, including loud expired status. content: application/json: schema: {$ref: "#/components/schemas/RevisionPinList"} "401": {$ref: "#/components/responses/Unauthenticated"} "404": {$ref: "#/components/responses/NotFound"} "429": {$ref: "#/components/responses/TooManyRequests"} "500": {$ref: "#/components/responses/Internal"} post: operationId: createRevisionPin tags: [revisions] summary: Create, reassign, or renew a workload revision pin. description: | One durable pin may exist per workload and environment. Repeating this operation renews the same revision or reassigns a different one, with authorization rechecked. Omitted expiry defaults to 180 days; the maximum is 365 days. A non-current revision requires reveal-history, reauthentication, and one disclosure event per historical secret key; a current-schema failure requires override_schema. x-hikyo-class: tenant x-hikyo-operation: pin.set x-hikyo-formula: [pin@environment, publish@environment] x-hikyo-dynamic-formula: ["reveal-history@environment when target is non-current"] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 requestBody: required: true content: application/json: schema: {$ref: "#/components/schemas/RevisionPinRequest"} responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The created, reassigned, or renewed pin. content: application/json: schema: {$ref: "#/components/schemas/RevisionPinResult"} "400": {$ref: "#/components/responses/BadRequest"} "401": {$ref: "#/components/responses/Unauthenticated"} "404": {$ref: "#/components/responses/NotFound"} "409": {$ref: "#/components/responses/Conflict"} "429": {$ref: "#/components/responses/TooManyRequests"} "500": {$ref: "#/components/responses/Internal"} /api/v1/orgs/{org}/projects/{project}/environments/{environment}/pins/{workloadPrincipal}: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ProjectID" - $ref: "#/components/parameters/EnvironmentID" - name: workloadPrincipal in: path required: true schema: {type: string, minLength: 1} delete: operationId: releaseRevisionPin tags: [revisions] summary: Release a workload revision pin. description: | Returns the released revision payload's retention consequence from the same locked transaction that removes the pin. This is transaction-time truth, not a durable retention promise: collection_eligible may be collected immediately after commit, while concurrent GC that commits first is reported as already_collected. x-hikyo-class: tenant x-hikyo-operation: pin.release x-hikyo-formula: [pin@environment] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: Released, with the server-derived retention consequence. content: application/json: schema: {$ref: "#/components/schemas/RevisionPinReleaseResult"} "401": {$ref: "#/components/responses/Unauthenticated"} "404": {$ref: "#/components/responses/NotFound"} "429": {$ref: "#/components/responses/TooManyRequests"} "500": {$ref: "#/components/responses/Internal"} /api/v1/orgs/{org}/projects/{project}/environments/{environment}/revisions: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ProjectID" - $ref: "#/components/parameters/EnvironmentID" get: operationId: listRevisions tags: [revisions] summary: One page of an environment's revision history, newest first. description: | LINEAGE ONLY: revision numbers, publishers, timestamps, the pinned schema revision, and which keys changed. It never contains a value in any form - not a plaintext, not a length, not a digest, not a changed-from marker. That is what lets lineage be retained forever while payloads are collected by retention policy. The history is paged by revision, newest first. A page holds the revisions strictly below `before` (default: above the newest), at most `limit` of them. When the page was full, `next_before` carries the smallest revision returned; pass it as the next `before` to continue. Its absence means the page reached the oldest revision. x-hikyo-class: tenant x-hikyo-operation: revision.list x-hikyo-formula: [read@environment] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 parameters: - $ref: "#/components/parameters/RevisionBefore" - $ref: "#/components/parameters/RevisionLimit" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The history. content: application/json: schema: $ref: "#/components/schemas/RevisionList" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/orgs/{org}/projects/{project}/environments/{environment}/revisions/{revision}: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ProjectID" - $ref: "#/components/parameters/EnvironmentID" - name: revision in: path required: true description: The revision number, or `latest`. schema: type: string pattern: "^(latest|[1-9][0-9]{0,18})$" get: operationId: getRevision tags: [revisions] summary: One revision, with its change token. description: | The change token is DERIVED here from the current root token key over the snapshot's delivery manifest; it is never stored. That is what makes `rotate-token-key` move every token while content, revision numbers and pinned input revisions stay exactly where they are. The token is non-secret metadata by construction - keyed, so unforgeable and un-invertible without the key, which is why it may flow into pod annotations and logs. The key set it accompanies carries names and classifications and no values. x-hikyo-class: tenant x-hikyo-operation: revision.show x-hikyo-formula: [read@environment] x-hikyo-artifacts: [human-session, machine-credential] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The revision. content: application/json: schema: $ref: "#/components/schemas/RevisionDetail" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/Conflict" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/orgs/{org}/projects/{project}/environments/{environment}/values/export: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ProjectID" - $ref: "#/components/parameters/EnvironmentID" post: operationId: exportValues tags: [values] summary: Export one environment's resolved snapshot. description: | The one BULK-DISCLOSURE verb, and what "fetch resolved" actually is. It reads a committed snapshot, never live values. The formulas are stated separately because the capabilities imply nothing about each other: current material is `read` AND `reveal`; historical material - any revision that is not the latest - is `read` AND `reveal-history`. Without `reveal`, `config` values are returned in plaintext (classification IS the sensitivity boundary) and `secret` keys report write-presence with no value. It is a POST because disclosure is an ACT: one audit event is written per disclosed key, durably, before any plaintext leaves the server. x-hikyo-class: tenant x-hikyo-operation: value.export-reveal x-hikyo-formula: [read@environment, reveal@environment] # Human-session only on the current wire contract. Machine credentials # receive secret material through delivery.fetch; workload # reveal-history is therefore bound to its active non-current pin rather # than exposed as an unpinned bulk-export route. x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/ExportValuesRequest" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The resolved snapshot. content: application/json: schema: $ref: "#/components/schemas/ExportedValues" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/Conflict" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/orgs/{org}/projects/{project}/events: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ProjectID" get: operationId: watchProjectEvents tags: [revisions] summary: The advisory live-update stream (SSE). description: | Server-sent events, metadata only. An event says that something changed - an environment advanced (`revision.published`), a principal staged a draft (`pending.staged`), a published cell moved (`cell.changed`) - and never what it changed to. It carries no value in any form and NO CHANGE TOKEN. `cell.changed` claims exactly what the publish knew: that cell advanced. A subscriber holding a draft on the named cell derives "my draft is stale" from its own draft plus this event; the server never asserts staleness it did not check. Every event is authorized against the environment it names, at emit time, so a grant revoked mid-session takes effect on the NEXT event rather than at the next reconnect. Events referencing objects the recipient may not see are dropped, and an event reduced to nothing is not sent. ADVISORY ONLY: correctness never depends on delivery. `Last-Event-ID` replays nothing; a reconnecting client refetches current state from the signals endpoint, which is the documented polling fallback. A client that cannot keep up is disconnected rather than buffered. Connection admission is bounded per principal, per organization, and instance-wide; a refusal is 429. Accepted streams begin with a jittered `retry:` hint. x-hikyo-class: tenant x-hikyo-operation: advisory.watch x-hikyo-formula: [read@project] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: An event stream. content: text/event-stream: schema: type: string "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/orgs/{org}/audit: parameters: - $ref: "#/components/parameters/OrgID" get: operationId: queryOrgAudit tags: [audit] summary: One bounded page of the organization's audit trail. description: | Reads one bounded page of the append-only trail in stable commit-sequence order, oldest first. Filtering by principal, operation, outcome and resource identifiers is applied to the page after it is read, so a page SCANS `limit` rows and RETURNS the subset that matched: `next_after_seq` is the seq of the last row scanned (the resume cursor), and `exhausted` is true only when the scan reached the end of the trail. A sparse filter therefore returns few or zero items with `exhausted` false — that means keep paging, not "no such events". Reading the trail is itself audited, unconditionally: each page commits one `audit.query` event recording the normalized filter and the count returned, durably, in the same transaction as the read. No value material is ever in the trail, so none can be in this response. Consistency limit of interactive paging: pages are ordered by the allocation `seq`. A transaction that commits out of allocation order can land below a cursor that has already passed it, so that event can be absent from the remainder of one paging session. It is durable and a fresh query recovers it. Exports do not have this limit: they page in commit order behind the writer barrier and are gap-free. x-hikyo-class: tenant x-hikyo-operation: audit.query-org x-hikyo-formula: [audit-read@org] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 parameters: - $ref: "#/components/parameters/AuditFrom" - $ref: "#/components/parameters/AuditTo" - $ref: "#/components/parameters/AuditAfterSeq" - $ref: "#/components/parameters/AuditToSeq" - $ref: "#/components/parameters/AuditLimit" - $ref: "#/components/parameters/AuditActor" - $ref: "#/components/parameters/AuditActorName" - $ref: "#/components/parameters/AuditOperation" - $ref: "#/components/parameters/AuditOutcome" - $ref: "#/components/parameters/AuditOutcomes" - $ref: "#/components/parameters/AuditObjectType" - $ref: "#/components/parameters/AuditObjectID" - $ref: "#/components/parameters/AuditCorrelationID" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: One page of trail events. content: application/json: schema: {$ref: "#/components/schemas/AuditPage"} "400": {$ref: "#/components/responses/BadRequest"} "401": {$ref: "#/components/responses/Unauthenticated"} "404": {$ref: "#/components/responses/NotFound"} "429": {$ref: "#/components/responses/TooManyRequests"} "500": {$ref: "#/components/responses/Internal"} /api/v1/orgs/{org}/audit/export: parameters: - $ref: "#/components/parameters/OrgID" get: operationId: exportOrgAudit tags: [audit] summary: Stream the organization's audit trail as JSONL. description: | Streams the filtered slice of the trail as newline-delimited JSON, one schema-versioned event per line, in gap-free commit-sequence order up to a server-fixed snapshot taken before the first byte. The stream is bounded on both ends: the server reads the trail one bounded page at a time under a freshly minted proof, and the client writes bytes to disk as they arrive rather than accumulating the history in memory. The export is itself an audited act: an `audit.export_started` INTENT event is durable before the first byte and an `audit.export_completed` OUTCOME event terminates the stream on success, page failure or client disconnect. Authorization lost mid-stream stops it at the next page boundary — a truncated download whose cause is durable in the trail. No value material is ever in the trail, so none can be in the stream. x-hikyo-class: tenant x-hikyo-operation: audit.export-org x-hikyo-formula: [audit-read@org] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 parameters: - $ref: "#/components/parameters/AuditFrom" - $ref: "#/components/parameters/AuditTo" - $ref: "#/components/parameters/AuditActor" - $ref: "#/components/parameters/AuditActorName" - $ref: "#/components/parameters/AuditOperation" - $ref: "#/components/parameters/AuditOutcome" - $ref: "#/components/parameters/AuditOutcomes" - $ref: "#/components/parameters/AuditObjectType" - $ref: "#/components/parameters/AuditObjectID" - $ref: "#/components/parameters/AuditCorrelationID" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The trail as a JSONL byte stream. content: application/x-ndjson: schema: {type: string} "400": {$ref: "#/components/responses/BadRequest"} "401": {$ref: "#/components/responses/Unauthenticated"} "404": {$ref: "#/components/responses/NotFound"} "429": {$ref: "#/components/responses/TooManyRequests"} "500": {$ref: "#/components/responses/Internal"} /api/v1/orgs/{org}/projects/{project}/audit: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ProjectID" get: operationId: queryProjectAudit tags: [audit] summary: One bounded page of the project's audit trail. description: | The project-scoped audit page. Same shape and paging contract as `queryOrgAudit`, refined to the project chain. x-hikyo-class: tenant x-hikyo-operation: audit.query-project x-hikyo-formula: [audit-read@project] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 parameters: - $ref: "#/components/parameters/AuditFrom" - $ref: "#/components/parameters/AuditTo" - $ref: "#/components/parameters/AuditAfterSeq" - $ref: "#/components/parameters/AuditToSeq" - $ref: "#/components/parameters/AuditLimit" - $ref: "#/components/parameters/AuditActor" - $ref: "#/components/parameters/AuditActorName" - $ref: "#/components/parameters/AuditOperation" - $ref: "#/components/parameters/AuditOutcome" - $ref: "#/components/parameters/AuditOutcomes" - $ref: "#/components/parameters/AuditObjectType" - $ref: "#/components/parameters/AuditObjectID" - $ref: "#/components/parameters/AuditCorrelationID" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: One page of trail events. content: application/json: schema: {$ref: "#/components/schemas/AuditPage"} "400": {$ref: "#/components/responses/BadRequest"} "401": {$ref: "#/components/responses/Unauthenticated"} "404": {$ref: "#/components/responses/NotFound"} "429": {$ref: "#/components/responses/TooManyRequests"} "500": {$ref: "#/components/responses/Internal"} /api/v1/orgs/{org}/projects/{project}/audit/export: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ProjectID" get: operationId: exportProjectAudit tags: [audit] summary: Stream the project's audit trail as JSONL. description: | The project-scoped JSONL export. Same shape and INTENT/OUTCOME contract as `exportOrgAudit`, refined to the project chain. x-hikyo-class: tenant x-hikyo-operation: audit.export-project x-hikyo-formula: [audit-read@project] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 parameters: - $ref: "#/components/parameters/AuditFrom" - $ref: "#/components/parameters/AuditTo" - $ref: "#/components/parameters/AuditActor" - $ref: "#/components/parameters/AuditActorName" - $ref: "#/components/parameters/AuditOperation" - $ref: "#/components/parameters/AuditOutcome" - $ref: "#/components/parameters/AuditOutcomes" - $ref: "#/components/parameters/AuditObjectType" - $ref: "#/components/parameters/AuditObjectID" - $ref: "#/components/parameters/AuditCorrelationID" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The trail as a JSONL byte stream. content: application/x-ndjson: schema: {type: string} "400": {$ref: "#/components/responses/BadRequest"} "401": {$ref: "#/components/responses/Unauthenticated"} "404": {$ref: "#/components/responses/NotFound"} "429": {$ref: "#/components/responses/TooManyRequests"} "500": {$ref: "#/components/responses/Internal"} /api/v1/orgs/{org}/projects/{project}/environments/{environment}/audit: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ProjectID" - $ref: "#/components/parameters/EnvironmentID" get: operationId: queryEnvAudit tags: [audit] summary: One bounded page of the environment's audit trail. description: | The environment-scoped audit page. Same shape and paging contract as `queryOrgAudit`, refined to the environment chain. x-hikyo-class: tenant x-hikyo-operation: audit.query-env x-hikyo-formula: [audit-read@environment] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 parameters: - $ref: "#/components/parameters/AuditFrom" - $ref: "#/components/parameters/AuditTo" - $ref: "#/components/parameters/AuditAfterSeq" - $ref: "#/components/parameters/AuditToSeq" - $ref: "#/components/parameters/AuditLimit" - $ref: "#/components/parameters/AuditActor" - $ref: "#/components/parameters/AuditActorName" - $ref: "#/components/parameters/AuditOperation" - $ref: "#/components/parameters/AuditOutcome" - $ref: "#/components/parameters/AuditOutcomes" - $ref: "#/components/parameters/AuditObjectType" - $ref: "#/components/parameters/AuditObjectID" - $ref: "#/components/parameters/AuditCorrelationID" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: One page of trail events. content: application/json: schema: {$ref: "#/components/schemas/AuditPage"} "400": {$ref: "#/components/responses/BadRequest"} "401": {$ref: "#/components/responses/Unauthenticated"} "404": {$ref: "#/components/responses/NotFound"} "429": {$ref: "#/components/responses/TooManyRequests"} "500": {$ref: "#/components/responses/Internal"} /api/v1/orgs/{org}/projects/{project}/environments/{environment}/audit/export: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ProjectID" - $ref: "#/components/parameters/EnvironmentID" get: operationId: exportEnvAudit tags: [audit] summary: Stream the environment's audit trail as JSONL. description: | The environment-scoped JSONL export. Same shape and INTENT/OUTCOME contract as `exportOrgAudit`, refined to the environment chain. x-hikyo-class: tenant x-hikyo-operation: audit.export-env x-hikyo-formula: [audit-read@environment] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 parameters: - $ref: "#/components/parameters/AuditFrom" - $ref: "#/components/parameters/AuditTo" - $ref: "#/components/parameters/AuditActor" - $ref: "#/components/parameters/AuditActorName" - $ref: "#/components/parameters/AuditOperation" - $ref: "#/components/parameters/AuditOutcome" - $ref: "#/components/parameters/AuditOutcomes" - $ref: "#/components/parameters/AuditObjectType" - $ref: "#/components/parameters/AuditObjectID" - $ref: "#/components/parameters/AuditCorrelationID" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The trail as a JSONL byte stream. content: application/x-ndjson: schema: {type: string} "400": {$ref: "#/components/responses/BadRequest"} "401": {$ref: "#/components/responses/Unauthenticated"} "404": {$ref: "#/components/responses/NotFound"} "429": {$ref: "#/components/responses/TooManyRequests"} "500": {$ref: "#/components/responses/Internal"} /api/v1/instance/rotate-token-key: post: operationId: rotateTokenKey tags: [instance] summary: Rotate the root change-token key. description: | Mints a new root token key, retires the old one, and derives every subsequent change token under it. WHAT IT DOES NOT TOUCH: no snapshot's content, no revision number, and no pinned input revision moves. It cannot - a change token is never stored, it is derived at read from the live key over the snapshot's delivery manifest, so there is no cache to invalidate and nothing to resume after a crash. Every outstanding delivery cursor mismatches once and every consumer performs one full fetch. This is NOT a restart wave. It rides `rotate-dek`: the capability set is closed and names four rotation atoms for five rotation verbs, and the root token key is a tier-3 key alongside the DEKs. x-hikyo-class: instance x-hikyo-operation: crypto.rotate-token-key x-hikyo-formula: [rotate-dek@instance] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The rotation. content: application/json: schema: $ref: "#/components/schemas/TokenKeyRotation" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/instance/rotate-dek: post: operationId: rotateDEK tags: [instance] summary: Append a new DEK version for one project or the instance scope. description: | Appends a new data-encryption-key version. New writes seal under it immediately; ciphertext under the previous version stays readable until `reencrypt` walks it onto the new one (Vault keyring semantics). Free on its own, and incomplete without a following reencrypt. Its own capability, `rotate-dek`, never bundled - the post-compromise recovery order runs each rotation under a distinct authority. x-hikyo-class: instance x-hikyo-operation: crypto.rotate-dek x-hikyo-formula: [rotate-dek@instance] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/RotateDEKRequest" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The rotation. content: application/json: schema: $ref: "#/components/schemas/DEKRotation" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/Conflict" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/instance/reencrypt: post: operationId: reencryptInstance tags: [instance] summary: Walk the instance credential ciphertext onto the active DEK version. description: | Moves every instance-DEK credential ciphertext (password verifiers, TOTP seeds, recovery codes, OIDC/SAML secrets, remote credentials) onto the active instance DEK version and retires the superseded versions — the completion of an instance `rotate-dek`. Chunked and resumable; re-run to continue after an interruption. x-hikyo-class: instance x-hikyo-operation: crypto.reencrypt-instance x-hikyo-formula: [reencrypt@instance] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The reencrypt result. content: application/json: schema: $ref: "#/components/schemas/ReencryptResult" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/Conflict" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/instance/rotate-master-key: post: operationId: rotateMasterKey tags: [instance] summary: Generate a new master key and re-wrap every tier-3 key under it. description: | Generates a new master key, re-wraps every tier-3 key (all project DEKs, the instance DEK, the root token key) under it, and retires the old master after a fenced zero-reference check. Bounded by the number of projects, so seconds. Step 2 of the post-compromise recovery order: an attacker who held process memory holds the master, so every DEK must be re-wrapped under a master they never saw. Refused while the root is dual-wrapped — finalize the root rotation first. x-hikyo-class: instance x-hikyo-operation: crypto.rotate-master-key x-hikyo-formula: [rotate-master-key@instance] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The rotation. content: application/json: schema: $ref: "#/components/schemas/MasterKeyRotation" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/Conflict" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/instance/rotate-root-key: post: operationId: rotateRootKey tags: [instance] summary: Run one phase of the crash-safe root-key rotation. description: | Replaces the operator-held root through a three-phase protocol over a dual-wrapped master. NO key material crosses the wire: `prepare` reads the NEW root from the server-side source (HIKYO_NEW_ROOT_KEY_FILE) and seals a second master wrapper; the operator then installs the new root at the PRIMARY source; `verify` re-reads the primary source and confirms it now unwraps the new wrapper; `finalize` retires the old wrapper. A crash at any point leaves the instance bootable under either root, and every start warns until finalize. This is the rotation operators need after an env-file leak — it re-wraps the master only, touching no data. x-hikyo-class: instance x-hikyo-operation: crypto.rotate-root-key x-hikyo-formula: [rotate-root-key@instance] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/RotateRootKeyRequest" responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The phase result. content: application/json: schema: $ref: "#/components/schemas/RootKeyRotation" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/Conflict" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/orgs/{org}/projects/{project}/reencrypt: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ProjectID" post: operationId: reencryptProject tags: [instance] summary: Walk a project's ciphertext onto the active DEK version. description: | Moves every ciphertext in the project (values, drafts, snapshot payloads, adapter credentials) onto the active project DEK version and retires the superseded versions — the completion of a project `rotate-dek`. Chunked and resumable. x-hikyo-class: tenant x-hikyo-operation: crypto.reencrypt-project x-hikyo-formula: [reencrypt@instance] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The reencrypt result. content: application/json: schema: $ref: "#/components/schemas/ReencryptResult" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/Conflict" "500": $ref: "#/components/responses/Internal" /api/v1/instance/rotate-scanning-key: post: operationId: rotateScanningKey tags: [instance] summary: Rotate the secret-scanning fingerprint key. description: | Mints a new scanning-fingerprint key, retires the old one, and drops every dismissal row in the same transaction. Outright replacement: there is no version keyring and no reencrypt walk, because a fingerprint is a keyed digest, not a ciphertext to preserve. Every stored fingerprint becomes unrecomputable under the new key, so keeping the dismissals would silently suppress warns that must now re-fire - dropping them is the safe direction. It rides `rotate-dek`, the same tier-3 rotation authority as `rotate-token-key`. x-hikyo-class: instance x-hikyo-operation: crypto.rotate-scanning-key x-hikyo-formula: [rotate-dek@instance] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: The rotation. content: application/json: schema: $ref: "#/components/schemas/ScanningKeyRotation" "401": $ref: "#/components/responses/Unauthenticated" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/Internal" /api/v1/orgs/{org}/projects/{project}/adapters: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ProjectID" get: operationId: listAdapters tags: [adapters] summary: List project deployment adapters with redacted credential metadata. x-hikyo-class: tenant x-hikyo-operation: adapter.inspect x-hikyo-formula: [manage-adapters@project] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": {description: Adapter list., content: {application/json: {schema: {$ref: "#/components/schemas/AdapterList"}}}} "401": {$ref: "#/components/responses/Unauthenticated"} "404": {$ref: "#/components/responses/NotFound"} "500": {$ref: "#/components/responses/Internal"} post: operationId: createAdapter tags: [adapters] summary: Atomically create a Forgejo adapter, credential, and first valid target. x-hikyo-class: tenant x-hikyo-operation: adapter.configure x-hikyo-formula: [manage-adapters@project] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 requestBody: {required: true, content: {application/json: {schema: {$ref: "#/components/schemas/CreateAdapterRequest"}}}} responses: "503": $ref: "#/components/responses/ServiceUnavailable" "201": {description: Fully valid adapter., content: {application/json: {schema: {$ref: "#/components/schemas/Adapter"}}}} "400": {$ref: "#/components/responses/BadRequest"} "401": {$ref: "#/components/responses/Unauthenticated"} "403": {$ref: "#/components/responses/Forbidden"} "404": {$ref: "#/components/responses/NotFound"} "409": {$ref: "#/components/responses/Conflict"} "500": {$ref: "#/components/responses/Internal"} /api/v1/orgs/{org}/projects/{project}/adapters/{adapter}: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ProjectID" - $ref: "#/components/parameters/AdapterID" get: operationId: showAdapter tags: [adapters] summary: Show one adapter and its targets; the credential is always redacted. x-hikyo-class: tenant x-hikyo-operation: adapter.inspect x-hikyo-formula: [manage-adapters@project] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": {description: Adapter., content: {application/json: {schema: {$ref: "#/components/schemas/Adapter"}}}} "401": {$ref: "#/components/responses/Unauthenticated"} "404": {$ref: "#/components/responses/NotFound"} "500": {$ref: "#/components/responses/Internal"} patch: operationId: updateAdapterOrigin tags: [adapters] summary: Replace the adapter origin after its old remote custody is resolved. x-hikyo-class: tenant x-hikyo-operation: adapter.configure x-hikyo-formula: [manage-adapters@project] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 requestBody: {required: true, content: {application/json: {schema: {$ref: "#/components/schemas/UpdateAdapterOriginRequest"}}}} responses: "503": $ref: "#/components/responses/ServiceUnavailable" "202": {description: Durable scrub-before-switch move., content: {application/json: {schema: {$ref: "#/components/schemas/AdapterMove"}}}} "400": {$ref: "#/components/responses/BadRequest"} "401": {$ref: "#/components/responses/Unauthenticated"} "403": {$ref: "#/components/responses/Forbidden"} "404": {$ref: "#/components/responses/NotFound"} "409": {$ref: "#/components/responses/Conflict"} "500": {$ref: "#/components/responses/Internal"} delete: operationId: deleteAdapter tags: [adapters] summary: Tombstone an adapter and scrub its remote names by default. x-hikyo-class: tenant x-hikyo-operation: adapter.delete x-hikyo-formula: [manage-adapters@project] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 parameters: - {name: keep_remote, in: query, required: false, schema: {type: boolean, default: false}} responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": {description: Teardown result., content: {application/json: {schema: {$ref: "#/components/schemas/AdapterTeardown"}}}} "401": {$ref: "#/components/responses/Unauthenticated"} "404": {$ref: "#/components/responses/NotFound"} "409": {$ref: "#/components/responses/Conflict"} "500": {$ref: "#/components/responses/Internal"} /api/v1/orgs/{org}/projects/{project}/adapter-moves/{move}: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ProjectID" - {name: move, in: path, required: true, schema: {$ref: "#/components/schemas/ID"}} get: operationId: showAdapterMove tags: [adapters] summary: Poll a durable adapter route move without exposing either credential. x-hikyo-class: tenant x-hikyo-operation: adapter.inspect x-hikyo-formula: [manage-adapters@project] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": {description: Move state., content: {application/json: {schema: {$ref: "#/components/schemas/AdapterMove"}}}} "401": {$ref: "#/components/responses/Unauthenticated"} "404": {$ref: "#/components/responses/NotFound"} "500": {$ref: "#/components/responses/Internal"} patch: operationId: resumeAdapterMove tags: [adapters] summary: Replace an attention-required pending route and resume activation. x-hikyo-class: tenant x-hikyo-operation: adapter.configure x-hikyo-formula: [manage-adapters@project] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 requestBody: {required: true, content: {application/json: {schema: {$ref: "#/components/schemas/ResumeAdapterMoveRequest"}}}} responses: "503": $ref: "#/components/responses/ServiceUnavailable" "202": {description: Updated move resumed., content: {application/json: {schema: {$ref: "#/components/schemas/AdapterMove"}}}} "400": {$ref: "#/components/responses/BadRequest"} "401": {$ref: "#/components/responses/Unauthenticated"} "403": {$ref: "#/components/responses/Forbidden"} "404": {$ref: "#/components/responses/NotFound"} "409": {$ref: "#/components/responses/Conflict"} "500": {$ref: "#/components/responses/Internal"} delete: operationId: cancelAdapterMove tags: [adapters] summary: Cancel an attention-required move and reconverge the old route. x-hikyo-class: tenant x-hikyo-operation: adapter.configure x-hikyo-formula: [manage-adapters@project] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "202": {description: Move canceled and old-route converge queued., content: {application/json: {schema: {$ref: "#/components/schemas/AdapterMove"}}}} "401": {$ref: "#/components/responses/Unauthenticated"} "403": {$ref: "#/components/responses/Forbidden"} "404": {$ref: "#/components/responses/NotFound"} "409": {$ref: "#/components/responses/Conflict"} "500": {$ref: "#/components/responses/Internal"} /api/v1/orgs/{org}/projects/{project}/adapters/{adapter}/credential: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ProjectID" - $ref: "#/components/parameters/AdapterID" put: operationId: setAdapterCredential tags: [adapters] summary: Replace the write-only Forgejo token. x-hikyo-class: tenant x-hikyo-operation: adapter.credential-set x-hikyo-formula: [manage-adapters@project] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 requestBody: {required: true, content: {application/json: {schema: {$ref: "#/components/schemas/SetAdapterCredentialRequest"}}}} responses: "503": $ref: "#/components/responses/ServiceUnavailable" "204": {description: Credential replaced. No job was enqueued.} "400": {$ref: "#/components/responses/BadRequest"} "401": {$ref: "#/components/responses/Unauthenticated"} "403": {$ref: "#/components/responses/Forbidden"} "404": {$ref: "#/components/responses/NotFound"} "409": {$ref: "#/components/responses/Conflict"} "500": {$ref: "#/components/responses/Internal"} delete: operationId: revokeAdapterCredential tags: [adapters] summary: Destroy outbound credential custody; remote scrub may then be impossible. x-hikyo-class: tenant x-hikyo-operation: adapter.credential-revoke x-hikyo-formula: [manage-adapters@project] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "204": {description: Credential revoked. No job was enqueued.} "401": {$ref: "#/components/responses/Unauthenticated"} "404": {$ref: "#/components/responses/NotFound"} "500": {$ref: "#/components/responses/Internal"} /api/v1/orgs/{org}/projects/{project}/adapters/{adapter}/targets: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ProjectID" - $ref: "#/components/parameters/AdapterID" get: operationId: listAdapterTargets tags: [adapters] summary: List targets nested under an adapter. x-hikyo-class: tenant x-hikyo-operation: adapter.inspect x-hikyo-formula: [manage-adapters@project] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": {description: Targets., content: {application/json: {schema: {$ref: "#/components/schemas/AdapterTargetList"}}}} "401": {$ref: "#/components/responses/Unauthenticated"} "404": {$ref: "#/components/responses/NotFound"} "500": {$ref: "#/components/responses/Internal"} post: operationId: addAdapterTarget tags: [adapters] summary: Add a tested target and atomically reassign adapter authority. x-hikyo-class: tenant x-hikyo-operation: adapter.configure x-hikyo-formula: [manage-adapters@project] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 requestBody: {required: true, content: {application/json: {schema: {$ref: "#/components/schemas/AdapterTargetInput"}}}} responses: "503": $ref: "#/components/responses/ServiceUnavailable" "201": {description: Target., content: {application/json: {schema: {$ref: "#/components/schemas/AdapterTarget"}}}} "400": {$ref: "#/components/responses/BadRequest"} "401": {$ref: "#/components/responses/Unauthenticated"} "403": {$ref: "#/components/responses/Forbidden"} "404": {$ref: "#/components/responses/NotFound"} "409": {$ref: "#/components/responses/Conflict"} "500": {$ref: "#/components/responses/Internal"} /api/v1/orgs/{org}/projects/{project}/adapter-targets/{target}: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ProjectID" - $ref: "#/components/parameters/AdapterTargetID" get: operationId: showAdapterTarget tags: [adapters] summary: Show target state, conflicts, mapping, or canonical workflow mapping. x-hikyo-class: tenant x-hikyo-operation: adapter.inspect x-hikyo-formula: [manage-adapters@project] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 parameters: - {name: format, in: query, required: false, schema: {type: string, enum: [detail, workflow], default: detail}} responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": description: Target detail or names-only workflow YAML. content: application/json: {schema: {$ref: "#/components/schemas/AdapterTargetDetail"}} text/plain: {schema: {type: string}} "401": {$ref: "#/components/responses/Unauthenticated"} "404": {$ref: "#/components/responses/NotFound"} "500": {$ref: "#/components/responses/Internal"} patch: operationId: updateAdapterTarget tags: [adapters] summary: Replace target routing fields and the complete key subset. x-hikyo-class: tenant x-hikyo-operation: adapter.configure x-hikyo-formula: [manage-adapters@project] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 requestBody: {required: true, content: {application/json: {schema: {$ref: "#/components/schemas/UpdateAdapterTargetRequest"}}}} responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": {description: In-place key subset or prefix update with replacement converge queued., content: {application/json: {schema: {$ref: "#/components/schemas/AdapterTarget"}}}} "202": {description: Durable scrub-before-switch move., content: {application/json: {schema: {$ref: "#/components/schemas/AdapterMove"}}}} "400": {$ref: "#/components/responses/BadRequest"} "401": {$ref: "#/components/responses/Unauthenticated"} "403": {$ref: "#/components/responses/Forbidden"} "404": {$ref: "#/components/responses/NotFound"} "409": {$ref: "#/components/responses/Conflict"} "500": {$ref: "#/components/responses/Internal"} delete: operationId: removeAdapterTarget tags: [adapters] summary: Tombstone a target and scrub its remote names by default. x-hikyo-class: tenant x-hikyo-operation: adapter.delete x-hikyo-formula: [manage-adapters@project] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 parameters: - {name: keep_remote, in: query, required: false, schema: {type: boolean, default: false}} responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": {description: Teardown result., content: {application/json: {schema: {$ref: "#/components/schemas/AdapterTeardown"}}}} "401": {$ref: "#/components/responses/Unauthenticated"} "404": {$ref: "#/components/responses/NotFound"} "409": {$ref: "#/components/responses/Conflict"} "500": {$ref: "#/components/responses/Internal"} /api/v1/orgs/{org}/projects/{project}/adapter-targets/{target}/plan: parameters: [{$ref: "#/components/parameters/OrgID"}, {$ref: "#/components/parameters/ProjectID"}, {$ref: "#/components/parameters/AdapterTargetID"}] post: operationId: planAdapterTarget tags: [adapters] summary: Produce a value-blind name plan and a conflict artifact when needed. x-hikyo-class: tenant x-hikyo-operation: adapter.plan x-hikyo-formula: [manage-adapters@project] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": {description: Plan., content: {application/json: {schema: {$ref: "#/components/schemas/AdapterPlan"}}}} "401": {$ref: "#/components/responses/Unauthenticated"} "404": {$ref: "#/components/responses/NotFound"} "409": {$ref: "#/components/responses/Conflict"} "500": {$ref: "#/components/responses/Internal"} /api/v1/orgs/{org}/projects/{project}/adapter-targets/{target}/sync: parameters: [{$ref: "#/components/parameters/OrgID"}, {$ref: "#/components/parameters/ProjectID"}, {$ref: "#/components/parameters/AdapterTargetID"}] post: operationId: syncAdapterTarget tags: [adapters] summary: Enqueue a newest-wins manual converge. x-hikyo-class: tenant x-hikyo-operation: adapter.sync x-hikyo-formula: [manage-adapters@project] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "202": {description: Queued job., content: {application/json: {schema: {$ref: "#/components/schemas/AdapterJob"}}}} "401": {$ref: "#/components/responses/Unauthenticated"} "403": {$ref: "#/components/responses/Forbidden"} "404": {$ref: "#/components/responses/NotFound"} "409": {$ref: "#/components/responses/Conflict"} "500": {$ref: "#/components/responses/Internal"} /api/v1/orgs/{org}/projects/{project}/adapter-targets/{target}/pause: parameters: [{$ref: "#/components/parameters/OrgID"}, {$ref: "#/components/parameters/ProjectID"}, {$ref: "#/components/parameters/AdapterTargetID"}] post: operationId: pauseAdapterTarget tags: [adapters] summary: Stop every push for the target without releasing owned destination state. description: | Pause supersedes the target's active job and fences any worker still inside it; the ownership ledger is retained, so resume needs no re-adoption. Publishes skip a paused target and a manual sync is refused with 409. Pause narrows what Hikyo does at the destination, so it carries `manage-adapters` alone and no reauthentication ceremony. Idempotent. x-hikyo-class: tenant x-hikyo-operation: adapter.configure x-hikyo-formula: [manage-adapters@project] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": {description: The paused target., content: {application/json: {schema: {$ref: "#/components/schemas/AdapterTarget"}}}} "401": {$ref: "#/components/responses/Unauthenticated"} "403": {$ref: "#/components/responses/Forbidden"} "404": {$ref: "#/components/responses/NotFound"} "409": {$ref: "#/components/responses/Conflict"} "500": {$ref: "#/components/responses/Internal"} /api/v1/orgs/{org}/projects/{project}/adapter-targets/{target}/resume: parameters: [{$ref: "#/components/parameters/OrgID"}, {$ref: "#/components/parameters/ProjectID"}, {$ref: "#/components/parameters/AdapterTargetID"}] post: operationId: resumeAdapterTarget tags: [adapters] summary: Resume a paused target and queue one catch-up converge. description: | Resume pushes again, so it carries the manual-sync formula and the adapter reauthentication ceremony over the adapter's environment set. The response names the published revision the queued converge will reach. A target that is not paused is refused with 409. x-hikyo-class: tenant x-hikyo-operation: adapter.sync x-hikyo-formula: [manage-adapters@project] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "202": {description: Queued catch-up converge., content: {application/json: {schema: {$ref: "#/components/schemas/AdapterResume"}}}} "401": {$ref: "#/components/responses/Unauthenticated"} "403": {$ref: "#/components/responses/Forbidden"} "404": {$ref: "#/components/responses/NotFound"} "409": {$ref: "#/components/responses/Conflict"} "500": {$ref: "#/components/responses/Internal"} /api/v1/orgs/{org}/projects/{project}/adapter-targets/{target}/test: parameters: [{$ref: "#/components/parameters/OrgID"}, {$ref: "#/components/parameters/ProjectID"}, {$ref: "#/components/parameters/AdapterTargetID"}] post: operationId: testAdapterTarget tags: [adapters] summary: Probe Forgejo version and destination identity without values or writes. x-hikyo-class: tenant x-hikyo-operation: adapter.test x-hikyo-formula: [manage-adapters@project] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": {description: Connection result., content: {application/json: {schema: {$ref: "#/components/schemas/AdapterConnection"}}}} "401": {$ref: "#/components/responses/Unauthenticated"} "404": {$ref: "#/components/responses/NotFound"} "500": {$ref: "#/components/responses/Internal"} /api/v1/orgs/{org}/projects/{project}/adapter-targets/{target}/adoptions: parameters: [{$ref: "#/components/parameters/OrgID"}, {$ref: "#/components/parameters/ProjectID"}, {$ref: "#/components/parameters/AdapterTargetID"}] post: operationId: adoptAdapterTargetNames tags: [adapters] summary: Adopt an exact enumerated conflict artifact and enqueue convergence. x-hikyo-class: tenant x-hikyo-operation: adapter.adopt x-hikyo-formula: [manage-adapters@project] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 requestBody: {required: true, content: {application/json: {schema: {$ref: "#/components/schemas/AdapterAdoptionRequest"}}}} responses: "503": $ref: "#/components/responses/ServiceUnavailable" "202": {description: Adoption committed and converge queued., content: {application/json: {schema: {$ref: "#/components/schemas/AdapterJob"}}}} "400": {$ref: "#/components/responses/BadRequest"} "401": {$ref: "#/components/responses/Unauthenticated"} "403": {$ref: "#/components/responses/Forbidden"} "404": {$ref: "#/components/responses/NotFound"} "409": {$ref: "#/components/responses/Conflict"} "500": {$ref: "#/components/responses/Internal"} /api/v1/orgs/{org}/projects/{project}/dynamic-providers: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ProjectID" get: operationId: listDynamicProviders tags: [dynamic] summary: List dynamic-secret providers with redacted credential metadata. x-hikyo-class: tenant x-hikyo-operation: dynamic-provider.inspect x-hikyo-formula: [manage-identities@project] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": {description: Provider list., content: {application/json: {schema: {$ref: "#/components/schemas/DynamicProviderList"}}}} "401": {$ref: "#/components/responses/Unauthenticated"} "404": {$ref: "#/components/responses/NotFound"} "500": {$ref: "#/components/responses/Internal"} post: operationId: createDynamicProvider tags: [dynamic] summary: Configure a PostgreSQL provider; the admin credential is write-only. x-hikyo-class: tenant x-hikyo-operation: dynamic-provider.configure x-hikyo-formula: [manage-identities@project] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 requestBody: {required: true, content: {application/json: {schema: {$ref: "#/components/schemas/CreateDynamicProviderRequest"}}}} responses: "503": $ref: "#/components/responses/ServiceUnavailable" "201": {description: Provider configured and reachable., content: {application/json: {schema: {$ref: "#/components/schemas/DynamicProvider"}}}} "400": {$ref: "#/components/responses/BadRequest"} "401": {$ref: "#/components/responses/Unauthenticated"} "404": {$ref: "#/components/responses/NotFound"} "409": {$ref: "#/components/responses/Conflict"} "500": {$ref: "#/components/responses/Internal"} /api/v1/orgs/{org}/projects/{project}/dynamic-providers/{provider}: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ProjectID" - $ref: "#/components/parameters/DynamicProviderID" get: operationId: showDynamicProvider tags: [dynamic] summary: Show one provider's status and metadata. x-hikyo-class: tenant x-hikyo-operation: dynamic-provider.inspect x-hikyo-formula: [manage-identities@project] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": {description: Provider., content: {application/json: {schema: {$ref: "#/components/schemas/DynamicProvider"}}}} "401": {$ref: "#/components/responses/Unauthenticated"} "404": {$ref: "#/components/responses/NotFound"} "500": {$ref: "#/components/responses/Internal"} delete: operationId: deleteDynamicProvider tags: [dynamic] summary: Delete a provider; refuses while active leases exist unless revoke_all. x-hikyo-class: tenant x-hikyo-operation: dynamic-provider.delete x-hikyo-formula: [manage-identities@project] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 parameters: - name: revoke_all in: query required: false description: Revoke every live lease of this provider before deleting it. schema: {type: boolean, default: false} responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": {description: Provider deleted; any leases queued for revocation., content: {application/json: {schema: {$ref: "#/components/schemas/DynamicProviderDeletion"}}}} "401": {$ref: "#/components/responses/Unauthenticated"} "404": {$ref: "#/components/responses/NotFound"} "409": {$ref: "#/components/responses/Conflict"} "500": {$ref: "#/components/responses/Internal"} /api/v1/orgs/{org}/projects/{project}/dynamic-providers/{provider}/credential: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ProjectID" - $ref: "#/components/parameters/DynamicProviderID" put: operationId: setDynamicProviderCredential tags: [dynamic] summary: Replace the write-only admin credential. x-hikyo-class: tenant x-hikyo-operation: dynamic-provider.credential-set x-hikyo-formula: [manage-identities@project] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 requestBody: {required: true, content: {application/json: {schema: {$ref: "#/components/schemas/SetDynamicProviderCredentialRequest"}}}} responses: "503": $ref: "#/components/responses/ServiceUnavailable" "204": {description: Credential replaced.} "400": {$ref: "#/components/responses/BadRequest"} "401": {$ref: "#/components/responses/Unauthenticated"} "404": {$ref: "#/components/responses/NotFound"} "409": {$ref: "#/components/responses/Conflict"} "500": {$ref: "#/components/responses/Internal"} delete: operationId: revokeDynamicProviderCredential tags: [dynamic] summary: Destroy admin custody; renewal and revocation may then be impossible. x-hikyo-class: tenant x-hikyo-operation: dynamic-provider.credential-revoke x-hikyo-formula: [manage-identities@project] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "204": {description: Credential revoked.} "401": {$ref: "#/components/responses/Unauthenticated"} "404": {$ref: "#/components/responses/NotFound"} "500": {$ref: "#/components/responses/Internal"} /api/v1/orgs/{org}/projects/{project}/environments/{environment}/leases: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ProjectID" - $ref: "#/components/parameters/EnvironmentID" get: operationId: listLeases tags: [dynamic] summary: List an environment's leases with status and metadata, never the secret. x-hikyo-class: tenant x-hikyo-operation: lease.inspect x-hikyo-formula: [read@environment] x-hikyo-artifacts: [human-session, machine-credential] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": {description: Lease list., content: {application/json: {schema: {$ref: "#/components/schemas/DynamicLeaseList"}}}} "401": {$ref: "#/components/responses/Unauthenticated"} "404": {$ref: "#/components/responses/NotFound"} "500": {$ref: "#/components/responses/Internal"} post: operationId: mintLease tags: [dynamic] summary: Mint a short-lived credential; the secret is disclosed exactly once. description: | Display-once: the password is in this response and nowhere else, ever. A retry mints a NEW lease with a NEW secret; the old secret is never returned again. A machine principal mints under the project's machine-reveal opt-in; a human mint needs the mint reauthentication ceremony. x-hikyo-class: tenant x-hikyo-operation: lease.mint x-hikyo-formula: [read@environment] x-hikyo-artifacts: [human-session, machine-credential] x-hikyo-min-revision: 1 requestBody: {required: true, content: {application/json: {schema: {$ref: "#/components/schemas/MintLeaseRequest"}}}} responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": {description: The lease and its display-once secret., content: {application/json: {schema: {$ref: "#/components/schemas/MintLeaseResult"}}}} "400": {$ref: "#/components/responses/BadRequest"} "401": {$ref: "#/components/responses/Unauthenticated"} "403": {$ref: "#/components/responses/Forbidden"} "404": {$ref: "#/components/responses/NotFound"} "409": {$ref: "#/components/responses/Conflict"} "500": {$ref: "#/components/responses/Internal"} /api/v1/orgs/{org}/projects/{project}/environments/{environment}/leases/{lease}: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ProjectID" - $ref: "#/components/parameters/EnvironmentID" - $ref: "#/components/parameters/LeaseID" get: operationId: showLease tags: [dynamic] summary: Show one lease's status and metadata, never the secret. x-hikyo-class: tenant x-hikyo-operation: lease.inspect x-hikyo-formula: [read@environment] x-hikyo-artifacts: [human-session, machine-credential] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": {description: Lease., content: {application/json: {schema: {$ref: "#/components/schemas/DynamicLease"}}}} "401": {$ref: "#/components/responses/Unauthenticated"} "404": {$ref: "#/components/responses/NotFound"} "500": {$ref: "#/components/responses/Internal"} /api/v1/orgs/{org}/projects/{project}/environments/{environment}/leases/{lease}/renew: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ProjectID" - $ref: "#/components/parameters/EnvironmentID" - $ref: "#/components/parameters/LeaseID" post: operationId: renewLease tags: [dynamic] summary: Extend a lease's validity, bounded by its maximum TTL. x-hikyo-class: tenant x-hikyo-operation: lease.renew x-hikyo-formula: [read@environment] x-hikyo-artifacts: [human-session, machine-credential] x-hikyo-min-revision: 1 requestBody: {required: false, content: {application/json: {schema: {$ref: "#/components/schemas/RenewLeaseRequest"}}}} responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": {description: Lease after renewal., content: {application/json: {schema: {$ref: "#/components/schemas/DynamicLease"}}}} "400": {$ref: "#/components/responses/BadRequest"} "401": {$ref: "#/components/responses/Unauthenticated"} "404": {$ref: "#/components/responses/NotFound"} "409": {$ref: "#/components/responses/Conflict"} "500": {$ref: "#/components/responses/Internal"} /api/v1/orgs/{org}/projects/{project}/environments/{environment}/leases/{lease}/revoke: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ProjectID" - $ref: "#/components/parameters/EnvironmentID" - $ref: "#/components/parameters/LeaseID" post: operationId: revokeLease tags: [dynamic] summary: Revoke a lease; idempotent, an already-gone role is a success. x-hikyo-class: tenant x-hikyo-operation: lease.revoke x-hikyo-formula: [read@environment] x-hikyo-artifacts: [human-session, machine-credential] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": {description: Lease after revocation was queued., content: {application/json: {schema: {$ref: "#/components/schemas/DynamicLease"}}}} "401": {$ref: "#/components/responses/Unauthenticated"} "404": {$ref: "#/components/responses/NotFound"} "500": {$ref: "#/components/responses/Internal"} /api/v1/orgs/{org}/projects/{project}/environments/{environment}/leases/{lease}/settle: parameters: - $ref: "#/components/parameters/OrgID" - $ref: "#/components/parameters/ProjectID" - $ref: "#/components/parameters/EnvironmentID" - $ref: "#/components/parameters/LeaseID" post: operationId: settleLease tags: [dynamic] summary: Settle an uncertain lease by re-probing it at the provider. x-hikyo-class: tenant x-hikyo-operation: lease.settle x-hikyo-formula: [read@environment] x-hikyo-artifacts: [human-session] x-hikyo-min-revision: 1 responses: "503": $ref: "#/components/responses/ServiceUnavailable" "200": {description: Lease after settlement., content: {application/json: {schema: {$ref: "#/components/schemas/DynamicLease"}}}} "401": {$ref: "#/components/responses/Unauthenticated"} "404": {$ref: "#/components/responses/NotFound"} "409": {$ref: "#/components/responses/Conflict"} "500": {$ref: "#/components/responses/Internal"} components: securitySchemes: hikyoSession: type: http scheme: bearer bearerFormat: hik_1_cli_... description: | A CLI session artifact minted by an identity-protocol endpoint. The server trusts nothing inside the value: kind, principal, expiry and credential epoch are read from the row the verifier resolves to. parameters: ChallengeId: name: challenge in: path required: true description: Login-challenge identifier returned by `localLogin` (202). schema: $ref: "#/components/schemas/ID" OrgID: name: org in: path required: true description: Organisation identifier. schema: $ref: "#/components/schemas/ID" ProjectID: name: project in: path required: true description: Project identifier. schema: $ref: "#/components/schemas/ID" EnvironmentID: name: environment in: path required: true description: Environment identifier. schema: $ref: "#/components/schemas/ID" AdapterID: name: adapter in: path required: true schema: {$ref: "#/components/schemas/ID"} AdapterTargetID: name: target in: path required: true schema: {$ref: "#/components/schemas/ID"} DynamicProviderID: name: provider in: path required: true schema: {$ref: "#/components/schemas/ID"} LeaseID: name: lease in: path required: true schema: {$ref: "#/components/schemas/ID"} FolderID: name: folder in: path required: true description: Folder identifier. schema: $ref: "#/components/schemas/ID" ServiceAccountID: name: serviceAccount in: path required: true description: Service-account identifier. schema: $ref: "#/components/schemas/ID" CredentialID: name: credential in: path required: true description: Machine-credential identifier. schema: $ref: "#/components/schemas/ID" GrantPrincipal: name: principal in: query required: true description: The principal whose grant is being revoked. schema: $ref: "#/components/schemas/ID" GrantCapability: name: capability in: query required: true description: The capability atom being revoked. schema: $ref: "#/components/schemas/Capability" KeyID: name: key in: path required: true description: Key identifier - the immutable id, never the mutable name. schema: $ref: "#/components/schemas/ID" DefinitionsPlanID: name: plan in: path required: true description: Immutable definitions-plan identifier. schema: $ref: "#/components/schemas/ID" ValueKeyName: name: key in: path required: true description: | The key's NAME, not its id. Values are addressed the way an operator holds them - `values set DATABASE_URL` - and the id is server vocabulary that appears only in responses and audit records. schema: $ref: "#/components/schemas/KeyName" KeyGroupID: name: group in: path required: true description: Key group identifier. schema: $ref: "#/components/schemas/ID" ProviderSlug: name: provider in: path required: true description: Identity-provider slug. schema: type: string minLength: 1 maxLength: 64 pattern: "^[a-z0-9][a-z0-9-]{0,63}$" ProviderSlugPath: name: slug in: path required: true description: Identity-provider slug. schema: type: string minLength: 1 maxLength: 64 pattern: "^[a-z0-9][a-z0-9-]{0,63}$" IdentityID: name: id in: path required: true description: External identity identifier. schema: $ref: "#/components/schemas/ID" WebauthnCredentialID: name: id in: path required: true description: Enrolled passkey credential identifier. schema: $ref: "#/components/schemas/ID" ResetTargetPrincipal: name: principal in: path required: true description: The target principal whose credential is being reset. schema: $ref: "#/components/schemas/ID" FederationIssuerID: name: issuer in: path required: true description: | Federation issuer identifier. It is the server-minted id, not the issuer URL: a URL in a path segment would have to be escaped, and an escaping rule on a byte-exact identity is a canonicalization step waiting to happen. schema: $ref: "#/components/schemas/ID" DeliveryCursor: name: cursor in: query required: false description: | The opaque cursor a previous fetch returned. Absent means a full authorized delivery. A cursor is never parsed by the caller and never constructed by one: the server recomputes it for the state it is about to serve and compares. schema: type: string maxLength: 128 DeliveryProjection: name: projection in: query required: false description: | A SERVER-SIDE AUTHORIZED TERM, never a client-side filter (k8s ADR § Refresh). `config-only` omits `secret` keys from the delivery entirely — not presence-only, gone — and from the manifest the change token is computed over, and the mode is bound into the conditional-fetch cursor so a projection change invalidates it. `full` (the default) delivers every key the caller is authorized to see. schema: type: string enum: [full, config-only] default: full DefinitionsAcknowledgements: name: acknowledge in: query required: false style: form explode: false description: | Secret-scanning override token(s) from a prior `definitions plan` refusal (#74 SS3). One content-bound token per finding, re-scanned against the bundle's leaves; a stale, version-skewed, or surplus token is rejected by name. The plan request body is the canonical bundle bytes, so the tokens ride the query rather than the body — the same exposure surface as the CLI `--acknowledge` flag that carries them. Comma-separated. There is no blanket ignore-all input. schema: type: array maxItems: 100 items: {type: string} DeliveryAcknowledgedKeys: name: acknowledged_keys in: query required: false style: form explode: false description: | The loader-control keys the consumer explicitly acknowledges, so the server's fetch audit record carries which acknowledgement was in force for this delivery (k8s ADR § Loader-control). The server RECORDS it and otherwise ignores it — it filters nothing and refuses nothing; the loader-control refusal is enforced by the operator against its mapping, which the server cannot see. Comma-separated, each item under the key grammar, at most 64. schema: type: array maxItems: 64 items: $ref: "#/components/schemas/KeyName" ScimBindingID: name: binding in: path required: true description: SCIM binding identifier. schema: $ref: "#/components/schemas/ID" ScimCount: name: count in: query required: false description: | Desired page size. It is a REQUEST: a value above this provider's bound is answered with the bound rather than refused. Typed `string` and unconstrained for the same reason as `startIndex`. schema: type: string ScimFilter: name: filter in: query required: false description: | An RFC 7644 filter, closed to `attribute eq "value"` over the four probes Okta and Entra actually issue. Anything else is `invalidFilter`. Deliberately unconstrained HERE. Contract validation runs BEFORE the provisioning credential is authenticated, so any constraint declared on a SCIM wire parameter answers an unauthenticated caller with a Hikyo 400 instead of the uniform 401 — telling them something about their request before they have proved they may ask. Every bound and every grammar refusal for this parameter lives in the protocol layer, after authentication, where it is rendered as an RFC 7644 error. schema: type: string ScimResourceID: name: id in: path required: true description: | A server-minted resource identifier. The identity provider can only reference ids this server minted. schema: $ref: "#/components/schemas/ID" ScimSortBy: name: sortBy in: query required: false description: | Present only so its presence can be REFUSED by name with 501. Sorting is advertised absent in ServiceProviderConfig and is not half-implemented. Unconstrained: see `filter`. schema: type: string ScimSortOrder: name: sortOrder in: query required: false description: | Present only so its presence can be REFUSED by name with 501, exactly like `sortBy`. Refusing one and ignoring the other would be the half-implementation the ADR forbids. Unconstrained: see `filter`. schema: type: string ScimStartIndex: name: startIndex in: query required: false description: | 1-based index of the first result; a value below 1 is read as 1, per RFC 7644 §3.4.2.4. Typed `string`, not `integer`, and carrying no `minimum`: see the note on `filter`. `minimum: 1` made `startIndex=0` a pre-authentication 400, which is both an unauthenticated refusal and a contradiction of the RFC's own "read as 1". Parsing and clamping happen in the protocol layer after authentication. schema: type: string RemoteName: name: remote in: path required: true description: | The entry's display name. A NAME, never a URL or host: an endpoint whose target is named by the request is a proxy whatever it is called, and the server looks the entry up so the stored, immutable URL is what is reached. schema: $ref: "#/components/schemas/EntityName" ConnectionID: name: connection in: path required: true description: Connection credential identifier. schema: $ref: "#/components/schemas/ID" SessionID: name: session in: path required: true description: Session identifier, always one of the caller's own. schema: $ref: "#/components/schemas/ID" InstanceUpdateJobID: name: job in: path required: true description: Durable updater-helper job identifier. schema: $ref: "#/components/schemas/ID" AuditFrom: name: from in: query required: false description: Inclusive lower time bound (recorded-at). Absent means the epoch. schema: type: string format: date-time AuditTo: name: to in: query required: false description: Inclusive upper time bound (recorded-at). Absent means unbounded. schema: type: string format: date-time AuditAfterSeq: name: after_seq in: query required: false description: | Resume cursor: return events whose seq is strictly greater. Pass the previous page's `next_after_seq` to page forward. schema: type: integer format: int64 minimum: 0 AuditToSeq: name: to_seq in: query required: false description: | The session ceiling. Absent on the first page (the server pins it to the trail's current head and returns it as `upper_seq`); pass that value back on every later page so paging terminates and never chases the `audit.query` events the reads themselves append. schema: type: integer format: int64 minimum: 0 AuditLimit: name: limit in: query required: false description: | Maximum rows SCANNED for this page (clamped to 1000). Matched rows may be fewer when a filter is set. schema: type: integer minimum: 1 maximum: 1000 default: 100 RevisionBefore: name: before in: query required: false description: | Page cursor, exclusive: return revisions strictly below this number. Omit it for the first page (the newest revisions); pass the previous page's `next_before` to continue. schema: type: integer format: int64 minimum: 1 RevisionLimit: name: limit in: query required: false description: Maximum revisions returned on this page. schema: type: integer minimum: 1 maximum: 500 default: 100 AuditActor: name: actor in: query required: false description: Match only events whose acting principal has this id (exact). schema: type: string maxLength: 64 AuditActorName: name: actor_name in: query required: false description: | Match only events whose acting principal's display name (or username) matches this pattern. Supports `*` wildcards (e.g. `alice*`, `*bot`). Names are resolved only for principals that already appear in the trail you are authorized to read; this filter narrows that view and performs no directory lookup, so it discloses nothing you cannot already see. schema: type: string maxLength: 128 AuditOperation: name: operation in: query required: false description: | Match only events of this type (the operation), e.g. `value.set`. Supports `*` wildcards (e.g. `value.*`). An unknown or unmatched pattern is not an error; it simply matches nothing. schema: type: string maxLength: 128 AuditOutcome: name: outcome in: query required: false description: | Match only events with this outcome. To match several outcomes, use the repeatable `outcomes` parameter; both may be given and are unioned. schema: type: string enum: [intent, success, denied, failure, unknown, disconnected] AuditOutcomes: name: outcomes in: query required: false description: | Match events with any of these outcomes (set membership). Repeat the parameter to select several, e.g. `outcomes=denied&outcomes=failure`. Unioned with the singular `outcome` when both are present. schema: type: array items: type: string enum: [intent, success, denied, failure, unknown, disconnected] explode: true style: form AuditObjectType: name: object_type in: query required: false description: | Match only events acting on this object type. Supports `*` wildcards (e.g. `secret*`). schema: type: string maxLength: 64 AuditObjectID: name: object_id in: query required: false description: | Match only events acting on this object id. Supports `*` wildcards. schema: type: string maxLength: 64 AuditCorrelationID: name: correlation_id in: query required: false description: | Match only events sharing this correlation id — the link between an act's INTENT and OUTCOME events. schema: type: string maxLength: 64 responses: BadRequest: description: | The request does not satisfy this document. Decided before any tenant resolution, so `detail` leaks nothing about tenancy — it is the only error response permitted to carry one. content: application/json: schema: $ref: "#/components/schemas/Error" Unauthenticated: description: | No usable authentication artifact was presented. Uniform: absent, malformed, unknown, expired, revoked and epoch-superseded artifacts are indistinguishable. content: application/json: schema: $ref: "#/components/schemas/Error" Forbidden: description: | Either the principal does not hold the operation's formula at instance scope — instance-class operations have no tenant object whose nonexistence could be mimicked, so the probe contract there is grant refusal, not tenancy — or the principal DOES hold it and the acting session's assurance is inadequate for an MFA-mandatory operation. The second case is why two tenant-scoped operations (`renameOrg`, `deleteOrg`) declare this status: their formula atom `instance-config` is MFA-mandatory, and the refusal fires only AFTER the grant check succeeded. A caller who reaches it can already reach the object, so naming the step-up discloses nothing the uniform 404 was protecting — and hiding it would tell a capability holder the object is missing. Grant refusal on a tenant-scoped operation is always the 404. content: application/json: schema: $ref: "#/components/schemas/Error" NotFound: description: | The addressed object does not exist **or** the principal may not reach it — indistinguishable by design, byte-identical in status and body. content: application/json: schema: $ref: "#/components/schemas/Error" Conflict: description: | The caller is authorized, but the current state refuses: a name already in use among live siblings, a parent that still has children (deletes never cascade), or a structural bound reached (`limit_exceeded`, whose message names the bound). Decided after authorization, so it discloses nothing a caller could not already read. content: application/json: schema: $ref: "#/components/schemas/Error" UnprocessableContent: description: | The caller is authorized and the request is well formed, but a value is outside the vocabulary this server accepts for it. `detail` names the member, never its value. Decided after authorization. content: application/json: schema: $ref: "#/components/schemas/Error" PayloadTooLarge: description: | The request body exceeds this operation's bound. The body is not parsed, so the refusal is tied to nothing it carries. Decided after authorization. content: application/json: schema: $ref: "#/components/schemas/Error" TooManyRequests: description: | The instance-wide admission budget or a per-source limit is exhausted. Uniform on every path, with no unbounded work performed. headers: Retry-After: required: true description: Whole seconds to wait before retrying. schema: type: integer minimum: 1 content: application/json: schema: $ref: "#/components/schemas/Error" ServiceUnavailable: description: The owner is temporarily unable to serve this operation while configuration converges. headers: Retry-After: required: true schema: {type: integer, minimum: 1} content: application/json: schema: $ref: "#/components/schemas/Error" ScimServiceUnavailable: description: Configuration is converging. Retry after the indicated interval. headers: Retry-After: required: true schema: {type: integer, minimum: 1} content: application/scim+json: schema: $ref: "#/components/schemas/ScimResource" Internal: description: An unexpected server fault. The cause is logged, never returned. content: application/json: schema: $ref: "#/components/schemas/Error" schemas: RuntimeStatus: type: object additionalProperties: false required: [state, phase] properties: state: type: string enum: [ready, maintenance, recovery-required] phase: type: [string, "null"] enum: [preparing, backup, restore-check, migration, health-check, null] AccountProfile: type: object required: [username, display_name, email, email_verified, managed, username_editable] properties: username: type: string display_name: type: string email: type: [string, "null"] description: The account email in canonical form (domain lowercased), or null when the account has none. Read-only. See email_verified for what it is used for. Never an identity linking key. email_verified: type: boolean description: True when email is the verified sign-in email, set only by verified local sign-up. False when email is null or is a contact address kept from before sign-in email existed; such an address is display-only and is never used to sign in. managed: type: boolean description: SCIM controls the username and display name. username_editable: type: boolean description: A local password or confirmed authenticator can prove a username change. UpdateAccountProfileRequest: type: object additionalProperties: false required: [username, display_name] properties: username: type: string minLength: 1 maxLength: 256 display_name: type: string maxLength: 256 proof: type: string ID: type: string description: A prefixed UUIDv7, e.g. `org_0198…`. minLength: 3 maxLength: 64 pattern: "^[a-z]{2,8}_[0-9a-fA-F-]{36}$" Timestamp: type: string format: date-time description: RFC 3339 UTC, microsecond precision. AdapterDestinationKind: type: string enum: [repository, organization, environment] AdapterVisibility: type: string enum: [all, private, selected] AdapterProvider: type: string description: >- Open provider discriminator. Clients preserve unknown response values; creation requires a provider supported by the receiving server. sealed-webhook binds only to an origin an instance admin activated in the server's sealed webhook registry. x-extensible-enum: [forgejo, github-actions, sealed-webhook] AdapterTargetInput: type: object additionalProperties: false required: [environment_id, destination_kind, destination_owner, destination_name, destination_environment, visibility, selected_repository_ids, name_prefix, key_ids] properties: allow_environment_create: type: boolean default: false description: Explicit consent to create missing GitHub environments using Administration:write. environment_id: {$ref: "#/components/schemas/ID"} destination_kind: {$ref: "#/components/schemas/AdapterDestinationKind"} destination_owner: {type: string, minLength: 1, maxLength: 255} destination_name: type: string maxLength: 255 description: Repository name; empty for organization destinations. destination_environment: type: string maxLength: 255 description: GitHub environment name; empty for repository and organization destinations. visibility: type: string enum: ["", all, private, selected] description: GitHub organization recipient visibility; empty for other destinations. selected_repository_ids: type: array maxItems: 500 uniqueItems: true items: {type: integer, format: int64, minimum: 1} name_prefix: {type: string, maxLength: 64, pattern: "^(?:[A-Z_][A-Z0-9_]*)?$"} key_ids: type: array minItems: 0 maxItems: 512 uniqueItems: true items: {$ref: "#/components/schemas/ID"} description: >- Explicit member key ids. May be empty only when `key_selection` resolves to at least one key; the union must be non-empty. key_selection: {$ref: "#/components/schemas/AdapterKeySelection"} AdapterKeySelection: type: object additionalProperties: false description: | Configuration-time selection conveniences (#157). They are resolved against the project catalogue when the target is saved, the resolved key ids are stored as the explicit subset, and the patterns are NOT kept: a key created later that would have matched is never added. `names` are exact key names; `include` and `exclude` are bounded glob patterns (`*`, `?`, `[...]`) over key names; `classification` keeps only keys of that classification. `exclude` applies to what the patterns and classification selected, never to explicit ids or names. properties: names: type: array maxItems: 512 uniqueItems: true items: {type: string, minLength: 1, maxLength: 128} include: type: array maxItems: 32 uniqueItems: true items: {type: string, minLength: 1, maxLength: 64} exclude: type: array maxItems: 32 uniqueItems: true items: {type: string, minLength: 1, maxLength: 64} classification: {type: string, enum: ["", secret, config]} CreateAdapterRequest: type: object additionalProperties: false required: [provider, origin, credential, target] properties: provider: {$ref: "#/components/schemas/AdapterProvider"} origin: {type: string, format: uri, maxLength: 2048} credential: type: string minLength: 1 maxLength: 4096 description: Write-only provider credential. Never returned. target: {$ref: "#/components/schemas/AdapterTargetInput"} UpdateAdapterOriginRequest: type: object additionalProperties: false required: [origin, credential] properties: origin: {type: string, format: uri, maxLength: 2048} credential: {type: string, minLength: 1, maxLength: 4096, description: Write-only credential for the pending origin.} keep_remote: {type: boolean, default: false} SetAdapterCredentialRequest: type: object additionalProperties: false required: [credential] properties: credential: {type: string, minLength: 1, maxLength: 4096} UpdateAdapterTargetRequest: type: object additionalProperties: false required: [environment_id, destination_kind, destination_owner, destination_name, destination_environment, visibility, selected_repository_ids, name_prefix, key_ids, expected_generation] properties: environment_id: {$ref: "#/components/schemas/ID"} destination_kind: {$ref: "#/components/schemas/AdapterDestinationKind"} destination_owner: {type: string, minLength: 1, maxLength: 255} destination_name: {type: string, maxLength: 255} destination_environment: {type: string, maxLength: 255} visibility: {type: string, enum: ["", all, private, selected]} selected_repository_ids: type: array maxItems: 500 uniqueItems: true items: {type: integer, format: int64, minimum: 1} name_prefix: {type: string, maxLength: 64, pattern: "^(?:[A-Z_][A-Z0-9_]*)?$"} key_ids: type: array minItems: 0 maxItems: 512 uniqueItems: true items: {$ref: "#/components/schemas/ID"} key_selection: {$ref: "#/components/schemas/AdapterKeySelection"} expected_generation: {type: integer, format: int64, minimum: 1} keep_remote: {type: boolean, default: false} ResumeAdapterMoveRequest: oneOf: - $ref: "#/components/schemas/ResumeAdapterOriginMoveRequest" - $ref: "#/components/schemas/ResumeAdapterTargetMoveRequest" ResumeAdapterOriginMoveRequest: type: object additionalProperties: false required: [origin, credential] properties: origin: {type: string, format: uri, maxLength: 2048} credential: {type: string, minLength: 1, maxLength: 4096, description: Write-only replacement for the pending origin credential.} ResumeAdapterTargetMoveRequest: type: object additionalProperties: false required: [target_id, environment_id, destination_kind, destination_owner, destination_name, destination_environment, visibility, selected_repository_ids, name_prefix, key_ids] properties: target_id: {$ref: "#/components/schemas/ID"} environment_id: {$ref: "#/components/schemas/ID"} destination_kind: {$ref: "#/components/schemas/AdapterDestinationKind"} destination_owner: {type: string, minLength: 1, maxLength: 255} destination_name: {type: string, maxLength: 255} destination_environment: {type: string, maxLength: 255} visibility: {type: string, enum: ["", all, private, selected]} selected_repository_ids: type: array maxItems: 500 uniqueItems: true items: {type: integer, format: int64, minimum: 1} name_prefix: {type: string, maxLength: 64, pattern: "^(?:[A-Z_][A-Z0-9_]*)?$"} key_ids: type: array minItems: 1 maxItems: 512 uniqueItems: true items: {$ref: "#/components/schemas/ID"} AdapterTarget: type: object additionalProperties: false required: [id, adapter_id, environment_id, destination_kind, destination_owner, destination_name, destination_environment, destination_id, repository_id, visibility, selected_repository_ids, name_prefix, generation, state, sync_status, converged_revision, last_attempted_revision, last_attempted_at, last_error_class, retry_at, paused_at, drift_attention, failure_names, warnings, keys, conflicts] properties: findings: type: array items: {$ref: "#/components/schemas/AdapterFinding"} id: {$ref: "#/components/schemas/ID"} adapter_id: {$ref: "#/components/schemas/ID"} environment_id: {$ref: "#/components/schemas/ID"} destination_kind: {$ref: "#/components/schemas/AdapterDestinationKind"} destination_owner: {type: string} destination_name: {type: string} destination_environment: {type: string} destination_id: {type: integer, format: int64, minimum: 1} repository_id: {type: integer, format: int64, minimum: 0} visibility: {type: string, enum: ["", all, private, selected]} selected_repository_ids: type: array items: {type: integer, format: int64, minimum: 1} name_prefix: {type: string} generation: {type: integer, format: int64, minimum: 1} state: {type: string, enum: [active, moving, tombstoned]} sync_status: type: string enum: [never, pending, converging, converged, degraded, failed, paused] description: | Operator health (#157), derived at read time. `pending` is a queued, not yet claimed converge; `converging` a running one; `degraded` a failed attempt on a target that converged before, so the destination still holds `converged_revision`; `paused` wins over every other state while `paused_at` is set. converged_revision: type: [integer, "null"] format: int64 description: The last revision a converge completed. last_attempted_revision: type: [integer, "null"] format: int64 description: The revision the most recent attempt loaded, successful or not. last_attempted_at: {type: [string, "null"], format: date-time} last_error_class: type: string enum: ["", auth, network, conflict, provider_limit, provider_ambiguous, refused] description: Bounded cause of the last failed attempt; empty after a success. Never a provider response body. retry_at: type: [string, "null"] format: date-time description: When the queued retry becomes due, if the active job is waiting on one. paused_at: {type: [string, "null"], format: date-time} drift_attention: type: boolean description: The destination disagrees with the ownership ledger in a way only an operator can settle (unowned name in the way, destination identity moved, orphaned names). Cleared by the next successful converge. failure_names: {type: array, items: {type: string}} warnings: {type: array, items: {type: string}} keys: type: array description: The resolved explicit key subset by name. Membership is by immutable id; names are echoed for the operator. items: {$ref: "#/components/schemas/AdapterTargetKey"} conflicts: type: array description: Pending exact conflict artifacts eligible for adoption on this target. items: {$ref: "#/components/schemas/AdapterConflictArtifact"} AdapterTargetKey: type: object additionalProperties: false required: [key_id, name, classification] properties: key_id: {$ref: "#/components/schemas/ID"} name: {type: string} classification: {type: string, enum: [secret, config]} AdapterResume: type: object additionalProperties: false required: [job_id, generation, revision] properties: job_id: {$ref: "#/components/schemas/ID"} generation: {type: integer, format: int64, minimum: 1} revision: type: integer format: int64 minimum: 0 description: The published revision the queued converge catches up to; 0 when the environment has never published. AdapterTargetList: type: object additionalProperties: false required: [items] properties: items: {type: array, items: {$ref: "#/components/schemas/AdapterTarget"}} Adapter: type: object additionalProperties: false required: [id, provider, origin, credential_present, authority_principal_id, state, created_at, targets] properties: id: {$ref: "#/components/schemas/ID"} provider: {$ref: "#/components/schemas/AdapterProvider"} origin: {type: string, format: uri} credential_present: {type: boolean} credential_set_at: {type: [string, "null"], format: date-time} credential_expires_at: {type: [string, "null"], format: date-time} authority_principal_id: {$ref: "#/components/schemas/ID"} state: {type: string, enum: [active, moving, tombstoned]} created_at: {$ref: "#/components/schemas/Timestamp"} targets: {type: array, items: {$ref: "#/components/schemas/AdapterTarget"}} AdapterList: type: object additionalProperties: false required: [items] properties: items: {type: array, items: {$ref: "#/components/schemas/Adapter"}} DynamicProviderKind: type: string enum: [postgres] DynamicProviderState: type: string enum: [active, tombstoned] DynamicLeaseState: type: string description: | minting/renewing/revoking are in-flight; active is a usable credential; revoked/expired/failed are terminal; unknown is an ambiguous provider outcome awaiting reconcile and is NEVER reported as success. enum: [minting, active, renewing, revoking, revoked, expired, unknown, failed] CreateDynamicProviderRequest: type: object additionalProperties: false required: [kind, origin, grant_role, credential] properties: kind: {$ref: "#/components/schemas/DynamicProviderKind"} origin: type: string minLength: 1 maxLength: 2048 description: PostgreSQL origin as host:port/dbname. tls_mode: type: string enum: [verify-full] default: verify-full description: Only verify-full is accepted; a weaker mode is refused. grant_role: type: string minLength: 1 maxLength: 63 description: Parent role every minted lease role inherits (IN ROLE). credential: type: string minLength: 1 maxLength: 4096 description: Write-only admin credential. Never returned. SetDynamicProviderCredentialRequest: type: object additionalProperties: false required: [credential] properties: credential: {type: string, minLength: 1, maxLength: 4096} DynamicProvider: type: object additionalProperties: false required: [id, kind, origin, tls_mode, grant_role, credential_present, authority_principal_id, state, created_at] properties: id: {$ref: "#/components/schemas/ID"} kind: {$ref: "#/components/schemas/DynamicProviderKind"} origin: {type: string} tls_mode: {type: string, enum: [verify-full]} grant_role: {type: string} credential_present: {type: boolean} credential_set_at: {type: [string, "null"], format: date-time} authority_principal_id: {$ref: "#/components/schemas/ID"} state: {$ref: "#/components/schemas/DynamicProviderState"} created_at: {$ref: "#/components/schemas/Timestamp"} DynamicProviderList: type: object additionalProperties: false required: [items] properties: items: {type: array, items: {$ref: "#/components/schemas/DynamicProvider"}} DynamicProviderDeletion: type: object additionalProperties: false required: [provider_id, revoked_lease_ids] properties: provider_id: {$ref: "#/components/schemas/ID"} revoked_lease_ids: type: array items: {$ref: "#/components/schemas/ID"} description: Leases queued for revocation as part of the delete. DynamicLease: type: object additionalProperties: false required: [id, provider_id, environment_id, principal_id, principal_class, provider_handle, state, max_ttl_seconds, last_transition_at, created_at] properties: id: {$ref: "#/components/schemas/ID"} provider_id: {$ref: "#/components/schemas/ID"} environment_id: {$ref: "#/components/schemas/ID"} principal_id: {$ref: "#/components/schemas/ID"} principal_class: {type: string} provider_handle: {type: string, description: The provider-side role name. Public metadata; not a secret.} state: {$ref: "#/components/schemas/DynamicLeaseState"} issued_at: {type: [string, "null"], format: date-time} expires_at: {type: [string, "null"], format: date-time} max_ttl_seconds: {type: integer, format: int64, minimum: 1} last_transition_at: {$ref: "#/components/schemas/Timestamp"} created_at: {$ref: "#/components/schemas/Timestamp"} DynamicLeaseList: type: object additionalProperties: false required: [items] properties: items: {type: array, items: {$ref: "#/components/schemas/DynamicLease"}} MintLeaseRequest: type: object additionalProperties: false required: [provider_id, max_ttl_seconds] properties: provider_id: {$ref: "#/components/schemas/ID"} max_ttl_seconds: type: integer format: int64 minimum: 1 description: Requested maximum lifetime; the provider clamps to its own ceiling. RenewLeaseRequest: type: object additionalProperties: false properties: max_ttl_seconds: type: [integer, "null"] format: int64 minimum: 1 description: Optional new ceiling; renewal never extends past the lease's max TTL. MintLeaseResult: type: object additionalProperties: false required: [lease, username, password] properties: lease: {$ref: "#/components/schemas/DynamicLease"} username: type: string description: The minted role name; also the lease's provider_handle. password: type: string description: | The credential secret, returned EXACTLY ONCE to exactly one caller. No other route in this contract returns it. expires_at: {type: [string, "null"], format: date-time} AdapterConflictEntry: type: object additionalProperties: false required: [surface, effective_name] properties: surface: {type: string, enum: [secret, variable]} effective_name: {type: string} AdapterConflictArtifact: type: object additionalProperties: false required: [id, destination_id, repository_id, target_generation, entries, created_at] properties: id: {$ref: "#/components/schemas/ID"} destination_id: {type: integer, format: int64, minimum: 1} repository_id: {type: integer, format: int64, minimum: 0} target_generation: {type: integer, format: int64, minimum: 1} entries: {type: array, items: {$ref: "#/components/schemas/AdapterConflictEntry"}} created_at: {$ref: "#/components/schemas/Timestamp"} AdapterMappingEntry: type: object additionalProperties: false required: [key_id, canonical_name, surface, effective_name] properties: key_id: {$ref: "#/components/schemas/ID"} canonical_name: {type: string} surface: {type: string, enum: [secret, variable]} effective_name: {type: string} AdapterTargetDetail: type: object additionalProperties: false required: [target, conflicts, mapping] properties: target: {$ref: "#/components/schemas/AdapterTarget"} conflicts: {type: array, items: {$ref: "#/components/schemas/AdapterConflictArtifact"}} mapping: {type: array, items: {$ref: "#/components/schemas/AdapterMappingEntry"}} AdapterChange: type: object additionalProperties: false required: [surface, effective_name, disposition] properties: surface: {type: string, enum: [secret, variable]} effective_name: {type: string} disposition: {type: string, enum: [create, update, delete, conflict, refused, unknown-until-sync]} reason: {type: string} AdapterPlan: type: object additionalProperties: false required: [artifact_id, changes, warnings] properties: artifact_id: {$ref: "#/components/schemas/ID"} changes: {type: array, items: {$ref: "#/components/schemas/AdapterChange"}} warnings: {type: array, items: {type: string}} AdapterAdoptionRequest: type: object additionalProperties: false required: [artifact_id, target_generation, destination_id, repository_id, entries] properties: artifact_id: {$ref: "#/components/schemas/ID"} target_generation: {type: integer, format: int64, minimum: 1} destination_id: {type: integer, format: int64, minimum: 1} repository_id: {type: integer, format: int64, minimum: 0} entries: type: array minItems: 1 uniqueItems: true items: {$ref: "#/components/schemas/AdapterConflictEntry"} AdapterJob: type: object additionalProperties: false required: [job_id, generation] properties: job_id: {$ref: "#/components/schemas/ID"} generation: {type: integer, format: int64, minimum: 1} AdapterConnection: type: object additionalProperties: false required: [version, destination_id, repository_id] properties: version: {type: string} destination_id: {type: integer, format: int64, minimum: 1} repository_id: {type: integer, format: int64, minimum: 0} credential_expires_at: {type: [string, "null"], format: date-time} AdapterTeardown: type: object additionalProperties: false required: [orphaned, jobs] properties: orphaned: {type: array, items: {type: string}} jobs: {type: array, items: {$ref: "#/components/schemas/AdapterJob"}} AdapterMoveJob: type: object additionalProperties: false required: [id, target_id, kind, state] properties: id: {$ref: "#/components/schemas/ID"} target_id: {$ref: "#/components/schemas/ID"} kind: {type: string, enum: [scrub, activate, converge]} state: {type: string, enum: [queued, running, retry, succeeded, failed, superseded]} AdapterMoveTarget: type: object additionalProperties: false required: [target_id, environment_id, destination_kind, destination_owner, destination_name, destination_environment, destination_id, repository_id, visibility, selected_repository_ids, name_prefix, orphaned_names, jobs] properties: target_id: {$ref: "#/components/schemas/ID"} environment_id: {$ref: "#/components/schemas/ID"} destination_kind: {$ref: "#/components/schemas/AdapterDestinationKind"} destination_owner: {type: string} destination_name: {type: string} destination_environment: {type: string} destination_id: {type: integer, format: int64, minimum: 0} repository_id: {type: integer, format: int64, minimum: 0} visibility: {type: string, enum: ["", all, private, selected]} selected_repository_ids: type: array items: {type: integer, format: int64, minimum: 1} name_prefix: {type: string} orphaned_names: {type: array, items: {type: string}} jobs: {type: array, items: {$ref: "#/components/schemas/AdapterMoveJob"}} AdapterMove: type: object additionalProperties: false description: Durable route transition status. Credentials are never represented. required: [id, adapter_id, kind, state, keep_remote, pending_origin, targets, created_at] properties: id: {$ref: "#/components/schemas/ID"} adapter_id: {$ref: "#/components/schemas/ID"} kind: {type: string, enum: [target, origin]} state: {type: string, enum: [scrubbing, activating, attention_required, completed, canceled]} keep_remote: {type: boolean} pending_origin: {type: string, format: uri} targets: {type: array, minItems: 1, items: {$ref: "#/components/schemas/AdapterMoveTarget"}} created_at: {$ref: "#/components/schemas/Timestamp"} Error: type: object required: [error] properties: error: type: object required: [code, message] properties: code: $ref: "#/components/schemas/ErrorCode" message: type: string description: | Fixed per code. Never derived from the request, so two refusals of the same class are byte-identical. detail: type: [string, "null"] description: | Present on `bad_request` and `unprocessable`, where it names the offending request member, and on a `conflict` whose refusal names the caller's own state. `null` and absent are equivalent; every other error code omits the member entirely. findings: type: array description: | Secret-scanning refusal detail (#74, Surface 2). Present only on a `bad_request` that a declaration ingress refused because an author-controlled field carried a credential-shaped string. Each entry names the immutable declaration-field locator, the rule id, and a short-lived content-bound acknowledgement token the resubmission presents to override. Never the matched text. items: $ref: "#/components/schemas/ScanFinding" ScanFinding: type: object description: | One redacted secret-scanning result (#74, secret-scanning ADR §4). It carries a rule id, the surface/ingress it fired on, an immutable locator (key identity for Surface 1, schema-location class for Surface 2), and — where an acknowledgement is possible — an opaque token. It NEVER carries the matched text, an offset, a length, or an excerpt. required: [rule_id, surface, locator] additionalProperties: false properties: rule_id: type: string description: The matched rule's id (e.g. `aws-access-token`). surface: type: string description: | Which ingress produced the finding. Surface-1 warns carry the value write surface; Surface-2 blocks carry the declaration ingress (`edit` for direct edits, `plan`/`apply` for the definitions Git flow); `check` is the read-only `definitions check` dry-run. enum: [value_write, declassification, import_value, edit, plan, apply, check] locator: type: string description: | The immutable locator. Surface 1: the key identity. Surface 2: the schema-location class of the offending field (e.g. `key.declaration.pattern`), never an instance-derived path. acknowledgement: type: string description: | An opaque, short-lived, content-bound token (Surface-1 keep-as-config dismissal or Surface-2 override). Present only where an acknowledgement is possible. Resubmit the write presenting it in `acknowledgements`. It embeds no plaintext. ErrorCode: type: string description: Closed response-code set. Clients branch on this, not on prose. enum: - bad_request - unauthenticated - forbidden - not_found - conflict - limit_exceeded - unprocessable - payload_too_large - too_many_requests - service_unavailable - internal Meta: type: object description: | An exact closed allowlist. `additionalProperties: false` is the allowlist's teeth: a field added here is a visible spec change, not a quiet leak of instance configuration to unauthenticated callers. additionalProperties: false required: [server_version, api_revision, protocol_capabilities] properties: instance_identity: type: string minLength: 1 description: Stable server-generated instance identity, independent of URL aliases. server_version: type: string description: The build's version string; `dev` for unreleased builds. api_revision: type: integer minimum: 1 description: | This server's API revision. A client refuses an operation whose `x-hikyo-min-revision` exceeds it, naming the server version — a loud refusal, never a silent misbehave. protocol_capabilities: type: array description: | Which protocols this instance serves: the authentication flows `login` needs before any session exists, and the machine protocols an integration probes before using them, such as `delivery-target-report/` for each delivery-target report vocabulary this server accepts. items: $ref: "#/components/schemas/ProtocolCapability" ProtocolCapability: type: string description: | OPEN enum: an instance may advertise a flow this client has never heard of, and every generated consumer must preserve and tolerate the unknown value rather than reject the response. x-extensible-enum: - local-password - loopback-handoff - device-code - oidc - saml - webauthn - delivery-target-report/1 EstablishCredentialRequest: description: | Accepts a credential-establishment authority issued by bootstrap, invitation, credential-reset, break-glass, or recovery. The issuer is server-owned metadata, not a request discriminator. type: object additionalProperties: false required: [authority, password] properties: authority: type: string description: | The single-use credential-establishment authority, exactly as delivered. Never logged, never audited, never echoed. minLength: 16 maxLength: 256 password: type: string description: | The initial credential. Minimum length 12, no composition rules, no forced rotation (NIST SP 800-63B shape); checked against a bundled common-password list at set time only, never at login. minLength: 12 maxLength: 1024 LocalLoginRequest: type: object additionalProperties: false required: [username, password] properties: username: type: string minLength: 1 maxLength: 256 password: type: string minLength: 1 maxLength: 1024 artifact: type: string enum: [cli, browser] default: cli description: | Which session artifact to mint. Omitted or `cli` mints a CLI session whose token is returned in the body. `browser` mints a browser session delivered ONLY on the `__Host-hikyo` cookie, with its synchronizer token on the `__Host-hikyo-csrf` cookie; the body then carries no token at all. The two artifacts have distinct lifetimes, distinct CSRF contracts and distinct revocation surfaces, so the caller states which one it is asking for rather than the server guessing from a header. LoginResult: type: object required: [session, principal] properties: session_token: type: string description: | The bearer value, returned exactly once. A replayable credential: cookie attributes constrain browsers, not an attacker holding the value, which is why lifetimes are short and revocation is a delete. OMITTED for a browser-artifact session: that token is delivered ONLY on the `__Host-hikyo` HttpOnly cookie so injected same-origin script cannot read it. A CLI-artifact session, which has no cookie channel, still carries it here. session: $ref: "#/components/schemas/Session" principal: $ref: "#/components/schemas/Principal" LoginChallenge: type: object additionalProperties: false description: | A single-use, expiring authority proving the password step passed for one account, issued by `localLogin` (202) when a factor stands and a browser session was requested. No session and no cookie exist until a finish operation at `/api/v1/auth/login/challenge/{challenge}/...` consumes it. Discloses nothing beyond which factor classes may satisfy it. required: [challenge_id, expires_at, factors] properties: challenge_id: $ref: "#/components/schemas/ID" expires_at: $ref: "#/components/schemas/Timestamp" factors: type: array description: The factor classes that can satisfy this challenge. items: $ref: "#/components/schemas/FactorClass" WhoAmI: type: object required: [session, principal, capabilities] properties: session: $ref: "#/components/schemas/Session" principal: $ref: "#/components/schemas/Principal" capabilities: $ref: "#/components/schemas/PrincipalCapabilities" enrolment_required: type: boolean description: | True when this session authenticated by password on an instance whose `second_factor` policy is `required` and the account had no factor enrolled. Such a session is confined to factor enrolment, recovery-code generation, `whoami`, and `logout` at the authorization chokepoint until a factor stands. Absent or false otherwise. PrincipalCapabilities: type: object description: >- UI-gating hints for the calling principal: what instance surfaces this session may even attempt, so the SPA does not fire an operator-only request it will only be refused. These are disclosure-safe reflections of the caller's own grants — never authorization itself, which is still evaluated per request at the server chokepoint. required: [instance_operator, delivery_report_grant] properties: instance_operator: type: boolean description: >- True when the caller holds instance-config authority — the grant the retention-health and update-status reads require. The instance administration chrome and those background polls gate on it. delivery_report_grant: $ref: "#/components/schemas/UnheldGrantReach" UnheldGrantReach: type: object additionalProperties: false description: >- Where the caller may grant `report-delivery-status`. No human holds that atom, so every grant of it is an unheld grant, which only `manage-members` at instance or organisation scope may make. The machine-access grant dialog offers the atom only inside this reach. required: [instance, orgs] properties: instance: type: boolean description: The caller holds `manage-members` at instance scope, which reaches every organisation. orgs: type: array description: The organisations where the caller holds `manage-members` at organisation scope, sorted. items: {type: string} UpdateStatus: type: object x-hikyo-zod-strict: true additionalProperties: false required: [channel, current_version, available, prerelease, apply_supported] properties: channel: type: string enum: [stable, nightly, off] current_version: type: string latest_version: type: string release_url: type: string format: uri available: type: boolean prerelease: type: boolean published_at: type: string format: date-time apply_supported: type: boolean description: Always false while legacy remote apply is retired for migration safety. apply_backend: $ref: "#/components/schemas/InstanceUpdateBackend" apply_error: type: string description: Operator-safe reason remote apply is unsupported; manual upgrades remain available. InstanceUpdateBackend: type: string enum: [flux, compose, systemd] InstanceUpdateRequest: type: object x-hikyo-zod-strict: true additionalProperties: false required: [version] properties: version: type: string pattern: "^[0-9]+\\.[0-9]+\\.[0-9]+$" maxLength: 64 InstanceUpdateJob: type: object x-hikyo-zod-strict: true additionalProperties: false required: [id, backend, version, state, phase, requested_at] properties: id: $ref: "#/components/schemas/ID" backend: $ref: "#/components/schemas/InstanceUpdateBackend" version: type: string pattern: "^[0-9]+\\.[0-9]+\\.[0-9]+$" maxLength: 64 state: type: string enum: [queued, running, succeeded, failed, rolled-back, rollback-failed] phase: type: string enum: [queued, plan, backup, verify, apply, health, rollback, complete, admission] failure_code: type: string maxLength: 64 requested_at: $ref: "#/components/schemas/Timestamp" started_at: $ref: "#/components/schemas/Timestamp" finished_at: $ref: "#/components/schemas/Timestamp" TotpEnrolStartRequest: type: object additionalProperties: false required: [password] properties: password: type: string description: | The account-security proof: the pre-existing password. Verified before a pending seed is staged; never logged, never echoed. minLength: 1 maxLength: 1024 TotpEnrolStartResult: type: object additionalProperties: false required: [otpauth_uri] properties: otpauth_uri: type: string description: | The `otpauth://totp/...` provisioning URI, returned exactly once. The seed is never re-retrievable. TotpCodeRequest: type: object additionalProperties: false required: [code] properties: code: type: string description: A TOTP code from the enrolled authenticator. minLength: 6 maxLength: 10 TotpReauthRequest: description: A disclosure reauthentication by TOTP in exactly one canonical intent shape. oneOf: - $ref: "#/components/schemas/TotpEnvironmentReauthRequest" - $ref: "#/components/schemas/TotpAdapterReauthRequest" - $ref: "#/components/schemas/TotpSelfConfigReauthRequest" TotpSelfConfigReauthRequest: type: object x-hikyo-zod-strict: true additionalProperties: false required: [purpose, self_config, code] properties: purpose: {type: string, const: self-config} self_config: {$ref: "#/components/schemas/SelfConfigReauthIntent"} code: {type: string, minLength: 6, maxLength: 10} SelfConfigReauthIntent: type: object additionalProperties: false required: [action, owner_instance_id, revision, schema_version, expected_generation, preview_token, to, confirm_restored_credentials] properties: plan_digest: {type: string, pattern: "^[a-f0-9]{64}$", description: Exact prepared deployment plan authorized by this decision.} action: {type: string, enum: [adopt, apply, mail-test, rollout-restore]} owner_instance_id: {type: string, minLength: 1, maxLength: 128} revision: {type: integer, format: int64, minimum: 0} schema_version: {type: integer, minimum: 1} expected_generation: {type: integer, format: int64, minimum: 0} preview_token: {type: string, maxLength: 1024} to: {type: string, maxLength: 320} confirm_restored_credentials: {type: boolean, description: Explicit confirmation of reviewed credentials and reconciled access grants after restore; false for other decisions.} TotpEnvironmentReauthRequest: type: object x-hikyo-zod-strict: true additionalProperties: false required: [environment_id, code] description: | A disclosure reauthentication by TOTP. `environment_id` is what the window is opened OVER — the reveal guard is per environment, so a window on staging authorizes nothing in production. properties: environment_id: $ref: "#/components/schemas/ID" code: type: string description: A TOTP code from the enrolled authenticator. minLength: 6 maxLength: 10 TotpAdapterReauthRequest: type: object x-hikyo-zod-strict: true additionalProperties: false required: [purpose, operation, environment_ids, code] description: A TOTP proof bound to one adapter operation over an exact environment set. properties: purpose: allOf: - $ref: "#/components/schemas/ReauthPurpose" - const: adapter operation: type: string enum: [adapter.configure, adapter.credential-set, adapter.adopt, adapter.sync] environment_ids: type: array minItems: 1 uniqueItems: true items: {$ref: "#/components/schemas/ID"} code: type: string description: A TOTP code from the enrolled authenticator. minLength: 6 maxLength: 10 CLIReauthStartRequest: type: object additionalProperties: false required: [purpose, operation, environment_ids, pkce_challenge, redirect_uri] description: | `adapter` carries an adapter-routing decision over an environment set. Every non-adapter purpose carries an exact environment and key set. The browser runs the same purpose-bound ceremony the UI runs, so `key_ids` names exactly the unit the decision covers. properties: self_config: {$ref: "#/components/schemas/SelfConfigReauthIntent"} purpose: {type: string, enum: [adapter, reveal, copy, publish, approve, reject, bypass, self-config]} operation: {type: string, enum: [adapter.configure, adapter.credential-set, adapter.adopt, adapter.sync, value.reveal, value.copy-source, value.copy-destination, approval.vote, approval.bypass, self-config.adopt, self-config.apply, self-config.test]} environment_ids: {type: array, uniqueItems: true, items: {$ref: "#/components/schemas/ID"}} key_ids: type: array uniqueItems: true maxItems: 500 items: {$ref: "#/components/schemas/ID"} description: The enumerated unit of a non-adapter purpose; absent or empty for `adapter`. pkce_challenge: {type: string, minLength: 43, maxLength: 43, pattern: "^[A-Za-z0-9_-]{43}$"} redirect_uri: type: string format: uri maxLength: 256 description: Exact ephemeral loopback callback, http://127.0.0.1:PORT/callback or the bracketed ::1 equivalent. CLIReauthStart: type: object additionalProperties: false required: [state, expires_at] properties: state: {type: string, minLength: 1} expires_at: {$ref: "#/components/schemas/Timestamp"} CLIReauthApproveRequest: type: object additionalProperties: false required: [state] properties: state: {type: string, minLength: 1} CLIReauthApproved: type: object additionalProperties: false required: [code, state, redirect_uri] properties: code: {type: string, minLength: 1, description: Single-use front-channel authorization code; never a session bearer.} state: {type: string, minLength: 1, description: Exact opaque state supplied by the initiating CLI.} redirect_uri: {type: string, format: uri, description: Exact server-bound loopback callback URI.} CLIReauthEnvironmentPolicy: type: object additionalProperties: false required: [environment_id, effective_window_seconds, requires_webauthn] properties: environment_id: {$ref: "#/components/schemas/ID"} effective_window_seconds: {type: integer, minimum: 0} requires_webauthn: {type: boolean} CLIReauthTransaction: type: object additionalProperties: false required: [state, purpose, operation, environments, key_ids, redirect_uri, expires_at] properties: self_config: {$ref: "#/components/schemas/SelfConfigReauthIntent"} state: {type: string, minLength: 1} purpose: {type: string, enum: [adapter, reveal, copy, publish, approve, reject, bypass, self-config]} operation: {type: string, enum: [adapter.configure, adapter.credential-set, adapter.adopt, adapter.sync, value.reveal, value.copy-source, value.copy-destination, approval.vote, approval.bypass, self-config.adopt, self-config.apply, self-config.test]} environments: {type: array, items: {$ref: "#/components/schemas/CLIReauthEnvironmentPolicy"}} key_ids: type: array items: {$ref: "#/components/schemas/ID"} description: The enumerated unit the ceremony binds; empty for `adapter`. redirect_uri: {type: string, format: uri} expires_at: {$ref: "#/components/schemas/Timestamp"} CLIReauthRedeemRequest: type: object additionalProperties: false required: [code, pkce_verifier] properties: code: {type: string, minLength: 1} pkce_verifier: {type: string, minLength: 43, maxLength: 128, pattern: "^[A-Za-z0-9._~-]+$"} CLIReauthRedeemed: type: object additionalProperties: false required: [session_id, session_token, windows] properties: session_id: {$ref: "#/components/schemas/ID"} session_token: {type: string, minLength: 1, description: Rotated CLI bearer. Client stores it silently and never prints or logs it.} windows: {type: array, items: {$ref: "#/components/schemas/ReauthResult"}} TotpProofRequest: type: object additionalProperties: false required: [password] properties: password: type: string description: The account-security proof for removing the factor. minLength: 1 maxLength: 1024 RecoveryProofRequest: type: object additionalProperties: false required: [proof] properties: proof: type: string description: | The account-security proof: the confirmed TOTP code where one stands, else the password. minLength: 1 maxLength: 1024 RecoveryCodesResult: type: object required: [recovery_codes, login] properties: recovery_codes: type: array description: The plaintext single-use codes, displayed once and never again. items: type: string login: $ref: "#/components/schemas/LoginResult" RecoveryBeginRequest: type: object additionalProperties: false required: [username, code] properties: username: type: string minLength: 1 maxLength: 256 code: type: string minLength: 1 maxLength: 256 RecoveryBeginResult: type: object additionalProperties: false required: [authority, expires_at] properties: authority: type: string description: | The single-use credential-establishment authority, returned once. It creates no session: establish a password with it, then log in. expires_at: $ref: "#/components/schemas/Timestamp" InviteMemberRequest: type: object additionalProperties: false required: [username] properties: username: type: string minLength: 1 maxLength: 128 description: The invitee's local login handle; globally unique. display_name: type: string maxLength: 256 description: Defaults to the username. template: allOf: - $ref: "#/components/schemas/RoleTemplate" description: | Optional initial grants, expanded at the invitation's scope in the same transaction. Absent means the account can sign in and see nothing until someone grants it something. InvitationResult: type: object additionalProperties: false required: [principal_id, account_id, authority, expires_at] properties: principal_id: $ref: "#/components/schemas/ID" account_id: $ref: "#/components/schemas/ID" authority: type: string description: | The single-use credential-establishment authority for the new account, returned once. It creates no session and may only ever establish a password; hand it to the invitee out of band. expires_at: $ref: "#/components/schemas/Timestamp" CredentialResetResult: type: object additionalProperties: false required: [authority, expires_at] properties: authority: type: string description: | The single-use credential-establishment authority for the target, returned once. It creates no session and may only ever establish a password; hand it to the target out of band. expires_at: $ref: "#/components/schemas/Timestamp" Session: type: object required: [id, artifact, created_at, idle_expires_at, absolute_expires_at, assurance] properties: id: $ref: "#/components/schemas/ID" artifact: $ref: "#/components/schemas/SessionArtifact" created_at: $ref: "#/components/schemas/Timestamp" idle_expires_at: $ref: "#/components/schemas/Timestamp" absolute_expires_at: $ref: "#/components/schemas/Timestamp" assurance: $ref: "#/components/schemas/Assurance" SessionArtifact: type: string description: | OPEN enum: human sessions are per-client-class artifacts and new classes are additive. A browser cookie's attributes protect nothing on a non-browser client, and conflating the two hides which artifact was actually used. x-extensible-enum: - cli - browser Assurance: type: object description: | How **this session** authenticated — not what the account owns. Authorization of an MFA-mandatory capability consults this record at the same chokepoint as `authorize()`, in the same transaction, uncached. required: [method, factors, authenticated_at] properties: method: $ref: "#/components/schemas/AuthMethod" provider: type: string description: The configured provider slug, present only for OIDC sessions. factors: type: array description: The factor classes actually presented in this session. items: $ref: "#/components/schemas/FactorClass" authenticated_at: $ref: "#/components/schemas/Timestamp" ceremony_id: type: [string, "null"] description: | The factor ceremony this session's assurance came from, where one occurred. `null` for a single-factor password login. AuthMethod: type: string description: | OPEN enum — `oidc:` and `saml:` values are instance-specific by construction. x-extensible-enum: - local-password - local-passkey FactorClass: type: string description: OPEN enum — new factor classes are additive. x-extensible-enum: - password - totp - webauthn - recovery-code Principal: type: object required: [id, kind] properties: id: $ref: "#/components/schemas/ID" kind: $ref: "#/components/schemas/PrincipalKind" display_name: type: [string, "null"] description: For display only. Never a linking key, at any point, for any provider. PrincipalKind: type: string description: Closed set — the human/machine distinction is structural in audit attribution. enum: [human, machine] CreateOrgRequest: type: object additionalProperties: false required: [name] properties: name: type: string minLength: 1 maxLength: 128 active: type: boolean default: true metadata: type: [object, "null"] description: | Free-form operator metadata. `null` and absent both mean "no metadata"; the round-trip of absent/null/value is fixture-pinned through both the Go and the TypeScript consumer. acknowledgements: $ref: "#/components/schemas/Acknowledgements" Org: type: object required: [id, name, active, created_at, origin] properties: id: $ref: "#/components/schemas/ID" name: type: string active: type: boolean metadata: type: [object, "null"] created_at: $ref: "#/components/schemas/Timestamp" origin: type: string enum: [manual, registration] description: | How the org came to exist (#585 d8): `manual` (an operator's create) or `registration` (a sign-up under a registration policy, a self-served org). Typed on the row, never editable. registration_policy_id: type: [string, "null"] description: | The registration policy that minted the org, a trail pointer with no foreign key (the org keeps it after the policy is deleted); null for a manual org. OrgList: type: object required: [items, count] properties: items: type: array items: $ref: "#/components/schemas/Org" count: type: integer minimum: 0 description: Total rows matching, which for an unpaged list equals `items` length. MyOrg: type: object additionalProperties: false required: [id, name] description: | An organisation as a navigation destination: what the caller needs to show it and route to it, and nothing else. Deliberately narrower than `Org` — `metadata` and `active` are operator-set state that belongs to `getOrg`, which authorizes; a member listing does not. properties: id: $ref: "#/components/schemas/ID" name: type: string MyOrgList: type: object required: [items, count] properties: items: type: array items: $ref: "#/components/schemas/MyOrg" count: type: integer minimum: 0 description: Total rows matching, which for an unpaged list equals `items` length. EntityName: type: string description: | A display name for an organisation, project or environment. Identity is the immutable id, so this is a label and a rename never breaks a reference. The 128-byte bound is the one the organisation contract has carried since the first slice, adopted for every entity so there is one number rather than four; no ADR fixes a per-entity name length. The grammar itself (non-empty, no control characters, no surrounding whitespace) is enforced in the domain rather than restated as a pattern here — two grammars would be two things to keep in sync. **The bound is 128 UTF-8 BYTES, and the server is authoritative.** `maxLength` below counts Unicode code points, which is the only length JSON Schema can express, so a name of 100 emoji satisfies this schema and is still refused by the server with `bad_request`. Clients that want to pre-validate must measure the UTF-8 encoding, not the string length. minLength: 1 maxLength: 128 RenameRequest: type: object additionalProperties: false required: [name] properties: name: $ref: "#/components/schemas/EntityName" acknowledgements: $ref: "#/components/schemas/Acknowledgements" CreateProjectRequest: type: object additionalProperties: false required: [name] properties: name: $ref: "#/components/schemas/EntityName" acknowledgements: $ref: "#/components/schemas/Acknowledgements" Project: type: object required: [id, org_id, name, created_at] properties: can_manage_policy: type: boolean description: Whether the caller can manage policy for this project. can_delete: type: boolean description: Whether the caller can delete this project. id: $ref: "#/components/schemas/ID" org_id: $ref: "#/components/schemas/ID" name: type: string created_at: $ref: "#/components/schemas/Timestamp" ProjectList: type: object required: [items, count] properties: items: type: array items: $ref: "#/components/schemas/Project" count: type: integer minimum: 0 description: Total rows matching, which for an unpaged list equals `items` length. CreateEnvironmentRequest: type: object additionalProperties: false required: [name] properties: name: $ref: "#/components/schemas/EntityName" acknowledgements: $ref: "#/components/schemas/Acknowledgements" CloneEnvironmentRequest: type: object additionalProperties: false required: [name, source_environment_id] properties: name: $ref: "#/components/schemas/EntityName" source_environment_id: $ref: "#/components/schemas/ID" acknowledgements: $ref: "#/components/schemas/Acknowledgements" ClonedEnvironment: type: object required: [environment, copied, uncopied_secrets] properties: environment: $ref: "#/components/schemas/Environment" copied: type: array description: The key names whose values the clone carried over. items: $ref: "#/components/schemas/KeyName" uncopied_secrets: type: array description: | The `secret` key names the source-material gate blocked. They land `absent` in the new environment - never silently, which is why they are enumerated here. items: $ref: "#/components/schemas/KeyName" findings: type: array description: | Secret-scanning warnings the cloned config values produced (#74, Surface 1), warn-not-block. items: $ref: "#/components/schemas/ScanFinding" PendingChange: type: object description: | One draft in the caller's own working state. `version_id` is the IMMUTABLE id a selective publish names; editing the same cell mints a new one and collects this row rather than mutating it. It never carries the staged value. A draft holds real material and that material is reveal-gated exactly as a published one is. required: [version_id, key_id, name, classification, operation, staged_from_revision, created_at] additionalProperties: false properties: version_id: $ref: "#/components/schemas/ID" key_id: $ref: "#/components/schemas/ID" name: $ref: "#/components/schemas/KeyName" classification: $ref: "#/components/schemas/KeyClassification" operation: type: string description: What publishing this draft does to the cell. enum: [set, unset] staged_from_revision: type: integer format: int64 description: | The environment's published revision when the edit was staged. Provenance a client shows; the freshness check is per entry. created_at: type: string format: date-time findings: type: array description: | Secret-scanning warnings this save produced (#74, Surface 1). The save SUCCEEDED regardless; each finding carries a keep-as-config acknowledgement token. Absent or empty on a clean save. items: $ref: "#/components/schemas/ScanFinding" PendingDraft: type: object description: | One caller-owned pending draft. Secret and unset drafts never carry material on this surface; `value` is present if and only if `revealed` is true. required: [version_id, key_id, name, classification, operation, staged_from_revision, created_at, revealed] additionalProperties: false properties: advisory: type: object additionalProperties: false required: [owner_id, valid] properties: owner_id: {$ref: "#/components/schemas/ID"} valid: {type: boolean} validation_deferred: type: boolean description: True when caller parameters are needed for final config schema validation at fetch. Valid then describes template structure and presence only. version_id: $ref: "#/components/schemas/ID" key_id: $ref: "#/components/schemas/ID" name: $ref: "#/components/schemas/KeyName" classification: $ref: "#/components/schemas/KeyClassification" operation: type: string enum: [set, unset] staged_from_revision: type: integer format: int64 created_at: $ref: "#/components/schemas/Timestamp" revealed: type: boolean value: type: string PendingDraftList: type: object required: [items, count] additionalProperties: false properties: items: type: array items: $ref: "#/components/schemas/PendingDraft" count: type: integer minimum: 0 RollbackRequest: type: object additionalProperties: false properties: key: $ref: "#/components/schemas/KeyName" RollbackResult: type: object additionalProperties: false required: [revision, changes, preview] properties: revision: type: integer format: int64 changes: type: array items: {$ref: "#/components/schemas/PendingChange"} preview: $ref: "#/components/schemas/ImpactPreview" ImpactPreview: type: object additionalProperties: false required: [token, environments] properties: token: type: string description: Opaque exact-input token required when publishing restore-authored drafts. environments: type: array items: {$ref: "#/components/schemas/ImpactEnvironment"} ImpactEnvironment: type: object additionalProperties: false required: [environment_id, base_revision, schema_revision, protected, changes] properties: environment_id: {$ref: "#/components/schemas/ID"} base_revision: {type: integer, format: int64, minimum: 0} schema_revision: {type: integer, format: int64, minimum: 0} protected: type: boolean description: Protection state bound into the preview token. changes: type: array items: {$ref: "#/components/schemas/ImpactChange"} ImpactChange: type: object additionalProperties: false required: [version_id, key_id, name, classification, operation, status] properties: version_id: {$ref: "#/components/schemas/ID"} key_id: {$ref: "#/components/schemas/ID"} name: {$ref: "#/components/schemas/KeyName"} classification: {$ref: "#/components/schemas/KeyClassification"} operation: {type: string, enum: [set, unset]} status: {type: string, enum: [added, edited, removed, not-edited]} before: type: string description: Config plaintext when the caller also holds read; absent for secrets or unreadable config. after: type: string description: Config plaintext when the caller also holds read; absent for secrets, clears, or unreadable config. RevisionPinRequest: type: object additionalProperties: false required: [workload_principal_id, revision] properties: workload_principal_id: {$ref: "#/components/schemas/ID"} revision: {type: integer, format: int64, minimum: 1} expires_at: {$ref: "#/components/schemas/Timestamp"} override_schema: {type: boolean, default: false} RevisionPin: type: object additionalProperties: false required: [id, workload_principal_id, revision, authority_principal_id, expires_at, created_at, authorized_at, history_authorized, schema_override, expired, release_retention_consequence] properties: id: {$ref: "#/components/schemas/ID"} workload_principal_id: {$ref: "#/components/schemas/ID"} revision: {type: integer, format: int64} authority_principal_id: {$ref: "#/components/schemas/ID"} expires_at: {$ref: "#/components/schemas/Timestamp"} created_at: {$ref: "#/components/schemas/Timestamp"} authorized_at: {$ref: "#/components/schemas/Timestamp"} history_authorized: {type: boolean} schema_override: {type: boolean} expired: {type: boolean} release_retention_consequence: allOf: - $ref: "#/components/schemas/RetentionConsequence" description: | Server-derived preview of releasing this pin. It controls the existing collection-risk confirmation, but may become stale; the release response is authoritative transaction-time truth. RevisionPinResult: type: object additionalProperties: false required: [action, pin] properties: action: {type: string, enum: [created, reassigned, renewed]} pin: {$ref: "#/components/schemas/RevisionPin"} RetentionConsequence: type: string enum: [retained, collection_eligible, already_collected] description: | Transaction-time payload state after a pin release. retained means the effective policy or another live pin still keeps the payload; collection_eligible means a sweep may collect it immediately; already_collected means GC committed collection before this result. RevisionPinReleaseResult: type: object additionalProperties: false required: [revision, retention_consequence] properties: revision: {type: integer, format: int64, minimum: 1} retention_consequence: {$ref: "#/components/schemas/RetentionConsequence"} RevisionPinList: type: object additionalProperties: false required: [items, count] properties: items: type: array items: {$ref: "#/components/schemas/RevisionPin"} count: {type: integer, minimum: 0} PublishRequest: type: object additionalProperties: false properties: version_ids: type: array minItems: 1 uniqueItems: true description: | The pending-change version ids to commit. Naming an id the caller does not own, or one that has been superseded or already published, is refused loud rather than resolved to whatever the owner typed since. Required for an ordinary publish; omitted when merging or bypassing an approval request (whose reviewed selection is used instead). items: $ref: "#/components/schemas/ID" preview_token: type: string minLength: 1 description: Required when any selected or closure-added draft came from rollback. confirmed_protected_environments: type: array uniqueItems: true items: {$ref: "#/components/schemas/ID"} description: Exact protected-environment set reviewed by a machine principal; humans must use the bound ceremony. approval_request_id: $ref: "#/components/schemas/ID" description: | Secret-change approvals (#151). Merge or bypass this approval request instead of publishing a caller-named selection: the request's reviewed version ids are used, and the request must be approved (quorum from currently-eligible approvers) unless `bypass` is set. Only the requester may merge their own reviewed change. bypass: $ref: "#/components/schemas/ApprovalBypass" purpose: type: string maxLength: 1024 description: | Free-text reason recorded on the request when a covered publish stages one. Ignored where no policy covers the environment. ApprovalBypass: type: object required: [reason] additionalProperties: false description: | Emergency bypass of a covered environment's approval quorum (#151). A named bypasser force-merges an existing request with a mandatory reason, under a current reauthentication ceremony; emits a dedicated high-signal audit event and never mutates the policy. properties: reason: type: string minLength: 1 maxLength: 512 description: Why the quorum was bypassed. Recorded in the audit trail. ApprovalApprover: type: object required: [kind, subject_id] additionalProperties: false properties: kind: type: string enum: [principal, scim_group] subject_id: $ref: "#/components/schemas/ID" description: A principal id, or a SCIM group id when kind is scim_group. binding_id: $ref: "#/components/schemas/ID" description: The SCIM binding a group approver's members are resolved through. ApprovalPolicyInput: type: object required: [min_approvals, request_ttl_seconds, enabled] additionalProperties: false properties: environment_id: type: string maxLength: 64 description: The environment this policy covers; empty means every environment in the project. min_approvals: type: integer format: int32 minimum: 1 allow_self_approval: type: boolean request_ttl_seconds: type: integer format: int32 minimum: 1 enabled: type: boolean approvers: type: array maxItems: 100 items: {$ref: "#/components/schemas/ApprovalApprover"} bypassers: type: array maxItems: 100 uniqueItems: true items: {$ref: "#/components/schemas/ID"} ApprovalPolicy: type: object required: [id, environment_id, min_approvals, allow_self_approval, request_ttl_seconds, enabled, version, approvers, bypassers, created_at, updated_at] additionalProperties: false properties: principal_names: type: object additionalProperties: type: string description: Current names for principal approvers and bypassers already disclosed by this policy; not a user directory. id: {$ref: "#/components/schemas/ID"} environment_id: type: string maxLength: 64 min_approvals: {type: integer, format: int32} allow_self_approval: {type: boolean} request_ttl_seconds: {type: integer, format: int32} enabled: {type: boolean} version: {type: integer, format: int64} approvers: type: array items: {$ref: "#/components/schemas/ApprovalApprover"} bypassers: type: array items: {$ref: "#/components/schemas/ID"} created_at: {$ref: "#/components/schemas/Timestamp"} updated_at: {$ref: "#/components/schemas/Timestamp"} ApprovalPolicyList: type: object required: [items] additionalProperties: false properties: items: type: array items: {$ref: "#/components/schemas/ApprovalPolicy"} ApprovalVote: type: object required: [principal_id, decision, created_at] additionalProperties: false properties: principal_name: type: string description: Current display name of the referenced principal, when available. principal_id: {$ref: "#/components/schemas/ID"} decision: type: string enum: [approve, reject] created_at: {$ref: "#/components/schemas/Timestamp"} ApprovalRequest: type: object required: [id, environment_id, policy_id, policy_version, requester, change_count, base_revision, purpose, state, invalidated_cause, min_approvals, approvals, key_ids, votes, created_at, expires_at] additionalProperties: false properties: requester_name: type: string description: Current display name of the referenced principal, when available. id: {$ref: "#/components/schemas/ID"} environment_id: {$ref: "#/components/schemas/ID"} policy_id: {$ref: "#/components/schemas/ID"} policy_version: {type: integer, format: int64} requester: {$ref: "#/components/schemas/ID"} change_count: {type: integer, format: int32} base_revision: {type: integer, format: int64} purpose: {type: string} state: type: string enum: [open, approved, merged, rejected, expired, invalidated, bypassed] invalidated_cause: type: string enum: ["", policy_changed, draft_edited, env_advanced, approver_removed] min_approvals: {type: integer, format: int32} approvals: {type: integer, format: int32} key_ids: type: array uniqueItems: true items: {$ref: "#/components/schemas/ID"} description: The exact key set bound to vote, merge, and bypass ceremonies. votes: type: array items: {$ref: "#/components/schemas/ApprovalVote"} created_at: {$ref: "#/components/schemas/Timestamp"} expires_at: {$ref: "#/components/schemas/Timestamp"} resolved_at: {$ref: "#/components/schemas/Timestamp"} ApprovalRequestList: type: object required: [items] additionalProperties: false properties: items: type: array items: {$ref: "#/components/schemas/ApprovalRequest"} ApprovalRequestSummary: type: object required: [id, environment_id, policy_id, state, expires_at] additionalProperties: false properties: id: {$ref: "#/components/schemas/ID"} environment_id: {$ref: "#/components/schemas/ID"} policy_id: {$ref: "#/components/schemas/ID"} state: type: string enum: [open, approved, merged, rejected, expired, invalidated, bypassed] expires_at: {$ref: "#/components/schemas/Timestamp"} ApprovalVoteRequest: type: object required: [decision] additionalProperties: false properties: decision: type: string enum: [approve, reject] PublishResult: type: object required: [published, closed_in, environments] additionalProperties: false properties: published: type: array description: Every version id that committed, closure included. items: $ref: "#/components/schemas/ID" closed_in: type: array description: | The version ids key-group closure pulled in beyond what the caller named, so the caller can tell what it asked for from what the coupling required. items: $ref: "#/components/schemas/ID" environments: type: array items: $ref: "#/components/schemas/PublishedEnvironment" PublishedEnvironment: type: object required: [environment_id, revision, schema_revision, change_token, changed_keys] additionalProperties: false properties: environment_id: $ref: "#/components/schemas/ID" revision: type: integer format: int64 schema_revision: type: integer format: int64 description: The pinned schema revision this snapshot was validated at. change_token: type: string description: | The keyed, versioned change token for this snapshot. Non-secret by construction: it is an HMAC under a per-scope key, so it is unforgeable and un-invertible, which is why it may travel in pod annotations and logs. changed_keys: type: array items: $ref: "#/components/schemas/ChangedKey" ChangedKey: type: object description: | One lineage row. It carries a key id, the key's name at that revision and the transition - and nothing derived from a value. required: [key_id, name, change] additionalProperties: false properties: key_id: $ref: "#/components/schemas/ID" name: $ref: "#/components/schemas/KeyName" change: type: string enum: [added, edited, removed] RevisionDiffRequest: type: object additionalProperties: false required: [left_revision, right_revision] properties: left_revision: {type: integer, format: int64, minimum: 1} right_revision: {type: integer, format: int64, minimum: 1} RevealRevisionDiffRequest: type: object additionalProperties: false required: [left_revision, right_revision, key_id] properties: left_revision: {type: integer, format: int64, minimum: 1} right_revision: {type: integer, format: int64, minimum: 1} key_id: {$ref: "#/components/schemas/ID"} RevisionDiff: type: object additionalProperties: false required: [left_revision, right_revision, items] properties: left_revision: {type: integer, format: int64} right_revision: {type: integer, format: int64} items: type: array items: {$ref: "#/components/schemas/RevisionDiffRow"} RevisionDiffRow: type: object additionalProperties: false required: [key_id, name, classification, status, revealed] properties: key_id: {$ref: "#/components/schemas/ID"} name: {type: string} classification: {$ref: "#/components/schemas/KeyClassification"} status: {type: string, enum: [added, removed, edited, not_edited, changed, unchanged]} revealed: {type: boolean} before: {type: string} after: {type: string} RevisionList: type: object required: [items, count] additionalProperties: false properties: items: type: array items: $ref: "#/components/schemas/Revision" count: type: integer next_before: type: integer format: int64 description: | Present when the page was full: the smallest revision on it, to pass as `before` for the next page. Absent once the page reached the oldest revision. Revision: type: object required: [revision, schema_revision, published_by, published_at, changed_keys, payload_present] additionalProperties: false properties: revision: type: integer format: int64 schema_revision: type: integer format: int64 published_by: $ref: "#/components/schemas/ID" published_by_name: type: string description: Current display name of the publisher, absent if the principal no longer exists. published_at: type: string format: date-time changed_keys: type: array items: $ref: "#/components/schemas/ChangedKey" payload_present: type: boolean description: | Whether this revision's payload still exists. Lineage is retained forever; a payload collected by retention policy leaves the entry in the history with this bit false, and diff, restore, pin and reveal on it are refused loud rather than reconstructed. collected_policy: type: string description: | The retention policy stamped at collection, verbatim, and present only when `payload_present` is false. It is what the named refusal reports forever, so it is carried as an opaque string. RevisionDetail: type: object required: [revision, schema_revision, published_by, published_at, changed_keys, change_token, keys] additionalProperties: false properties: revision: type: integer format: int64 schema_revision: type: integer format: int64 published_by: $ref: "#/components/schemas/ID" published_by_name: type: string description: Current display name of the publisher, absent if the principal no longer exists. published_at: type: string format: date-time changed_keys: type: array items: $ref: "#/components/schemas/ChangedKey" change_token: type: string keys: type: array description: | The delivered key set of THIS revision, under the schema revision it pinned. Names and classifications only; values live behind the export verb and its formula. items: $ref: "#/components/schemas/SnapshotKey" SnapshotKey: type: object required: [key_id, name, classification] additionalProperties: false properties: key_id: $ref: "#/components/schemas/ID" name: $ref: "#/components/schemas/KeyName" classification: $ref: "#/components/schemas/KeyClassification" EnvironmentSignals: type: object required: [environment_id, revision, cells] additionalProperties: false properties: environment_id: $ref: "#/components/schemas/ID" revision: type: integer format: int64 description: The environment's current published revision, 0 when never materialized. cells: type: array items: $ref: "#/components/schemas/CellSignal" CellSignal: type: object required: [key_id, name, classification, pending_by_others] additionalProperties: false properties: key_id: $ref: "#/components/schemas/ID" name: $ref: "#/components/schemas/KeyName" classification: $ref: "#/components/schemas/KeyClassification" pending_version_id: $ref: "#/components/schemas/ID" pending_operation: type: string enum: [set, unset] pending_by_others: type: boolean description: | At least one OTHER principal holds a draft here. Write-presence and nothing more: no id, no owner, no operation. changed_in_revision: type: integer format: int64 description: Set to the environment's LATEST revision when this cell changed in it, absent otherwise. This is the revision ADR's "recently changed" signal, which names only the last published revision -- it is not a per-cell last-change history; older changes live in the revision list. EnvironmentParameters: type: object maxProperties: 32 additionalProperties: type: string maxLength: 512 FetchParameters: type: object maxProperties: 32 additionalProperties: type: string maxLength: 256 ExportValuesRequest: type: object additionalProperties: false properties: parameters: $ref: "#/components/schemas/FetchParameters" revision: type: integer format: int64 minimum: 1 description: The revision to export; omitted means the latest. reveal: type: boolean description: | Ask for `secret` plaintext. Without it a `secret` key reports write-presence and no value; `config` values are returned either way, because classification IS the sensitivity boundary. ExportedValues: type: object required: [revision, items, count] additionalProperties: false properties: revision: type: integer format: int64 description: The revision actually served. items: type: array items: $ref: "#/components/schemas/ExportedValue" count: type: integer ExportedValue: type: object required: [name, classification, revealed] additionalProperties: false properties: name: $ref: "#/components/schemas/KeyName" classification: $ref: "#/components/schemas/KeyClassification" revealed: type: boolean value: type: string description: Present only when `revealed` is true. RotateDEKRequest: type: object required: [scope] additionalProperties: false properties: scope: type: string enum: [project, instance] description: | `project` rotates one project's DEK (org and project required); `instance` rotates the instance DEK for rows belonging to no project. org: type: string description: Organisation id. Required when scope is `project`, absent otherwise. project: type: string description: Project id. Required when scope is `project`, absent otherwise. DEKRotation: type: object required: [scope, key_version] additionalProperties: false properties: scope: type: string enum: [project, instance] org: type: string project: type: string key_version: type: integer format: int64 description: The new active DEK version. Old versions remain readable until reencrypt. RotateRootKeyRequest: type: object required: [phase] additionalProperties: false properties: phase: type: string enum: [prepare, verify, finalize] description: | `prepare` seals the master under the new root; `verify` confirms the operator installed it at the primary source; `finalize` retires the old wrapper. Run in that order. RootKeyRotation: type: object required: [phase, root_key_epoch] additionalProperties: false properties: phase: type: string enum: [prepare, verify, finalize] root_key_epoch: type: integer format: int64 description: The epoch this phase concerns — the new epoch for prepare/verify, the surviving epoch after finalize. ReencryptResult: type: object required: [scope, rows_moved] additionalProperties: false properties: scope: type: string enum: [project, instance] org: type: string project: type: string rows_moved: type: integer format: int64 description: How many ciphertext rows this run moved onto the active DEK version. MasterKeyRotation: type: object required: [key_version] additionalProperties: false properties: key_version: type: integer format: int64 description: The new master key version. Every tier-3 key is now wrapped under it. TokenKeyRotation: type: object required: [token_key_version] additionalProperties: false properties: token_key_version: type: integer format: int64 description: | The new root token key version. Operator bookkeeping only - the change token's public contract carries the SCHEME version, never the key version, because a consumer that could tell key versions apart could tell a rotation from a content change. ScanningKeyRotation: type: object required: [scanning_key_version, dismissals_dropped] additionalProperties: false properties: scanning_key_version: type: integer format: int64 description: | The new scanning-fingerprint key version. Operator bookkeeping only; a fingerprint is never exported, displayed or compared. dismissals_dropped: type: integer format: int64 description: | How many dismissal rows the rotation invalidated. Every stored fingerprint became unrecomputable under the new key, so all dismissals were dropped and their warns will re-fire. ValueCell: type: object description: | One `(key, environment)` cell. PRESENCE IS THE SINGLE BOOLEAN `set`. That is the whole presence model: there is no mode, no enum with a third member, and nothing named `masked` anywhere in this contract - the flat-model ADR deleted that state with the inheritance it existed to explain. A cell that is not `set` carries no value from any source. required: [key_id, name, classification, set, revealed] properties: key_id: $ref: "#/components/schemas/ID" name: $ref: "#/components/schemas/KeyName" classification: $ref: "#/components/schemas/KeyClassification" set: type: boolean description: | `true` is the `set` state, `false` is `absent`. Those are the only two states there are. revealed: type: boolean description: | Whether `value` carries plaintext. It exists so a caller can tell an empty value the operator actually set from a `secret` they are not authorized to read - two cases that would otherwise both look like an absent `value` member. value: type: string description: | Present only where the reader was authorized to see it: `config` under `read`, `secret` under `read` AND `reveal`. updated_at: $ref: "#/components/schemas/Timestamp" updated_by: type: string description: The principal whose act produced this occurrence. ValueList: type: object required: [items, count] properties: items: type: array items: $ref: "#/components/schemas/ValueCell" count: type: integer minimum: 0 findings: type: array description: | Secret-scanning warnings the declared config values produced (#74, Surface 1), warn-not-block. Absent or empty when nothing matched. items: $ref: "#/components/schemas/ScanFinding" Acknowledgements: type: array description: | Secret-scanning acknowledgement tokens (#74). On a value write, a keep-as-config token dismisses a Surface-1 warning for exactly that value. On a declaration write, one override token per finding is re-scanned against the current content; a stale, version-skewed, or surplus token is rejected by name. There is no blanket ignore-all input. items: type: string SetValueRequest: type: object additionalProperties: false required: [value] properties: value: type: string maxLength: 65536 description: | The plaintext. It is validated against the key's declaration before it commits, and stored sealed under the project data key with associated data binding it to this row alone. acknowledgements: $ref: "#/components/schemas/Acknowledgements" DeclareValuesRequest: type: object additionalProperties: false required: [key, environment_ids, value] properties: key: $ref: "#/components/schemas/KeyName" environment_ids: type: array minItems: 1 maxItems: 50 uniqueItems: true items: $ref: "#/components/schemas/ID" value: type: string maxLength: 65536 CopyValuesRequest: type: object additionalProperties: false required: [source_environment_id, keys, destination_environment_ids] properties: source_environment_id: $ref: "#/components/schemas/ID" keys: type: array minItems: 1 maxItems: 1000 uniqueItems: true description: | The keys to copy, by name. Explicit: an empty list quietly meaning "everything" is how a mistyped bulk apply becomes an incident, so it is refused instead. Naming a key twice is refused: it is one logical cell requested twice. items: $ref: "#/components/schemas/KeyName" destination_environment_ids: type: array minItems: 1 maxItems: 50 uniqueItems: true items: $ref: "#/components/schemas/ID" confirm_protected: type: boolean default: false description: | The protected-environment confirmation. A protected destination refuses the copy without it, naming the environment. CopyValuesResult: type: object required: [copied] properties: copied: type: array items: type: object required: [key, destination_environment_id] properties: key: $ref: "#/components/schemas/KeyName" destination_environment_id: $ref: "#/components/schemas/ID" findings: type: array description: | Secret-scanning warnings the copied config values produced (#74, Surface 1), warn-not-block. Absent or empty when nothing matched. items: $ref: "#/components/schemas/ScanFinding" ValueOccurrencesRequest: type: object additionalProperties: false required: [candidates] properties: candidates: type: array maxItems: 5000 uniqueItems: true description: | The candidate declarations this run intends to propose. Names the project already declares are answered from the catalogue; names it does not are answered as `declared: false` with a token binding the undeclared-and-absent state to the intended classification and type. items: $ref: "#/components/schemas/ValueOccurrenceCandidate" ValueOccurrenceCandidate: type: object additionalProperties: false required: [name, intended_classification, intended_type] properties: name: $ref: "#/components/schemas/KeyName" intended_classification: $ref: "#/components/schemas/KeyClassification" intended_type: type: string enum: [string, integer, boolean, enum, url, json] ValueOccurrence: type: object required: [name, declared, set, token] properties: key_id: $ref: "#/components/schemas/ID" name: $ref: "#/components/schemas/KeyName" declared: type: boolean description: | Whether the project declares this key at all. False for a candidate the run proposes that does not exist yet. classification: $ref: "#/components/schemas/KeyClassification" declared_type: type: string description: | The key catalogue's canonical textual type expression: a primitive, or `any_of(branch|branch)`. An importer uses the full expression to check its effective primitive against every permitted branch. set: type: boolean description: | The whole presence model: `set` or `absent`. There is no third state. token: type: string description: | The server-minted OPAQUE occurrence token for this (key, environment). Copy it into the run manifest; never parse it. It is keyed and scoped, so it is meaningful only within the environment that produced it, and a fabricated one is exactly as informative as a stale one. ValueOccurrenceList: type: object required: [items, definitions_revision, environment_id] properties: environment_id: $ref: "#/components/schemas/ID" definitions_revision: type: integer format: int64 description: | The project's key-catalogue revision as phase 1 observed it. The run manifest pins it, and phase 2 refuses a run whose declarations moved. items: type: array items: $ref: "#/components/schemas/ValueOccurrence" ImportValuesRequest: type: object additionalProperties: false required: [entries] properties: entries: type: array minItems: 1 maxItems: 5000 description: | The values file's entries. A key named twice is refused: it is one logical cell requested twice. items: type: object additionalProperties: false required: [key, value] properties: key: $ref: "#/components/schemas/KeyName" value: type: string maxLength: 65536 overwrite: type: array uniqueItems: true description: | The enumerated `set`-bucket consent. Skip-by-default otherwise. Naming a key this import does not carry is refused: a typo in a consent list is the one place a silent no-op is dangerous. items: $ref: "#/components/schemas/KeyName" precondition: $ref: "#/components/schemas/ImportPrecondition" ImportPrecondition: type: object additionalProperties: false required: [definitions_revision, environment_ids, occurrences] description: | The run manifest's expected-state half - the one declared additive input `values import` gained. Verified inside the import's own authorized transaction, after `read@project AND read@environment` is re-evaluated for every environment named here. properties: definitions_revision: type: integer format: int64 environment_ids: type: array minItems: 1 uniqueItems: true items: $ref: "#/components/schemas/ID" occurrences: type: array items: type: object additionalProperties: false required: [key, environment_id, token] properties: key: $ref: "#/components/schemas/KeyName" environment_id: $ref: "#/components/schemas/ID" token: type: string ImportValuesResult: type: object required: [imported, skipped] properties: imported: type: array items: $ref: "#/components/schemas/KeyName" skipped: type: array description: | Keys already `set` in the target environment that no enumerated overwrite named. Listed by name, never silently dropped. items: $ref: "#/components/schemas/KeyName" findings: type: array description: | Secret-scanning warnings the imported config values produced (#74, Surface 1, surface `import_value`), warn-not-block. items: $ref: "#/components/schemas/ScanFinding" ValueDiff: type: object required: [items, left_environment_id, right_environment_id] properties: left_environment_id: $ref: "#/components/schemas/ID" right_environment_id: $ref: "#/components/schemas/ID" items: type: array items: $ref: "#/components/schemas/ValueDiffRow" RevealDiffRequest: type: object additionalProperties: false required: [left, right] properties: left: $ref: "#/components/schemas/ID" right: $ref: "#/components/schemas/ID" ValueDiffRow: type: object required: [key_id, name, classification, left, right] properties: key_id: $ref: "#/components/schemas/ID" name: $ref: "#/components/schemas/KeyName" classification: $ref: "#/components/schemas/KeyClassification" left: $ref: "#/components/schemas/ValueCell" right: $ref: "#/components/schemas/ValueCell" equal: type: boolean description: | ABSENT where the comparison could not be made without disclosing something the caller may not see - both sides `set` with at least one unreadable. Whether two secrets match is itself material, so it is not answered as `false` either. DefinitionsBundle: type: object additionalProperties: false required: [format_version, environments, key_groups, keys] x-go-type: json.RawMessage properties: format_version: {type: integer, const: 1} base_revision: {type: integer, format: int64, minimum: 0} environments: type: array maxItems: 10000 items: {$ref: "#/components/schemas/DefinitionsBundleEntity"} key_groups: type: array maxItems: 10000 items: {$ref: "#/components/schemas/DefinitionsBundleEntity"} keys: type: array maxItems: 10000 items: {$ref: "#/components/schemas/DefinitionsBundleKey"} DefinitionsBundleEntity: type: object additionalProperties: false required: [name] properties: id: {$ref: "#/components/schemas/ID"} name: {type: string, minLength: 1, maxLength: 128} DefinitionsBundlePresence: type: object additionalProperties: false required: [mode, environments] properties: mode: {type: string, enum: [all, none, explicit]} environments: type: array maxItems: 50 uniqueItems: true items: {type: string, minLength: 1, maxLength: 128} DefinitionsBundleKey: type: object additionalProperties: false required: [name, folder_path, classification, description, deprecated, deprecation_note, group, declaration, required_in, forbidden_in] properties: id: {$ref: "#/components/schemas/ID"} name: {$ref: "#/components/schemas/KeyName"} folder_path: {$ref: "#/components/schemas/KeyFolderPath"} classification: {$ref: "#/components/schemas/KeyClassification"} description: {type: string, maxLength: 4096} deprecated: {type: boolean} deprecation_note: {type: string, maxLength: 4096} group: {type: string, maxLength: 128} declaration: {$ref: "#/components/schemas/KeyDeclaration"} required_in: {$ref: "#/components/schemas/DefinitionsBundlePresence"} forbidden_in: {$ref: "#/components/schemas/DefinitionsBundlePresence"} DefinitionsRename: type: object additionalProperties: false required: [id, from, to] properties: id: {type: string} from: {type: string} to: {type: string} DefinitionsKindDiff: type: object additionalProperties: false required: [creates, updates, renames, deletes] properties: creates: {type: array, items: {type: string}} updates: {type: array, items: {type: string}} renames: {type: array, items: {$ref: "#/components/schemas/DefinitionsRename"}} deletes: {type: array, items: {type: string}} DefinitionsDiff: type: object additionalProperties: false required: [environments, key_groups, keys, reveal_required] properties: environments: {$ref: "#/components/schemas/DefinitionsKindDiff"} key_groups: {$ref: "#/components/schemas/DefinitionsKindDiff"} keys: {$ref: "#/components/schemas/DefinitionsKindDiff"} reveal_required: {type: array, items: {type: string}} DefinitionsCheckResult: type: object additionalProperties: false required: [state, current_revision, differences] properties: state: {type: string, enum: [equal, file_ahead, db_ahead, diverged]} base_revision: {type: integer, format: int64, minimum: 0} current_revision: {type: integer, format: int64, minimum: 0} differences: {$ref: "#/components/schemas/DefinitionsDiff"} findings: type: array description: | Non-blocking secret-scanning results (#74 SS3): the credential-shaped author-controlled leaves of the submitted bundle. Check is a read-only dry-run, so these carry no acknowledgement token and persist nothing — they warn an operator that a `plan` would be refused. Omitted when the bundle is clean. items: {$ref: "#/components/schemas/ScanFinding"} DefinitionsKeyDeletion: type: object additionalProperties: false required: [name, live_in] properties: name: {type: string} live_in: {type: array, items: {type: string}} DefinitionsEnvironmentDeletion: type: object additionalProperties: false required: [name, occurrences] properties: name: {type: string} occurrences: {type: integer, format: int64, minimum: 0} DefinitionsPlanDiff: type: object additionalProperties: false required: [environments, key_groups, keys, key_deletions, env_deletions, reveal_required] properties: environments: {$ref: "#/components/schemas/DefinitionsKindDiff"} key_groups: {$ref: "#/components/schemas/DefinitionsKindDiff"} keys: {$ref: "#/components/schemas/DefinitionsKindDiff"} key_deletions: {type: array, items: {$ref: "#/components/schemas/DefinitionsKeyDeletion"}} env_deletions: {type: array, items: {$ref: "#/components/schemas/DefinitionsEnvironmentDeletion"}} reveal_required: {type: array, items: {type: string}} DefinitionsPlan: type: object additionalProperties: false required: [id, digest, current_revision, additive, expires_at, protected_environments, diff, deletions_present, reveal_required] properties: id: {$ref: "#/components/schemas/ID"} digest: {type: string, minLength: 64, maxLength: 64, pattern: "^[0-9a-f]{64}$"} base_revision: {type: integer, format: int64, minimum: 0} current_revision: {type: integer, format: int64, minimum: 0} additive: {type: boolean} expires_at: {$ref: "#/components/schemas/Timestamp"} protected_environments: {type: array, items: {type: string}} diff: {$ref: "#/components/schemas/DefinitionsPlanDiff"} deletions_present: {type: boolean} reveal_required: {type: array, items: {type: string}} DefinitionsPlanResponse: type: object additionalProperties: false required: [plan] properties: plan: {$ref: "#/components/schemas/DefinitionsPlan"} ApplyDefinitionsPlanRequest: type: object additionalProperties: false required: [allow_delete] properties: allow_delete: {type: boolean} digest: {type: string, maxLength: 64} commit: {type: string, maxLength: 256} ref: {type: string, maxLength: 256} actor: {type: string, maxLength: 256} acknowledgements: {$ref: "#/components/schemas/Acknowledgements"} ApplyDefinitionsPlanResult: type: object additionalProperties: false required: [revision, published, plan_id] properties: revision: {type: integer, format: int64, minimum: 0} published: {type: array, items: {type: string}} plan_id: {$ref: "#/components/schemas/ID"} DefinitionsLastApply: type: object additionalProperties: false required: [plan_id, applied_at, applied_by, revision] properties: plan_id: {$ref: "#/components/schemas/ID"} applied_at: {$ref: "#/components/schemas/Timestamp"} applied_by: {type: string} commit: {type: string, maxLength: 256} ref: {type: string, maxLength: 256} actor: {type: string, maxLength: 256} revision: {type: integer, format: int64, minimum: 0} DefinitionsSettings: type: object additionalProperties: false required: [definitions_source] properties: definitions_source: {type: string, enum: [db, git]} last_apply: {$ref: "#/components/schemas/DefinitionsLastApply"} SetDefinitionsSettingsRequest: type: object additionalProperties: false required: [definitions_source] properties: definitions_source: {type: string, enum: [db, git]} Environment: type: object description: | An environment carries NO `base` pointer and no defaults layer, here or anywhere: the flat-model ADR deleted both, and every value is explicit per environment. required: [id, org_id, project_id, name, display_order, created_at] properties: id: $ref: "#/components/schemas/ID" org_id: $ref: "#/components/schemas/ID" project_id: $ref: "#/components/schemas/ID" name: type: string display_order: type: integer minimum: 0 description: | The environment's position in the project's display order. A real mutable property, rewritten as a whole set by `PUT .../environments/order`; ties break on name. created_at: $ref: "#/components/schemas/Timestamp" EnvironmentList: type: object required: [items, count] properties: items: type: array items: $ref: "#/components/schemas/Environment" count: type: integer minimum: 0 description: Total rows matching, which for an unpaged list equals `items` length. EnvironmentOrderRequest: type: object additionalProperties: false required: [environment_ids] properties: environment_ids: type: array description: | Every environment in the project, exactly once, in the order they should display. The whole set — not a subset — so no transaction can leave two environments sharing a position. # A project with no environments has the empty list as its exact # whole set, so `[]` is a legal no-op rather than a schema error — # the service accepts it and the schema must agree, or the two layers # disagree about what "the whole set" means. minItems: 0 # The upper bound IS the environment-count cap: a project cannot hold # more environments than this, so it cannot legitimately order more. maxItems: 50 items: $ref: "#/components/schemas/ID" FolderPath: type: string description: | A slash-separated namespace: no leading or trailing separator, no empty segment, no `.` or `..` segment, at most 32 segments. Organizational only — no grant is scoped to a folder and no value attaches to one. **Two bounds the schema cannot express, both server-authoritative and both measured in UTF-8 BYTES:** the whole path is at most 256 bytes (`maxLength` below counts code points), and EACH SEGMENT is at most 128 bytes — the same bound entity names carry. A 129-character ASCII segment satisfies this schema and is refused with `bad_request`. minLength: 1 maxLength: 256 CreateFolderRequest: type: object additionalProperties: false required: [path] properties: path: $ref: "#/components/schemas/FolderPath" acknowledgements: $ref: "#/components/schemas/Acknowledgements" RenameFolderRequest: type: object additionalProperties: false required: [path] properties: path: $ref: "#/components/schemas/FolderPath" acknowledgements: $ref: "#/components/schemas/Acknowledgements" Folder: type: object required: [id, org_id, project_id, path, created_at] properties: id: $ref: "#/components/schemas/ID" org_id: $ref: "#/components/schemas/ID" project_id: $ref: "#/components/schemas/ID" path: $ref: "#/components/schemas/FolderPath" created_at: $ref: "#/components/schemas/Timestamp" FolderList: type: object required: [items, count] properties: items: type: array items: $ref: "#/components/schemas/Folder" count: type: integer minimum: 0 description: Total rows matching, which for an unpaged list equals `items` length. ServiceAccountKind: type: string description: | The closed set a service account may be created with. Declared at creation and IMMUTABLE afterwards. enum: [workload, automation] CredentialKind: type: string description: | How a credential authenticates its service account. The discriminator exists before a second kind does, so adding one is a new row type rather than a schema change. enum: [hikyo-token, oidc-federation] CredentialLifetime: type: string description: | `indefinite` is a VALUE, not a large number: it is unreachable by raising the instance ceiling, and is admitted only under a separate, default-off instance opt-in. enum: [finite, indefinite] CreateServiceAccountRequest: type: object additionalProperties: false required: [name, kind] properties: name: type: string minLength: 1 maxLength: 64 kind: $ref: "#/components/schemas/ServiceAccountKind" ServiceAccount: type: object additionalProperties: false required: [id, principal_id, name, kind, created_at, created_by, live_credentials] properties: id: $ref: "#/components/schemas/ID" principal_id: $ref: "#/components/schemas/ID" name: type: string maxLength: 64 kind: $ref: "#/components/schemas/ServiceAccountKind" created_at: $ref: "#/components/schemas/Timestamp" created_by: $ref: "#/components/schemas/ID" live_credentials: type: integer minimum: 0 description: How many of its credentials currently authenticate. ServiceAccountList: type: object additionalProperties: false required: [items, count] properties: items: type: array items: $ref: "#/components/schemas/ServiceAccount" count: type: integer minimum: 0 MachineCredential: type: object additionalProperties: false required: [id, kind, lifetime, created_at, created_by, expiring_soon] description: | Metadata only. There is deliberately no value property: a credential value is displayed exactly once, at mint, and is never retrievable afterwards. A row of kind `oidc-federation` carries no `prefix_hint` — a binding has no minted value to hint at — and carries the binding members instead. properties: id: $ref: "#/components/schemas/ID" kind: $ref: "#/components/schemas/CredentialKind" prefix_hint: type: string maxLength: 32 description: | The non-secret leading slice of the minted value - the grammar prefix plus a few body characters, enough to tell two live credentials apart and far short of anything a search can narrow. Absent for an `oidc-federation` row. issuer: type: string maxLength: 512 description: "`oidc-federation` only: the byte-exact issuer this binding trusts." subject: type: string maxLength: 512 description: "`oidc-federation` only: the byte-exact `sub` it names." audience: type: string maxLength: 512 description: "`oidc-federation` only: the audience it accepts, never the issuer's default." required_claims: type: array maxItems: 16 items: $ref: "#/components/schemas/FederatedClaimPin" description: "`oidc-federation` only: every pinned claim, all of which validation requires." reactivated_at: allOf: - $ref: "#/components/schemas/Timestamp" description: | `oidc-federation` only, and null unless this binding has been through a restore. When set, the binding permanently refuses any token whose `iat` is not strictly after this instant plus the maximum accepted positive clock skew. lifetime: $ref: "#/components/schemas/CredentialLifetime" expires_at: $ref: "#/components/schemas/Timestamp" created_at: $ref: "#/components/schemas/Timestamp" created_by: $ref: "#/components/schemas/ID" revoked_at: $ref: "#/components/schemas/Timestamp" last_used_at: $ref: "#/components/schemas/Timestamp" expiring_soon: type: boolean description: | The in-product expiry warning, computed rather than stored - a stored flag would be stale the moment the clock moved past it. MachineCredentialList: type: object additionalProperties: false required: [items, count] properties: items: type: array items: $ref: "#/components/schemas/MachineCredential" count: type: integer minimum: 0 MintCredentialRequest: type: object additionalProperties: false description: | The zero request asks for the finite default, which is the rule that the easy path is bounded and a long-lived credential is a typed choice someone made. properties: indefinite: type: boolean description: | Selects the distinct typed lifetime. A separate property rather than a sentinel duration, because `indefinite` must be unreachable by raising any ceiling. Naming it together with `lifetime_seconds` is refused rather than resolved by precedence: a silent precedence rule on a credential is exactly the quiet ambiguity fail-loud exists to prevent. lifetime_seconds: type: integer minimum: 1 description: Requested finite lifetime; absent means the instance default. MintCredentialResult: type: object additionalProperties: false required: [value, credential, clamped, expires_at] properties: value: type: string description: | The credential value, returned EXACTLY ONCE to exactly one caller. No other route in this contract returns it. credential: $ref: "#/components/schemas/MachineCredential" expires_at: type: [string, "null"] format: date-time description: Effective expiry; null means explicitly indefinite. clamped: type: boolean description: | True when the instance ceiling shortened what was asked for, so the caller can say so rather than let the operator discover it when the credential dies early. IssuerType: type: string description: | The federation issuer's platform. It is DECLARED rather than inferred from the issuer URL, because the per-platform binding rules differ — a Forgejo or GitHub Actions binding MUST pin `event_name`, a Kubernetes one has no such claim — and inferring the type from a URL would let renaming a deployment change the security rules that apply to it. enum: [kubernetes, forgejo, github-actions] x-enum-varnames: [IssuerTypeKubernetes, IssuerTypeForgejo, IssuerTypeGithubActions] JWKSMode: type: string description: | Where the issuer's signing keys come from. `discovery` is the default: keys are fetched and cached with a bounded staleness window. `static` is the configured alternative for air-gapped installations and for deployments whose issuer discovery endpoint this instance cannot reach — deliberately not the default, because a static-only installation breaks silently on the day someone rotates the issuer's keys. enum: [discovery, static] FederationIssuer: type: object additionalProperties: false required: [id, issuer, issuer_type, jwks_mode, refused_audiences, created_at, created_by, live_bindings, ca_bundle_configured] description: | One instance-scoped issuer configuration. There is deliberately no `static_jwks` property on the READ shape: the document is configuration an operator supplied and can re-supply, nothing needs it back, and a read surface that returned it would carry a key document for no reason. properties: id: $ref: "#/components/schemas/ID" issuer: type: string maxLength: 512 description: | The byte-exact `iss`. Nothing folds case, resolves the URL or strips a trailing slash: OpenID Connect defines `iss` as case-sensitive, so any normalization could merge two distinct external issuers into one configuration. issuer_type: $ref: "#/components/schemas/IssuerType" jwks_mode: $ref: "#/components/schemas/JWKSMode" refused_audiences: type: array minItems: 1 maxItems: 16 items: type: string maxLength: 512 description: | The issuer's DEFAULT audiences, which no binding may name and which no token may carry. At least one is required: a Kubernetes API-server audience is whatever that cluster was configured with and is not derivable, so an empty list would silently accept the audience that must be refused. created_at: $ref: "#/components/schemas/Timestamp" created_by: $ref: "#/components/schemas/ID" updated_at: $ref: "#/components/schemas/Timestamp" updated_by: $ref: "#/components/schemas/ID" ca_bundle_configured: type: boolean description: Whether discovery uses an issuer-specific CA bundle instead of system roots. live_bindings: type: integer minimum: 0 description: | How many bindings name this issuer, LIVE OR HISTORICAL, so an operator sees the blast radius before attempting a delete — which is refused while any remain. Revoked bindings count: erasing the issuer a past binding trusted erases what it trusted, and the foreign key would refuse the delete anyway with a driver message instead of a reason. FederationIssuerList: type: object additionalProperties: false required: [items, count] properties: items: type: array items: $ref: "#/components/schemas/FederationIssuer" count: type: integer minimum: 0 CreateFederationIssuerRequest: type: object additionalProperties: false required: [issuer, issuer_type, jwks_mode, refused_audiences] properties: issuer: type: string minLength: 9 maxLength: 512 description: | The byte-exact `iss`, `https://` only. Discovery and JWKS are fetched from this URL, so an `http` issuer would rest the instance's whole federation trust on whoever holds the network path. issuer_type: $ref: "#/components/schemas/IssuerType" jwks_mode: $ref: "#/components/schemas/JWKSMode" ca_bundle_pem: type: string maxLength: 65536 description: | PEM CA certificates for discovery and JWKS HTTPS requests, including redirects. Replaces system roots for this issuer only. Nonempty values are refused in static mode. On update, omission preserves the current bundle in discovery mode; an empty string clears it. Static mode clears the bundle. This field is write-only; reads report ca_bundle_configured. static_jwks: type: string maxLength: 1048576 description: | The JWKS document, required under `static` mode and refused under `discovery`. The pairing is total: a document stored but unused is a key set nobody rotates. refused_audiences: type: array minItems: 1 maxItems: 16 items: type: string maxLength: 512 UpdateFederationIssuerRequest: type: object additionalProperties: false required: [jwks_mode, refused_audiences] description: | The MUTABLE half only. `issuer` and `issuer_type` are absent on purpose: changing either would silently re-point every binding underneath at a different external authority, which is a replacement, not an edit. properties: jwks_mode: $ref: "#/components/schemas/JWKSMode" ca_bundle_pem: type: string maxLength: 65536 description: | PEM CA certificates for discovery and JWKS HTTPS requests, including redirects. Replaces system roots for this issuer only. Nonempty values are refused in static mode. On update, omission preserves the current bundle in discovery mode; an empty string clears it. Static mode clears the bundle. This field is write-only; reads report ca_bundle_configured. static_jwks: type: string maxLength: 1048576 refused_audiences: type: array minItems: 1 maxItems: 16 items: type: string maxLength: 512 FederatedClaimPin: type: object additionalProperties: false required: [claim] description: | One pinned claim, as a DISCRIMINATED scalar rather than a free-form JSON value. Exactly one of `string_value`, `number_value` or `bool_value` is set, and that is what makes "a string is never folded to a number" true at the wire boundary rather than only inside the validator: `repository_id: 123` and `repository_id: "123"` are two different requests that cannot be mistaken for one another, and a 64-bit repository id survives without passing through a float. A pin naming none or several values is refused rather than resolved by precedence. properties: claim: type: string minLength: 1 maxLength: 128 string_value: type: string maxLength: 512 number_value: type: integer format: int64 bool_value: type: boolean CreateBindingRequest: type: object additionalProperties: false required: [issuer, subject, audience, required_claims] description: | A binding names exactly one service account, matched byte-for-byte on `(issuer, subject)`. There are no wildcards, no namespace patterns, no path prefixes, no case folding and no automatic provisioning: a pattern rule such as "any ServiceAccount in namespace prod" would hand a Hikyo principal to anyone holding `create serviceaccount` in that namespace, a far wider group than cluster-admin. properties: issuer: type: string maxLength: 512 description: The byte-exact `iss` of a configured issuer. subject: type: string minLength: 1 maxLength: 512 audience: type: string minLength: 1 maxLength: 512 description: | Mandatory, and may not be the issuer's default. A Kubernetes token minted for the default API-server audience would otherwise authenticate here, and a Forgejo Actions audience defaults to `/`, shared across every repository that owner has. required_claims: type: array minItems: 1 maxItems: 16 items: $ref: "#/components/schemas/FederatedClaimPin" description: | Every pinned claim, all of which validation requires. A Forgejo or GitHub Actions binding MUST pin `event_name`: `pull_request_target` carries the ordinary ref-form subject — the default branch's subject, the one a production binding names — so the protection comes from the pinned event, never from the subject's shape. indefinite: type: boolean lifetime_seconds: type: integer minimum: 1 replaces: allOf: - $ref: "#/components/schemas/ID" description: | The binding this one supersedes. Bindings are IMMUTABLE, so every change is a replacement mint carrying the full formula: the predecessor is revoked and the successor inserted in one transaction. FederatedBinding: type: object additionalProperties: false required: [credential, issuer_id, clamped] properties: credential: $ref: "#/components/schemas/MachineCredential" issuer_id: $ref: "#/components/schemas/ID" clamped: type: boolean description: True when the instance ceiling shortened the requested lifetime. replaced_id: allOf: - $ref: "#/components/schemas/ID" description: The predecessor this mint revoked, absent for a first issue. DeliveredKey: type: object additionalProperties: false required: [key_id, name, classification, presence] description: | One key as the machine surface delivers it. `value` is present IFF the plaintext was actually delivered to this caller; its absence means presence-only — the key exists and the snapshot delivers it, but this caller was not authorized to receive the plaintext (a `secret` key without `reveal`), so no plaintext crossed. Whether a value crosses is decided server-side per key: `config` under `read`, `secret` under `read ∧ reveal` (or `read ∧ reveal-history` for a pinned non-current revision). properties: key_id: $ref: "#/components/schemas/ID" name: type: string maxLength: 256 classification: $ref: "#/components/schemas/KeyClassification" value: type: string maxLength: 65536 description: | The delivered plaintext. Present ONLY when the caller was authorized to receive it (see the schema description); absent for a presence-only key. The maxLength matches the value-write bound. presence: type: string enum: [required, forbidden, optional, set] description: | What the fetch reports about this key in the addressed environment. `set` is a key the committed snapshot actually delivers (#51 -- values landed, and the change token moves with them); the other three are the declared presence rules for keys the snapshot does not carry. DeliveryResponse: type: object additionalProperties: false required: [current, cursor, change_token, schema_revision, revision, keys, pin_expired, issued_at, snapshot_expires_at, credential_id] description: | Either the authorized projection, or the statement that the presented cursor is current — in which case `keys` is empty and NOTHING was disclosed. Only a fetch that actually delivers values is a disclosure. properties: credential_id: allOf: - $ref: "#/components/schemas/ID" description: | The authenticated caller's credential identifier, returned on both dispositions so clients can bind snapshots and offline records to the server-asserted identity rather than infer it locally. current: type: boolean description: | True when the presented cursor named the state the server was about to serve. cursor: type: string maxLength: 128 description: | The opaque cursor for the state this answer describes, returned on BOTH dispositions so a caller told "current" can keep polling without re-fetching to learn its own cursor. It is bound to the change token, the caller's authorized delivery projection, the principal's authorization revision, the pin generation, and the projection mode — never to content alone. Any authorization or projection movement therefore invalidates it and produces a full authorized delivery rather than a "current" answer. change_token: type: string maxLength: 128 description: | The keyed delivery-manifest token, `v1:`-prefixed. It is change-detection material ONLY — the conditional-fetch cursor's input — and is never itself a workload-visible value (k8s ADR § Declared amendment): what reaches a pod annotation is the consumer's client-side per-target keyed stamp, not this token. Keyed rather than a content digest, so it is unforgeable and un-invertible without the server key and may flow into logs and change-detection caches as ordinary non-secret metadata. credential_expires_at: $ref: "#/components/schemas/Timestamp" description: | The presenting credential's expiry when it is finite — a bearer credential's `expires_at`, a federated binding's expiry. ABSENT for an indefinite credential. The operator surfaces it as the ADR's ahead-of-time expiry condition and event. schema_revision: type: integer minimum: 0 description: The project's monotonic key-catalogue revision. revision: type: integer format: int64 minimum: 0 description: The committed snapshot selected by this fetch; zero before first publication. pinned_revision: type: integer format: int64 minimum: 1 description: Present when a durable workload pin selected the snapshot. pin_expired: type: boolean description: Loud status; expiry never silently changes delivery. keys: type: array items: $ref: "#/components/schemas/DeliveredKey" description: Empty when `current` is true. issued_at: $ref: "#/components/schemas/Timestamp" snapshot_expires_at: $ref: "#/components/schemas/Timestamp" OfflineDeliveryRecord: type: object additionalProperties: false required: [record_id, key_id, key_name, classification, occurred_at, credential_id, generation, served_from] properties: record_id: type: string minLength: 1 maxLength: 64 key_id: type: string minLength: 1 maxLength: 64 key_name: type: string minLength: 1 maxLength: 256 classification: $ref: "#/components/schemas/KeyClassification" occurred_at: $ref: "#/components/schemas/Timestamp" credential_id: type: string minLength: 1 maxLength: 64 generation: type: string minLength: 1 maxLength: 64 served_from: $ref: "#/components/schemas/Timestamp" ReconcileOfflineRecordsRequest: type: object additionalProperties: false required: [records] properties: records: type: array minItems: 1 maxItems: 1000 items: $ref: "#/components/schemas/OfflineDeliveryRecord" ReconcileOfflineRecordsResponse: type: object additionalProperties: false required: [accepted, duplicates] properties: accepted: type: integer minimum: 0 duplicates: type: integer minimum: 0 # Delivery-target condition reporting (#788). Every string below is a # closed enum or a fixed grammar, pinned to internal/deliverytarget by # TestDeliveryTargetContractIsValueFree. KubernetesUID: type: string description: A Kubernetes object UID, canonical lower-case RFC 4122 form. maxLength: 36 pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$' KubernetesNamespace: type: string description: A namespace name (DNS-1123 label). A display label, never interpreted. maxLength: 63 pattern: '^[a-z0-9]([-a-z0-9]*[a-z0-9])?$' KubernetesObjectName: type: string description: An object name (DNS-1123 subdomain). A display label, never interpreted. maxLength: 253 pattern: '^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$' DeliveryTargetIdentity: type: object description: | The target's identity under the reporting principal: the cluster (the `kube-system` namespace UID), the `HikyoInstance` UID and the target object's UID. A recreated object is a new target. additionalProperties: false required: [cluster_id, instance_uid, uid] properties: cluster_id: {$ref: "#/components/schemas/KubernetesUID"} instance_uid: {$ref: "#/components/schemas/KubernetesUID"} uid: {$ref: "#/components/schemas/KubernetesUID"} DeliveryTargetRef: type: object description: The target's identity plus the namespace and name an operator types into `kubectl`. additionalProperties: false required: [cluster_id, instance_uid, namespace, name, uid] properties: cluster_id: {$ref: "#/components/schemas/KubernetesUID"} instance_uid: {$ref: "#/components/schemas/KubernetesUID"} namespace: {$ref: "#/components/schemas/KubernetesNamespace"} name: {$ref: "#/components/schemas/KubernetesObjectName"} uid: {$ref: "#/components/schemas/KubernetesUID"} DeliveryTargetCondition: type: object description: | One asserted condition. `type` and `reason` are bounded by the Kubernetes condition grammar (k8s.io/apimachinery `metav1.Condition` Type and Reason validation), not enumerated: a value outside the vocabulary `/meta` advertises (`delivery-target-report/`) refuses the whole report with 422 naming the member, so the refusal is recorded on the target's row. A string outside the grammar is a 400. additionalProperties: false required: [type, status, reason, observed_generation] properties: type: type: string minLength: 1 maxLength: 316 pattern: '^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$' status: type: string enum: ["False", "True", Unknown] reason: type: string minLength: 1 maxLength: 1024 pattern: '^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$' observed_generation: type: integer format: int64 minimum: 0 DeliveryTargetLifecycle: type: string description: The target's closed `status.lifecycle`. enum: [Refused, Retained, Scrubbed, Synced, Unreconciled] DeliveryTargetReporter: type: object description: The reporting integration, from a closed enum, and its SemVer 2.0 version. Never a free string. additionalProperties: false required: [integration, version] properties: integration: type: string enum: [kubernetes-operator] version: type: string maxLength: 64 pattern: '^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$' DeliveryTargetReportRequest: type: object description: | One closed, value-free report (ADR D4). It carries no condition message, event text, secret value, key name, cursor or credential material; there is no member that could. additionalProperties: false required: [vocabulary, target, generation, observed_generation, reported_at, report_interval_seconds, lifecycle, conditions, reporter] properties: vocabulary: type: integer minimum: 1 description: The vocabulary version `/meta` advertised. One this server does not accept is a 422. target: {$ref: "#/components/schemas/DeliveryTargetRef"} generation: type: integer format: int64 minimum: 1 observed_generation: type: integer format: int64 minimum: 0 reported_at: $ref: "#/components/schemas/Timestamp" report_interval_seconds: type: integer format: int64 minimum: 1 description: The target's heartbeat interval. The server clamps it to [300, 86400]. lifecycle: {$ref: "#/components/schemas/DeliveryTargetLifecycle"} conditions: type: array maxItems: 10 items: {$ref: "#/components/schemas/DeliveryTargetCondition"} reporter: {$ref: "#/components/schemas/DeliveryTargetReporter"} DeliveryTargetTombstoneRequest: type: object additionalProperties: false required: [target] properties: target: {$ref: "#/components/schemas/DeliveryTargetIdentity"} DeliveryTargetState: type: string description: | Derived at read time, never stored (ADR D5). `unknown` is the absence of a row, so no row carries it. enum: [refused, reported, reporter-revoked, stale] DeliveryTarget: type: object description: One target's latest accepted report, as its controller asserted it. additionalProperties: false required: [id, principal_id, target, vocabulary, generation, observed_generation, reported_at, received_at, report_interval_seconds, lifecycle, conditions, reporter, state] properties: id: {type: string} principal_id: {type: string} target: {$ref: "#/components/schemas/DeliveryTargetRef"} vocabulary: {type: integer, minimum: 1} generation: {type: integer, format: int64, minimum: 1} observed_generation: {type: integer, format: int64, minimum: 0} reported_at: {$ref: "#/components/schemas/Timestamp"} received_at: {$ref: "#/components/schemas/Timestamp"} report_interval_seconds: {type: integer, format: int64, minimum: 300, maximum: 86400} lifecycle: {$ref: "#/components/schemas/DeliveryTargetLifecycle"} conditions: type: array items: {$ref: "#/components/schemas/DeliveryTargetCondition"} reporter: {$ref: "#/components/schemas/DeliveryTargetReporter"} state: {$ref: "#/components/schemas/DeliveryTargetState"} refusal: type: object description: Present only while `state` is `refused`. The closed cause, never the refused value. additionalProperties: false required: [cause, refused_at] properties: cause: type: string enum: [vocabulary] refused_at: {$ref: "#/components/schemas/Timestamp"} DeliveryTargetPrincipal: type: object description: The server-observed layer for one reporting principal (ADR D2). additionalProperties: false required: [principal_id] properties: principal_id: {type: string} last_contact_at: $ref: "#/components/schemas/Timestamp" description: Its last authenticated delivery fetch in this environment. Absent when never observed. quota_refused_at: $ref: "#/components/schemas/Timestamp" description: The closed `quota-refused` notice's last time. Absent when never refused. DeliveryTargetList: type: object additionalProperties: false required: [principals, targets] properties: principals: type: array items: {$ref: "#/components/schemas/DeliveryTargetPrincipal"} targets: type: array items: {$ref: "#/components/schemas/DeliveryTarget"} InstanceConfigBinding: type: object additionalProperties: false required: [org_id, project_id, environment_id, schema_version] properties: org_id: {type: string} project_id: {type: string} environment_id: {type: string} schema_version: {type: integer, minimum: 1} InstanceConfigNode: type: object additionalProperties: false required: [node_id, active_generation, active_revision, state, updated_at] properties: node_id: {type: string} active_generation: {type: integer, format: int64, minimum: 0} active_revision: {type: [integer, "null"], format: int64, minimum: 1} state: {type: string, enum: [active, pending, unknown, fenced]} updated_at: {type: string, format: date-time} error: {type: string} InstanceConfigJob: type: object additionalProperties: false required: [id, state, revision, generation, created_at] properties: deployment_restore_pending: {type: boolean, description: Deployment restoration was authorized and awaits the controller receipt.} deployment_restored: {type: boolean, description: The controller verified restored deployment resources. A separate configuration repair Apply is still required.} prepared: {type: boolean, description: Exact fixed participants and any deployment plan are ready for administrator authorization.} plan_digest: {type: string, pattern: "^[a-f0-9]{64}$", description: Exact prepared deployment plan authorized by this decision.} id: {type: string} state: {type: string, enum: [preparing, pending, partial, completed, failed]} revision: {type: integer, format: int64, minimum: 1} generation: {type: integer, format: int64, minimum: 0} created_at: {type: string, format: date-time} completed_at: {type: string, format: date-time} error: {type: string} InstanceConfigStatus: type: object additionalProperties: false required: [owner_instance_id, managed, binding, generation, desired_revision, latest_revision, state, nodes, job] properties: owner_instance_id: {type: string} managed: {type: boolean} binding: oneOf: - $ref: "#/components/schemas/InstanceConfigBinding" - type: "null" generation: {type: integer, format: int64, minimum: 0} desired_revision: {type: [integer, "null"], format: int64, minimum: 1} latest_revision: {type: [integer, "null"], format: int64, minimum: 1} state: {type: string, enum: [unmanaged, active, pending, partial, recovery_required]} nodes: type: array items: $ref: "#/components/schemas/InstanceConfigNode" job: oneOf: - $ref: "#/components/schemas/InstanceConfigJob" - type: "null" InstanceConfigAdoptionPreview: type: object additionalProperties: false required: [owner_instance_id, schema_version, configured_keys, warnings, preview_token] properties: owner_instance_id: {type: string} schema_version: {type: integer, minimum: 1} configured_keys: type: array items: {type: string} warnings: type: array items: {type: string} preview_token: {type: string, minLength: 1} InstanceConfigAdoptRequest: type: object additionalProperties: false required: [preview_token, idempotency_key] properties: preview_token: {type: string, minLength: 1} idempotency_key: {type: string, minLength: 1, maxLength: 128} InstanceConfigApplyRequest: type: object additionalProperties: false required: [revision, expected_generation, idempotency_key, schema_version, confirm_restored_credentials] properties: restore_deployment: {type: boolean, default: false, description: Restore the partial controlled deployment with exact rollout-restore MFA. The desired runtime revision remains fenced until a separate repair Apply.} plan_digest: {type: string, pattern: "^[a-f0-9]{64}$", description: Exact prepared deployment plan authorized by this decision.} prepare_only: {type: boolean, default: false, description: Prepare and return the exact plan for review without committing or consuming reauthentication.} revision: {type: integer, format: int64, minimum: 1} expected_generation: {type: integer, format: int64, minimum: 0} idempotency_key: {type: string, minLength: 1, maxLength: 128} schema_version: {type: integer, minimum: 1} confirm_restored_credentials: {type: boolean, description: Required true only when clearing the restore fence after reviewing credentials and reconciling access grants.} InstanceConfigMailTestRequest: type: object additionalProperties: false required: [revision, expected_generation, schema_version, to] properties: revision: {type: integer, format: int64, minimum: 1} expected_generation: {type: integer, format: int64, minimum: 0} schema_version: {type: integer, minimum: 1} to: {type: string, format: email} InstanceConfigMailTestResult: type: object additionalProperties: false required: [revision, sent] properties: revision: {type: integer, format: int64, minimum: 1} sent: {type: boolean} CredentialPolicy: type: object additionalProperties: false required: [max_finite_lifetime_seconds, allow_indefinite, max_live_credentials] properties: max_finite_lifetime_seconds: type: integer minimum: 1 allow_indefinite: type: boolean max_live_credentials: type: integer minimum: 1 updated_at: $ref: "#/components/schemas/Timestamp" updated_by: $ref: "#/components/schemas/ID" SetCredentialPolicyRequest: type: object additionalProperties: false required: [max_finite_lifetime_seconds, allow_indefinite, max_live_credentials] properties: max_finite_lifetime_seconds: type: integer minimum: 1 allow_indefinite: type: boolean max_live_credentials: type: integer minimum: 1 confirm: type: boolean description: | Acknowledges the enumerated affected credentials. A tightening refuses with 409 until this is true, so a settings change never silently kills a live credential. AffectedCredential: type: object additionalProperties: false required: [id, service_account_id, reason] properties: id: $ref: "#/components/schemas/ID" service_account_id: $ref: "#/components/schemas/ID" expires_at: $ref: "#/components/schemas/Timestamp" reason: type: string enum: [clamped, indefinite-withdrawn] CredentialPolicyResult: type: object additionalProperties: false required: [applied, policy, affected, clamped_count] properties: applied: type: boolean description: | False when the request was a PREVIEW: a tightening whose affected credentials the caller has not acknowledged with `confirm`. Nothing was written, and `affected` is the enumeration the caller must see before asking again. A settings change never silently kills a live credential. policy: $ref: "#/components/schemas/CredentialPolicy" affected: type: array items: $ref: "#/components/schemas/AffectedCredential" clamped_count: type: integer minimum: 0 Capability: type: string description: | One atom from the permission ADR's CLOSED capability set. The server refuses anything outside it rather than storing a row nothing can ever evaluate, so this is a bounded string rather than an enum only to keep the contract additive as later tickets register their atoms. minLength: 1 maxLength: 64 pattern: "^[a-z][a-z-]{0,63}$" RoleTemplate: type: string description: The closed v1 role template set. enum: [viewer, editor, publisher, revealer, historian, maintainer, admin, operator] CreateGrantRequest: type: object additionalProperties: false required: [principal, capability] properties: principal: $ref: "#/components/schemas/ID" capability: $ref: "#/components/schemas/Capability" ApplyTemplateRequest: type: object additionalProperties: false required: [principal, template] properties: principal: $ref: "#/components/schemas/ID" template: $ref: "#/components/schemas/RoleTemplate" GrantOrigin: type: object additionalProperties: false required: [kind, subject] description: | One origin holding a grant row alive. A row exists while at least one origin holds it and is revoked, with the session-generation advance, when its last origin is released. Origins are never consulted by authorization: authority is the bare (principal, capability, scope) triple. properties: kind: type: string x-extensible-enum: [manual, break-glass, scim, structural, lockout-retention] subject: type: string description: | The origin's holder identity, discriminated by kind: the granting principal for `manual`, the fixed local-host marker for `break-glass`, the binding id for `scim`/`structural`. maxLength: 128 Grant: type: object additionalProperties: false required: [id, principal_id, capability, scope, origins, created_at] properties: id: $ref: "#/components/schemas/ID" principal_id: $ref: "#/components/schemas/ID" principal_name: type: string description: Current display name or username of this authorized member; not an identifier. capability: $ref: "#/components/schemas/Capability" scope: $ref: "#/components/schemas/GrantScope" origins: type: array description: The origin chips for this capability line. items: $ref: "#/components/schemas/GrantOrigin" created_at: $ref: "#/components/schemas/Timestamp" GrantScope: type: object additionalProperties: false description: | The scope the grant was made at. All three absent is instance scope. Grants inherit downward, so a grant at a scope applies to everything beneath it. properties: org_id: $ref: "#/components/schemas/ID" project_id: $ref: "#/components/schemas/ID" environment_id: $ref: "#/components/schemas/ID" GrantList: type: object additionalProperties: false required: [items, count] properties: items: type: array items: $ref: "#/components/schemas/Grant" count: type: integer minimum: 0 description: Total rows matching, which for an unpaged list equals `items` length. GrantResult: type: object additionalProperties: false required: [grant_id, capability, outcome] properties: grant_id: $ref: "#/components/schemas/ID" capability: $ref: "#/components/schemas/Capability" outcome: type: string enum: [created, origin_added, unchanged] x-go-type: api.GrantOutcome x-go-type-import: path: github.com/Hikyo-Org/hikyo/api name: api description: > The only possible mutation results: a new grant row, a new origin on an existing row, or an idempotent repeat that changed nothing. GrantResultList: type: object additionalProperties: false required: [items, count] properties: items: type: array items: $ref: "#/components/schemas/GrantResult" count: type: integer minimum: 0 EnvironmentSettings: type: object additionalProperties: false required: [protected] properties: protected: type: boolean reauth_window_seconds: type: [integer, "null"] minimum: 0 description: | The environment's own reauthentication window. Null or absent means it inherits the instance default - which is NOT the same statement as 0, and 0 is a legal value meaning every disclosure reauthenticates. MachineRevealSettings: type: object additionalProperties: false required: [enabled] properties: enabled: type: boolean description: | True while workload and automation principals in this project may hold `reveal` and receive secret plaintext on fetch. RetentionPolicy: type: object additionalProperties: false required: [mode] properties: mode: type: string enum: [keep-if-either, unlimited] max_age_seconds: type: [integer, "null"] minimum: 1 description: Required for keep-if-either; absent for unlimited. last_revisions: type: [integer, "null"] minimum: 1 description: Required for keep-if-either; absent for unlimited. OpsDiagnosticFinding: type: object additionalProperties: false required: [code, severity, message] properties: code: type: string severity: type: string enum: [ok, warn, error, unknown] message: type: string RetentionHealth: type: object additionalProperties: false required: [last_prune_success, stale, stale_after_seconds, peak_project_bytes, storage_warn, backup, adapter_targets_failed, adapter_targets_paused, adapter_targets_attention, adapter_jobs_queued] properties: diagnostics: type: array description: Instance operational findings. Unknown means the signal is not authoritatively measured, never healthy by default. No private custody or tenant identities. items: $ref: "#/components/schemas/OpsDiagnosticFinding" last_prune_success: type: [string, "null"] format: date-time stale: type: boolean stale_after_seconds: type: integer const: 86400 peak_project_bytes: type: integer description: >- Largest per-project stored payload across the instance, in bytes (ciphertext of value cells plus published snapshot entries). The per-project storage high-water surface. storage_warn: type: boolean description: >- True once peak_project_bytes reaches the 1 GiB warn threshold, well before the 4 GiB publish refusal. backup: $ref: "#/components/schemas/BackupHealth" adapter_targets_failed: type: integer minimum: 0 description: Active deployment-adapter targets whose last attempt failed and that are not paused (#157). adapter_targets_paused: type: integer minimum: 0 description: Active deployment-adapter targets an operator has paused. adapter_targets_attention: type: integer minimum: 0 description: Active deployment-adapter targets whose destination drifted from the ownership ledger and need an operator. adapter_jobs_queued: type: integer minimum: 0 description: Deployment-adapter outbox jobs waiting to be claimed, instance-wide. BackupHealth: type: object additionalProperties: false description: >- Disaster-recovery health: the latest successful export, its age against the configured recovery point objective, the latest failure, and the latest restore drill. Names archives and versions only; never a recipient, an identity or a key. required: [scheduled, last_success_at, artifact_age_seconds, rpo_seconds, rpo_exceeded, last_failure_at, last_failure_reason, last_prune_at, last_drill_at, last_drill_ok, drill_stale] properties: scheduled: type: boolean description: True when an export policy (recipients + destination) is configured and the in-process schedule runs. last_success_at: type: [string, "null"] format: date-time artifact_age_seconds: type: integer description: Age of the newest successful export; 0 when there has never been one. rpo_seconds: type: integer description: The configured recovery point objective; 0 when nothing is scheduled. rpo_exceeded: type: boolean description: True when exports are scheduled and no successful export is younger than the RPO. last_failure_at: type: [string, "null"] format: date-time last_failure_reason: type: string description: Bounded operator-facing text of the latest export failure; empty when none. last_prune_at: type: [string, "null"] format: date-time last_drill_at: type: [string, "null"] format: date-time last_drill_ok: type: boolean drill_stale: type: boolean description: True when no successful restore drill is younger than 90 days. ProjectRetentionPolicy: type: object additionalProperties: false required: [inherited, mode] properties: inherited: type: boolean description: True clears the project override and follows the org live. mode: type: string enum: [keep-if-either, unlimited] description: Unlimited can appear only when inherited from an unlimited org. max_age_seconds: type: [integer, "null"] minimum: 1 last_revisions: type: [integer, "null"] minimum: 1 SetProjectRetentionRequest: type: object additionalProperties: false required: [inherited] properties: inherited: type: boolean description: True clears the override; false requires both bounds. max_age_seconds: type: [integer, "null"] minimum: 1 last_revisions: type: [integer, "null"] minimum: 1 KeyName: type: string minLength: 1 maxLength: 128 pattern: "^[A-Z_][A-Z0-9_]*$" description: | The canonical key grammar: uppercase ASCII, digits and underscore, no leading digit. It is the environment-variable-safe grammar every delivery surface assumes - an execve environment block, a Kubernetes Secret data key, an adapter effective name - so it is a delivery constraint, not a style preference. `maxLength` counts code points here and bytes in the service; the grammar is ASCII, so they agree. KeyClassification: type: string enum: [secret, config] description: | Classification IS the sensitivity boundary. A matrix row is uniformly secret or config; it changes only through the reclassification ceremony. Closed, deliberately: a third value would be a third disclosure regime. KeyFolderPath: type: string maxLength: 256 description: | The key's namespace within the project. Organizational only: a plain slash-separated path, empty for the catalogue root. It is a PATH, not a folder reference - no folder row need exist for it. KeyRule: type: object additionalProperties: false required: [type] description: | One primitive type declaration with its constraints. Each constraint belongs to exactly one type, and a constraint declared on the wrong type is REFUSED rather than ignored: a silently ignored `pattern` on an integer key is the "appears to enforce something and does not" failure the schema model rejects everywhere. This document carries shape; the service is the authority on every rule below. properties: type: type: string enum: [string, integer, boolean, enum, url, json] min_length: type: integer minimum: 0 description: "`string` only." max_length: type: integer minimum: 0 description: "`string` only." pattern: type: string maxLength: 512 description: | `string` only. RE2, and ANCHORED TO THE WHOLE VALUE - implicitly `\A(?:...)\z`, never a substring search. Backreferences and lookaround do not exist in RE2 and are refused at declaration. allow_empty: type: boolean description: "`string` only; defaults to false." min: type: integer format: int64 description: "`integer` only." max: type: integer format: int64 description: "`integer` only." members: type: array maxItems: 64 items: type: string maxLength: 512 description: | `enum` only. Members must be non-empty and distinct after the write-time trim. schemes: type: array maxItems: 32 items: type: string description: "`url` only: the allowed scheme list." json_schema: type: string maxLength: 16384 description: | `json` only: a JSON Schema 2020-12 document, carried as a STRING so the bytes the profile checks are the bytes the client sent. A re-encoded object would lose duplicate-key detection (Go's decoder is last-wins) and number precision, and duplicate object keys are a rejection here, not last-wins. The accepted subset is a pinned, profiled, budgeted allowlist enforced by the service. KeyDeclaration: type: object additionalProperties: false description: | Exactly one of `rule` or `any_of`. `any_of` is a bounded union whose value is valid if it satisfies AT LEAST ONE alternative - deliberately not `oneOf`, whose JSON Schema meaning is exactly-one, because two meanings for one word inside one product is a trap. Alternatives may not nest. properties: rule: $ref: "#/components/schemas/KeyRule" any_of: type: array minItems: 2 maxItems: 8 items: $ref: "#/components/schemas/KeyRule" KeyPresence: type: object additionalProperties: false required: [mode] description: | A mode plus an explicit set, never a bare id list: `all` is SYMBOLIC and covers environments created later, so expanding it into the ids existing at declaration time would silently exempt a new environment from a rule the operator wrote as "always". `none` is the default. properties: mode: type: string enum: [all, none, explicit] environment_ids: type: array items: $ref: "#/components/schemas/ID" description: "`explicit` only; empty or absent for `all` and `none`." KeyPresenceRules: type: object additionalProperties: false required: [required_in, forbidden_in] description: | Presence is the ONLY thing that varies per environment; every other constraint is project-wide. `required` is a predicate about presence only - it means "resolves to set" and says nothing about content. properties: required_in: $ref: "#/components/schemas/KeyPresence" forbidden_in: $ref: "#/components/schemas/KeyPresence" Key: type: object required: [id, org_id, project_id, name, folder_path, classification, description, deprecated, deprecation_note, declaration, presence, group_id, created_at] properties: id: $ref: "#/components/schemas/ID" org_id: $ref: "#/components/schemas/ID" project_id: $ref: "#/components/schemas/ID" name: $ref: "#/components/schemas/KeyName" folder_path: $ref: "#/components/schemas/KeyFolderPath" classification: $ref: "#/components/schemas/KeyClassification" description: type: string maxLength: 4096 deprecated: type: boolean deprecation_note: type: string maxLength: 4096 declaration: $ref: "#/components/schemas/KeyDeclaration" presence: $ref: "#/components/schemas/KeyPresenceRules" group_id: type: string description: The key's group, or empty when it belongs to none. created_at: $ref: "#/components/schemas/Timestamp" findings: type: array description: | Secret-scanning warnings (#74, Surface 1). Populated only by the reclassification ceremony when declassifying (`secret` → `config`) a key whose existing values carry a credential-shaped string — the reclassify SUCCEEDS and each finding names its rule and key locator. Absent on every other key response. items: $ref: "#/components/schemas/ScanFinding" KeyList: type: object required: [items, count, schema_revision] properties: items: type: array items: $ref: "#/components/schemas/Key" count: type: integer minimum: 0 description: Total rows matching, which for an unpaged list equals `items` length. schema_revision: type: integer format: int64 minimum: 0 description: | The project's monotonic key-catalogue revision, which IS its schema revision. It advances on every semantic declaration change and on nothing else. CreateKeyRequest: type: object additionalProperties: false required: [name, classification, declaration] properties: name: $ref: "#/components/schemas/KeyName" classification: $ref: "#/components/schemas/KeyClassification" folder_path: $ref: "#/components/schemas/KeyFolderPath" description: type: string maxLength: 4096 deprecated: type: boolean deprecation_note: type: string maxLength: 4096 declaration: $ref: "#/components/schemas/KeyDeclaration" presence: $ref: "#/components/schemas/KeyPresenceRules" group_id: type: string acknowledgements: $ref: "#/components/schemas/Acknowledgements" UpdateKeyMetadataRequest: type: object additionalProperties: false properties: folder_path: $ref: "#/components/schemas/KeyFolderPath" description: type: string maxLength: 4096 deprecated: type: boolean deprecation_note: type: string maxLength: 4096 classification: $ref: "#/components/schemas/KeyClassification" acknowledgements: $ref: "#/components/schemas/Acknowledgements" RenameKeyRequest: type: object additionalProperties: false required: [name] properties: name: $ref: "#/components/schemas/KeyName" acknowledgements: $ref: "#/components/schemas/Acknowledgements" UpdateKeyDeclarationRequest: type: object additionalProperties: false required: [declaration, presence] properties: declaration: $ref: "#/components/schemas/KeyDeclaration" presence: $ref: "#/components/schemas/KeyPresenceRules" acknowledgements: $ref: "#/components/schemas/Acknowledgements" ReclassifyKeyRequest: type: object additionalProperties: false required: [classification] properties: classification: $ref: "#/components/schemas/KeyClassification" SetKeyGroupRequest: type: object additionalProperties: false required: [group_id] properties: group_id: type: string description: The group to join, or empty to leave every group. KeyGroup: type: object required: [id, org_id, project_id, name, members, inert, created_at] properties: id: $ref: "#/components/schemas/ID" org_id: $ref: "#/components/schemas/ID" project_id: $ref: "#/components/schemas/ID" name: type: string maxLength: 128 members: type: array items: $ref: "#/components/schemas/KeyName" description: The member key names, sorted. inert: type: boolean description: | A group with fewer than two members couples nothing. It is flagged rather than deleted behind the operator's back - they are usually mid-way through building it. created_at: $ref: "#/components/schemas/Timestamp" KeyGroupList: type: object required: [items, count] properties: items: type: array items: $ref: "#/components/schemas/KeyGroup" count: type: integer minimum: 0 CreateKeyGroupRequest: type: object additionalProperties: false required: [name] properties: name: type: string minLength: 1 maxLength: 128 acknowledgements: $ref: "#/components/schemas/Acknowledgements" RenameKeyGroupRequest: type: object additionalProperties: false required: [name] properties: name: type: string minLength: 1 maxLength: 128 acknowledgements: $ref: "#/components/schemas/Acknowledgements" AuthMethods: type: object additionalProperties: false required: [providers, local_login_enabled, signup_open, signup_paused, signup_methods] properties: providers: type: array items: $ref: "#/components/schemas/AuthMethodProvider" local_login_enabled: type: boolean signup_open: type: boolean description: | The addressed scope (the instance, or `?org=`) has an active registration policy: the sign-up door is open. signup_paused: type: boolean description: | The addressed scope has a policy that is inactive. The public page renders only "Sign-up is paused."; the cause renders on the Members panel (#587 d3). False when the scope has no policy. signup_methods: type: array description: | The methods the open door admits, empty unless `signup_open` (api-cli-spellings section 8: `[{kind, slug} | "local"]`). items: $ref: "#/components/schemas/SignupMethod" signup_landing: type: string enum: [org-template, none, fresh-org] description: | Where an admitted sign-up lands, present only while the door is open (#607): the addressed org (`org-template`), an account with no grants (`none`), or a fresh org of its own (`fresh-org`). Not secret: the landing kind is what the confirmation step tells the signer before the round-trip (#585 d5, #587 d1). SignupMethod: description: A federated provider by `{kind, slug}`, or the string `local` for the email + password entry. oneOf: - $ref: "#/components/schemas/ProviderRef" - $ref: "#/components/schemas/LocalSignupMethod" LocalSignupMethod: type: string description: The email + password sign-up entry. enum: [local] ProviderRef: type: object additionalProperties: false description: A federated provider named by kind and slug (slugs are unique per kind only). required: [kind, slug] properties: kind: $ref: "#/components/schemas/IdentityProviderKind" slug: type: string minLength: 1 maxLength: 128 RegistrationExternalEntry: type: object additionalProperties: false required: [provider] properties: provider: $ref: "#/components/schemas/ProviderRef" display_name: type: string description: The provider's display name; on responses only, ignored on input. claim: type: string maxLength: 256 description: | One issuer-specific string claim the signed ID token must carry, with a value in `values`. Never `email`. values: type: array maxItems: 64 items: type: string maxLength: 512 RegistrationLocalEntry: type: object additionalProperties: false description: Email + password sign-up; present means enabled. properties: domains: type: array maxItems: 64 description: Admitted address domains; empty or absent admits any address. items: type: string maxLength: 253 RegistrationLanding: type: object additionalProperties: false required: [kind] description: | Where a sign-up lands. An organisation policy is `org-template` with a template applicable at organisation scope; an instance policy is `none` (zero grants) or `fresh-org` (a new organisation with the signer as its first administrator) with a cap on live orgs minted. properties: kind: type: string enum: [org-template, none, fresh-org] template: $ref: "#/components/schemas/RoleTemplate" cap: type: integer RegistrationPolicyPutRequest: type: object additionalProperties: false required: [external, landing, proof] properties: external: type: array maxItems: 64 items: $ref: "#/components/schemas/RegistrationExternalEntry" local: $ref: "#/components/schemas/RegistrationLocalEntry" landing: $ref: "#/components/schemas/RegistrationLanding" proof: type: string maxLength: 1024 description: | The reauthentication proof: a TOTP code, or the account password where no factor is enrolled. RegistrationPolicyDeleteRequest: type: object additionalProperties: false required: [proof] properties: proof: type: string maxLength: 1024 description: The reauthentication proof, as on `put`. RegistrationPolicy: type: object additionalProperties: false required: [id, external, landing, authority_principal_id, state, row_version, created_at, updated_at] properties: id: type: string org: type: string description: The organisation; absent for the instance policy. external: type: array items: $ref: "#/components/schemas/RegistrationExternalEntry" local: $ref: "#/components/schemas/RegistrationLocalEntry" landing: $ref: "#/components/schemas/RegistrationLanding" authority_principal_id: type: string description: | The standing delegation's authority principal (empty when unassigned); re-checked against its current grants on every read and every sign-up. state: type: string enum: [active, inactive] inactive_cause: type: string enum: [authority-lost, authority-unassigned, precondition] inactive_precondition: type: string description: | When `inactive_cause` is `precondition`, the failing precondition by name, with the provider as `:` where one is involved. fresh_org_count: type: integer minimum: 0 description: For a `fresh-org` landing, the live orgs this policy minted (the `n` of `n / cap`). row_version: type: integer minimum: 1 created_at: type: string format: date-time updated_at: type: string format: date-time AuthMethodProvider: type: object additionalProperties: false required: [slug, display_name, kind] properties: slug: type: string display_name: type: string kind: $ref: "#/components/schemas/IdentityProviderKind" brand: type: string enum: [google, microsoft] description: | The provider's published button rules apply (#587 d4, docs/research/social-providers.md): Google's standard-colour G, or the Microsoft logo with the row's display name as the tenant. Derived from the pinned issuer (Google's, or an Entra tenant-specific issuer); absent for a generic OIDC provider. Presentation only: admission never keys on it. OidcStartRequest: type: object additionalProperties: false required: [purpose] properties: purpose: type: string x-extensible-enum: [login, link, reauth] intent: type: string enum: [sign-in, sign-up] description: | Valid only with purpose `login` (#604); absent = `sign-in`. It decides only what happens to an unknown identity at the callback: `sign-in` refuses it uniformly, `sign-up` enters the registration policy of the addressed scope. A known identity signs in under either. Supplied on any other purpose, the start refuses uniformly. signup_org: type: string maxLength: 64 description: | The org whose registration policy a `sign-up` addresses; absent = the instance scope. Valid only with intent `sign-up`. The start reads no policy: an unknown org refuses at the callback as a closed door. environment_id: type: string maxLength: 64 description: Required for reauth; the window scope. Refused (400) on any other purpose. proof: type: string maxLength: 1024 description: Required for link; the pre-existing password. browser: type: boolean default: false description: Redirect the callback to the SPA done page instead of returning JSON. OidcStartResult: type: object additionalProperties: false required: [authorization_url] properties: authorization_url: type: string description: The IdP authorization URL to redirect the browser to. SamlStartRequest: type: object additionalProperties: false required: [purpose] properties: purpose: type: string x-extensible-enum: [login, link, reauth] environment_id: type: string maxLength: 64 description: Required for reauth; the reveal-window scope. proof: type: string maxLength: 1024 description: Required for link; the pre-existing account-security proof. SamlStartResult: type: object additionalProperties: false required: [redirect_url] properties: redirect_url: type: string format: uri description: The IdP HTTP-Redirect URL carrying the AuthnRequest. SamlACSRequest: type: object additionalProperties: false required: [SAMLResponse, RelayState] properties: SAMLResponse: type: string minLength: 1 maxLength: 524288 description: Base64-encoded SAML Response; decoded XML is capped at 256 KiB. RelayState: type: string minLength: 16 maxLength: 512 description: Server-minted opaque transaction handle, never a continuation URL. IdentityLinkRequest: type: object additionalProperties: false required: [provider, proof] properties: provider: type: string maxLength: 64 proof: type: string maxLength: 1024 browser: type: boolean default: false description: Redirect the callback to the SPA done page instead of returning JSON. IdentityUnlinkRequest: type: object additionalProperties: false required: [proof] properties: proof: type: string maxLength: 1024 ExternalIdentity: type: object additionalProperties: false required: [id, kind, issuer, subject, provider_id, created_at] properties: id: $ref: "#/components/schemas/ID" kind: $ref: "#/components/schemas/IdentityProviderKind" issuer: type: string subject: type: string provider_id: type: string created_at: $ref: "#/components/schemas/Timestamp" IdentityList: type: object additionalProperties: false required: [identities] properties: identities: type: array items: $ref: "#/components/schemas/ExternalIdentity" OidcProviderInput: type: object additionalProperties: false required: [display_name, issuer, client_id, client_secret, scopes, enabled] properties: display_name: type: string maxLength: 256 issuer: type: string maxLength: 2048 description: Byte-exact, immutable after create (A3). client_id: type: string maxLength: 1024 client_secret: type: string maxLength: 4096 description: Envelope-encrypted at rest; never returned. scopes: type: string maxLength: 1024 assurance_policy: type: [string, "null"] maxLength: 4096 description: JSON `{acr_values[], amr_sets[][]}`; absent means single-factor. enabled: type: boolean OidcProvider: type: object additionalProperties: false required: [slug, display_name, issuer, client_id, scopes, redirect_uri, enabled] properties: slug: type: string display_name: type: string issuer: type: string client_id: type: string scopes: type: string redirect_uri: type: string assurance_policy: type: [string, "null"] enabled: type: boolean OidcProviderList: type: object additionalProperties: false required: [providers] properties: providers: type: array items: $ref: "#/components/schemas/OidcProvider" IdentityProviderKind: type: string description: OPEN protocol discriminator in the byte-exact external-identity key. x-extensible-enum: [oidc, saml] SamlMetadataSource: type: string description: Closed source set; URL means one-shot admin-initiated fetch, never a poller. enum: [file, url] SamlProviderInput: type: object additionalProperties: false description: | SAML authentication configuration. Exactly one of `metadata_document` and `metadata_url` must match `metadata_source`; the service enforces that conditional before fetching or parsing. No provisioning member exists: SAML never provisions accounts. required: - display_name - entity_id - metadata_source - allow_email_nameid - force_sign_requests - enabled properties: display_name: type: string minLength: 1 maxLength: 256 entity_id: type: string minLength: 1 maxLength: 2048 description: | Configured byte-exact IdP entityID used to select exactly one EntityDescriptor from metadata; immutable after create. metadata_source: $ref: "#/components/schemas/SamlMetadataSource" metadata_document: type: [string, "null"] maxLength: 262144 description: Raw IdP metadata XML for a file-backed configuration. metadata_url: type: [string, "null"] format: uri maxLength: 2048 description: One-shot WebPKI URL; redirects off-origin are refused. confirmed_fingerprints: type: array maxItems: 64 uniqueItems: true items: type: string minLength: 1 maxLength: 128 description: Fingerprints explicitly confirmed in the current ceremony. confirmed_endpoints: type: array maxItems: 32 uniqueItems: true items: type: string format: uri maxLength: 2048 description: New endpoint URLs explicitly confirmed in the current ceremony. assurance_policy: type: [array, "null"] maxItems: 32 uniqueItems: true items: type: string minLength: 1 maxLength: 2048 description: Accepted AuthnContextClassRef values; null means single-factor. allow_email_nameid: type: boolean description: Explicit opt-in to opaque emailAddress NameID values; never email linking. force_sign_requests: type: boolean description: Force signed AuthnRequests even when metadata does not demand them. enabled: type: boolean SamlMetadataRefreshRequest: type: object additionalProperties: false properties: metadata_document: type: [string, "null"] maxLength: 262144 description: Replacement XML for a file-backed provider; absent for URL-backed providers. confirmed_fingerprints: type: array maxItems: 64 uniqueItems: true items: type: string minLength: 1 maxLength: 128 confirmed_endpoints: type: array maxItems: 32 uniqueItems: true items: type: string format: uri maxLength: 2048 SamlProviderPatch: type: object additionalProperties: false description: Omitted members preserve their stored values. properties: display_name: type: string minLength: 1 maxLength: 256 assurance_policy: type: [array, "null"] maxItems: 32 uniqueItems: true items: type: string minLength: 1 maxLength: 2048 allow_email_nameid: type: boolean force_sign_requests: type: boolean enabled: type: boolean SamlProvider: type: object additionalProperties: false required: - slug - display_name - kind - entity_id - acs_url - sso_redirect_url - signing_certificate_fingerprints - allow_email_nameid - force_sign_requests - metadata_source - metadata_signed - warnings - enabled - row_version - created_at - updated_at properties: slug: type: string display_name: type: string kind: type: string enum: [saml] entity_id: type: string description: Byte-exact IdP entityID; immutable after create. acs_url: type: string format: uri sso_redirect_url: type: string format: uri signing_certificate_fingerprints: type: array items: type: string assurance_policy: type: [array, "null"] items: type: string allow_email_nameid: type: boolean force_sign_requests: type: boolean metadata_source: $ref: "#/components/schemas/SamlMetadataSource" metadata_url: type: [string, "null"] format: uri metadata_signed: type: boolean metadata_signing_fingerprint: type: [string, "null"] metadata_valid_until: oneOf: - $ref: "#/components/schemas/Timestamp" - type: "null" warnings: type: array items: $ref: "#/components/schemas/SamlProviderWarning" enabled: type: boolean row_version: type: integer minimum: 1 created_at: $ref: "#/components/schemas/Timestamp" updated_at: $ref: "#/components/schemas/Timestamp" SamlProviderWarning: type: object additionalProperties: false required: [code, severity, message, effective_at] properties: code: type: string description: >- Open diagnostic code. Clients display the server message and use severity even when the code is not yet known to them. x-extensible-enum: - metadata_expires_soon - metadata_expired - signing_certificate_not_yet_valid - signing_certificate_expired severity: type: string enum: [warning, error] message: type: string maxLength: 512 effective_at: $ref: "#/components/schemas/Timestamp" fingerprint: type: string description: Present only for certificate warnings. SamlProviderList: type: object additionalProperties: false required: [providers] properties: providers: type: array items: $ref: "#/components/schemas/SamlProvider" SamlSpKey: type: object additionalProperties: false required: [fingerprint, state, created_at] properties: fingerprint: type: string description: URL-safe unpadded base64 SHA-256 fingerprint of SubjectPublicKeyInfo, prefixed by `sha256:`. state: type: string enum: [active, retiring] created_at: $ref: "#/components/schemas/Timestamp" SamlSpKeyList: type: object additionalProperties: false required: [keys] properties: keys: type: array items: $ref: "#/components/schemas/SamlSpKey" SamlMetadataDiff: type: object additionalProperties: false required: - endpoints_added - endpoints_removed - certs_added_fps - certs_removed_fps properties: endpoints_added: type: array items: { type: string } endpoints_removed: type: array items: { type: string } certs_added_fps: type: array items: { type: string } certs_removed_fps: type: array items: { type: string } valid_until: oneOf: - $ref: "#/components/schemas/Timestamp" - type: "null" SamlProviderMutationResult: type: object additionalProperties: false description: | `applied=false` is the first leg of the metadata ceremony: no trust state changed, `diff` is displayed, and every value in `required_fingerprints` and `required_endpoints` must be explicitly supplied on a rerun. required: [applied, diff, required_fingerprints, required_endpoints] properties: applied: type: boolean provider: oneOf: - $ref: "#/components/schemas/SamlProvider" - type: "null" diff: $ref: "#/components/schemas/SamlMetadataDiff" required_fingerprints: type: array uniqueItems: true items: type: string required_endpoints: type: array uniqueItems: true items: type: string format: uri WebauthnOptions: type: object additionalProperties: true description: | Opaque WebAuthn ceremony options (creation or request), generated by the relying party and handed to the browser `navigator.credentials` API verbatim. The server treats the blob as raw bytes; the base64url fields the authenticator signs over are preserved by round-tripping. WebauthnResponse: type: object additionalProperties: true description: | Opaque WebAuthn authenticator response (attestation or assertion) as produced by the browser, passed to the relying party as raw bytes. WebauthnEnrolStartRequest: type: object additionalProperties: false description: | The account-security proof, selected possession-first over the account's pre-existing credentials: where a confirmed TOTP factor stands, `code` is required and a password alone is refused (400); where none does, `password` is required. Never the passkey being added. Both members are optional on the wire because the client may not know the factor state; the service enforces the selection, and the field it did not select is ignored. A TOTP code is spent when the ceremony is staged, so the same code cannot open a second ceremony. properties: password: type: string maxLength: 1024 description: The pre-existing password proof (an account with no confirmed factor). code: type: string maxLength: 16 description: The confirmed TOTP code proof (an account holding a factor). WebauthnReauthStartRequest: type: object additionalProperties: false required: [operation, environment_id, key_ids] properties: self_config: $ref: "#/components/schemas/SelfConfigReauthIntent" operation: $ref: "#/components/schemas/ReauthPurpose" environment_id: type: string maxLength: 64 key_ids: type: array description: The credential ids the reauthentication is bound to. items: type: string maxLength: 256 adapter_operation: type: string enum: [adapter.configure, adapter.credential-set, adapter.adopt, adapter.sync] environment_ids: type: array minItems: 1 uniqueItems: true items: {$ref: "#/components/schemas/ID"} ReauthPurpose: type: string description: | The decision a disclosure ceremony authorizes. It is part of the SIGNED binding, not a label: without it an assertion given to `reveal` would be spendable on `publish` over the same environment and keys — the same unit, a different decision, and the human agreed to only one of them. enum: - reveal - copy - publish - mint - adapter - self-config - approve - reject - bypass WebauthnCredentialProofRequest: type: object additionalProperties: false description: | The account-security proof for removing a credential, selected possession-first over the pre-existing credentials: where a confirmed TOTP factor stands, `code` is required and a password alone is refused (400); where none does, `password` is required. Never the credential being removed (B7). Both optional on the wire; the service enforces the selection and spends a TOTP code inside the removal itself, so the same code cannot remove a second credential. properties: password: type: string maxLength: 1024 code: type: string maxLength: 16 ReauthResult: type: object additionalProperties: false required: [session_id, environment_id, single_decision, window_expires] properties: session_token: type: string minLength: 1 description: Rotated bearer returned only to Authorization-header callers; cookie-authenticated browser callers receive the rotated HttpOnly cookie and this field is omitted. session_id: $ref: "#/components/schemas/ID" environment_id: type: string environment_ids: type: array uniqueItems: true items: {$ref: "#/components/schemas/ID"} single_decision: type: boolean description: True where the effective window is zero — the window authorizes exactly one decision. window_expires: $ref: "#/components/schemas/Timestamp" RevealWindow: type: object additionalProperties: false required: [ effective_window_seconds, protected, totp_offered, live, single_decision, can_reveal, ] description: | The reveal guard's state for one environment and the acting session. Nothing here is material: it is project settings plus the caller's own window. properties: effective_window_seconds: type: integer minimum: 0 description: | The environment's resolved reauthentication window. `0` means every disclosure takes its own ceremony — the state a protected environment is capped at. protected: type: boolean description: | The protected-environment flag. Reported beside the window rather than folded into it, because "this environment is protected" and "the window is set to 0" are different sentences to a human even when they produce the same gate. totp_offered: type: boolean description: | Whether a TOTP code may open a window here. False exactly where the effective window is `0`: TOTP cannot bind a challenge to the enumerated unit, so a passkey ceremony is the only path there. live: type: boolean description: Whether a window is open for this session right now. single_decision: type: boolean description: | True when the live window authorizes exactly the keys its ceremony enumerated and is spent by one decision — never presented as a standing window with a countdown. can_reveal: type: boolean description: | Whether this principal satisfies `read(E) AND reveal(E)` here. It is an AFFORDANCE, never a decision — the chokepoint judges every disclosure regardless — and it is what makes the write-only editing path first-class: `edit` without `reveal` is a supported state, so the editor offers a blind-replacement field with honest microcopy rather than guessing from whether a cell happens to be on screen. expires_at: $ref: "#/components/schemas/Timestamp" ApprovalCeremonyBinding: type: object additionalProperties: false required: [key_ids, window] properties: key_ids: type: array uniqueItems: true items: $ref: "#/components/schemas/ID" description: The immutable key unit pinned by the approval request. window: $ref: "#/components/schemas/RevealWindow" PasskeyList: type: object additionalProperties: false required: [passkeys] properties: passkeys: type: array items: $ref: "#/components/schemas/Passkey" TotpStatus: type: object additionalProperties: false required: [confirmed, pending] properties: confirmed: type: boolean description: True when a confirmed authenticator factor stands on the account. pending: type: boolean description: True when an enrolment is staged but its first confirming code is not yet in. Passkey: type: object additionalProperties: false required: [id, label, discoverable, disabled, created_at, last_used_at] properties: id: $ref: "#/components/schemas/ID" label: type: string discoverable: type: boolean disabled: type: boolean description: True for a credential disabled after a signature-count clone was detected. created_at: $ref: "#/components/schemas/Timestamp" last_used_at: $ref: "#/components/schemas/Timestamp" CreateScimBindingRequest: type: object additionalProperties: false required: [provider_kind, provider_slug, subject_source] properties: provider_kind: type: string enum: [oidc, saml] provider_slug: type: string maxLength: 256 subject_source: type: string maxLength: 256 description: | `externalId` or a declared enterprise/custom extension path. `userName` is refused by name: RFC 7643 defines it `caseExact: false` and server-unique, which contradicts byte-exact identity material. nameid_format: type: string maxLength: 256 description: | SAML bindings only. A scalar SCIM attribute cannot carry the locked SAML subject, so the binding declares the NameID PROFILE and the subject-source attribute supplies the NameID value alone. nameid_qualifier: type: string maxLength: 256 nameid_qualifier_present: type: boolean description: | Presence is carried separately from value because an absent qualifier and an empty one are different inputs to the injective encoder, and collapsing them would make two SAML subjects collide. nameid_sp_qualifier: type: string maxLength: 256 nameid_sp_qualifier_present: type: boolean MintScimCredentialRequest: type: object additionalProperties: false properties: proof: type: string maxLength: 512 description: | The reauthentication proof: a TOTP code, or the account password where no factor is enrolled. Minting a provisioning credential is `manage-members(org)` AND reauthentication, so a stolen elevated session cannot issue a durable bearer on its own. indefinite: type: boolean description: | Requires the instance opt-in, which is default-off. Without it an indefinite credential is refused by name. ScimAttention: type: object description: | One raised attention state on a binding. Each names its cause AND a server-authored remediation: a state that only says something is wrong makes the binding view a puzzle. additionalProperties: false required: [state, subject_ref, cause, entered_at, remediation] properties: state: type: string description: | provider_unavailable | lockout_retention | manual_grants_remain | inert_mapping | stale | post_restore enum: - provider_unavailable - lockout_retention - manual_grants_remain - inert_mapping - stale - post_restore x-enum-varnames: - ScimAttentionStateProviderUnavailable - ScimAttentionStateLockoutRetention - ScimAttentionStateManualGrantsRemain - ScimAttentionStateInertMapping - ScimAttentionStateStale - ScimAttentionStatePostRestore subject_ref: type: string description: Which object the state is about; empty for a binding-wide state. cause: type: string description: The triggering operation, empty where the state has no single trigger. entered_at: $ref: "#/components/schemas/Timestamp" remediation: type: string description: Server-authored — what to do about it. ScimBinding: type: object additionalProperties: false required: - id - org_id - provider_kind - provider_slug - provider_issuer - subject_source - connection_principal_id - created_at - attention properties: id: $ref: "#/components/schemas/ID" org_id: $ref: "#/components/schemas/ID" provider_kind: type: string enum: [oidc, saml] provider_slug: type: string provider_issuer: type: string description: | Frozen at creation. A provider whose issuer moved under a binding is a rebinding hazard, not a rename to follow silently. subject_source: type: string description: | The SCIM attribute path carrying identity material, immutable after creation. `userName` is refused by name. nameid_format: type: string description: The SAML NameID profile's Format URI; empty for an OIDC binding. nameid_qualifier: type: string description: The SAML NameID profile's fixed NameQualifier. nameid_qualifier_present: type: boolean description: | Whether the assertion carries a NameQualifier at all. Presence is reported separately from value because an absent qualifier and an empty one are different inputs to the injective subject encoder. nameid_sp_qualifier: type: string description: The SAML NameID profile's fixed SPNameQualifier. nameid_sp_qualifier_present: type: boolean connection_principal_id: $ref: "#/components/schemas/ID" last_contact_at: $ref: "#/components/schemas/Timestamp" created_at: $ref: "#/components/schemas/Timestamp" attention: type: array items: $ref: "#/components/schemas/ScimAttention" ScimBindingList: type: object additionalProperties: false required: [items, count] properties: items: type: array items: $ref: "#/components/schemas/ScimBinding" count: type: integer ScimBlastWarning: type: object additionalProperties: false required: [code, severity, message] description: | Server-authored consequence language. It is authored on the server, not in a client, because the locked rule is about what the human is TOLD, and a client rendering its own wording is a second, unreviewed policy. properties: code: type: string enum: [org_scope, reveal_expanding, production_environment, populated_group] severity: type: string enum: [warning, critical] message: type: string ScimCredential: type: object additionalProperties: false required: [id, binding_id, created_at, live] properties: id: $ref: "#/components/schemas/ID" binding_id: $ref: "#/components/schemas/ID" created_at: $ref: "#/components/schemas/Timestamp" expires_at: $ref: "#/components/schemas/Timestamp" revoked_at: $ref: "#/components/schemas/Timestamp" last_used_at: $ref: "#/components/schemas/Timestamp" live: type: boolean ScimCredentialList: type: object additionalProperties: false required: [items, count] properties: items: type: array items: $ref: "#/components/schemas/ScimCredential" count: type: integer ScimDirectoryGroup: type: object additionalProperties: false required: [id, display_name, member_count, created_at, updated_at] properties: id: $ref: "#/components/schemas/ID" display_name: type: string external_id: type: string member_count: type: integer created_at: $ref: "#/components/schemas/Timestamp" updated_at: $ref: "#/components/schemas/Timestamp" ScimDirectoryGroupList: type: object additionalProperties: false required: [items, count] properties: items: type: array items: $ref: "#/components/schemas/ScimDirectoryGroup" count: type: integer ScimDirectoryUser: type: object additionalProperties: false required: [id, user_name, account_id, active, groups, created_at, updated_at, attention] properties: id: $ref: "#/components/schemas/ID" user_name: type: string external_id: type: string account_id: $ref: "#/components/schemas/ID" active: type: boolean groups: type: array items: $ref: "#/components/schemas/ID" created_at: $ref: "#/components/schemas/Timestamp" updated_at: $ref: "#/components/schemas/Timestamp" attention: type: array items: $ref: "#/components/schemas/ScimAttention" ScimDirectoryUserList: type: object additionalProperties: false required: [items, count] properties: items: type: array items: $ref: "#/components/schemas/ScimDirectoryUser" count: type: integer ScimMapping: type: object additionalProperties: false required: [id, binding_id, group_id, template, inert, created_at, capabilities] properties: capability_origins: type: array items: {$ref: "#/components/schemas/ScimCapabilityOrigin"} id: $ref: "#/components/schemas/ID" binding_id: $ref: "#/components/schemas/ID" group_id: $ref: "#/components/schemas/ID" template: type: string project_id: type: string environment_id: type: string inert: type: boolean description: | The group this row names no longer exists at the identity provider. An inert row grants nothing and is never removed automatically. created_at: $ref: "#/components/schemas/Timestamp" capabilities: type: array description: | What the row expands into. Expansion at sync time is expansion at grant time, so this is exactly the set a human applying the same template at the same scope would create. items: type: string ScimMappingList: type: object additionalProperties: false required: [items, count] properties: items: type: array items: $ref: "#/components/schemas/ScimMapping" count: type: integer ScimMappingRequest: type: object additionalProperties: false required: [group_id, template] properties: group_id: $ref: "#/components/schemas/ID" template: type: string maxLength: 64 project_id: type: string maxLength: 64 description: | Absent means org scope, which is the WIDEST a binding can reach. Nothing preselects a scope for you, least of all a production environment. environment_id: type: string maxLength: 64 ScimMappingResult: type: object additionalProperties: false required: [mapping, warnings, members_affected, grants_created, origins_released] properties: mapping: $ref: "#/components/schemas/ScimMapping" warnings: type: array items: $ref: "#/components/schemas/ScimBlastWarning" members_affected: type: integer grants_created: type: integer origins_released: type: integer ScimMintResult: type: object additionalProperties: false required: [credential, token, rotated] properties: credential: $ref: "#/components/schemas/ScimCredential" token: type: string description: | Display-once. It is in this response and nowhere else, ever: only the SHA-256 verifier was stored. rotated: type: boolean description: | This mint joined an already-live credential. That IS overlap rotation — mint-new, update the identity provider, revoke-old, with identical authority throughout. ScimResource: type: object additionalProperties: true description: | A SCIM 2.0 resource or message, as RFC 7643/7644 shapes it. It is deliberately open: identity providers send and expect attributes this provider stores as round-tripped display metadata, and pinning the shape here would make a conformant IdP's request a contract violation. What IS closed lives in the server: the subject source, the filter grammar, the PATCH matrix and the error mapping, each with its own named refusal. DirectoryListing: type: object additionalProperties: false required: [identity, version, orgs, org_count, project_count] description: | What a connection credential authorizes, exhaustively. There is no field here that could carry a value, a key, an environment, a membership, a setting or an audit row, and adding one would widen every credential in circulation without any grant changing. properties: identity: type: string maxLength: 128 description: | This instance's opaque, server-generated identity, minted with the schema and preserved by backup/restore — a restored instance IS the instance. Returned ONLY here, never pre-authentication. version: type: string maxLength: 64 description: | Display only. It never feeds a compatibility decision: a snapshot can race a downgrade or a restore, so the shell reads the pre-auth meta endpoint live for that. orgs: type: array maxItems: 1000 items: $ref: "#/components/schemas/DirectoryOrg" org_count: type: integer minimum: 0 project_count: type: integer minimum: 0 DirectoryOrg: type: object additionalProperties: false required: [name, projects] properties: name: type: string maxLength: 200 projects: type: array maxItems: 1000 items: type: string maxLength: 200 Remote: type: object additionalProperties: false required: [id, name, url, spki_pin, created_at, created_by, state, last_attempt_at, stale] description: | One connection entry and its last-known state. There is deliberately no credential field: the stored credential is write-only after storage and is not retrievable through any surface. properties: id: $ref: "#/components/schemas/ID" name: $ref: "#/components/schemas/EntityName" url: type: string maxLength: 512 description: The canonical https origin. Immutable for the life of the entry. spki_pin: type: string maxLength: 64 description: | base64(sha256(SubjectPublicKeyInfo)), confirmed by a human at add time and verified on every connection before any bytes are written. Immutable for the life of the entry. created_at: $ref: "#/components/schemas/Timestamp" created_by: type: string maxLength: 64 state: type: string enum: [ok, unreachable, credential-rejected, pin-mismatch, redirect-refused, identity-conflict, self-connected] description: | The most recent fetch's outcome. A closed enum because the operator's fix differs per outcome, and collapsing them into one "error" is what makes a directory card useless. last_attempt_at: $ref: "#/components/schemas/Timestamp" observed_at: $ref: "#/components/schemas/Timestamp" stale: type: boolean description: | The listing below is a SNAPSHOT, not current. Computed from the outcome rather than the age: a fetch that just failed makes even a one-second-old snapshot stale. stale_for_seconds: type: integer minimum: 0 description: The snapshot's age, the number the card prints beside "unreachable". identity: type: string maxLength: 128 version: type: string maxLength: 64 org_count: type: integer minimum: 0 project_count: type: integer minimum: 0 orgs: type: array maxItems: 1000 items: $ref: "#/components/schemas/DirectoryOrg" RemoteList: type: object additionalProperties: false required: [items, count] properties: items: type: array items: $ref: "#/components/schemas/Remote" count: type: integer minimum: 0 AddRemoteRequest: type: object additionalProperties: false required: [name, url, spki_pin, credential] properties: name: $ref: "#/components/schemas/EntityName" url: type: string maxLength: 512 description: | A canonical `https://` origin — no userinfo, no path, no query, no fragment, no plaintext HTTP. A value carrying one is REFUSED, never silently normalised. spki_pin: type: string maxLength: 64 description: The fingerprint the human confirmed interactively. credential: type: string maxLength: 256 description: | The connection credential minted on the serving side. Verified by one authenticated fetch before the entry is committed, then sealed under the instance keyring and never returned again. RenameRemoteRequest: type: object additionalProperties: false required: [name] properties: name: $ref: "#/components/schemas/EntityName" InstanceConnection: type: object additionalProperties: false required: [id, principal, label, kind, prefix_hint, lifetime, created_at, created_by, live] properties: id: $ref: "#/components/schemas/ID" principal: type: string maxLength: 64 label: type: string maxLength: 200 description: | Names the intended peer for the audit trail. Descriptive, NOT enforced: this instance cannot verify who holds the token. kind: $ref: "#/components/schemas/CredentialKind" prefix_hint: type: string maxLength: 32 description: | The leading, non-secret slice of the minted value, so two connections are distinguishable in a list without either being retrievable. lifetime: $ref: "#/components/schemas/CredentialLifetime" expires_at: $ref: "#/components/schemas/Timestamp" created_at: $ref: "#/components/schemas/Timestamp" created_by: type: string maxLength: 64 revoked_at: $ref: "#/components/schemas/Timestamp" last_used_at: $ref: "#/components/schemas/Timestamp" live: type: boolean description: Whether it authenticates right now, under the current credential epoch. InstanceConnectionList: type: object additionalProperties: false required: [items, count] properties: items: type: array items: $ref: "#/components/schemas/InstanceConnection" count: type: integer minimum: 0 MintInstanceConnectionRequest: type: object additionalProperties: false required: [label] properties: label: type: string minLength: 1 maxLength: 200 lifetime_seconds: type: integer minimum: 1 description: | Clamped to the instance ceiling, and the response says so. Omit for the default; naming both this and `indefinite` is a refusal, never a precedence rule. indefinite: type: boolean description: Requires the instance to allow indefinite credentials. MintedInstanceConnection: type: object additionalProperties: false required: [value, connection, clamped] properties: value: type: string maxLength: 256 description: | THE ONLY DISCLOSURE. Not retrievable afterwards through any surface. connection: $ref: "#/components/schemas/InstanceConnection" clamped: type: boolean description: The instance ceiling shortened the requested lifetime. WorkspaceOrigin: type: object additionalProperties: false required: [origin, created_at, created_by] properties: origin: type: string maxLength: 512 description: An EXACT origin — scheme://host[:port]. No wildcards, no subdomains. created_at: $ref: "#/components/schemas/Timestamp" created_by: type: string maxLength: 64 WorkspaceOriginList: type: object additionalProperties: false required: [items, count] properties: items: type: array items: $ref: "#/components/schemas/WorkspaceOrigin" count: type: integer minimum: 0 WorkspaceOriginRequest: type: object additionalProperties: false required: [origin] properties: origin: type: string maxLength: 512 WorkspaceOriginRemoved: type: object additionalProperties: false required: [origin, sessions_revoked] properties: origin: type: string maxLength: 512 sessions_revoked: type: integer minimum: 0 description: | Workspace sessions killed by the SAME transaction. This is what makes de-allowlisting a kill switch rather than a headers change. StartWorkspaceHandoffRequest: type: object additionalProperties: false required: [origin, redirect_uri, pkce_challenge, purpose] properties: origin: type: string maxLength: 512 description: The requesting UI origin. Must be allowlisted, or the transaction refuses. redirect_uri: type: string maxLength: 512 description: | The exact pre-registered callback. Its authority IS the allowlist entry: a callback that does not live at the consented origin is not the consented code. pkce_challenge: type: string minLength: 43 maxLength: 43 pattern: "^[A-Za-z0-9_-]{43}$" description: S256 challenge (RFC 7636). Plain is not accepted. purpose: type: string enum: [establishment, step-up] session: $ref: "#/components/schemas/ID" operation: type: string maxLength: 128 description: | Step-up only, with `session`: the EXACT operation being elevated, so an elevated consent cannot be replayed against a different one. environment: $ref: "#/components/schemas/ID" key_set: type: array maxItems: 1000 items: type: string maxLength: 200 description: | Step-up only, where the operation is key-scoped: the enumerated key set the consent covers. WorkspaceHandoffStarted: type: object additionalProperties: false required: [handoff, state, expires_at] properties: handoff: $ref: "#/components/schemas/ID" state: type: string maxLength: 256 description: Crosses the front channel. The artifact never does. expires_at: $ref: "#/components/schemas/Timestamp" ApproveWorkspaceHandoffRequest: type: object additionalProperties: false required: [state] properties: state: type: string maxLength: 256 WorkspaceHandoffApproved: type: object additionalProperties: false required: [code, redirect_uri] properties: code: type: string maxLength: 256 description: Single-use, atomically consumed at redemption. redirect_uri: type: string maxLength: 512 RedeemWorkspaceHandoffRequest: type: object additionalProperties: false required: [code, pkce_verifier, origin] properties: code: type: string maxLength: 256 pkce_verifier: type: string minLength: 43 maxLength: 128 pattern: "^[A-Za-z0-9_-]{43,128}$" origin: type: string maxLength: 512 WorkspaceSession: type: object additionalProperties: false required: [value, session, origin, handoff, idle_expires_at, absolute_expires_at] properties: value: type: string maxLength: 256 description: | Keep it in JS MEMORY ONLY — never a cookie, never localStorage or sessionStorage. In-memory storage narrows the at-rest window; it is not non-stealability, and this contract does not sell it as such. session: $ref: "#/components/schemas/ID" origin: type: string maxLength: 512 handoff: $ref: "#/components/schemas/ID" idle_expires_at: $ref: "#/components/schemas/Timestamp" absolute_expires_at: $ref: "#/components/schemas/Timestamp" elevated: type: boolean description: | True when this redemption ELEVATED the session the step-up transaction was bound to instead of establishing a new one. `session` is then that same session's id and `value` is its ROTATED bearer — not a second session. The rotation is deliberate: a bearer stolen before an elevation must not become an elevated bearer after it. environment: $ref: "#/components/schemas/ID" window_expires_at: allOf: - $ref: "#/components/schemas/Timestamp" description: | When the reauthentication window this elevation opened lapses. Present only on an elevation. WorkspaceHandoffTransaction: oneOf: - $ref: "#/components/schemas/WorkspaceHandoffEstablishment" - $ref: "#/components/schemas/WorkspaceHandoffStepUp" WorkspaceHandoffEstablishment: type: object additionalProperties: false required: [state, requesting_origin, purpose, key_ids, expires_at] properties: requesting_origin: type: string format: uri minLength: 1 description: Exact canonical requesting origin from the stored transaction, never from approval-page URL fields. state: type: string minLength: 1 purpose: type: string enum: [establishment] key_ids: type: array items: {$ref: "#/components/schemas/ID"} description: Empty for an establishment. expires_at: $ref: "#/components/schemas/Timestamp" WorkspaceHandoffStepUp: type: object additionalProperties: false required: [state, requesting_origin, purpose, operation, environment, key_ids, expires_at] properties: requesting_origin: type: string format: uri minLength: 1 description: Exact canonical requesting origin from the stored transaction, never from approval-page URL fields. state: type: string minLength: 1 purpose: type: string enum: [step-up] operation: type: string enum: [reveal, copy, publish, approve, reject, bypass] description: The exact decision the reauthentication authorizes. environment: $ref: "#/components/schemas/ID" description: The environment the elevation covers. key_ids: type: array items: {$ref: "#/components/schemas/ID"} description: The enumerated unit the elevation binds. expires_at: $ref: "#/components/schemas/Timestamp" ActiveSession: type: object additionalProperties: false required: [id, artifact, auth_method, created_at, last_seen_at, idle_expires_at, absolute_expires_at] description: One of the caller's own sessions. Metadata only; no verifier is ever returned. properties: id: $ref: "#/components/schemas/ID" artifact: type: string enum: [cli, browser, workspace] description: | A workspace session appears as its own artifact type — the ADR's requirement, and the reason it is a session row at all. auth_method: type: string maxLength: 64 created_at: $ref: "#/components/schemas/Timestamp" last_seen_at: $ref: "#/components/schemas/Timestamp" idle_expires_at: $ref: "#/components/schemas/Timestamp" absolute_expires_at: $ref: "#/components/schemas/Timestamp" source_ip: type: string maxLength: 64 user_agent: type: string maxLength: 512 requesting_origin: type: string maxLength: 512 description: | Workspace sessions only: which foreign shell holds a session on this account. Removing that origin from the allowlist kills it atomically. handoff: type: string maxLength: 64 SessionList: type: object additionalProperties: false required: [items, count] properties: items: type: array items: $ref: "#/components/schemas/ActiveSession" count: type: integer minimum: 0 AuditEvent: type: object description: | One stored trail event, exactly as the JSONL export emits it. No value material is ever written to the trail, so none appears here. Consumers MUST tolerate members they do not recognise: the closed registry adds event types and payload fields over time, and a client that fails on an additive field would break on every forward-compatible server. required: [seq, id, type, schema_version, occurred_at, occurred_asserted, recorded_at, actor_class, scope_class, outcome, origin, payload] properties: seq: type: integer format: int64 description: Immutable public allocation order and event position. id: type: string type: type: string description: The event type from the closed audit registry. schema_version: type: integer occurred_at: type: string format: date-time occurred_asserted: type: boolean description: Whether occurred_at was asserted by a client rather than the server clock. recorded_at: type: string format: date-time actor_id: type: string actor_name: type: string description: Current display name of this event actor, when still available. This is a presentation label, not historical evidence. actor_class: type: string actor_credential_id: type: string authority_id: type: string scope_class: type: string org_id: type: string project_id: type: string env_id: type: string object_type: type: string object_id: type: string outcome: type: string correlation_id: type: string source_ip: type: string user_agent: type: string origin: type: string payload: type: object additionalProperties: true description: | The event's schema-versioned payload, verbatim. Its members vary by event type and grow additively; render unknown members rather than failing on them. AuditPage: type: object additionalProperties: false required: [items, count, next_after_seq, upper_seq, exhausted] properties: items: type: array description: The events on this page that matched the filter, in seq order. items: $ref: "#/components/schemas/AuditEvent" count: type: integer minimum: 0 description: Number of matched events returned (may be fewer than the rows scanned). next_after_seq: type: integer format: int64 description: | The seq of the last row SCANNED. Pass it as `after_seq` to resume; it advances over scanned rows, not just matched ones, so a filtered page never re-reads or skips. upper_seq: type: integer format: int64 description: | The session ceiling the server pinned for this paging run (the trail's head when the first page was read). Pass it back as `to_seq` on every later page; it never changes across the run. exhausted: type: boolean description: | True when this page reached the end of the trail (fewer rows scanned than the limit). While false, more pages remain even if `items` is empty for a sparse filter. AdapterFinding: type: object additionalProperties: false required: [surface, effective_name, finding] properties: surface: {type: string, enum: [secret, variable]} effective_name: {type: string} finding: {type: string, enum: [possible_capture, owned_missing, crash_window]} ScimCapabilityOrigin: type: object additionalProperties: false required: [capability, kind, binding_id, mapping_id, group_id] properties: capability: {type: string} kind: {type: string, enum: [scim]} binding_id: {$ref: "#/components/schemas/ID"} mapping_id: {$ref: "#/components/schemas/ID"} group_id: {$ref: "#/components/schemas/ID"} security: - hikyoSession: []