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)
/introspectnow reports the liveorgclaim for a token withorganizationscope 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: falsefor 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.activemeans the token is valid and unexpired; if you gate on membership, require theorgfield. - SCIM users now carry
urn:zanii:scim:schemas:extension:2.0:Userwithsubject- 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/Schemasand as aschemaExtensionsentry on the User resource type. - SDKs 0.7.2:
orgtyped 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 answering400 invalidPathon PATCH, which used to quarantine a whole tenant. A path in no schema is still an error. A discardedpasswordis named in the organization's audit export ascredential_discarded. attributes/excludedAttributes(RFC 7644 s3.9) on every Users and Groups read.excludedAttributes=members, the request Entra actually sends, now costs nothing;GET /Groupsscans the directory once for the page rather than once per group. Group members are paged only when the caller passesstartIndexorcount, because truncating by default is indistinguishable at the client from those people having been removed. A projected or windowed response carries nometa.version.- The organization's own directory groups:
POST/PUT/PATCH/DELETE /scim/v2/Groups, carried on theorgclaim asgroups. They are labels and grant nothing -memberandadminremain 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
b8c0e2f4a6d7addsscim_groupsandscim_group_membersand alters nothing. - Not done, on purpose:
access_token_ttl_secondsis 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 CAEPsession-revokedis sent. Documented rather than promised away.
2026-10-29, Plan 24: SCIM 2.0 for organizations, off by default
/scim/v2Users 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_ENABLEDis 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 inGET /orgs/events. - Migration
a7c9e1b3d5f8addsscim_tokensandscim_usersand 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,/readyzseparate 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_previousfor 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 oldclient_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-disabledsignal pushed. PATCHfor 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
resourcevalidated 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
agentclaim and the public registry, labelled as the operator's own statement. - Organizations with members, invitations and an org-wide two-step requirement; new
organizationscope andorgclaim. - Passwordless accounts for anyone with a passkey, and Arabic hosted pages negotiated from
Accept-Languagewith 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_hinton/authorizeand through PAR; a QR of the device-flow verification URI at/device/qr.svg, advertised asverification_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_alertsto turn it off). given_nameandfamily_nameas optionalprofileclaims;POST /ui/account/exporthands 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/reconcilenames 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 witnessco-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.jsonand/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:
cstclaim 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,
agescope. - 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_vccredentials; 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/backchannelAuthorizeand a poller that returnsdeniedandexpiredas 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_tokenwas previously discarded). OrgClientgains 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 oldclient_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/signupOrganizationis a module function, because it produces the keyOrgClientrequires.- A coverage guard in both packages.
docs/openapi.jsonis 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-boundedtaskgrant. The task's four fields are validated in the SDK, so a malformed one fails before the user is redirected rather than asinvalid_requestafter it. resourceonclient_credentialsand on token exchange.- Approvals:
create_approval/createApproval,get_approval/getApproval, andawait_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_statusandverify_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/agentProfilereads an agent's public registry page.- Deliberately still absent: the
/manage/v1admin 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
UserInfowas dropping claims the server sends. Python's model had noextra="allow", sogiven_nameandfamily_namewere discarded silently; TypeScript's interface did not declare them. Both now carrygiven_name,family_nameandorg, and both keep unknown claims rather than losing them — a new claim should degrade to "untyped", never to "gone".ZaniiClaims/IdTokenClaimsgain the same fields pluscst.
0.5.0, 2026-09-07
@zanii-id/react: opt-in popup sign-in (popupprop /mode: "popup"). The page stays mounted; the result comes back over a same-originBroadcastChannel, so aCross-Origin-Opener-Policyheader 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-idand@zanii-id/sdkcarry no changes; the three move in lockstep.
0.4.0, 2026-09-05
introspectin 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, ReactcompleteZaniiSpaLogin,offline_accessdefault.
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.