Skip to content

Latest commit

 

History

History
522 lines (447 loc) · 176 KB

File metadata and controls

522 lines (447 loc) · 176 KB

API Contracts

This document answers one question: what request and response shape does every XID HTTP endpoint promise to callers, and how strong is the evidence behind that promise. It is written for integrators wiring up the Management API, the Hosted Auth endpoints, or SCIM, and for contributors changing those endpoints.

For protocol-level support depth read docs/protocols/; for how to call the SDKs read docs/sdks/.

Status Semantics

The status column in the tables carries two distinct meanings; do not conflate them:

  • PASS means the contract has an implementation plus matching code or test evidence. It does not mean the path has been exercised in a real environment.
  • An entry marked L4 means the path has been verified in a real deployment environment.

Reading rules:

  • An entry backed only by mock D1, mock fetch, mock queue, fake provider, fake browser, component tests, or focused unit tests counts as L1 only, and MUST NOT be used to claim production readiness.
  • Full evidence for auth, cookies, route fallback, public docs, console, provider, D1, Queues, and deployment lives in the L2/L3/L4 columns of docs/protocols/source-map.md, docs/protocols/gap-audit.md, and docs/sdks/platform-matrix.md.
  • Even when a sign-in endpoint is PASS at the unit-test level, it MUST still be verified with real Worker HTTP, a real browser, and a real deployment environment: after the cookie is written, a request to /v1/me carrying that same cookie MUST succeed.

Product Language: Organization, Not Tenant

The public product model is Instance, Organization, Project, Application, User, Session. TenantContext, tenant_id, and the tenant query layer are internal resolver and data isolation implementation only, not product objects. User-visible UI, public docs, public API documentation, and SDK documentation MUST all use Organization.

The fixed contracts that follow from this:

  • The public paths are /v1/platform/organizations and /scim/v2/organizations/{organization_id}. /v1/platform/tenants, TenantItem, and /scim/v2/{tenant_id} are historical shapes, already removed, and requests return 404.
  • POST /v1/sessions/active-organization returns a lightweight ActiveOrganizationResponse; after a successful call the core SDK re-runs load() to read /v1/me. The request body MUST explicitly include organizationId as a string or null; a missing field or an invalid value returns 422.
  • SDK constructor options MUST NOT accept tenant and MUST NOT send an X-Xid-Tenant header. The SDK accepts only apiUrl as the self-hosting or custom domain entry override; organization resolution is done by Host, OIDC context, identifier resolver, session, and the explicit organizationId API.

Hosted UI auth

Authentication: public auth endpoints or a cookie session. The public product semantic is organization policy; internal configuration is read from TenantContext. Social provider configuration comes from the internal auth policy, and a third-party clientSecret is referenced through clientSecretRef pointing at a Workers Secret.

Method Path Status Response
GET /auth/config production L4 Publicly safe Hosted Auth configuration; provider secrets are never returned. Ambiguous matches expose public candidates through organizationId; tenant_id is only an internal resolver hint or a historical test parameter and MUST NOT appear in SDK constructor options or public documentation examples
POST /auth/password/sign-in PASS Unified password flow. An existing user that satisfies policy gets a session cookie; a new identifier that satisfies password.allowUserCreation creates the user and returns { nextStep: "verify_email" } or { nextStep: "complete" }
POST /auth/sign-out PASS Revokes the current active browser session, clears its refresh cookie and active-session pointer, and returns HTTP 200 { ok: true }; a request without a session returns the same idempotent response
POST /auth/passkey/challenge PASS passkey challenge
POST /auth/passkey/verify PASS { redirectUrl? }; on success writes the session cookie
POST /auth/forgot-password PASS enumeration-resistant success response
POST /auth/reset-password PASS refreshes the session view on success
POST /auth/verify-email PASS { ok: true }
POST /auth/resend-verification PASS enumeration-resistant success response
POST /auth/magic-link/send PASS enumeration-resistant success response
GET /auth/magic-link/verify PASS Mutation-free compatibility redirect from a legacy query-string token to the Hosted UI /magic-link#token=... confirmation page; a missing or unresolvable credential redirects to the tokenless branded error state instead of rendering API JSON; never consumes the token or writes a session
POST /auth/magic-link/verify PASS Explicit user-confirmed token consumption; on success writes the session cookie and returns { redirectUrl }
POST /auth/otp/email/send PASS enumeration-resistant success response
POST /auth/otp/email/verify PASS { redirectUrl? }; on success writes the session cookie
POST /auth/otp/whatsapp/send PASS When the provider is ready, writes the OTP token and enqueues it to WHATSAPP_QUEUE; when the provider is not configured, returns the enumeration-resistant response and writes a policy denial audit event
POST /auth/otp/whatsapp/verify PASS { redirectUrl? }; on success writes the session cookie
POST /auth/otp/sms/send PASS When the provider is ready, writes the OTP token and enqueues it to SMS_QUEUE; when the provider is not configured, returns the enumeration-resistant response and writes a policy denial audit event
POST /auth/otp/sms/verify PASS { redirectUrl? }; on success writes the session cookie
POST /auth/mfa/sms/send PASS When the provider is ready, enqueues to SMS_QUEUE; when the provider is not configured, rejects
POST /auth/mfa/verify PASS { redirectTo? }
GET /auth/consent-params PASS consent prompt payload
POST /auth/consent PASS OIDC authorize resume redirect
GET /auth/:provider/authorize PASS Upstream OAuth provider redirect
GET/POST /auth/:provider/callback L1/L3 fake covered, real provider L4 missing OAuth callback; on success writes the session cookie. A raw invitation capability is not accepted as social authentication input: an unauthenticated holder must complete /auth/invitation/claim and /auth/invitation/claim/verify before any later authenticated social linking. L1 covers the provider gate, the policy gate, root state tenant restore, and the default /console fallback not returning to the issuer root; the L3 fake social smoke test exists (apps/server/tests/smoke/l3-social-oauth.test.mjs); real OAuth provider end-to-end sign-in evidence is still missing
GET /auth/social/:provider PASS HTTP 404. The old social alias is no longer registered
POST /auth/sign-up PASS HTTP 404. User creation happens only through the unified /auth/password/sign-in policy branch
POST /sso/hrd PASS { connectionId, orgId, protocol } or { connectionId: null }, gated by the enterprise SSO policy and the domain policy

