zaniiid

Changelog

Dates are 2026. The plan log in tasks/todo.md has the detail behind every entry; the architecture decisions are AD-n in docs/architecture.md.

Server

2026-10-29, Plan 26: membership at introspection time, and a subject SCIM can be matched on (AD-89)

  • /introspect now reports the live org claim for a token with organization scope from an organization's client. Before this it checked the account and never the membership, so someone removed through SCIM still looked fine for up to the access-token lifetime. A role change shows here too, not only a removal.
  • It deliberately does not return active: false for a non-member: an organization's consumer-facing app signs in people who were never members and may ask for the scope anyway, and their tokens are valid. active means the token is valid and unexpired; if you gate on membership, require the org field.
  • SCIM users now carry urn:zanii:scim:schemas:extension:2.0:User with subject - the organization's own pairwise subject, identical to what its clients receive in a token, so a directory record and a signed-in person can be matched by equality. Never the global account id. Absent until the person accepts their invitation, which is what distinguishes invited from joined. Declared in /Schemas and as a schemaExtensions entry on the User resource type.
  • SDKs 0.7.2: org typed on the introspection response in both packages.
  • No migration: both are read paths over tables that already exist.

2026-10-29, Plan 25: SCIM a real directory can drive, and groups that grant nothing (AD-88)

  • A stock Entra or Okta attribute mapping now provisions without an administrator trimming it. Attributes this server does not keep - the enterprise extension, and core ones like title, userType, phoneNumbers, addresses, preferredLanguage, locale, timezone - are discarded instead of answering 400 invalidPath on PATCH, which used to quarantine a whole tenant. A path in no schema is still an error. A discarded password is named in the organization's audit export as credential_discarded.
  • attributes / excludedAttributes (RFC 7644 s3.9) on every Users and Groups read. excludedAttributes=members, the request Entra actually sends, now costs nothing; GET /Groups scans the directory once for the page rather than once per group. Group members are paged only when the caller passes startIndex or count, because truncating by default is indistinguishable at the client from those people having been removed. A projected or windowed response carries no meta.version.
  • The organization's own directory groups: POST/PUT/PATCH/DELETE /scim/v2/Groups, carried on the org claim as groups. They are labels and grant nothing - member and admin remain the only roles that decide anything, a group cannot shadow a role, and deleting one withdraws no access. The claim is read only while SCIM is on and omitted when empty.
  • Migration b8c0e2f4a6d7 adds scim_groups and scim_group_members and alters nothing.
  • Not done, on purpose: access_token_ttl_seconds is unchanged, so a removed person's already-issued access token stays valid for up to ten minutes at a resource server that validates it locally. Membership, refresh families and task grants go immediately and CAEP session-revoked is sent. Documented rather than promised away.