Hosted Auth policy rules:

  • The frontend GET /auth/config returns publicly safe fields only.
  • The UI shows only methods where enabled = true and either sign-in or user creation is allowed; when forceSso matches, only enterprise SSO is shown.
  • Frontend passkey Conditional UI MUST also be controlled by the same policy gate. When /auth/config does not enable passkey, the sign-in page MUST NOT automatically request /auth/passkey/challenge.
  • A social provider that is unconfigured or disabled MUST NOT appear in socialProviders.
  • The Worker re-validates policy for password, magic link, email OTP, WhatsApp OTP, SMS OTP, passkey, and social authorize/callback.
  • Worker-side policy denials are all written to AUDIT_QUEUE with event name auth.policy_denied. The payload contains method, action, reason, path, ip, identifierType, identifierHash, emailDomain, and optional provider; it MUST NOT contain a full email address, phone number, or plaintext OTP code.
  • After a successful send, the email, WhatsApp, and SMS consumers write to AUDIT_QUEUE with event name notification.sent. The payload contains channel, type, provider, recipientType, recipientHash, emailDomain; it MUST NOT contain a full email address, phone number, token, or plaintext OTP code. A provider send failure is recorded in notification_failures, but recipient stores only sha256:<hash>, and payload stores only non-secret fields such as channel, type, tenantId, orgId, userId, locale, purpose, action, expiresInMin, recipientType, recipientHash, emailDomain; it MUST NOT contain a full email address, phone number, link, token, code, or message body.
  • The unified password endpoint distinguishes login from user_creation by whether the identifier already exists. An existing user MUST satisfy password.allowLogin; a new user MUST satisfy password.allowUserCreation and pass the identifier mode, domain, password length, and HIBP checks.
  • For new user creation the social callback MUST check allowUserCreation, requireVerifiedEmail, allowedEmailDomains, blockedEmailDomains.
  • The Magic Link send endpoint distinguishes login from user_creation by whether the email already exists. An existing user MUST satisfy magicLink.allowLogin, a new user MUST satisfy magicLink.allowUserCreation; a policy denial still returns the enumeration-resistant success response and sends no mail.
  • The Magic Link JWT carries action = login | user_creation; after consuming the token the verify endpoint re-validates policy against that action and marks the primary email as verified.
  • The Email OTP send endpoint distinguishes login from user_creation by whether the email already exists. An existing user MUST satisfy emailOtp.allowLogin, a new user MUST satisfy emailOtp.allowUserCreation; a policy denial still returns the enumeration-resistant success response and sends no code.
  • On successful Email OTP verification the one-time token is atomically marked consumed, the primary email is marked verified, and a session is issued.
  • When forceSso = true and enterprise SSO has enabled + allowLogin + domainDiscovery, the Hosted UI shows only the enterprise SSO email discovery form.
  • POST /sso/hrd queries a verified org domain only when enterprise SSO has enabled + allowLogin + domainDiscovery, and it applies both the Hosted Auth global allowedEmailDomains / blockedEmailDomains and the enterprise SSO allowedEmailDomains / blockedEmailDomains; when enterprise SSO or domain discovery is denied by policy it returns { connectionId: null } and writes auth.policy_denied.
  • OIDC RP JIT and SAML JIT apply both the Hosted Auth global domain policy and the enterprise SSO domain policy. When either policy denies, no user is created, no existing user is synced, and no membership is written.
  • /sign-up enters the unified /sign-in?intent=sign-up and MUST NOT show a fixed email + password registration form; there is no standalone POST /auth/sign-up API.

Public docs