2026-10-29, Plan 24: SCIM 2.0 for organizations, off by default

  • /scim/v2 Users and Groups (the organization's roles) with filters, paging, PATCH, ETags and the three discovery documents, for HR and IT systems that provision members. Plan 25 added the organization's own groups alongside the roles.
  • Off unless ZANII_ID_SCIM_ENABLED is set and the organization's owner has created a SCIM token; otherwise every SCIM URL is a plain 404 and nothing else changes.
  • Creating a user is an invitation; deactivating one removes the membership and only that organization's tokens, with the CAEP signal to its clients. Accounts are never written.
  • SCIM tokens: hashed, organization-scoped, expiring, revocable from /orgs/scim/tokens, the console and (revoke only) /manage/v1. Per-token rate limit, body size limit, every change in GET /orgs/events.
  • Migration a7c9e1b3d5f8 adds scim_tokens and scim_users and alters nothing.

2026-09-07, Plan 22: CIBA, mix-up defence, and the rights ones (AD-83..86)

  • CIBA (POST /bc-authorize + the CIBA grant): a client authenticates a person who is not at a browser, and they approve on their own device. Poll delivery only.
  • RFC 9207: every authorization response carries iss. Discovery used to advertise that we did not support it.
  • A person can leave an organization themselves; recovery codes can be reissued without disabling two-step verification; a pending email change is visible and cancellable; a passkey can be renamed.
  • Operator visibility: audit search, /manage/v1/stats, /readyz separate from /healthz, and listing plus replay of stuck webhook deliveries.
  • Organizations: a test-webhook, per-client delivery history, self-deletion, and resendable invitations.

2026-09-07, Plan 21: the ten routes an incident would need (AD-78..82)

  • Forced signing-key rotation, with an explicit revoke_previous for a suspected compromise.
  • Operator reissue of an organization API key, behind AD-74's self-service recovery.
  • PATCH /manage/v1/clients/{id}, so first-party clients can be edited like org ones.
  • RFC 7592 client management at /register/{client_id} with a registration access token.
  • GET /orgs/events: an organization exports its own audit trail, paginated.
  • Resend a lost verification email; a client can drop its own consent by a token it holds; an operator can sign a user out without suspending, or delete an account on request.
  • Introspection understands opaque refresh tokens, which used to answer a confident and wrong active: false.
  • {action} path segments replaced by explicit routes: a catch-all segment silently swallows every sibling declared after it, which had caused two real bugs.

2026-09-07, Plan 20: the missing halves (AD-73..77)

  • A client can be edited in place (PATCH /orgs/clients/{id}) instead of delete-and-recreate, which used to discard every consent and token bound to the old client_id.
  • Organization key recovery through a confirmed contact address, single-use, key never emailed.
  • Console pages for members and invitations, protected resources, and the declared model.
  • An agent can withdraw a pending approval; a decision already made returns 409. Decided approvals are visible history on the account page.
  • One user account can be suspended and restored, with sessions and token families revoked and the CAEP account-disabled signal pushed.
  • PATCH for a resource's name and scopes and for a member's role; invitations can be listed and revoked.

2026-09-07, Plan 18: the identity layer an agent company needs (AD-66..72)

  • MCP-ready: RFC 8414 metadata where OAuth clients look, registered protected resources, RFC 8707 resource validated into the token's audience, RFC 9728 metadata per resource.
  • Task-bounded agent grants: a purpose, a number of uses, a deadline and a declared ceiling, approved on the consent screen, carried as tsk, spent per token, revocable, receipted.
  • Shared signals (CAEP/RISC): signed Security Event Tokens pushed or polled, emitted in the same transaction as the change they describe.
  • Ask-my-human approvals: an agent stops, the person decides on their account page, both outcomes receipted; an unanswered request expires refused.
  • Declared model provenance in the agent claim and the public registry, labelled as the operator's own statement.
  • Organizations with members, invitations and an org-wide two-step requirement; new organization scope and org claim.
  • Passwordless accounts for anyone with a passkey, and Arabic hosted pages negotiated from Accept-Language with a saved per-user preference.

2026-09-07, Plan 17: what Google Sign-In had that we did not (AD-62..65)

  • Consent is per permission: a checkbox per optional scope, and the granted subset is what the code, the consent row, the receipt and the tokens all carry.
  • login_hint on /authorize and through PAR; a QR of the device-flow verification URI at /device/qr.svg, advertised as verification_uri_qr.
  • The account page shows the twenty most recent security events about the account; a sign-in from an unfamiliar browser sends one email (new_device_alerts to turn it off).
  • given_name and family_name as optional profile claims; POST /ui/account/export hands the user their own records as JSON, with no credential material in it.

2026-09-05, Plan 16: accountable to others (AD-57..61)

  • Receipt outbox: every self-receipt is an intent row, retried with backoff on ledger stalls; /manage/v1/reconcile names what the ledger lacks.
  • A separate sealed cosigner key countersigns every recorded receipt (/.well-known/zanii-cosignatures.json).
  • The IdP witnesses the ledger: ledger_ops.py witness co-signs append-only tree heads (/.well-known/zanii-witness.json).
  • Sovereignty declaration: hosting region from the cloud, declared sub-processors, daily residency claim, per-mail transfer receipts (/.well-known/zanii-sovereignty.json).
  • Selective disclosure of one field of an age assertion or a consent from the account page.
  • ERC-8004 registration files and signed AgentCVs at /agents/{did}/erc8004.json and /cv.json.
  • Dark mode and writing-direction support on every hosted page.

2026-09-05, Plan 15: agent identity as the product (AD-53..56)

  • Public agent registry /agents/{did} and /profile.json.
  • Proof-carrying consent: cst claim on tokens, POST /introspect (RFC 7662) with the consent behind the token.
  • Every constitution version served by hash; zanii-id verify <did> in the Python SDK.
  • Agent-to-agent login: token exchange between two organizations' agents with mutual, receipted screening.

2026-09-05, Plan 14: SDKs catch up with the server (AD-52)

  • Server unchanged; see SDK 0.3.0.

2026-09-05, Plan 13: bounded by construction (AD-51)

  • Retention sweeps over nine tables, hot-path indexes, bounded feed and paginated admin listings.
  • Statement timeout, body cap, receipt backlog cap, bundle semaphore, own executor for ledger writes.
  • deploy.py --harden: systemd ceilings, uvicorn concurrency cap, journald cap, nginx include.

2026-09-05, Plan 12: the deliberately-left-out list (AD-45..50)

  • Front-channel logout; subject-tag index for relay ingestion; opt-in OpenTelemetry export.
  • Age assurance: 18+ gate at registration, provider hook, age scope.
  • Know-your-agent: org deny lists, screening at token exchange, /orgs/agents/screen.
  • WebAuthn PRF: passkey-wrapped identity key backup.

2026-09-05, Plan 11: the ledger ecosystem inside the IdP (AD-41..44)

  • Sealed signing identity; build and constitution stamps on every receipt; consent and retention receipts.
  • Organization DIDs with domain binding; ldp_vc credentials; evidence packs; sentinel; workload bindings; post-quantum key binding.

2026-09-05, Plan 10: audit remediation and the 2026 surface (AD-33..40)

  • Resend transport fixed; opaque session cookie; client credentials, token exchange, device flow, PAR, DPoP, private_key_jwt, back-channel logout, OID4VCI, passkey conditional UI, JSON logs, request ids, Prometheus.

2026-09-05, Plans 7..9: customer-ready controls, audit

  • Account page, TOTP, lifecycle webhooks, org quotas, console, email-change revert, step-up, terms versioning, operator MFA reset, passkeys, dynamic client registration; UAE-law legal drafts.

2026-08-26..27, Plans 1..6

  • OAuth 2.1 / OIDC core with PKCE, refresh rotation with reuse detection, consent, pairwise subjects per organization, agent activity with offline verification, custody verdicts, ledger watchdog, self-receipting.

SDKs (zanii-id, @zanii-id/sdk, @zanii-id/react, in lockstep)

0.7.0, 2026-09-07

  • CIBA: backchannel_authorize / backchannelAuthorize and a poller that returns denied and expired as answers rather than raising - an unanswered request expires refused, and code that reads a timeout as success is the failure the flow prevents.
  • Approval cancellation, and RFC 7592 self-management for dynamically registered clients (the registration_access_token was previously discarded).
  • OrgClient gains everything Plans 20-22 added: update_client (the most damaging gap - without it, changing a callback URL meant delete-and-recreate, discarding every consent bound to the old client_id), resource and member-role edits, invitation list/revoke/ resend, the audit export, client-initiated consent revocation, the test webhook, delivery history, and organization deletion. signup_organization / signupOrganization is a module function, because it produces the key OrgClient requires.
  • A coverage guard in both packages. docs/openapi.json is a committed contract, and a test now fails when a server route has no SDK counterpart and no recorded decision. The server outran its clients twice in one day; this is what makes the third time noisy.

0.6.0, 2026-09-07

  • The SDKs can now ask for everything the server can do. Audited every route against both clients and closed each gap; the request side had fallen behind the server through Plans 17 and 18, so features shipped that no SDK user could reach.
  • Authorization request: login_hint, resource (RFC 8707) and a task-bounded task grant. The task's four fields are validated in the SDK, so a malformed one fails before the user is redirected rather than as invalid_request after it.
  • resource on client_credentials and on token exchange.
  • Approvals: create_approval / createApproval, get_approval / getApproval, and await_approval / awaitApproval, which polls at the server's interval and returns refused or expired rather than raising - silence is an answer your code must handle.
  • Shared signals: configure_stream, poll_signals, signal_status and verify_security_event (camelCase in TypeScript). Verify before acting: a push endpoint is a URL anyone can POST to.
  • OrgClient: protected resources, members and invitations, declared agent model, require_member_mfa, and RFC 7591 dynamic registration.
  • agent_profile / agentProfile reads an agent's public registry page.
  • Deliberately still absent: the /manage/v1 admin API. It is authenticated by the single global admin token, and an application SDK is the wrong place to invite that token.

0.5.1, 2026-09-07

  • UserInfo was dropping claims the server sends. Python's model had no extra="allow", so given_name and family_name were discarded silently; TypeScript's interface did not declare them. Both now carry given_name, family_name and org, and both keep unknown claims rather than losing them — a new claim should degrade to "untyped", never to "gone".
  • ZaniiClaims / IdTokenClaims gain the same fields plus cst.

0.5.0, 2026-09-07

  • @zanii-id/react: opt-in popup sign-in (popup prop / mode: "popup"). The page stays mounted; the result comes back over a same-origin BroadcastChannel, so a Cross-Origin-Opener-Policy header does not break it. A blocked popup falls back to a redirect, a closed window rejects distinctly, five-minute timeout, and a mobile tab that refuses to close navigates home instead of stranding the user. Redirect remains the default.
  • zanii-id and @zanii-id/sdk carry no changes; the three move in lockstep.

0.4.0, 2026-09-05

  • introspect in both SDKs with typed consent proof; zanii-id verify <did> command (extra [verify]) in Python.

0.3.0, 2026-09-05

  • Client credentials, token exchange with actor tokens, device flow polling at the server's pace.
  • DPoP proofs, private_key_jwt, PAR; logout URL with state; logout token verification and framework handlers for both logout channels.
  • Receipt relay, organization API client, typed act / cnf / age claims.
  • One bounded connection pool (Python) or a timeout on every fetch (TS); reads retried once on 429/5xx with Retry-After; token grants never retried.

0.2.0, 2026-09-05

  • Public-client client_id, revoke auth, step-up options, memoised JWKS, Next refresh route, React completeZaniiSpaLogin, offline_access default.

0.1.3, 0.1.2, 0.1.1, 2026-09-05

  • Lifecycle verify helper; homepage corrected to ids.zanii.agency; Apache-2.0 metadata.

0.1.0, 2026-08-27

  • First release: authorization URL with PKCE, code exchange, refresh, revoke, userinfo, id-token verification, FastAPI / Express / Next helpers, React login button.