Authentication: none. The apex root is the Nimbus Site product landing page, not a browser for the repository-internal docs/ directory. English uses /docs for the documentation hub and /<slug> for document details. The other 7 locales use lowercase locale prefixes: zh-hans, ja, ko, fr, de, es, and pt-br; their hubs are /<locale>/docs. The exact /scim documentation path belongs to Site, while /scim/v2/* and /scim/outbound/* remain Core protocol routes.

Method Path Current evidence Response
GET / local Site PASS Static Nimbus product landing page with concrete capability scope, evidence boundaries, and documentation entry points
GET /docs local Site PASS English Nimbus documentation hub
GET /getting-started local Site PASS Getting started. Current source and i18n use organization semantics
GET /hosted-auth local Site PASS Hosted Auth. Current source and i18n use organization semantics
GET /organizations local Site PASS Organization, OrgUnit, project access policy, and access-request model
GET /oidc-oauth local Site PASS OIDC and OAuth
GET /enterprise-sso local Site PASS Enterprise SSO. Distinguishes inbound SAML/OIDC, downstream SaaS SAML/OIDC, and the L4 boundary
GET /social-login local Site PASS Social login. Distinguishes the GitHub, Google, Microsoft account, and Apple provider-ready boundary from the production-supported boundary
GET /management-api local Site PASS Management API. Current source and i18n use organization API key and organization resource semantics
GET /scim local Site PASS Exact SCIM documentation route; examples use the Core protocol path /scim/v2/organizations/{organization_id}
GET /saml local Site PASS SAML SSO
GET /sdks local Site PASS SDKs. Distinguishes current package, local evidence, and planned design
GET /self-hosting local Site PASS Self-hosting technical documentation
GET /status local Site PASS English Nimbus status shell backed by Core /v1/public/status; remains renderable when the live API is unavailable
GET /<locale>/status local Site PASS Localized status shell for each of the 7 non-English route segments, with complete hreflang alternates
GET /(<locale>/)?status/index.md(x) local Site PASS Localized Markdown downlevel and raw MDX twins; each locale section index and root corpus includes the surface
GET /zh-hans local Site PASS Localized Nimbus product landing page
GET /zh-hans/docs local Site PASS Localized Nimbus documentation hub
GET /zh-hans/oidc-oauth local Site PASS Localized OIDC and OAuth documentation
GET /docs/getting-started local Site PASS Compatibility 308 to /getting-started; query parameters are preserved
GET /docs/api local Site PASS Site HTTP 404; does not render public Management API technical documentation
GET /docs/deployment local Site PASS Site HTTP 404; does not render docs/deployment.md
GET /docs/api-contracts local Site PASS Site HTTP 404; does not render this file
GET /docs/design local Site PASS Site HTTP 404; does not render docs/design/**
GET /docs/* local Site PASS An unregistered topic returns the Nimbus Site HTTP 404 and does not fall through to Core or redirect to /sign-in

The committed public allowlist is packages/types/src/public-docs.ts; Nimbus route validation lives in apps/site/src/lib/docs-registry.ts, and the localized document AST lives in apps/site/src/content-source/docs/documents.json. Only the hub and the 41 allowlisted technical documents enter the Nimbus content collection. Repository-internal documents such as docs/design, docs/api-contracts.md, and docs/deployment.md stay outside that collection and MUST return the Site 404. /docs/api is not a public alias. Historical public aliases such as /docs/oidc, /docs/oauth, and /docs/sso return 308 to their canonical Site paths with the query preserved.

Agent-readable surfaces

Every published documentation page has two static twins:

  • /<page>/index.md is the downleveled Markdown render for readers and retrieval pipelines. Its Source: line points to the same page's index.mdx.
  • /<page>/index.mdx is the authored MDX source after localized content generation.

The Site publishes the 352-page global index at /llms.txt and the byte-stable 352-page global corpus at /llms-full.txt. /en/llms.txt and /en/llms-full.txt each cover the 44 English pages; each of the other seven locales publishes the same 44-page pair under its locale segment, such as /zh-hans/llms.txt and /zh-hans/llms-full.txt. Each index links every page in its scope to the Markdown twin. Core-owned /.well-known/llms.txt returns 308 to the canonical Site /llms.txt.

Nimbus page heads carry title, description, canonical URL, hreflang alternates, Open Graph metadata, JSON-LD, and a Markdown alternate. The Site also owns /robots.txt, /sitemap.xml, /sitemap-index.xml, generated OG routes, and Pagefind. A content entry marked draft: true is not published; noindex: true excludes it from the agent index and search unless the content contract explicitly overrides searchability.

Static asset metadata MUST produce these response types:

Path pattern Content-Type
/*.md text/markdown; charset=utf-8
/*.mdx text/markdown; charset=utf-8
/*.txt text/plain; charset=utf-8

All Site responses, including redirects and 404 responses, carry X-XID-Route-Owner: site. Production L4 for the new three-Worker route split remains gated on the coordinated deployment and live owner-header smoke; local Site build evidence is not production evidence.

Platform console

Authentication: cookie session. Authorization: manager_assignments.manager_role = instance_manager and scope_type = instance.

The user.instanceManager field of GET /v1/me is used only by the same-origin console guard. It comes from a platform-layer manager_assignments query and MUST NOT be written into a business token claim.

The organizations[].role field of GET /v1/me comes from membership; for a user holding manager_assignments.manager_role = org_manager (scope_type = org), a member is returned promoted to admin, and a managed org with no membership is also backfilled into the list as admin, which matches the pass semantics of requireOrgManager.

GET /v1/me.managerAssignments is a Console discovery surface, not a token claim. It contains only the current Tenant and current user's project control-plane scopes: { id, managerRole, scopeType, scopeId, scopeStatus }. A project_manager remains discoverable for an active or deleted Project while its owning Organization is active, so the Console can restore it; scopeStatus is active or deleted. A project_grant_manager is returned only while both the Grant and its Project are active, with scopeStatus = active. Org managers remain represented through organizations, and Instance Managers through user.instanceManager.

The platform view entry points live under the unified console: /console/platform, /console/platform/organizations, /console/platform/users, /console/platform/events, /console/platform/flags, /console/platform/billing, /console/platform/plans, /console/platform/announcements, /console/platform/status, /console/platform/compliance, /console/platform/dead-letters, and /console/platform/settings. /platform-admin/* is no longer registered as a compatibility entry and MUST NOT host a standalone admin SPA. The old /v1/platform/tenants has been removed and returns 404; public docs, SDKs, and the catalog no longer reference the old naming.

Method Path Status Response
GET /v1/platform/stats PASS PlatformStats
GET /v1/platform/tenants historical wrong path, production 404 Old public contract target. The source has been removed, and local Worker HTTP with the localhost host returns 404; current main production HTTP returns 404, and public UI, docs, and SDKs do not reference it
PATCH /v1/platform/tenants/:tenantId historical wrong path, source removed Old public contract target. The source has been removed and it MUST NOT be treated as a public API PASS
GET /v1/platform/organizations production L4 The production Chrome console /console/platform/organizations has been accessed successfully with a real Instance Manager cookie. Response { data, nextCursor, total }, with items using PlatformOrganization semantics
PATCH /v1/platform/organizations/:organizationId production L4 Response PlatformOrganization. Local smoke:l2:platform and pnpm smoke:production:browser have verified: seed a temporary top-level organization, PATCH suspended -> active with a real cookie succeeds, then clean up the temporary org. The default organization MUST NOT be PATCHed to suspended or deleted and returns 409 conflict
GET /v1/platform/users PASS { data, nextCursor, total }
GET /v1/platform/manager-assignments local PASS Cookie-only Instance Manager list. Returns { data, nextCursor, total }; items are camelCase and contain only instance_manager / instance / null-scope assignments
POST /v1/platform/manager-assignments local PASS Body { user_id }; provisions an active user from any Organization as an Instance Manager. Self-provision is rejected and the assignment stores the target user's tenant as its isolation owner. The privilege mutation and platform.instance_manager.granted outbox row commit in one D1 batch before Queue delivery
DELETE /v1/platform/manager-assignments/:id local PASS Revokes one Instance Manager assignment; self-revocation and removal of the final Instance Manager are rejected atomically. The delete and platform.instance_manager.revoked outbox row commit in one D1 batch
GET /v1/platform/audit-events PASS { data, nextCursor, total }
GET /v1/platform/audit/verify local PASS Requires tenant_id; optional positive from_seq / to_seq. Recomputes the selected append-only chain in D1 batches of at most 1000 rows and returns { tenant_id, verified_range, chain_valid, broken_at_seq, failure_reason, record_count, computed_at }. A range after seq 1 is anchored to its stored predecessor; full verification starts at seq 1. The Console exposes the same diagnostic. An asynchronous Queue/KV job is not implemented
GET /v1/platform/dead-letters PASS Cursor-paginated redacted Queue dead-letter metadata. Ciphertext and plaintext payloads are never returned
GET /v1/platform/dead-letters/:id PASS One redacted Queue dead-letter record. Requires a verified instance_manager cookie session
POST /v1/platform/dead-letters/:id/replay PASS Claims the KEK-encrypted original message only for its recorded source Queue, rejects a concurrent live lease, reclaims a lease older than five minutes, and emits an audit event after completion. Completed records are idempotent; crash recovery is at-least-once because Queue acceptance can precede the D1 completion write
GET /v1/platform/billing PASS { data, nextCursor, total }
GET /v1/platform/feature-flags PASS FeatureFlag[]
PATCH /v1/platform/feature-flags/:key PASS FeatureFlag
GET /v1/platform/settings PASS PlatformSettings: instance default locale, MFA policy, password/session policy; Workers Secrets are never echoed back
PATCH /v1/platform/settings PASS PlatformSettings; can update mfaPolicy, passwordPolicy, sessionPolicy, defaultLocale; gated by an instance_manager cookie session
GET /v1/platform/plans/:tenantId local PASS Returns the top-level Organization's accounting plan, status, source, support label, trial/effective timestamps, seat limit, and persisted quota rows. This is local D1/route evidence only and does not prove an external billing-provider or production L4 path
PATCH /v1/platform/plans/:tenantId local PASS Updates plan, status, trial end, seat limit, and quota rows in one D1 batch with a durable platform audit intent. Seat limits use block_creation; usage counters such as API calls, email, and MAU remain observe-only. No external billing-provider or production L4 behavior is claimed
GET /v1/platform/billing/stripe-config local PASS, provider L4 UNKNOWN Optional adapter readiness; accepts optional tenantId query and returns enabled Checkout plans, Portal readiness, and MAU metering readiness without exposing any Stripe credential
POST /v1/platform/billing/checkout local PASS, provider L4 UNKNOWN Body { tenantId, plan, idempotencyKey }; Instance Manager only. Creates a Stripe-hosted Checkout session with tenant/plan metadata and a caller-stable idempotency key. Requires both Stripe secrets plus the matching plan price id
POST /v1/platform/billing/portal local PASS, provider L4 UNKNOWN Body { tenantId }; Instance Manager only. Creates a Stripe-hosted Billing Portal session for the reconciled external customer. Requires both Stripe secrets and an existing customer binding
POST /v1/platform/impersonation/start PASS Body { userId, organizationId }; an Instance Manager receives an opaque two-minute form-POST handoff descriptor for the exact target Organization host. Target identity never appears in the grant fields or URL
POST /auth/impersonation/consume PASS Body { grantId, secret }; exact target-host TenantContext plus ImpersonationGrantDO consume-once validation creates a 15-minute session restricted to GET / HEAD / OPTIONS under /v1/* and explicit impersonation end; protocol, auth, SSO, session-token exchange, and every mutation path return 403
POST /auth/impersonation/handoff PASS application/x-www-form-urlencoded twin of consume; succeeds with 303 /console, never places the opaque grant in a query string
POST /auth/impersonation/end PASS Requires the current impersonation session, durably records the end through platform_audit_outbox, revokes the session, and returns { ok: true, redirectUrl }; redirectUrl is derived from the target TenantContext instance issuer and returns the operator to /console/platform/users on the manager host
GET /v1/platform/announcements local PASS Cursor-paginated global, tenant, and accounting-plan announcement ledger
POST /v1/platform/announcements local PASS Creates a draft or published time-bounded announcement and a redacted durable platform audit intent
PATCH /v1/platform/announcements/:id local PASS Updates targeting, content, severity, publication state, or time window; the mutation and audit intent commit in one D1 batch
DELETE /v1/platform/announcements/:id local PASS Deletes the announcement and durably records the operator action
GET /v1/platform/status-incidents local PASS Cursor-paginated incident ledger with timestamped updates
GET /v1/platform/status-incidents/:id local PASS One incident and its append-only update timeline
POST /v1/platform/status-incidents local PASS Opens an incident; accepted impact values are none, minor, major, and critical
PATCH /v1/platform/status-incidents/:id local PASS Changes incident metadata or state; resolved_at follows the resolved state
POST /v1/platform/status-incidents/:id/updates local PASS Appends a timestamped state update and atomically advances the incident
DELETE /v1/platform/status-incidents/:id local PASS Deletes an incident plus its updates in the same D1 batch and records a durable audit intent
GET /v1/platform/compliance-documents local PASS Cursor-paginated versioned global and tenant-specific evidence metadata
POST /v1/platform/compliance-documents local PASS Registers metadata for an immutable compliance/ R2 object and required lowercase sha256: checksum
PATCH /v1/platform/compliance-documents/:id local PASS Publishes, retires, or corrects evidence metadata before tenant acceptance; accepted evidence is immutable
GET /v1/platform/compliance-documents/:id/artifact local PASS Authenticated private attachment proxy; rejects unsafe keys, objects over 10 MiB, and checksum mismatches
DELETE /v1/platform/compliance-documents/:id local PASS Deletes only when the document itself is unaccepted and no Organization acceptance copy references it. The delete predicate and tenant acceptance write both verify this relation inside their D1 batch, so a concurrent acceptance cannot leave an orphaned evidence record

Public operational API

Authentication: none. The response exposes only published incident state and timestamped public updates; it contains no tenant, operator, or audit metadata.

Method Path Status Response
GET /v1/public/status local PASS Overall state derived from unresolved incident impact plus the current public incident timeline

POST /v1/billing/stripe/webhook is also public because Stripe cannot hold an XID cookie. It is not a public management mutation: Core first verifies the raw request body with Stripe-Signature, enforces the timestamp tolerance, validates the event shape, and only then reconciles the top-level Organization plan, quotas, seat mirror, durable audit intent, and event receipt in one D1 batch. Duplicate ids are idempotent and older or lower-priority same-timestamp events cannot revert newer state. Local SQLite/route evidence is PASS; real Stripe delivery and provider L4 remain UNKNOWN.

Authenticated tenant operations

These paths use the request TenantContext. Active announcements require an active cookie session; compliance list, download, and DPA acceptance additionally require Organization Manager access to the tenant root.

Method Path Status Response
GET /v1/announcements/active local PASS Active published global, current-tenant, and current accounting-plan announcements; private cache for 30 seconds
GET /v1/compliance/documents local PASS Available global plus tenant-specific evidence, with a tenant version taking precedence for the same document type and version
GET /v1/compliance/documents/:id/artifact local PASS Private checksum-verified evidence attachment
POST /v1/compliance/documents/:id/accept local PASS DPA only; creates an immutable tenant acceptance record with actor, timestamp, source version, and checksum; concurrent retries converge idempotently

Organization console

Authentication: cookie session. Authorization: the current user MUST hold an admin or owner membership in the target org, or a manager_assignments row with org_manager / instance_manager. The same path presented with Bearer sk_* is still authenticated as a Management API key.

org_manager is narrowed to the target org by the internal isolation query layer; instance_manager is resolved through the platform-layer ManagerAssignment query and does not require the current session active org to equal the target org. The current platform list enters the same policy configuration page through /console/org/auth-policy?orgId=<org_id> and MUST NOT add a standalone admin app. Future public copy MUST say Organizations and MUST NOT say Platform tenants.

Method Path Status Response
GET /v1/organizations/:orgId/stats PASS OrgStats, containing dau, mau, loginSuccessRate, mfaAdoptionRate, activeMemberCount, pendingInvitationCount
GET /v1/organizations/:orgId/members PASS { data, nextCursor, total }
DELETE /v1/organizations/:orgId/members/:memberId PASS 204; writes memberships.status = inactive
GET /v1/organizations/:orgId/invitations PASS { data, nextCursor, total }
POST /v1/organizations/:orgId/invitations PASS OrgInvitation
DELETE /v1/organizations/:orgId/invitations/:invitationId PASS 204; writes invitations.status = revoked
GET /v1/organizations/:orgId/roles PASS OrgRole[]
GET /v1/organizations/:orgId/sso-connections PASS SsoConnection[]
POST /v1/organizations/:orgId/sso-connections PASS SsoConnection; supports first-version SAML/OIDC configuration and returns the editable fields: IdP metadata, certificate, OIDC discovery, JIT, attribute mapping, role mapping, signature requirements
PATCH /v1/organizations/:orgId/sso-connections/:connectionId PASS SsoConnection; can update SAML/OIDC connection configuration, JIT, attribute mapping, role mapping, and signature requirements; the target connection MUST belong to the current org and MUST be active
DELETE /v1/organizations/:orgId/sso-connections/:connectionId PASS 204; writes sso_connections.status = deleted, and normal lists filter out deleted
GET /v1/organizations/:orgId/directories PASS ScimDirectory[]
POST /v1/organizations/:orgId/directories PASS CreatedScimDirectory; scimToken is returned in this response only
POST /v1/organizations/:orgId/directories/:directoryId/rotate-token PASS RotateScimTokenResult; scimToken is returned in this response only
GET /v1/organizations/:orgId/domains PASS OrgDomain[]
POST /v1/organizations/:orgId/domains PASS OrgDomain
GET /v1/organizations/:orgId/custom-hostnames local PASS, provider L4 UNKNOWN Cursor-paginated { data, next_cursor, has_more }; org-scoped custom_hostnames:read; returns lifecycle state, ownership expiry, ownership TXT, DCV CNAMEs, traffic CNAME, verification codes and requires_passkey_reregistration; deleted tombstones are hidden
GET /v1/organizations/:orgId/custom-hostnames/:customHostnameId local PASS, provider L4 UNKNOWN One non-deleted custom hostname from the target org; a cross-tenant id is the same opaque 404 as a missing id
POST /v1/organizations/:orgId/custom-hostnames local PASS, provider L4 UNKNOWN Body { hostname }; rejects schemes, paths, ports, IP/private/special-use hosts, wildcard hosts and the instance primary domain or its subdomains. Globally reserves the normalized hostname before Cloudflare create. Returns ownership/DCV/traffic DNS instructions and the passkey warning flag. A cross-tenant hostname collision is an opaque 409 already_exists
POST /v1/organizations/:orgId/custom-hostnames/:customHostnameId/refresh local PASS, provider L4 UNKNOWN Re-reads the exact Cloudflare object and updates hostname/SSL/DCV state. A mismatched provider id or hostname fails closed with 503. Local active requires both provider hostname and SSL status to be active
DELETE /v1/organizations/:orgId/custom-hostnames/:customHostnameId local PASS, provider L4 UNKNOWN Calls Cloudflare delete before local deletion. Remote failure records deletion_failed and retains the binding. Success preserves a global hostname tombstone and returns the traffic CNAME the operator must remove
GET /v1/organizations/:orgId/auth-policy PASS OrgAuthPolicy; returns only the Hosted Auth policy and the read-only deliveryChannelReadiness.whatsappOtp/smsOtp. deliveryChannelReadiness is computed live from Workers env and delivery channel secret completeness and MUST NOT be read from the request body or the D1 policy; Social provider configuration and the old providerReadiness field are not returned
PATCH /v1/organizations/:orgId/auth-policy PASS Saves the Hosted Auth policy only. Even when the request body contains socialProviders, providerReadiness, deliveryChannelReadiness, or credentialsReady, it MUST NOT write or overwrite Social provider configuration or delivery channel readiness
GET /v1/organizations/:orgId/social-providers PASS OrgSocialProviders. Returns provider connection configuration but MUST NOT return clientSecretRef; it returns only hasClientSecret and credentialsReady. hasClientSecret means a secret ref is stored in the policy, credentialsReady means enabled + clientId + authorizationEndpoint + tokenEndpoint + clientSecretRef + Workers Secret are all complete; an OIDC provider additionally requires a complete issuer and JWKS URI, and GitHub uses a dedicated profile flow
PATCH /v1/organizations/:orgId/social-providers PASS Saves the Social provider set. When the request contains socialProviders, that object is the provider set after saving and any missing provider is deleted; when a provider field is present, clearing optional strings, redirect URIs, scopes, allowed domains, and blocked domains is allowed; when a field is absent the old value is kept. The provider secret ref is never echoed back, and a normal save that omits the secret ref MUST NOT erase the existing secret ref. credentialsReady is a read-only field: even when supplied in the request body it is ignored and recomputed from the current Workers env
GET /v1/organizations/:orgId/branding PASS OrgBranding, KV key brand:{tenant_id}:{org_id}
PATCH /v1/organizations/:orgId/branding PASS OrgBranding, KV key brand:{tenant_id}:{org_id}
GET /v1/organizations/:orgId/outbound-saml-apps PASS OutboundSamlApp[]; returns downstream SaaS SAML app configuration, ACS URL, NameID policy, assignmentGate (mode / allowed_user_ids / allowed_roles), and provider preset metadata, and never echoes back the secret ref
POST /v1/organizations/:orgId/outbound-saml-apps PASS OutboundSamlApp; supports Slack, GitHub Enterprise, Microsoft custom app, Atlassian, Salesforce, and Zoom preset fields; optional assignment_gate; local L3 already covers metadata and SSO POST with a fake SaaS SP
PATCH /v1/organizations/:orgId/outbound-saml-apps/:appId PASS OutboundSamlApp; can update ACS, SLO, attribute mapping, assignment_gate, and preset metadata; restricted mode gates outbound SSO launch by allowed_roles and allowed_user_ids
DELETE /v1/organizations/:orgId/outbound-saml-apps/:appId PASS 204; hard-deletes the saml_service_providers row (the table has no status column; after deletion the metadata/SSO routes return 404)
GET /v1/organizations/:orgId/scim-targets PASS ScimTarget[]; downstream SaaS SCIM push configuration; returns baseUrl, assignmentGate, requiredTokenSecretName, hasTokenSecret, and syncPath. The server derives the only accepted Workers Secret name as SCIM_TARGET_TOKEN_<normalized target id>; it never accepts or echoes a tenant-selected token_secret_ref
POST /v1/organizations/:orgId/scim-targets PASS ScimTarget; the body MUST contain provider and a public HTTPS base_url, and MAY contain assignment_gate. The server creates the target id and returns requiredTokenSecretName; callers configure that exact Workers Secret out of band. Supplying token_secret_ref is rejected. Requires the connections:write scope or an org admin cookie
PATCH /v1/organizations/:orgId/scim-targets/:targetId PASS ScimTarget; can update provider, a public HTTPS base URL, and assignment_gate. Supplying token_secret_ref is rejected; a tenant cannot select an arbitrary Worker binding
DELETE /v1/organizations/:orgId/scim-targets/:targetId PASS 204; writes scim_targets.status = deleted
POST /v1/organizations/:orgId/scim-targets/:targetId/sync PASS 202 { runId, targetId, status: "queued" }; authorizes and enqueues the outbound run without calling the SaaS in the request path. The consumer applies the Organization Membership plus assignment-gate intersection, persists stable downstream User/Group mappings, performs idempotent upsert/deprovision, retries 408/429/5xx, and emits correlated audit events
GET /v1/organizations/:orgId/delivery-channels PASS OrgDeliveryChannels; WhatsApp/SMS OTP and MFA delivery channel configuration; credentialsReady is computed live from Workers env, and the secret ref is never echoed back
PATCH /v1/organizations/:orgId/delivery-channels PASS OrgDeliveryChannels; saves the provider choice and secret ref; an omitted secret ref MUST NOT erase the existing value
GET /v1/organizations/:orgId/audit-events PASS { data, nextCursor, total }; org-scoped read-only audit stream; org_manager sees only this org, instance_manager can query across orgs; cursor pagination

Product orchestration

Authentication: cookie session for self-service org creation. Invitation claim initiation and verification are public, capability-bound endpoints; all invitation holders use them, including callers that already have a session. Covers organization invitations, self-service org creation, Device Flow activation, and CIBA user approval. The public product semantic is Organization; tenant_id is internal isolation only.

Method Path Status Response
GET /auth/invitation/preview PASS Query MUST contain the raw token. Its locator selects a same-Instance candidate Tenant, but the complete token hash must match inside that Tenant before metadata is returned. Signed User/Session continuations are not accepted. Invalid and expired capabilities return a 200 preview with status = invalid or expired. The capability authorizes an attempt; preview never proves Email ownership or selects a User
POST /auth/invitation/claim PASS Body { token, turnstileToken? }. Returns the opaque { ok: true } without revealing invitation validity or account existence. For a valid pending capability, resolves through the target Tenant's scoped database and sends only to the invitation's exact normalized Email. The signed claim JWT has purpose = invitation_email_claim, tenant_id, sub = invitationId, jti, and email_hash, expires after 15 minutes, and is single use. This endpoint performs no User lookup/reuse or write and persists no password, phone, social identity, passkey, MFA factor, session, or Membership
POST /auth/invitation/claim/verify PASS Body { token, recoveryKey }, where recoveryKey is a browser-generated random string of 32-256 characters. Verifies the instance signature, purpose, Tenant, invitation row, exact Email hash, expiry, and first-use jti. The proof winner atomically marks the stored SHA-256(jti) consumed, freezes a random server-side consumption id, binds SHA-256(recoveryKey), and commits pending -> claim_verified. It reuses only an exact active User plus verified-primary Email row carrying durable invitation_email_claim_v1 provenance; every other collision detaches only that Email row and creates a credential-free User without transferring or scrubbing the old account. The original signed claim plus the same recovery key then issues or recovers one session, applies the post-auth MFA gate, creates/reactivates the Membership, and commits claim_verified -> accepted. Accepted retries return the same { redirectUrl } and may repair that browser session, but never create another Membership or emit another acceptance webhook
POST /auth/invitation/accept PASS Always rejects with the opaque invitation_invalid response. Raw or signed-continuation acceptance is disabled; every invitation holder uses /auth/invitation/claim and /auth/invitation/claim/verify
POST /v1/organizations/self PASS A signed-in user self-service creates an organization; the body MUST contain slug and name; creates the owner membership, switches the active org, and returns { organization, redirectUrl }
GET /auth/device-activation PASS Device Flow user-side view; query user_code; returns the client name, scope summary, and expiry; returns 401 when not signed in
POST /auth/device-activation PASS Device Flow approve or deny; body { userCode, approved }; on approval binds userId to the device code; returns 401 when not signed in
GET /auth/ciba-activation PASS CIBA user-side view; query auth_req_id; returns authReqId, clientId, scope, loginHint, expiresAt, and firstParty; returns 401 when not signed in
POST /auth/ciba-activation PASS CIBA approve or deny; body { authReqId, approved }; returns 401 when not signed in

Both invitation claim endpoints re-resolve the active target Organization inside the token's trusted Instance. Claim start checks its Email and Magic Link user-creation policy but folds a policy denial into the same opaque { ok: true }. Claim verification rechecks the target policy before any proof-stage write and uses that Organization's MFA enforcement when selecting active, pending_mfa_setup, or pending_mfa; it never falls back to the Instance-root policy. Recovery does not extend the Email proof: every retry still verifies the original 15-minute signed claim as well as the browser-owned recovery key.

Hosted UI SPA routes: /accept-invitation, /create-organization, /select-organization, /activate. The initial invitation entry uses the raw token query parameter, but that capability is never authentication or proof of its Email. An unauthenticated visitor must use the dedicated claim endpoints; generic password, phone, social, passkey, MFA, magic-link, and OTP flows MUST NOT create or reuse a User from the invitation before proof. The claim message is sent only to the invitation's exact normalized Email, and the raw invitation token is never persisted in the claim row or signed claim JWT. A signed-in user follows the same claim path; a session or verified flag alone never selects an existing User for invitation acceptance. Reuse requires the exact primary Email/User tuple previously created by an invitation_email_claim_v1 ceremony. Every other exact collision detaches only that Email association plus its stale pointers/artifacts and creates a credential-free invited User, without transferring or scrubbing the old account. The browser-owned recovery key binds proof, session recovery, and acceptance to the same claimant without becoming a server-stored bearer secret. Raw or signed-continuation /auth/invitation/accept is not an acceptance path: the compatibility route remains registered only to return the opaque invitation_invalid rejection. A successful product sign-up without an invitation defaults to /create-organization.

Migration 0006_expire_legacy_invitation_tokens.sql revokes pre-xid_inv_v1 pending capabilities because their hash-only rows cannot restore a Tenant from the Instance apex without a global lookup. The retained revoked rows are the resend audit list. New create and bulk-create paths always persist the internal token_version = locator_v1; neither tokenHash nor tokenVersion is returned by the Management API. Migration 0011_invitation_email_claim.sql adds proof/recovery markers, normalizes historical pending Emails, deterministically revokes older duplicate (tenant_id, org_id, email) rows, then creates a partial unique index covering pending and claim_verified. Create and bulk create reject an existing pending claim with HTTP 409. They reject an already-active member only when the exact primary Email/User tuple carries accepted invitation_email_claim_v1 provenance; an ordinary verified or unverified Email collision cannot block a rightful owner from starting the proof flow. Management list/detail surfaces map internal claim_verified to pending, and revoke or delete accepts both states, revoking any reserved claim session before a new invitation can be created. API responses expose neither claim internals nor displaced identity references.

Account portal

Authentication: cookie session. Authorization: only resources belonging to the current session userId may be operated on, with tenant_id injected by createTenantDb.

Method Path Status Response
GET /v1/me PASS Browser account summary. Anonymous requests return { user: null, activeOrg: null, organizations: [], managerAssignments: [], session: null, activeSessionId: null, sessions: [] }. Signed-in responses include the active session, only other sessions backed by valid refresh cookies held by this browser, and active-valid Project/Grant ManagerAssignment discovery metadata described above
GET /v1/me/profile PASS UserProfile
PATCH /v1/me/profile PASS UserProfile
GET /v1/me/passkeys PASS PasskeyCredential[]
PATCH /v1/me/passkeys/:id PASS PasskeyCredential
DELETE /v1/me/passkeys/:id PASS 204
GET /v1/me/mfa-factors in progress, negative production L4 covered MfaFactor[]; SMS is returned only when there is a verified phone and the SMS provider is ready. Local L2/L3 verified that the MFA UI does not always show SMS; in production with no SMS provider configured, a real cookie verified that SMS is not returned and that /mfa?method=sms shows no SMS challenge and no secondary menu. The positive provider-ready L4 still lacks a real SMS provider secret and a verified phone number
DELETE /v1/me/mfa-factors/:id PASS 204
GET /v1/me/social-connections PASS SocialConnection[]
DELETE /v1/me/social-connections/:id PASS 204
GET /v1/me/trusted-devices PASS TrustedDevice[]
DELETE /v1/me/trusted-devices/:id PASS 204
GET /v1/me/sessions PASS ActiveSession[]
DELETE /v1/me/sessions/:id PASS 204
POST /v1/me/sessions/revoke-all PASS { revoked: true }
GET /v1/me/privacy/requests local PASS Up to the current user's 100 newest privacy requests, tenant and user scoped. Export responses expose an authenticated relative downloadUrl only while the private R2 object is available and unexpired
POST /v1/me/privacy/requests local PASS Accepts type: export, or type: delete together with the exact destructive confirmation confirmation: DELETE. The Account UI exposes deletion only through a second confirmation dialog. Export is queued immediately; deletion is scheduled after a cancelable 30-day grace period. Scheduling returns an opaque 409 conflict when erasure would remove an Organization's sole active owner or the last active instance_manager in the same Instance scope; the Queue consumer rechecks after the grace period and its D1 batch atomically rolls back if roles changed. A concurrent pending or processing request of the same type is returned instead of duplicated
GET /v1/me/privacy/requests/:id local PASS Returns only a request owned by the current tenant and session user
POST /v1/me/privacy/requests/:id/cancel local PASS Cancels a pending request. An already canceled request is idempotent; processing or terminal requests return 409
GET /v1/me/privacy/requests/:id/download local PASS Streams only the current user's completed, unexpired export with private, no-store, attachment, sandbox, and nosniff headers. The URL is not a public or signed R2 URL
POST /v1/sessions/token PASS Exchanges a normal active cookie session for a short-lived first-party JWT and returns { token }; an impersonation cookie is rejected with HTTP 403 at the global impersonation boundary
POST /v1/sessions/active PASS Accepts { sessionId }; the matching browser-held HttpOnly refresh cookie is the credential. On success updates the HttpOnly active-session pointer and returns { activeSessionId }; an id without its valid cookie returns an opaque 401
POST /v1/sessions/active-organization production L4 The request body MUST explicitly include organizationId with a value of string or null. For a non-empty string it validates the current user's active membership and that the organization is active and not soft deleted; null only clears the current session active org. An invalid body returns 422 with meta.paramName=organizationId. The Worker currently returns a lightweight { session, activeOrganizationId }; the SPA auth context and the core SDK MUST re-read /v1/me after success. Focused L1, local Worker HTTP L2, local browser L3, and production browser L4 all exist

Management API

Authentication: Bearer sk_live_* or Bearer sk_test_*. Tenant isolation: TenantContext + createTenantDb. API key scope rules:

  • scopes = []: no permission. Both write and read operations MUST match an explicit scope.
  • resource:read: allows GET / export style read operations on the matching resource.
  • resource:write: allows POST / PATCH / PUT / DELETE / revoke / rotate style write operations on the matching resource.
  • resource:*: allows read and write on the matching resource.
  • *: allows every Management API resource.
  • Successful authentication with insufficient scope returns 403 and code = insufficient_permission.

/v1/projects, /v1/roles, /v1/permissions, /v1/role-permissions, /v1/manager-assignments, /v1/project-grants, and /v1/user-grants additionally accept a same-origin cookie session. That path is authorized against an exact Organization, Project, or ProjectGrant ManagerAssignment and never promotes a Project manager role to Organization Admin.

Resource Status Confirmed
/v1/users PASS CRUD, ban, unban, bulk metadata, export, and restore exist; delete is a soft delete, and normal lists and detail filter out deleted; restore clears deleted_at and returns the row to active, and returns 409 already_exists when an active non-deleted user already holds the same username or external_id; username and external_id are unique among active non-deleted users, and a soft deleted user releases the identifier; scopes users:read / users:write are tested
/v1/organizations PASS CRUD, logo, domains, and restore exist. POST creates one-level child organizations only: parent_org_id is required and must identify the current active top-level Organization; cross-Tenant parents, child-of-child creation, and implicit reparenting are rejected. Child delete/restore is soft; top-level status changes stay on the separate Instance Manager path. POST rejects seat_limit. PATCH accepts seat_limit only for the root tenant and atomically updates both its compatibility mirror and the authoritative organization_quotas(seats) hard quota. Service validation and migration-owned triggers enforce the hierarchy. Scopes organizations:read / organizations:write are tested
/v1/api-keys PASS Bearer sk_* goes through scopes api_keys:read / api_keys:write; a cookie session is gated by instance_manager; create returns the plaintext key once only; revoke semantics exist, normal lists and detail filter out revoked, and a revoked key can no longer authenticate; API keys provide no restore
/v1/projects local PASS Full CRUD with scopes projects:read / projects:write. Delete is reversible (status = deleted, deleted_at set); restore requires the owning Organization to remain active. Every shared client lookup, including token refresh and client credentials, rejects applications linked to a deleted Project. List defaults to status=active and accepts deleted or all; cookie sessions must still supply org_id or exact project_id. Organization owner/admin creates; exact project_manager can read/update/delete/restore only its Project
/v1/roles PASS Delete is a soft delete and restore clears deleted_at. List defaults to status=active, accepts deleted or all, and returns status plus deleted_at; detail remains active-only. API keys use roles:read / roles:write. Cookie reads require project_id; a Grant-scoped reader also supplies grant_id. Only the Project owner/admin or exact project_manager can mutate definitions
/v1/permissions PASS Delete is a soft delete and restore clears deleted_at. List defaults to status=active, accepts deleted or all, and returns status plus deleted_at; detail remains active-only. API keys use permissions:read / permissions:write. Cookie reads require project_id; a Grant-scoped reader also supplies grant_id. Only the Project owner/admin or exact project_manager can mutate definitions
/v1/role-permissions local PASS List/create/update/delete with scopes role_permissions:read / role_permissions:write. Role and Permission must both be active, tenant-scoped, and belong to the same active Project. condition_expression accepts only the documented ABAC v1 leaf or non-empty and grammar; malformed JSON structure, operator, variable path, or in operand is rejected with 422 before persistence
/v1/manager-assignments local PASS Tenant-scoped list/provision/revoke with scopes manager_assignments:read / manager_assignments:write. Only org_manager/org, project_manager/project, and project_grant_manager/grant pairs are accepted. Target user and scope must be active in the current tenant. Cookie actors cannot modify their own assignment; Project and Grant manager provisioning requires the owning Organization manager boundary
/v1/applications PASS Management API delete sets status = deleted, and restore returns the row to active but does not return the client secret; secret rotate and detail accept active only; scopes applications:read / applications:write
/v1/connections PASS delete sets status = deleted, and restore returns the row to active; the SSO runtime path accepts active only; scopes connections:read / connections:write
/v1/directories PASS delete sets status = deleted and sync_status = disabled; restore returns the row to active, resets sync_status = idle, regenerates the SCIM token, and returns scim_token in this response only; the SCIM token accepts an active directory only; scopes directories:read / directories:write
/v1/webhooks PASS delete sets status = deleted, and restore returns the row to active but does not return the signing secret; normal lists and detail filter out deleted; scopes webhooks:read / webhooks:write
/v1/organizations/:orgId/memberships PASS delete sets status = inactive, and normal lists and detail filter out inactive; restore returns the row to active and re-validates that the target user is active and deleted_at IS NULL; scopes memberships:read / memberships:write. API keys cannot create, promote, restore, or reactivate an owner; demote, deactivate, and delete use one conditional D1 mutation that preserves another active owner backed by an active user
/v1/organizations/:orgId/invitations PASS delete and revoke set status = revoked; normal lists default to pending and detail returns pending only; invitation tokens provide no restore; scopes invitations:read / invitations:write
/v1/project-grants PASS List/get/create/revoke/delete exist; delete/revoke set status = revoked and revoked_at, and cascade to user_grants.revoked_at. API keys use project_grants:read / project_grants:write. Cookie list is narrowed by granted_project_id or granted_to_org_id; exact project_grant_manager and recipient Org admin can read a grant, but only the Project owner/admin or exact project_manager can create or revoke it
/v1/user-grants local PASS List/get/create/reactivate/revoke/delete exist; normal list/detail return unrevoked rows only. API keys use user_grants:read / user_grants:write. Cookie writes require an exact Project owner/admin, project_manager, active project_grant_manager, or recipient Org owner/admin boundary. Grant-scoped writes accept only an active member of the recipient Org and a Role in the granted Project
/v1/sessions PASS list/get/revoke/revoke_all exist; revoke and revoke_all set status = revoked; normal lists and detail return active only; revoke_all rejects a deleted user; scopes sessions:read / sessions:write
/v1/organizations/:orgId/custom-hostnames local PASS, provider L4 UNKNOWN Org-scoped list/get/create/refresh/delete; scopes custom_hostnames:read / custom_hostnames:write; global hostname reservation, remote-first deletion, status polling, active-only TenantContext reverse lookup and Console passkey warning are covered locally. No production Cloudflare for SaaS, customer DNS, certificate or traffic evidence exists yet

Project authorization control-plane contracts

All tenant routes below use { data, next_cursor, has_more } for lists. Project responses are { id, org_id, name, description, status, deleted_at, created_at, updated_at }. RolePermission responses are { id, role_id, permission_id, condition_expression, created_at }. ManagerAssignment responses are { id, user_id, manager_role, scope_type, scope_id, created_at, updated_at }. Role and Permission responses include status and deleted_at; their list routes accept status=active, status=deleted, or status=all and default to active.

Tenant-scoped Project, Role, Permission, RolePermission, and ManagerAssignment mutations enqueue redacted management.* records through AUDIT_QUEUE after the scoped D1 mutation. This path is asynchronous and is not yet atomic with the relational write. Instance Manager grant/revoke uses the persisted platform audit outbox and is atomic with its privilege mutation.

Method Path Request / behavior
GET /v1/projects Query org_id?, project_id?, grant_id?, status?, limit?, cursor?; status accepts active, deleted, or all and defaults to active. Cookie sessions require org_id or project_id; an exact ProjectGrant reader supplies grant_id
POST /v1/projects { org_id, name, description? }
GET /v1/projects/:id Active Project only
PATCH /v1/projects/:id { name?, description? }; description: null clears it
DELETE /v1/projects/:id 204; soft delete
POST /v1/projects/:id/restore Restores a deleted Project under its still-active owning Organization
GET /v1/roles Query project_id?, grant_id?, status?, limit?, cursor?; status accepts active, deleted, or all, defaults to active, and cookie sessions require project_id
GET /v1/permissions Query project_id?, grant_id?, status?, limit?, cursor?; status accepts active, deleted, or all, defaults to active, and cookie sessions require project_id
GET /v1/role-permissions Query role_id?, grant_id?, limit?, cursor?; cookie sessions require role_id
POST /v1/role-permissions { role_id, permission_id, condition_expression? }
GET /v1/role-permissions/:id API key or exact Project/Grant read boundary
PATCH /v1/role-permissions/:id { condition_expression }; use null for an unconditional mapping
DELETE /v1/role-permissions/:id 204; physically removes only the mapping
GET /v1/manager-assignments Query scope_type?, scope_id?, manager_role?, user_id?, limit?, cursor?; cookie sessions require the exact scope_type and scope_id
POST /v1/manager-assignments { user_id, manager_role, scope_type, scope_id }; fixed role/scope pairs only
DELETE /v1/manager-assignments/:id 204; tenant assignment only. instance_manager rows are deliberately invisible here

Protocol routes

Area Status File entry
OIDC discovery PASS apps/server/worker/oidc/discovery.ts
OIDC authorize PASS apps/server/worker/oidc/authorize.ts
OIDC token PASS apps/server/worker/oidc/token.ts
OIDC userinfo PASS apps/server/worker/oidc/userinfo.ts
OAuth PAR PASS apps/server/worker/oidc/par.ts
OAuth device flow PASS apps/server/worker/oauth/device.ts
OAuth revoke PASS apps/server/worker/oauth/revoke.ts
OAuth introspection PASS apps/server/worker/oauth/introspect.ts
OAuth protected resource metadata PASS apps/server/worker/oidc/protected-resource.ts
OAuth token exchange PASS apps/server/worker/oidc/token-exchange.ts
OIDC request object PASS apps/server/worker/oidc/request-object.ts
OIDC RAR authorization details PASS apps/server/worker/oidc/authorization-details.ts
SCIM users and groups provider-ready, real IdP provisioning L4 missing XID acts as an inbound SCIM Service Provider. apps/server/worker/scim/**; the public path is /scim/v2/organizations/{organization_id}. Local L3 already covers Users/Groups CRUD, filter, projection, and the enterprise extension; local/production paths and a 401 without Bearer are not real IdP provisioning L4; real Microsoft Entra/Okta/Auth0/Clerk/Zitadel provisioning into XID is still missing
SCIM downstream SaaS target clients implemented, real SaaS L4 missing XID acts as an outbound SCIM client pushing users and groups to the Slack/GitHub Enterprise Cloud/Atlassian/Salesforce/Zoom SCIM API. Both sync endpoints enqueue xid-scim-sync; the serialized consumer uses scim_target_resources plus deterministic externalId discovery for idempotent User/Group upsert, maps Group members to downstream User ids, deprovisions only after a complete upsert phase, honors Retry-After, and records the run through AUDIT_QUEUE. The Organization Membership intersection remains the mandatory upper bound before the assignment gate. Real Slack/GitHub/Atlassian/Salesforce/Zoom admin L4 is still missing; inbound SCIM Service Provider evidence MUST NOT be reused as SCIM push-to-SaaS L4, and public docs MUST NOT promise production-supported
SAML inbound SSO provider-ready, L4 missing XID acts as the SAML SP for an upstream enterprise IdP. /sso/saml/:connection/acs, /metadata, /login; the local fake IdP L3 (smoke:l3:inbound-saml) already covers metadata refresh, ACS POST, and JIT. Real IdP metadata/config and production L4 are still missing
SAML outbound SaaS SSO provider-ready, real SaaS L4 missing XID acts as the SAML/OIDC IdP for downstream SaaS. /sso/outbound/saml/:appId/metadata, /sso, /slo; the console OrgOutboundSso.tsx (including assignment gate editing) and the provider presets have landed; assertUserPassesAssignmentGate gates SSO launch; Slack/GitHub/Microsoft/Atlassian/Salesforce/Zoom templates already have local L3. Real SaaS admin L4 is still missing. Public docs MUST NOT promise production-supported
Enterprise legacy SSO implemented, real gateway/L4 missing LDAP POST /sso/ldap/:connectionId/login, WS-Fed GET | POST /sso/wsfed/:connectionId/\*, SWA POST /sso/swa/:connectionId/authenticateand the auth-gatedPOST /sso/swa/:connectionId/vault, header SSO POST /sso/header/:connectionId/authenticate(requiresattribute_mapping.\_legacy.trustedProxySecret), directory connector GET /sso/directory-connectors/typesandPOST /sso/directory-connectors/:connectionId/validate. Local fake LDAP/WS-Fed/SWA/header L3 is already covered; the production WS-Fed callback requires a signed wresult; real AD/LDAP gateway, AD FS WS-Fed, and Application Proxy L4 are still missing. Kerberos/IWA is a documented deployment mode only, and Workers does not terminate native Kerberos

SDK API

Package Status Contract
@xid-kit/core PASS XidClient.signInPassword calls /auth/password/sign-in and refreshes /v1/me; password user creation is handled by the same method according to tenant policy; listApiKeys / createApiKey / revokeApiKey call /v1/api-keys; XidApiClient optionally takes secretKey and sends Authorization: Bearer sk_*; listUsers / getUser / listOrganizations / listSessions call the Management API and map snake_case to camelCase; the export lock is packages/core/src/__tests__/exports.test.ts
@xid-kit/core active organization production L4 XidApiClient.setActiveOrganization returns a lightweight ActiveOrganizationResponse; on success XidClient.setActiveOrganization clears the token cache and calls load() to re-read /v1/me. Focused mock tests, the local Worker cookie flow, and the production browser smoke test already cover this
@xid-kit/react public surface partial production L4; full surface local PASS The package entry exports XidProvider, 8 hooks, SignedIn/SignedOut/Protect, the hydration control components, AuthenticateWithRedirectCallback, RedirectToSignIn/RedirectToSignUp/RedirectToUserProfile/RedirectToOrganizationProfile/RedirectToCreateOrganization, the sign-in/out/up buttons, the UI components, and the organization components. packages/react/src/__tests__/exports.test.ts locks the complete public export surface to match docs/sdks/react.md. Production browser smoke covers only XidProvider, useAuth, useUser, useOrganization, useOrganizationList, SignedIn, SignedOut, and Protect; every other public export has local evidence only and MUST NOT be reported as production L4. A standalone useSignUp is no longer exported
@xid-kit/backend public surface PASS The package entry exports PACKAGE, verifyToken, authenticateRequest, verifyWebhook, toVerifyKeySet, JwksCache, AppError, BACKEND_ERROR_CODES, and their associated types. Access token verification accepts RFC 9068 typ=at+jwt and application/at+jwt. packages/backend/src/__tests__/exports.test.ts locks the public exports to match docs/sdks/backend.md
@xid-kit/nextjs PASS xidMiddleware, auth, getAuth, currentUser, xidClient, and the React/backend re-exports are the current package contract
@xid-kit/react-native implemented, local evidence only Hosted redirect, deep-link callback, state-keyed PKCE S256, and an injected secure-token-storage adapter are implemented with unit tests and typecheck. Real device storage, deep links, and a real-IdP round trip remain unverified, so this is not a production L4 claim
Flutter, iOS, Android, macOS SDK implemented, local evidence only Source packages and their platform flows exist and their documented local toolchain tests pass. Device/emulator-specific storage and browser behavior plus real-IdP round trips remain unverified; package presence and local tests MUST NOT be reported as production L4

SPA router compatibility

Area Status Contract
NavLink active class local Console PASS packages/web-ui/src/tanstack-router.tsx is compatible with the react-router className={({ isActive }) => ...} form and children render function. packages/web-ui/src/tanstack-router.test.tsx verifies that rendered anchors contain the resolved class and never contain function source
Link href local Console PASS The shared Link emits a real <a href> and takes over same-runtime SPA navigation with TanStack navigate on click. Focused tests cover dynamic string and object routes without __link__; links crossing the Core and Console Worker boundary retain their real href and use document navigation so the request host and host-only cookie remain intact

Error Contract

The four OAuth protocol endpoints /introspect, /revoke, /device_authorization, and /register return errors in the RFC shape {error, error_description}, constructed directly inside the endpoint and never routed through the global onError. They MUST NOT return the XidAPIError {code, message} shape.

Error shapes are split by endpoint family:

  • Protocol endpoints (RFC family): the OIDC/OAuth endpoints /authorize, /token, /userinfo, /introspect, /revoke, /device_authorization, /register, /par, /backchannel_authentication, /end_session and the like return {error, error_description} (RFC6749 5.2 plus the per-RFC extension codes). Client authentication failure is a 401 carrying WWW-Authenticate (Basic client: Basic realm="xid", error="invalid_client"; DCR management RAT: Bearer realm="xid", error="invalid_client"); /register uses the RFC7591 codes invalid_redirect_uri / invalid_client_metadata / invalid_software_statement.

  • Management API and Hosted UI endpoints (/v1/**, /auth/**): errors are XidAPIError {code, message, meta?}, that is, each of the entries below.

  • SCIM endpoints (/scim/**): errors use the scimError shape (application/scim+json), and an unmatched path 404 uses the same shape.

  • Boundary errors all use AppError.

  • A missing cookie session returns 401 and code = unauthorized.

  • Insufficient platform permission returns 403 and code = forbidden.

  • A nonexistent resource returns 404 and MUST NOT leak cross-tenant resource existence.

  • An input field error returns 422 and code = validation_failed, and when meta.paramName is required the field name MUST be explicit.