Menu

Changelog

What changed, and whether you must act.

Each release, taken from Signet's own release notes rather than retyped here. Every entry keeps its line saying what you must do, or that you need do nothing.

Newest release first. Each entry: the date, what changed, what you must do.

Source commit 1d92ec9aa31b. Synced 2026-09-06.

Your build day is at your instance's /certification.json; the hash is on its admin dashboard.

2026-09-06 — an instance says which day it was built, and how to upgrade it

/certification.json now carries build.date, the binary's UTC build day, on the existing signet.certification/v1 schema. If the build timestamp is unavailable or invalid, the value is unknown. The exact Git hash stays behind the admin key. The instance's /docs page names the same day and points to https://signetauth.dev/docs/upgrading.

The upgrading guide explains how to replace the process against the same database and secret, drain before migrations, check the replacement, and recover. Its migration wording is pinned by a test to the boot path that awaits migrations before binding. Use the build day with the developer changelog to find changes since your build; builds made on the same day share a date.

What you must do. Nothing.

2026-09-06 — signetauth.dev is a developer portal

The developer site no longer wears the marketing shell. It has five sections (Start · Guides · Reference · Admin and CLI · Changelog), its own nav and footer with no pricing or buying links, and a home that says what Signet is in one line and where the buyer material lives. The fifteen guides are grouped by the job a reader arrives with instead of listed flat; /docs on the developer site is that index, and the in-binary route reference it used to proxy now lives at /reference/routes. Two pages are new: /changelog, which is this file published from the repository with its operator notes stripped, and the grouped guides index. The Admin API page, /llms.txt and /llms-full.txt now take their operation count from one snapshot of docs/admin-api/openapi.yaml stamped with the engine commit it came from; the three had read 46, 22 and 64 for the same document. Nothing an instance serves changed. Copy was read cold by the reader panel before the build and again on the shipped pages. (D360, R509)

2026-09-06 — the API document describes what to send and what comes back

The OpenAPI document every instance serves at {base_path}/open-api/generate-schema, and the reference page that renders it, now carry a request body for every operation that reads one, the real success shape, and the refusal codes the handler actually returns, for every route family. Measured on the default build at the end of the landing: 158 operations, 101 with a request body (every one that reads one), 149 with a real success schema, 158 with refusal codes. Before this, 37 of 173 operations had a body and none had a response shape, so most Send buttons on the reference page needed JSON typed by hand. Component models were added for the recurring records (organization, member, invitation, API key, OAuth client, passkey, device code, SSH certificate, the standard error and the RFC 6749 error). Operations with no reachable success say so rather than inventing one; form-encoded bodies, redirects and HTML pages are documented as the media they are; refusals that are not the standard code/message envelope are documented as their real shape. Compile-time features change the document (the adoption routes, private_key_jwt, SSO, SCIM), and the tests hold across the default, external-jwt, scim and enterprise-sso builds. Built in eleven family-sized steps on 2026-09-05 (Core first; the rest in two landings). (R508)

Since 2026-08-21 (engine 912d6060), a URL-identified client on a PostgreSQL or SQLite instance could receive a 500 at the real consent POST or at /oauth2/token when requesting offline_access. R292 opened a client class with no oauthClient row, but 0021's consent and refresh-token foreign keys still required one. This broke interactive document sign-in on every hosted instance; the R292 MemoryAdapter tests could not see it because that adapter enforces no foreign keys.

PostgreSQL 0095 and SQLite 0009 remove only those two client foreign keys. Registered-client deletion now removes its consents and refresh tokens explicitly in the existing transaction; surviving tokens still fail the live-client lineage check. Product-path tests for OAuth2 and MCP were red on both SQL backends before the migration and green afterward, including real consent, offline_access code exchange, and refresh. Apply 0095/0009 before serving this flow. No fleet deployment is claimed by this worktree change.

2026-09-05 — organizations can require adoption for interactive agent sign-in

R305/D267 adds an opt-in, default-off policy at GET and POST /oauth2/agent-admission-policy, for an organization owner or admin in external-jwt builds. Every strict organization the user belongs to must have adopted the document client's current keys. Both authorization doors check before consent; both token doors refuse banked codes and refreshes after the policy changes, and a refused refresh tears down its family. The policy and its mandatory audit commit together. Concurrency contract: a policy takes effect for every mint that starts after the policy write commits; a mint already in flight at the instant it commits may complete, and that token is refused at its next refresh or code redemption. No mint waits for an in-flight policy write. Client refusals disclose no organization; details and remedies remain in server logs.

The switch does not kill outstanding access tokens. Registered clients are exempt, public clients without keys cannot currently be adopted, and key changes are detected through the caches. Adoption does not pin redirect URIs or authentication-method metadata. There is no deny-list or instance-wide block. See docs/agent-auth.md §4b; no fleet deployment is claimed.

2026-09-05 — every instance serves an interactive API reference

GET {base_path}/reference now renders this instance's own OpenAPI document with Scalar, the same page upstream's openAPI() plugin serves, and lets a developer send any request to the instance from the page. Try-it runs on the instance's own origin, so the session cookie travels and the CSRF origin check passes without the customer adding anything to trusted_origins. Unlike upstream, the Scalar bundle is embedded in the binary and served from {base_path}/reference/scalar-<version>.js rather than a CDN, and Scalar's fonts, request proxy and telemetry are switched off: the page names no origin but the instance's, which keeps the zero-external-fetch rule and works air-gapped. /docs and /llms.txt name the page. The document itself is unchanged — most operations still lack request and response shapes, which is the next item (R508). (D359, R507)

2026-09-05 — automation runners obtain short-lived SSH certificates

R493 adds a MACHINE arm to POST {base_path}/ssh/certificates, alongside the unchanged human session arm. D354 was ratified on 2026-09-04 in signet-records; both a service-principal bearer and a real Semaphore task JWT were driven to an SSH login as ansible on PVE node C in the metal-01 lab. This is built and lab-driven, with no production fleet rollout or new package claimed.

Machine issuance defaults OFF. Enable [ssh_ca] machine_issuance with a separate [ssh_ca.machine] principal that differs from the human one. The default and maximum request TTL is 3600 seconds, configurable up to 14400; validity additionally includes clock skew. All extensions default off. The ledger-derived per-principal quota defaults to six mints per minute and survives restarts, with possible overshoot under concurrent requests.

A service bearer is a standing mint capability until expiry or revocation. The Semaphore form requires an external-jwt build and registered issuer/template trust, consumes each JWT once, and caps certificate expiry at the task JWT's expiry. A runner can generate its SSH key per task and keep no standing SSH key or Signet bearer; job completion does not itself invalidate the JWT.

Platform administrators register trust through POST /admin/v1/ssh-machine-issuers and POST /admin/v1/service-principals/{id}/ssh-machine-templates, also exposed by signetctl. PostgreSQL 0093 and SQLite 0007 retain human ledger rows while adding machine credential/task attribution, template mappings and durable assertion replay records. Revoking mint authority stops future issuance; already-issued certificates still need expiry or host-side revocation.

2026-09-05 — engine landings run on dedicated hardware

The serial landing queue that gates every engine push (D296/R370) moved from kapable-containers-01, the host that serves customer instances, to kapable-metal-01, a 16-core bare-metal box bought for CI. Nothing a customer sees changes; what changes is that a full engine battery no longer competes with their instances for CPU, and a cold battery takes about 36 minutes instead of 90 to 147. Rehearsed on an isolated copy of the repo, with a green landing and refused bad commits, before the real queue was cut over. scripts/landing-queue/ documents the new host; the old install stays masked for a week as the rollback. (R-068)

2026-09-04 — credential ceilings are visible without weakening resource bindings

API-key and service-token mint receipts now include droppedPermissions only when the principal's live role ceiling removed an ordinary declared action. If that ceiling would drop a workspace:, project:, or grant: binding, minting instead refuses with MINT_BINDING_DROPPED (409), naming the binding that the minter must gain or remove.

2026-09-04 — the Hobby waitlist tells a person their place and when the gate opens

The Hobby waitlist now has its missing mail path (R482 item 4d; D343 and its Addendum). On entry, the person is told “You are number N” and that batches open without any date, duration, countdown, or ETA. When an operator releases that numbered row, the second mail says the instance is being built; the existing provisioning flow remains responsible for the later live receipt.

Both messages go through the instance's configured Signet delivery channel. The control plane can request only this fixed message kind through POST /admin/v1/messages/hobby-waitlist, and its scoped credential has only the new hobby-waitlist:send admin permission. It cannot supply arbitrary subject or body text. Migration 0090 admits that key on PostgreSQL and updates existing platform credentials to the new maximal set, while the current SQLite validator admits the same key (R482/D348). A mute instance refuses with 503 DELIVERY_NOT_CONFIGURED and the [delivery] repair; the portal records any send failure on the durable intent without undoing signup or release.

This is not deployed and does not turn on SIGNET_CLOUD_HOBBY_DAILY_QUOTA. The engine must land and reach the dogfood instance, then the portal credential must be minted and installed, before the operator enables the quota.

2026-09-03 — Hobby is sign-in for people; its limits are enforced; a Hobby instance can move up to Postgres

Three things landed together for the hobby tier (R482; rulings D343, its addendum, D344).

  • The plan gate. A Hobby instance's config carries a [plan] section. With it, every enterprise SSO route answers 403 PLAN_FORBIDS_SSO, service-token minting answers PLAN_FORBIDS_SERVICE_TOKENS, live agent tokens are capped (PLAN_AGENT_TOKEN_LIMIT) and agents get no refresh token (PLAN_FORBIDS_REFRESH). Every refusal names the plan and where the upgrade is. Existing tokens can still be listed, introspected and revoked. Instances without a [plan] section are unchanged.
  • signet migrate-backend. Moves a SQLite (Hobby) instance into a dedicated PostgreSQL database in one transaction, verifies it, and prints a receipt; sessions, passwords, two-factor secrets, organisations, API keys, revocations and passkeys survive. Requires the same [server] secret on both sides and says so.
  • Hosted provisioning. New Hobby instances are provisioned on encrypted SQLite with the plan block written in; paid plans are provisioned exactly as before. Moving a Hobby instance to Team runs the migration first, then redeploys, and proves the original session still works.

2026-09-04 — Hobby: a free instance that is sign-in for people, on its own encrypted database

Hobby instances now exist as their own shape of instance (R482; the scope ruling is D343 with its addendum). A Hobby instance is provisioned with --plan hobby: it runs on one encrypted SQLite file on the instance's own data volume instead of a Postgres database, and its signet.toml carries a [plan] block. On that plan the engine refuses, with a named code and the upgrade path in the message, the things Hobby does not include: enterprise SSO (PLAN_FORBIDS_SSO), service tokens (PLAN_FORBIDS_SERVICE_TOKENS), refresh tokens for agents (PLAN_FORBIDS_REFRESH), and more than five active short-lived agent tokens (PLAN_AGENT_TOKEN_LIMIT). Everything a person uses to sign in — passwords, passkeys, magic links, one-time codes, social sign-in, two-factor — is unchanged. Paid plans have no [plan] block and behave exactly as before.

When a Hobby account upgrades, scripts/reconfigure-instance.sh --plan team moves the instance to Postgres with signet migrate-backend; sessions and users come across, and the instance URL does not change.

What is not yet true: no Hobby instance has been provisioned on the hosted platform with this shape yet (the portal's signup throttle and the sleep/wake policy land alongside), and this entry will be amended when the first one is.

2026-09-03 — the importer refuses a passwordless identity on an instance that cannot send mail

signet import now refuses, before writing anything, when the input carries a passwordless row (passwordless=true, no password hash) and the instance's [delivery] section resolves to no sender. Such a person could only ever sign in by magic link, and until now the import reported complete success and the person was locked out on arrival. The refusal names how many passwordless rows were found, that the instance cannot send mail, and both fixes: configure [delivery] (mode plus that mode's own section), or import only rows that carry a password. A dry run refuses too. Rows with passwords still import on a no-mail instance; passwordless rows still import when a sender is configured. (R431, D327 §2.1)


2026-09-02 — phone OTP and phone password-reset are rate-limited like their email siblings

/phone-number/send-otp and /phone-number/request-password-reset now take the built-in 3-requests-per-60-seconds rule that /request-password-reset, /send-verification-email and the email-OTP routes already had. Until now they fell to the default bucket (100 per 10 seconds), and each accepted request sends a billable SMS. This is a deliberate, recorded divergence from better-auth, whose table has the same gap (D336): an external monetary cost outranks parity. /phone-number/verify sends nothing and is unchanged. A custom [rate_limit.rules] entry still wins over the built-in row, so an operator can loosen it in config. (R450)


2026-09-02 — the provisioner says why it cannot read the mail secret, instead of exiting silently

scripts/provision-instance.sh used to exit 1 with no output at all in two ordinary cases: when the vault CLI refused to print the SMTP credential (it now requires --plaintext, and the script threw that refusal away), and when a value-taking flag was given last with no value. Both now refuse by name: the vault case quotes the CLI's own message and states the two ways forward (fix the vault entry, or pass --no-delivery and lose invitations, password reset, verification, OTP and magic-link on that instance); the flag case names the flag. A dry run no longer reads the vault, since it makes no external calls. Proven end to end the same day: a throwaway instance provisioned with delivery sent one magic link, the mail arrived a second later, the link signed a session in and refused a second redemption; the instance was then destroyed. (R437)


2026-08-31 — split-horizon domains can be admitted by the admin API, and the DNS check fails quieter

Operators on networks where mail domains resolve only internally (split-horizon DNS) could not create those users: the mail-deliverability check sees no public records and refuses. The Bearer admin API's create-user now accepts bypass_domain_check for exactly that case — audited, with every static control (block, allow, disposable) still applied. The refusal message already names the email_policy.allow alternative for operators who prefer config. The check's fail-open warning is now throttled per domain (an attacker with a black-holed nameserver can no longer buy a log line per attempt; first occurrence always logs), and the shared DNS resolver rebuilds after six hours so a host resolver change cannot strand a long-lived instance.

2026-08-30 — sign-up refuses an email domain that cannot receive mail

Signet now refuses a sign-up address whose domain has no MX and no A or AAAA record. Mail can never reach such a domain, so the account would be unverifiable and unrecoverable from the moment someone made it. The refusal carries EMAIL_DOMAIN_UNREACHABLE and says what to do instead.

An operator creating a user through the admin API meets the same check behind a longer message. That message names the allow list as the way through for a corporate domain that resolves inside your network but not in public DNS. The anonymous sign-up message deliberately leaves it out.

Three deliberate limits. Sign-up only — verifying a one-time code or a magic link never touches DNS, because a delivered link already proved the domain receives mail. Re-checking it would lock out people who already hold accounts. It fails open — a lookup that times out or errors still lets the sign-up through and writes a warning, so one resolver blip cannot stop every registration. It bounds the lookup at 1.5 seconds. Production answers a normal domain in roughly four milliseconds.

This goes live on every hosted instance today, and it is the first release where the check makes real DNS queries rather than test ones. Signet accepts a domain that has an A record but no MX, following the implicit-MX rule that a great many small legitimate domains rely on.

2026-08-30 — a password reset now signs every device out

Finding your account compromised and resetting your password used to leave the attacker's stolen session alive for up to seven days. A completed password reset — by link or by one-time code — now revokes every session of that user inside the reset's own transaction, with an audit row and a session.revoked event per device. Operators who want upstream better-auth's exact default behaviour (sessions survive a reset) set password.revoke_sessions_on_password_reset = false. The secure posture is the default.

2026-08-28 — Signet issues its own fleet's SSH certificates, and records who used them

An estate opened by one shared root key has no answer to "who logged in". Signet now signs short-lived OpenSSH user certificates, so an operator signs in, gets a ten-minute certificate, and connects — and the shared key can go.

POST {base_path}/ssh/certificates is off unless [ssh_ca] is configured, and it refuses with 403 unless all four hold: the signer is enabled, the caller is a member of the named infrastructure organization, the caller is an owner there, and the caller authenticated recently. That organization must be named in config and has no default — the only available fallback is "any authenticated caller", and on an instance that also serves customer tenants that would let a trial signup mint root for every host.

Every issuance is recorded, and the record is not bookkeeping. The certificates carry one shared Unix principal and an opaque key ID, so sshd's log names a certificate and nothing else; only the sshCertificateIssuance row names a human. Serial allocation, signing and that row share one database transaction, so a certificate cannot reach a caller without it. Serials come from the ledger's own maximum rather than a sequence, because a sequence restored out of step with its table re-mints a serial a revocation list already covers.

The signer carries its own re-authentication window, max_authentication_age_seconds (default 300), deliberately separate from [session] fresh_age. Reusing the session setting — which defaults to 86400 — would have meant a stolen cookie could mint fleet root for a day, through the check meant to prevent exactly that.

Two operator-facing notes. maximum_ttl_seconds caps the REQUEST, not the certificate's validity window: valid_after is backdated by clock_skew_seconds, so a 3600 cap yields 3660 seconds of wall-time validity, and the response reports requestedTtlSeconds and validitySeconds separately rather than hiding that behind one number. And revocation is still expiry — Signet offers no instant revoke, so certificate lifetime is the bound.

2026-08-26 — the certification receipt states which release it covers

The receipt reported gap 0 against better-auth 1.6.23, recorded 2026-07-22, and said nothing about what that pin covered. A prospect who read the drop-in-compatibility claim and then ran bun add better-auth installed 1.7.1, so the proof they were shown was older than the client they would install, and no surface admitted the lag (R337).

version_boundary now joins the receipt as an honest-posture disclosure, in the same spirit as the operating-envelope note: the receipt covers the pinned release and no other, releases published after it sit outside it, they are not known to break but have not been replayed, and we will not imply coverage we have not run. It is carried in /certification.json, on the rendered /certification page directly under the version row it bounds, and in the text receipt beside the operating envelope. /enterprise states the same boundary where the claim is read.

This NARROWS what may be claimed and licenses nothing new. Re-certifying against a later upstream is a separate piece of work: it means re-pinning conformance/vendor/PIN, reapplying three harness patches to a new upstream tree, and replaying 2,165 cases against both the reference and Signet. Adapting the harness to make the claim easier is not on the table.

2026-08-26 — one truthful story about leaving, and a stated offboarding turnaround

Three marketing surfaces described the on-prem exit as though it covered hosted customers too. /product said "Leaving does not require us", /pricing said "leaving is an export you already control", /buy said continuity does not require us to exist. None of that is true on the hosted service: we operate that database, there is no connection string and no export endpoint, so leaving requires us to hand over the dump. The front door and /renewal already said so, and were outnumbered three to one (R359).

Every surface now names both paths. On your own hardware you run the export yourself and leaving needs nobody; on the hosted service you ask and we hand you the dump. The on-prem claims are unchanged, because they were never the defect.

The question a security reviewer actually asks now has a published answer: offboarding is within two business days. Two is the number that goes in a contract, because the two teams are not always in the same timezone; same day is what we aim at and usually hit, and the copy says both in that order. /renewal and /buy carry it.

Automating it is filed as R396 and is not built. Until it is, that turnaround is a promise a person keeps rather than a button a customer presses, and the copy does not pretend otherwise.

2026-08-26 — /enterprise stops leading its proof with a compatibility ratio

D292 positions Signet on being verifiable rather than better-auth compatible, and names its own refutation: a surface that leads with a compatibility ratio. /enterprise was one. Under the heading "The proof a licence carries", the first sentence was the conformance number.

That is the page where the number misleads most. R363 measured 29 of 43 certified SCIM gap cases failing BECAUSE Signet scopes connections to an organization while upstream's plugin has no tenancy concept, and D290 found they cannot be closed without abandoning org-scoped SCIM — the thing enterprise buyers pay for. The published score therefore falls as the product improves at its differentiator.

The section now opens on the discipline: every release is measured against a public suite, the measurement is published alongside it, and deliberate deviations are named with their reason rather than quietly excluded. The compatibility receipt follows as one of those measurements, every figure intact. A reorder, not a deletion — D292 keeps the receipt as a published proof.

2026-08-25 — RS256 ID-token signing alongside EdDSA

The engine now signs ID tokens with either RS256 or EdDSA, stores the algorithm per OAuth client, and publishes a default-first discovery list derived from the active key lanes it can actually use. A Rust-issued RS256 token is independently verified by Node/OpenSSL against the served JWKS, including its kid, alg, use, modulus, exponent, and cache contract.

Fresh installs default omitted client metadata to RS256 and bootstrap both signing lanes. An upgraded database that already held an Ed25519 signing key is explicitly pinned to EdDSA and does not acquire unused RSA custody. Each lane rotates and dwells independently; retiring verification keys remain published until the token/JWKS-cache grace expires.

This closes signing-algorithm conformance only, not complete OIDC Core conformance. It also does not establish hosted Tailscale compatibility: customer-domain WebFinger, public reachability, and a live-tailnet receipt remain separate prerequisites (D300 §8).

2026-08-25 — declarative auth-flow hooks and token claim mappers, config not code, with the hot path measured

Wave 4 of the ratified pluggability roadmap (D298, design D305, item R385). An operator can now gate a sign-up or sign-in and shape token claims from signet.toml alone — no rebuild, no bespoke code. [[hooks.pre_sign_up]], [[hooks.pre_sign_in]] and [[hooks.post_sign_in]] carry a closed predicate vocabulary (email and email-domain globs, auth method, social/SSO provider, OAuth client) and refuse or allow; [[claims.mappers]] add per-client id_token and userinfo claims (user field, org role, static or a bounded template); [user.fields] declare typed custom fields validated at the boundary. The contract is served at /docs/flow-hooks.

Safety is the wave's spine. A hook refusal renders EXACTLY the family it sits on — a refused password, username, OTP, magic-link or phone attempt is byte-, timing- and lockout-identical to that family's own wrong-credential outcome, so a configured policy can never be used to enumerate accounts or trip a victim's lockout. allow never bypasses ban, lockout, email policy, rate limit or the origin/callback fences. On the OIDC path, a hook refusal fires only after the redirect-URI fence, as a fixed access_denied with no operator prose or identifiers. An instance with no hooks configured serves a byte-identical wire — pinned per sign-in family. IP predicates are deliberately out of this version: the instance reads a client-supplied forwarding header, so gating on it would be a decision on attacker-controlled input.

Performance was measured, not asserted. A baseline was captured before any hook code existed; after the wave, an unconfigured instance is within run-to-run noise of it on every hot path (get-session, the sensitive detector, is equal-or-faster). With a hook and a mapper configured, the added cost is also indistinguishable from noise — but read that second number narrowly: the mapper in the measured run never matched the exchanging client, so what is bounded is the cost of the plan being present and scanned, not of a mapper that actually emits a claim. That last figure could not be produced, because a mapper opts in by exact client id and the test rig cannot pin one; it is bounded by argument rather than measured, and filed as its own item. The receipt, including this limit, is perf/hook-baseline/after.md.

The design and its security shape were hardened by a cross-family adversarial review that found three enumeration oracles a first implementation's own tests reported green; the fix closed them and the review's second pass caught the fix's own regression. post_sign_in annotate emits a new session.annotated event through the wave-3 webhook path. Not a deploy claim; no hosted instance was bumped in this change.

2026-08-25 — event webhooks a stranger can build against: contract page, receiver kit, durable retry

Wave 3 of the pluggability roadmap (D298, design D299, item R378). The instance now serves /docs/event-webhooks — the complete language-neutral receiver contract with pinned test vectors that a test computes with the real signing function, so page and code cannot drift. signet events verify-receiver --url --secret ships in the binary: nine conformance cases a customer runs against their own endpoint, each failure line naming the corrective action. The proof, in commit order: a fresh implementor with no source access built examples/event-webhook-receiver.ts from the served page alone, both vectors matched, and the kit passed it 9/9 first try — before any transport change landed.

Delivery itself became durable. A failed webhook attempt now persists to the new eventDelivery table (Postgres migration 0087, SQLite migration 0002) and one scheduler retries it through a bounded worker pool on a jittered schedule — at most 8 attempts over at least 17 hours, surviving restart; graceful shutdown parks started attempts for immediate resume. Every non-2xx retries (only 410 Gone exhausts early); Retry-After is parsed in both forms and clamped; five consecutive failures stretch the schedule until a success. The delivery row is also the receipt — attempts, last error class, exact HTTP status — listed paginated at GET /admin/v1/events/deliveries and via signetctl events deliveries. Exhaustion writes the eventDeadLetter row in its existing shape, so the shipped replay contract is unchanged; retention prunes finished rows by age and row count, never pending ones. Boot now warns when [events].secret equals the delivery webhook secret (verify authority is sign authority — the events secret is the one handed to third parties). A no-sink instance is byte-identical on the certified wire, now pinned by a certification case.

Driven live before landing: sign-up with the receiver down → auth unaffected, two pending receipts with connect; receiver revived → the scheduler delivered on the jittered second attempt, signature verified, rows flipped delivered with attempts: 2. This entry is not a deploy claim; no hosted instance was bumped in this change.

2026-08-25 — an organization's OAuth clients admit only that organization's members

An OAuth client created while an organization is active is bound to it, and now refuses sign-in for anyone who is not a member of that organization. The refusal happens before consent and names the organization and the remedy. Clients created with no organization active are platform-scoped and are unchanged: they still admit anyone with an account on the instance.

Nothing running was affected. No organization-scoped client existed on any hosted tenant when this shipped, measured across all ten tenant databases rather than assumed.

If you operate an instance and your application's own customers sign in through it, note that they are application users on a separate path and are unaffected. Do not add customers to an organization as members to restore access; membership carries roles and administrative meaning that a customer should not have.

The groups claim's format also changed on 2026-08-24: it now carries a bare role name (owner, admin, member) scoped to the client's bound organization, replacing the earlier <organization-slug>:<role> form, which Proxmox VE and other consumers reject as an invalid group name. Group mappings built against the old values must be re-pointed.

2026-08-25 — the gate now runs the SCIM suite, and cannot go blind to a feature again

  • The SCIM test suite was outside every gate and nobody knew. r307_scim is required-features = ["scim"]; the pre-push battery built vault, external-jwt, saml and enterprise-sso and never scim, so the whole R307 arc — deprovision revoking live credentials, the derived meta.lastModified, the tenant-scoped profile ownership, the non-vetoable deactivation — could be broken by any change and land green. Its coverage during the arc came from builders running the suite by hand, which is diligence rather than a system. A new rung runs it, and states a decision on every push including the skips.
  • The class is closed, not just the instance. A new audit enumerates every [[test]] carrying required-features and refuses the push if no called rung builds that package with that feature. A feature-gated suite can no longer be added without a gate that runs it.
  • A refusal now names the failing test binary. The battery used to say "the saml-feature test suite is red" when the failing binary was s4_rate_limit — a wrong-subsystem message that cost four cycles across two lanes on 2026-08-24. Both the SCIM and SAML rungs now print the real name.
  • Known and filed, not silently excluded: signet-conformance-pg requires scim and nothing builds it. It is a [[bin]], and the audit reads [[test]] only — so the class above is closed for test targets and open for every other target kind. No exclusion was added to make the new check quiet.

2026-08-24 — delivery becomes a typed, config-selected provider surface

[delivery].mode can now select signed webhook, SMTP, the Amazon SES v2 API, or the Resend Email API without customer code. The Sender contract returns a provider receipt or a classified transport, timeout, rate-limit, permanent or invalid-config error; SMTP now has an explicit 10-second default deadline. One shared contract suite covers success receipts, failure typing, deadlines, no permanent retry, CRLF/address injection, stable idempotency where supported, concurrency, secret redaction, dead-letter replay and the closed mode vocabulary. Its fresh result is 44/44: the same 11 cases pass for all four transports.

SES is one plain-text SendEmail call with hand-rolled SigV4 over the exact serialized bytes. It accepts static IAM credentials only through env: or file: references; ambient, instance-profile, ECS/EKS and STS/session credentials are out of scope. SES sandbox recipients must be verified. Existing SES SMTP credentials continue to work through SMTP mode. Resend API mode uses bearer auth and a stable Idempotency-Key.

The new delivery-live-receipt command sends without opening the application database and prints machine JSON. The real Resend SMTP relay accepted this phase's message but returned no provider id, so the receipt was providerMessageId: null. No SES or Resend API credentials were exported for a live run. SMTP, SES and Resend therefore remain EXPERIMENTAL under D294's per-provider support bar; transport acceptance is not presented as a support receipt. SMS remains out of scope. This entry is from source and local/live receipts, not a deployment claim.


2026-08-24 — SQLite graduates to SUPPORTED; the repo splits in three

  • adapter = "sqlite" is now a supported storage tier (was experimental): one encrypted SQLCipher file per instance, key from operator material, offline rekey, plaintext import, and a checkpointed signet backup flow — all driven with receipts. The label moved only after the full workspace suite was observed green (D278's condition, not a date). What is still NOT claimed: "certified" (that requires the design-partner conformance sweep), HA, or multi-process access to one file.
  • The repository split in three (engine / working record / marketing site) so record writes stop racing 40-minute code gates; customer-visible surfaces are unchanged.

2026-08-23 — encrypted SQLite operations land; support graduation is held

The SQLite tier now encrypts its database with SQLCipher and refuses to boot without an operator-supplied 256-bit key. Configure database.key as an env: reference or a file: reference stored away from the database; inline keys and key files beside the database are refused. Boot verifies that SQLCipher is really linked and that its SQLite is at least 3.51.3, then applies and checks the cipher and WAL profile on every connection. A wrong key produces a typed corrective refusal and never prints key material.

Three offline commands complete the operating story: signet sqlite import-plaintext --from copies a phase-1 file into a new encrypted database.path; signet sqlite rekey --new-key rotates key A to key B; and signet backup --to checkpoints WAL and makes a non-overwriting ciphertext copy that is integrity-checked before success. The driven backup was restored by booting the copy and signing in the same real user.

The tier remains EXPERIMENTAL. Its encryption, import, rotation, backup/restore, adapter, Postgres, differential-conformance, formatting, lint and benchmark gates are green, but the brief also requires the entire workspace suite. That suite is red on a pre-existing cross-surface check: the engine palette test still expects the site's retired direct CSS template while the site now constructs a Direction-D stylesheet with a different effective palette. R186 does not own either design ruling, so R186-P2-GATE-004 records the blocker rather than changing unrelated product styling to earn this label.

Network filesystems, external writers, multi-process access and HA remain refused or unsupported; use PostgreSQL for HA. The retained differential conformance result remains 244/245, so this is not a certified-parity claim. The encrypted local benchmark measured a 3.73 ms get-session p50 and 279 reads/s on the phase-2 machine, versus 2.89 ms and 310 reads/s before encryption.


2026-08-24 — one logo everywhere, and the tab colour finally matches the page

Two mark defects, both customer-visible, both live as of 2026-08-24 02:36 UTC.

  • The engine served a DIFFERENT logo from the marketing site, for nine days. Every instance's /certification and /docs drew a mark in an 0 0 86 88 box while signetauth.com drew the 0 0 2048 2048 one. The engine's copy sat under a comment claiming it matched site/lib/layout.ts verbatim; that stopped being true on 2026-08-13 when the site moved to a file-backed mark and deleted the constant the comment named. Nothing compared the two, so nothing failed. certification.rs now carries the site's own three paths, and served_mark_matches_site_seal reads BOTH files at compile time, so the build fails if they diverge again.
  • The favicon could not track the accent. The Direction D reskin moved --accent from teal #19A487 to amber #FBB817; the page followed, the browser tab did not, because seal-cut.svg is a standalone file that cannot read the site's stylesheet. Favicon, OG card and apple-touch icon are now amber. Still DETECTED, not prevented — a test pins the literal, but nothing derives these assets from the palette.

Deployed and verified on the wire, not inferred from a green push: all four engine instances (auth, acme-corp, kapable-dev, signet-cloud-auth) plus signetauth.com were fetched and grepped for the site mark's own first path — five of five match.

2026-08-23 — buttons, fonts, and the contact route

Three presentation passes in one (D284; no claim, price or positioning string moved):

  • The buttons. The Direction D primary was a flat gold plate over a solid Ember offset shadow that travelled on hover; the operator asked for it to go. The primary is now the ink plate — solid ink, ground text, no shadow — and the gold arrives on hover (the pair ::selection already uses); :active presses 1px. On dark plates it inverts via the local token re-point. The secondary keeps its same-day replacement (one hairline that fills). The D282/D283 A/B arms share this chrome, so the arm comparison stays internally valid; absolute levels shift.
  • The fonts. The site still preloaded hanken.woff2 — a face nothing has rendered since the reskin re-pointed the families at IBM Plex/Barlow — so every cold visit downloaded 34KB that never displayed, while the faces that DO render (all font-display: swap) raced first paint and usually lost, flashing Arial. The preloads now name the render faces (plex-sans-500, barlow-condensed-700, plex-mono-400), which makes swap first-paint-safe. Also fixed: the audience-tab weight compensation shipped under a ..audience selector — an invalid rule the browser discarded, so it had never applied.
  • The contact route (R135). The intake page is /contact; /early-access is a permanent 308 that carries the query, so old procurement emails and partner decks keep resolving and land pre-flagged. Footer, CTAs, llms.txt, sitemap and intake receipts all name the new path; the retired one is test-pinned as a 308. Written from source and the green site suite (139/139); not a deploy claim until it lands and the routes are probed live.

2026-08-23 — the homepage headline A/B can actually run

The two campaign tags minted under D282/D283 now select real arms: signetauth.com/go/go-ownership (control — the live ownership screen) and /go/go-offer (challenger — "Unlimited users &amp; SSO. Flat price." with the $299 quiet line D283 allows in that arm only) set a 7-day cookie and serve their variant of the founders door. Both arms log impressions and hero-CTA clicks to data/arms.jsonl; the default door for ordinary visitors is byte-identical to before, pinned by test. The challenger body is derived at boot by exact-string replacement that refuses to start if the ratified copy drifts. Not a deployment claim until the deploy lands and the arms are probed live; the write-up rule travels from D283: this tests two propositions, not two headlines.

2026-08-22 — experimental single-file SQLite for evaluation

Signet can now boot against one local SQLite file by setting database.adapter = "sqlite" and database.path in signet.toml, or by exporting a sqlite:// SIGNET_DATABASE_URL. The same email/password sign-up, sign-in and session round-trip used for PostgreSQL has been driven through the real server on a fresh file.

This is explicitly EXPERIMENTAL. The file is plaintext, one Signet process owns it, network filesystems and external writers are refused, and phase 1 adds no encryption. The bundled runtime also reports SQLite 3.46.0 while the pinned WAL-reset-fix minimum is 3.51.3; boot names that gap and the required upgrade while applying an interim single-writer checkpoint mitigation. The managed conformance comparison matched 244 of 245 assertions that passed the same-run reference. The one remaining sign-up enumeration response-shape mismatch is recorded rather than presented as parity.


2026-08-22 — dark mode, with a switch

signetauth.com now renders a dark scheme — a warm near-black in the same ink family as the code samples, teal seal unchanged. It follows your system setting by default, and a small switch in the navigation overrides it either way, remembered per browser. That switch is the one piece of script the site now carries (an operator ruling the same night amended the zero-JavaScript charter to "nothing but the toggle"; the tests pin the amended form). Light mode, and the printed vendor pack, are untouched.


2026-08-21 — everything we shipped for agents last week is now actually running

Three fleet deploys in three days moved the agent-auth work from main onto every hosted instance. All four are serving it now, and each migration was checked on its own database rather than assumed from a clean start. If you read an earlier entry that said "not yet deployed", that caveat is spent — the entries say so in place.

What this means in practice: the enterprise agent flow works end to end for the first time. It was advertised in every instance's discovery document before this, but a redemption passed every check and then rolled back on its own audit write, so nothing could actually complete. That is fixed.


2026-08-21 — your organization can adopt an agent nobody registered

An agent can identify itself by publishing a small JSON file at an https URL. Until now that identity could authenticate but could not get a token from your enterprise identity provider's decision, because it had no organization — and the whole grant is built on knowing whose tenant an agent is acting in.

Now you adopt it. An owner or admin allow-lists the exact URL:

POST /api/auth/oauth2/adopt-client
{ "organizationId": "…", "clientId": "https://agent.example.com/client" }

That row is what gives the agent a tenant when it redeems an assertion from your identity provider. List them with GET /api/auth/oauth2/client-adoptions?organizationId=… and withdraw one with POST /api/auth/oauth2/revoke-client-adoption. Adopting is also re-confirming — same call.

Four things worth knowing before you use it:

  • Adopting an agent is your decision about your own organization, not ours. These routes sit with the clients they govern and take an owner or admin session, exactly like registering your identity provider. There is no step where we approve an agent for you.
  • Two organizations can adopt the same agent. That is the ordinary case, not a conflict — a public agent serves many customers. When an assertion arrives, the tenant is the organization that registered the assertion's issuer, and where several have, the one where its subject is already a member. Nobody can take an agent away from you by adopting it first.
  • ⚠ We pin the keys you approved, so an agent that rotates its keys must be re-adopted. We record a fingerprint of the key set at the moment you adopt, and refuse redemption on any later difference until an owner or admin confirms the new one. This is deliberate. The file that defines the agent lives on a web server somebody else controls, and a key set nobody approved is what a takeover looks like from here. The cost is real: rotation needs a call from every organization that adopted that agent.
  • Withdrawing keeps the record. Revoking marks the adoption revoked rather than deleting it. Who authorized an agent to act inside your organization, and when that was withdrawn, is most worth having at the moment it is withdrawn.

What you still cannot do: block an agent you never adopted. If a URL-identified agent misbehaves, you can revoke its tokens, invalidate sessions and ban the user — none of which stop it starting again. There is no row to delete, so there is nothing to switch off. A deny-list is tracked (R305) and not built. (R291, D264.)


2026-08-21 — an agent that publishes a URL can sign a person in

Same identity, different road. An agent identified only by an https URL can now run the ordinary sign-in flow — authorize, consent screen, code, token — with no registration, no client secret and nobody at your instance setting anything up. The MCP door serves the same road, consent step included.

This is the flow the MCP specification's own diagram shows, and until today a URL-identified client was simply an unknown client at that door.

Three things are true of it by design:

  • The consent screen shows the URL beside the claimed name, and says which is which. The name comes out of a file anyone can publish, so two agents can claim the same name and one of them can claim ours. The URL is the identity: it is the exact address we fetched.
  • Your redirect URL must match the published list exactly, by the same check every registered client passes. The list comes from the agent's own document instead of from a record we hold.
  • PKCE with S256 is required. At the MCP door this is a new requirement, and it applies only to agents identified this way — a client you registered is unaffected.

⚠ This flow needs no adoption. Adoption governs the enterprise-assertion grant, which has no person in it; here the tenant comes from whoever signed in. (R292, D265.)


2026-08-20 — you can fix an SSO connection you got wrong

~~⚠ Dated by the work, not by a shipped binary. This is on main and is NOT YET DEPLOYED to any live instance, so no hosted tenant can use it today.~~ Deployed 2026-08-21 — all four hosted instances serve it, and both routes were read on the wire that day. It remains compile-gated: it exists only in a binary built --features enterprise-sso, the same gate the rest of SSO sits behind, and hosted instances are built with it.

Until today a connection was write-once. Mistype the audience, the entity ID or the domain, and there was no way to correct it. The ACS URL contains the connection's id, so inventing a second connection to replace the broken one also meant reconfiguring your identity provider to point at a new URL. One wrong character cost a round trip through your IdP admin. The broken connection then sat in the database forever.

Now it is one call.

POST /api/auth/sso/update-provider
{ "providerId": "acme-okta", "samlConfig": { "audience": "https://your-sp/metadata" } }

The connection id never changes, so your identity provider is not touched. Same ACS URL, same everything on their side. That is the whole point of the route. Send any of issuer, domain, samlConfig or oidcConfig; fields you leave out keep their current values.

Owner or admin only, same as registering one.

Three things worth knowing before you use it:

  • Identity fields freeze once people have signed in. Changing the audience, callback URL, entry point, identifier format or metadata while accounts are linked is refused. Moving those underneath an existing user would point them at a different asserted identity. Correcting a connection nobody has used yet, which is the case this route was built for, is unaffected.
  • Changing the domain resets its DNS proof. The proof you passed was about the old domain. Re-run the verification. Until you do, existing members still sign in but no new ones are created.
  • A replacement certificate is checked before it is written. You can send one here, and an expired one is refused rather than accepted and discovered at the next sign-in.

You can also remove a connection now:

POST /api/auth/sso/delete-provider
{ "providerId": "acme-okta" }

Deleting a connection signs out the people who used it. That is deliberate. The case this exists for is removing an identity provider you no longer trust, and leaving those sessions running would leave whoever compromised it signed in. Someone who also has a password loses that session too, because a session does not record which credential created it. If you are deleting a connection only to re-register it cleanly, expect everyone to sign in again. Your users and their organization memberships are not deleted. A person whose only login was that connection still exists and simply has no way in until you give them one.

Known limits, stated rather than discovered: you can correct and delete a connection through the API, but you cannot yet list or read one back through it (R301), and the console shows a connection without offering an edit or delete button (R130). Both are tracked.


2026-08-20 — a refused signup now tells you why, and the answer stays put

If Signet Cloud cannot give you an instance, the claim form now says so in place: the reason and the way forward stay visible on the form. Before this, the answer appeared only in a small corner notice that vanished after a few seconds. The most common refusal reads: someone at your company already runs the free instance. Sign in to it, or start a paid plan for another.


2026-08-19 — a second factor's emailed code could not be guessed twice as fast

If you use an emailed second-factor code, a burst of resend clicks can no longer weaken its guess limit. Requesting a new code while one was already in flight could, under exact timing, leave two limit counters alive for the same code, and each counter allowed its own five wrong guesses. The code and its counter are now replaced as one indivisible database step, so however many resends race, exactly one code and one limit exist. Nothing changes for normal use — request a code, type it in, done.

Self-hosting customers get a standing security sweep. Every night Signet's own red-team lenses now probe a fresh throwaway instance of the engine for known attack-surface drift (starting with redirect and callback handling). It is our internal practice, not a feature you configure — it means regressions in guarded attack classes are caught within a day of landing.


2026-08-19 — the contact form remembers you, and asks before it may email you

The form at signetauth.com/early-access gained one optional checkbox: "Keep me posted on Signet: product updates and news by email. Unsubscribe anytime." Ticking it records an explicit, evidenced opt-in. Leaving it unticked changes nothing about your request: we can still answer you, and you are on no list. Either way, your submission now lands in Signet's own CRM. A reply comes from a person who can see your note, not a triage screen. Submitting twice cannot create two records. (R296, D260.)


2026-08-18 — your identity provider decides which agents reach your apps

~~⚠ Dated by the work, NOT by a shipped binary. It merged to main on 2026-08-19 (fbfc1e4dcbe45a13) but is NOT YET DEPLOYED — no live instance serves it.~~ Deployed 2026-08-19, and genuinely usable since 2026-08-20: what shipped on the 19th was advertised but could not complete, because the grant's audit write named a ceremony no migration had added and the database rolled every redemption back. Migration 0082 fixed it and reached all four instances on 2026-08-20. The second half of this warning is permanent: it is compile-gated, existing only in a binary built --features external-jwt. A default build advertises none of it and refuses the grant by name, so a client talking to one fails safely instead of half-working. Building the feature does not pull in SAML.

  • An agent can now reach your apps through the identity provider you already run. Signet accepts an ID-JAG — the authorization decision your enterprise IdP mints for a specific agent, a specific user and a specific API — at POST /oauth2/token, and returns a Signet access token bound to that API. Okta Cross-App Access is the launch IdP; the mechanism is MCP's Enterprise-Managed Authorization profile. What this deletes is the manual step: no per-agent consent screen, no per-tenant client provisioned by hand out of band. Which IdP an organization trusts stays configured, never discovered — an assertion from an issuer you did not register is refused, and the refusal never tells the caller which issuers exist. The person's org membership is checked on every redemption, so removing someone from the organization cuts their agents' access at the same moment it cuts their own. (R276)
  • A client can authenticate with a signature instead of a shared secret. Register with token_endpoint_auth_method: "private_key_jwt" and exactly one key source — your key set inline (jwks) or a URL we fetch it from (jwks_uri), never both. No client secret is minted for such a client, deliberately: a shared secret sitting beside your key would be a second, weaker way in that nothing rotates. Server-side this arm is complete for registered clients — it is the path that works end to end today. (R279)
  • A client can also identify itself by URL. Present an https URL as your client_id and Signet fetches the metadata document you publish there, validates it (the document's own client_id must be byte-equal to the URL, served as JSON, under 5 KiB, no redirects, publicly routable host) and honours it at the token endpoint's client authentication. ~~What it cannot do yet is redeem an enterprise assertion: that grant reads which organization you belong to from a client record, and a client that registered itself by publishing a document has none. Recognized, not yet grant-eligible.~~ Closed 2026-08-20: an organization adopts the client, and that adoption is what supplies the tenant. See the 2026-08-21 entries above. (R278, R291)
  • No refresh tokens on this grant, by design — re-present the assertion. The ID-JAG profile makes re-submitting the same assertion the refresh mechanism, so Signet issues none, strips offline_access even from a client registered for it, and lets the same assertion redeem again for a fresh token for as long as the enterprise's decision is still valid. The token itself never outlives that decision: its lifetime is the shorter of your configured TTL and the assertion's own remaining life. (R276)
  • Discovery tells a client, before it tries, whether this build can do any of it. In a build with the feature, both discovery documents name the jwt-bearer grant, the ID-JAG profile, private_key_jwt, and client_id_metadata_document_supported: true. In a build without it they name none of those and the token endpoint answers unsupported_grant_type. Neither document ever lists an issuer, a client or a host — which IdPs and which agents an organization has registered stays private. (R280 for the grant and its profile; R278/R279 for the two client-authentication members)
  • ⚠ A resource-bound MCP token no longer resolves a session at someone else's server. /mcp/get-session and /mcp/userinfo now read the token's binding, so a token authorized for a third-party MCP server stops resolving here — which is the confinement working, and a wire change worth planning for if your client both sends a resource and then asks this server to resolve its own bearer. Authorize with the resource this server's own protected-resource document advertises, or without one. A token with no binding is unaffected. (R281)

2026-08-16 — an agent's token can be locked to one API, and made short-lived

~~⚠ Dated by the work, NOT by a shipped binary: none of this is on main yet. It lives on audit/crown-jewel-security (cdded0f47e2fc76e). Strike this warning when the lane merges and the fleet is bumped.~~ Both happened: merged to main 2026-08-19, and live on all four hosted instances since the 2026-08-21 fleet deploy, which descends from those commits. The warning was written the same way the 2026-08-10 MCP registration entry had to be corrected — a customer reading this needs to know which builds actually behave this way — and it is kept struck rather than deleted so the dates stay legible.

  • Name the APIs a token is for. An authorization request to /oauth2/authorize or /mcp/authorize may now carry RFC 8707 resource — the absolute URI of the API the token will be presented to — repeated up to eight times. The issued access token is live at those targets and refused anywhere else, and the set survives the login page, the consent screen and refresh rotation. A refresh copies its targets from the token being rotated, never from the request, so it can neither widen the grant nor invent a target the authorization did not give it. Until this, every token a browser or an agent obtained from these doors was structurally unrestricted. (R251-R255)
  • A resource server asking about the wrong token now gets the right answer. /oauth2/introspect enforces the resource it is handed: a token bound to one API and presented for another answers invalid_target instead of active: true. The certified introspection response is otherwise byte-identical to what it always was — the bound resources are readable on the operator's /tokens/introspect, not added to the RFC 7662 shape. (D232)
  • Access tokens can expire in minutes instead of an hour. [oauth] access_ttl_seconds sets the lifetime of OAuth2 and MCP access tokens together; the default is still 3600 and the accepted range is 3003600. An agent holding a token for the length of a task wants the short end — five to thirty minutes — so a leaked bearer is useful for minutes rather than an hour; the cost is one refresh round-trip per interval for clients holding offline_access. One key moves both doors on purpose: a five-minute OAuth token beside an hour-long MCP token would be a hole with a configuration screen in front of it. (R251)
  • ⚠ Revoking an MCP access token now actually revokes it. POST /oauth2/revoke searched only the OAuth2 access-token store, so an MCP token was answered 200 — which RFC 7009 requires for a token the server does not recognise — while nothing was deleted. A leaked MCP token could not be killed, only waited out. It is now found and consumed. What a caller may delete has not changed: the token must belong to the authenticated client, and the endpoint still answers 200 with an empty body in every case, so it cannot be used to probe whether a token exists. MCP refresh tokens were never affected. (R257) Symmetry closed 2026-08-17: /oauth2/introspect and /oauth2/userinfo now resolve the whole access family, so a live MCP access token reports active: true at every door a client can reach. The RFC 7662 envelope is byte-identical to before for classic tokens; the divergence from upstream (which has no MCP token class at all) is recorded in docs/certified-divergences.md. (R262, built — D242)
  • code_challenge_method=plain is refused at /mcp/authorize. /oauth2/authorize already refused it, so the same PKCE downgrade had two different answers depending on which door a client used. An MCP client sending plain now fails; send S256. (D233)

2026-08-18 — a licence can be locked to your deployment

A licence activated against a machine fingerprint is now checked at runtime, not just issued: if the licence says deployment A and the instance declares a different identity, the licence is refused with a message naming both and the fix. Declare your deployment's identity with the new [license] fingerprint key (or SIGNET_LICENSE_FINGERPRINT) — use ONE stable value for the whole estate ("acme-prod-signet"), never a pod hostname or boot ID, which re-trips the binding on every reschedule. Instances that declare no fingerprint keep booting with a warning, so nothing breaks on upgrade. Licences with no machine binding are unaffected. (D247; R284.)

2026-08-17 — the engine serves its fonts as same-origin files

/docs, /certification and the front door now load their typefaces from this instance's own /fonts/*.woff2 routes (cached a day) instead of inlining them as base64 — same visual result, no late font swaps on first navigation, and still zero external fetches: the files come from the instance you are talking to. (D239; R267.)

2026-08-17 — licences re-verify every minute while the server runs

A licence that expires or is revoked mid-day now turns off paid features within sixty seconds, without a restart — previously the check ran only at boot. A rolled-back system clock is also detected against a persisted high-water mark. Deploy/restart overlap never blocks boot (the licence counts steady-state instances; one replacement may overlap during a handover). (D229, D240; R250, R263.)

2026-08-16 — second-factor lockouts now bind every factor

Repeated wrong step-up codes lock the session's verification budget across ALL factors — TOTP, backup codes and emailed codes — so switching method no longer bypasses a lockout earned by another. (D237; R261.)

2026-08-18 — your account page works on a phone, and follows your dark mode

You can manage your account from a phone now. The page listed every session you are signed in from, told you to sign out any you did not recognise, and then put the Sign out button off the side of the screen. The buttons now stay in view while the rest of the row scrolls, the page no longer scrolls sideways, and every control is big enough to hit with a thumb.

It also stopped assuming you want a light screen. If you have not chosen a theme, Signet now follows your device's setting. If you have chosen one, that still wins — nothing changes for you.

Smaller things you may notice: the page title is now clearly larger than the Signet wordmark above it (it used to be the other way round), and keyboard focus draws in Signet's own green on buttons and links instead of the browser's default blue.


2026-08-17 — only owners and admins can connect an identity provider

Two changes to who can do what. Both close a way to get into an organization you were never invited to.

Connecting an identity provider now requires the owner or admin role. Any member of the organization could do it before. That matters more than it sounds: the identity provider you connect decides who becomes a member, so being able to connect one meant being able to add anyone. A member who tries now gets told to ask an owner or admin.

An unverified connection can no longer add new members. Proving control of your domain used to affect only which email addresses were routed to your connection — it did not stop the connection from creating accounts. Now a connection that has not proved its domain cannot add anyone new to the organization. Verify the domain and it works normally.

People already in your organization are unaffected either way — they keep signing in, including through a connection registered before today. The restriction applies at the point someone NEW would be added.


2026-08-17 — you can rotate your identity provider's signing certificate

Your IdP will rotate its signing certificate eventually — Okta, Entra, all of them do. Until today that meant your SSO connection simply stopped working, and there was no way to fix it from inside the product.

Now there is: POST /sso/rotate-certificate/{providerId} with the new certificate. Your callback URL does not change, so there is nothing to reconfigure on the identity provider's side.

Three things it does deliberately:

  • It refuses a certificate that is already out of date. Rotating onto a dead certificate would appear to work and then lock you out at the next sign-in.
  • It requires owner or admin for an organization's connection. Replacing the signing certificate decides who can mint sessions for everyone in the organization; being a member is not enough.
  • The old certificate stops working immediately. We swap, we do not overlap. Accepting two certificates at once would keep honouring a key you might be rotating because it was stolen. The trade is that a sign-in already in flight at the moment you rotate will fail and need a retry — seconds, and a failed sign-in rather than a wrong one.

Every rotation is recorded in the audit log.


2026-08-17 — an expired identity-provider certificate now stops sign-in

Behaviour change, and it can lock a connection out. If your identity provider's signing certificate is outside its validity window, SSO sign-in is now refused. It used to be allowed through with a warning in our logs that nobody outside our team ever saw.

Why we changed it: that certificate's validity window is your IdP's own statement about how long its signing key should be believed. Honouring it is the only thing that retires a key nobody has noticed was stolen. Warning and then accepting retired nothing.

If this hits you, the refusal message names both ways out: re-register the connection with your IdP's current metadata, or — if you know the certificate is out of window and want it accepted anyway — set rejectExpiredCert to false on that connection. The opt-out is deliberate and supported.

Related: a certificate whose validity window cannot be read at all is now refused outright, under a separate code (SAML_CERT_WINDOW_UNEVALUABLE), and that one has no opt-out. "We could not tell" is not a reason to trust a key.

Superseded the same day: rotation landed — see the entry above. You no longer need to tell us before your IdP rotates.

2026-08-17 — each of our four domains now does one job

Sign-in on signetauth.cloud did not work. The page looked fine. The auth server did not trust that address, so it turned away every attempt. It works now. If you tried to sign in there and could not, this was why.

The four domains have separated:

  • signetauth.com — what Signet is, what it costs, where you buy it.
  • signetauth.cloud — your account, your instance, your billing. You sign in here.
  • signetauth.dev — developer material: quickstart, the configuration reference, the operator API, the command-line reference. It used to serve a copy of the customer portal.
  • signetauth.app — the admin plane, once we build it. For now it sends you to signetauth.cloud.

Until today, three of those four served the same page. Now one address answers for each thing. The others redirect to it.

The purchase page is /start. It was /pricing. That name clashed with the marketing pricing page, so "Sign up" dropped you on the name of the page you had just left. Old links still work. /pricing and /checkout both redirect, and they carry your chosen plan with them.

Signed in on signetauth.dev? Sign in again on signetauth.cloud. A session does not move between addresses.

Hosted instances did not change. {yourname}.signetauth.dev keeps its issuer, its discovery document and its OAuth callbacks. We are not moving those addresses in this change.


2026-08-16 — enterprise SSO actually works on the hosted product

If you are on a paid hosted plan, you can now connect your company's identity provider and sign in through it. Register the connection under /sso, hand it your IdP's metadata, and your people sign in with their work accounts. Proven end to end against a real Okta tenant on 2026-08-16: an Okta assertion creates a Signet session and provisions the user.

This was previously broken in a way the pricing page did not admit. Hosted instances were built without the SSO code compiled in at all, so the "unlimited SSO connections" the Team plan sells could not be used on the hosted product — only on-prem binaries carried it. Ruled and fixed in D238.

Known limits, stated rather than discovered. ⚠ Both limits this entry originally named have since been fixed, and the entry is left standing with the corrections rather than rewritten. It said the signing certificate could not be rotated: true when written, fixed 2026-08-17 (R268). It said a connection could not be edited or deleted once registered: true when written, fixed 2026-08-20 (R266). See those days' entries.


2026-08-16 — the first ten minutes no longer invent a plan

  • Sign in signs you in. The marketing chrome used to send Sign in and Get an instance to the same claim form. Sign in now opens https://signetauth.dev/login. Get an instance still opens the claim door.
  • Billing does not invent Hobby. An unsigned visit to /billing used to show plan Hobby and the internal checkout paths. It now says sign in to see your plan.
  • Start Team free still says Team. With no account, that button used to dump the visitor on the Hobby form with no note. It now lands on /?plan=team and names the tier.
  • Gmail can start Team without a Hobby box. Paid-intent signup reserves the instance name (awaiting_payment) and opens checkout. Nothing is built until billing says the account is entitled. Abandoned checkout is an account and a name, not a free instance. Work emails can convert the reserved name to Hobby.
  • The instance /certification page says Signet, not SIGNET. The leftover split "Sig"+"net" hero and the all-caps masthead are gone. Paper ground was already live; the name now matches the marketing site.

2026-08-10

  • BREAKING for new production installs using MCP: anonymous client registration is now off by default. POST /mcp/register previously accepted any unauthenticated caller and minted working OAuth client credentials with no owner. Production instances now refuse it with a typed 403 MCP_REGISTRATION_DISABLED naming the fix: set [mcp] registration_enabled = true in signet.toml if the deployment needs registration. U17 subsequently made the enabled production ceremony session- and Origin-gated, owner-bound, and transaction-audited; only the isolated conformance profile remains anonymous. Existing registered clients keep working; only NEW registration is gated. (R124; recorded divergence — the conformance profile keeps upstream's open behaviour.) ⚠ Dated 2026-08-10, but it reached main on 2026-08-11 (the R210 lane merge). Between those dates this entry described a gate no shipped binary carried: the code lived on an unmerged branch, and a live probe of POST /api/auth/mcp/register answered 400 — reaching validation — rather than the 403 above. Corrected in place rather than back-dated, because a customer reading this needs to know which builds actually refuse.

2026-08-11 — you can finally SEE the machine identities you created

  • The admin API can be read, not only written. Until today anything that created a service identity was unable to list one back: the surface minted, granted and revoked, but had no way to enumerate. Four endpoints close that — GET /admin/v1/service-principals (keyset-paged, filterable by organisation), GET /admin/v1/service-principals/{id} (its role grants, plus each credential's effective authority exactly as the engine computes it), GET /admin/v1/authorization-roles, and GET /admin/v1/principal-role-grants. They need the instance key, and they return digests and handles — never credential material. signetctl grew the matching list verbs and the official contract documents all four. (R204)
  • Signet Cloud manages service identities from the console. New screens list and create service principals and authorization roles, attach a role grant showing the effective ceiling as declared ∩ granted, mint a credential, and revoke one. The minted credential is shown once, in the response to the request that created it: reload the page and it is gone, and no second credential is minted. Lose it and you revoke and re-mint — the page says so beside the warning rather than leaving you to file a ticket. (R205)

2026-08-10 — your customers can now manage their own machine identities

  • Your instance's admin API is now agent-discoverable. GET /admin/v1/llms.txt (admin key required) serves an index of every operator endpoint — including the service-identity, role, grant, and credential surface — generated from the official contract so it cannot drift from what the instance actually serves. Point an AI agent holding your admin key at it and it can navigate the whole operator API. (R190)
  • Four new pages let a tenant administrator run service identities without you. Someone holding owner or admin in an organization signs in and reaches /org/identities and /org/roles: create a service identity, define a role, grant it, mint a credential, revoke one or all of them. No platform administrator credential, no ticket to the operator, no API call. Everything the machine-identity model could do since 2026-08-09 was previously reachable only through the admin key or the CLI; this is the customer's own door to it.
  • Every credential's page says what it can actually do, not just what it was asked for. A credential's live authority is its declaration narrowed by the roles its identity holds, so declaring more than you have granted shows as a visible reduction rather than a surprise at request time.
  • A minted credential is shown exactly once, in the response to the mint itself. There is no URL that re-displays it and a reload will not bring it back. If it is lost, revoke it and mint a new one — which the page tells you at the moment it matters.
  • Permissions are exact. The role form takes concrete resources and actions; * is refused, with an explanation, so a role always says what it grants.

2026-08-08 — signetctl: the operator CLI, agent harness, and MCP surface

  • signetctl ships (tag signetctl-v0.1.0): every Admin API operation as a CLI verb, bound to the new official contract docs/admin-api/openapi.yaml by a build gate.
  • signetctl doctor diagnoses the four instance failure modes that present as something else (alias redirect, stale binary, unmounted vs rejected admin key, dead delivery).
  • signetctl agent — bring-your-own OpenAI-compatible LLM; TUI REPL, headless -p, and stream-json so other harnesses can drive it. No default LLM, no telemetry.
  • signetctl mcp serve — stateless MCP (2026-07-28 revision) with durable tasks that survive client and server restarts.
  • The Admin API is now officially documented (OpenAPI 3.1, 22 operations) — the operator counterpart to the auth-wire schema instances already serve.

2026-08-08

  • Enterprise SSO over OIDC now signs users in. A provider registered with an oidcConfig gets a real authorization-code flow: endpoint discovery from the issuer, PKCE, a nonce, and ID-token validation against the IdP's JWKS (issuer, audience, signature, expiry, nonce) before any session is minted. Register {base_url}{base_path}/sso/callback/{providerId} as the redirect URI at your IdP. Proven end to end against a containerised IdP (dex). A provider carrying both SAML and OIDC configs dispatches OIDC first; pass providerType: "saml" to pin SAML. The interim refusal of OIDC-only registrations (2026-08-08 morning, D137) was removed the same day, and the certified-divergences register is empty again.
  • ⚠ SSO sign-in now creates organization membership. When a provider is registered under an organization, a successful SSO sign-in adds the user to that organization as a member (once — re-logins never duplicate it), and a failed membership insert refuses the sign-in. If you relied on SSO not granting organization access, this is a behaviour change.
  • SSO token storage is real. The account row now stores the full token set from the code exchange (access/refresh/ID token, absolute expiries, scopes) and refreshes it on every re-login, so IdPs that rotate refresh tokens stay usable.
  • The build feature gating enterprise SSO is now --features enterprise-sso; saml remains a working alias.
  • Account portal: regenerate your two-factor backup codes. Signed-in users with 2FA on can mint a fresh set of ten codes from /account after re-entering their password. The new set is shown exactly once and the old set stops working the moment it is issued.
  • Identity events now stream on stdout even with no webhook configured. Structured signet_identity log lines (event type, event id, opaque subject — never an address or credential) fire for sign-ups, sign-ins and the rest, so an air-gapped instance's SIEM can watch auth activity with zero outbound configuration.

2026-08-07

  • SP-initiated SAML single sign-on now works against conforming identity providers. The sign-in request Signet sends to your IdP was previously base64-encoded but not compressed. The SAML HTTP-Redirect binding requires DEFLATE compression, so a strict IdP could not read the request at all — only lenient ones accepted it. Signet now compresses the request as the specification requires. If SP-initiated login to your IdP has never worked, this is why, and it should now work without any change on your side.
  • BREAKING for some existing SAML providers: the sign-in request now identifies YOU (the service provider), not your identity provider. The Issuer of the authentication request used to carry the identity provider's own base URL. The SAML specification requires it to be the requesting service provider's entity ID, and Okta and Microsoft Entra ID check it against the service provider you registered with them — so this had to change.

What you may need to do. If you configured a SAML provider by uploading or pasting an SP metadata document, Signet now sends the entity ID from that document and there is nothing to do. If your identity provider was configured to expect the old value — for example, someone typed Signet's identity-provider URL into Okta's "SP entity ID" field to make login work — logins will fail until that field is corrected to your real service-provider entity ID. Nothing in your Signet configuration needs to change; the fix is on the identity-provider side, and it is the value your IdP was always supposed to have.

How to check before you upgrade. Start a sign-in, take the SAMLRequest parameter from the redirect URL, URL-decode it, base64-decode it, and inflate it as raw DEFLATE. The <saml:Issuer> element is the value your identity provider will see.

  • Still true after these two fixes, stated plainly: SP-initiated SSO has been proven end to end against a real containerised identity provider (simpleSAMLphp), including five negative controls that must refuse. It has not yet been driven against an Okta or Entra tenant, and the authentication request does not yet carry AssertionConsumerServiceURL, ProtocolBinding or NameIDPolicy — all optional in the specification, and accepted as-is by the identity provider we tested, but not yet verified against the ones most customers use.

2026-08-04

  • Organization SSO discovery now requires proof of domain ownership. Organization owners and admins receive a seven-day DNS TXT challenge, and email/domain or organization-slug sign-in can resolve only after the exact normalized domain is verified. A domain can be claimed by only one verified provider, including during database races, and seven-day-old proofs are re-resolved so removed DNS evidence suspends discovery. Password/reset enforcement and callback-time membership provisioning remain explicit residual work rather than implied by this discovery feature.
  • Users now carry public, private, and browser-owned metadata. The three JSON-object buckets are stored as native PostgreSQL JSONB. Public and unsafe metadata appear on ordinary user/session responses; private metadata is restricted to authenticated admin/operator responses. Email sign-up and self-service update accept only unsafeMetadata; admin creation/update can replace all three. A shared 8 KiB limit and object validation apply before every supported write.
  • The production binary now includes a non-interactive setup CLI. signet init scaffolds a valid secret-free config and never overwrites; signet doctor validates the config and can drive a live instance's health/schema surfaces; signet env pull updates only the public SIGNET_AUTH_URL in an env file while preserving unrelated values. Self-hosted server secrets remain in operator-owned env:/file: references and are never extracted by the CLI.
  • Password strength feedback and enforcement are available without changing existing policy. POST /api/auth/password-strength returns a zxcvbn score from 0–4, targeted suggestions, and whether the password meets this instance's length/strength policy; optional email/name inputs catch personal-data reuse. Set [password] min_strength = 1..4 to enforce the same estimator on every new password. The default remains 0, so upgrading does not reject passwords previously accepted by the better-auth-compatible length policy.
  • SMTP email copy is now configurable per flow. [delivery.smtp.templates] accepts subject and plain-text body overrides for email OTP, magic-link sign-in, address verification, password reset, and organization invitations. Values can be inline or loaded from env: / file: references. Placeholder typos, missing action links/codes, multi-line subjects, and action secrets in subjects stop boot with an exact repair; omitted fields keep the existing copy. Signed-webhook delivery is unchanged structured JSON, where the receiving app owns rendering.
  • Expired verification links now explain what happened and how to recover. The public front door and embedded docs recognize verification callback errors, say when the one-hour link expired, and point to a real resend recipe. The separate signetauth.com marketing apex does the same, so a callback to / no longer strands the reader on unrelated marketing copy. Signet now matches better-auth's TOKEN_EXPIRED versus INVALID_TOKEN distinction; unknown error codes are neither reflected nor guessed at.
  • Webhook delivery signing secrets now have the same minimum as event-webhook secrets. [delivery.webhook].secret must resolve to at least 32 characters; the loader checks the value after an env: or file: reference is resolved. Instances using a shorter delivery secret now refuse to boot and name the corrective openssl rand -hex 32 command. Rotate the secret before upgrading. The two webhook paths share one validator and the generated config reference carries the requirement.

2026-08-02 — instances are now deprovisionable

Operators can fully tear down a provisioned instance (scripts/deprovision-instance.sh): secrets, database, role, bot user, containers, state dir — with a verified-absence sweep. Previously every provisioned instance was permanent by default. Customer-visible effect: probe/trial instances no longer accumulate as live attack surface. (R82; contract in docs/deprovisioning-an-instance.md.)

2026-07-29

  • Signet is an OAuth 2.0 / OpenID Connect provider. Another application can now sign users in with your Signet instance. A relying party discovers the instance, redirects to /oauth2/authorize, the user signs in and consents on pages the binary itself serves, and the client redeems the code at POST /oauth2/token for an access token and an EdDSA-signed id_token it verifies against the published JWKS. Driven end to end in a real browser by a separate client process on a separate origin (ed91094, observed fc993c1). ⚠ Read the not-built list below before planning around this. Customer-facing summary: docs/oauth-provider.md.
  • The binary serves its own /login and /consent pages. better-auth treats these as application pages, which in a standalone binary meant a fresh install redirected to a 404 — it could not complete its own authorization flow out of the box. Both are now served at the root by default, zero-JS, in the existing dashboard house style. New [oauth] config keys login_page / consent_page point them elsewhere; ⚠ override either and Signet serves nothing at it — the page becomes yours (a49c789, fb1460a).
  • EdDSA (Ed25519) JWS signing in the engine, with the wire format pinned against fixtures minted by OpenSSL rather than by our own library (848df5e, hardened after a cross-family DO-NOT-SHIP verdict in 51e9fe3 and 3c970bc).
  • A 2FA code can now arrive by email. POST /two-factor/{send,verify}-otp — the half of two-factor a user without an authenticator app actually hits. Previously only TOTP could complete a second-factor challenge (ca9d788).
  • 🔴 SECURITY — an open redirect that leaked authorization codes is fixed and deployed to production. GET /mcp/authorize never checked the requested redirect_uri against the client's registered list, so a signed link could hand an attacker an exchangeable code. Worse than first understood: the victim need not already be signed in, because the attacker's query is replayed out of the login-prompt cookie — signing in is what leaked the code. Fixed in 75d6a25, deployed, and proven on the certified host as a true differential (attack → invalid_redirect, registered URI → normal login). ⚠ One live instance has not received it — see 02-SURFACES.md.
  • Four defects that made whole features unusable on PostgreSQL, all fixed (62ce495, 8142aec). Each was invisible to a green test suite because every server suite runs on a schemaless in-memory adapter:
    • every oauthClient write returned 500, so no OAuth client could ever be registered (R16);
    • POST /organization/invite-member returned 500 on every Postgres instance and zero invitations had ever been written (R27);
    • email-OTP sign-up was impossible for any address without an account (R29);
    • a valid magic link told its recipient the link was invalid — no error, nothing logged (R30).
  • A browser end-to-end harness with screenshot receipts (conformance/e2e/, 9ae252d, fc993c1). Drives 18 authentication and authorization modes in real Chromium against a throwaway instance, obtains OTPs and magic links through a real webhook sink with the delivery HMAC verified, and records the sha256 of the exact binary tested. Four modes are recorded as explicit skips with reasons — passkey/WebAuthn, social, SSO/SAML, SIWE — rather than being silently absent.

⚠ Not built — do not plan around these

/oauth2/userinfo, /oauth2/introspect and /oauth2/revoke are not implemented, and the discovery document still advertises all three: those URLs return 404 (tracked as R14, with the discovery-liveness gate built to catch exactly this class). The refresh_token and client_credentials grants are refused with unsupported_grant_type. Signing is EdDSA only — RS256, which OIDC Core names as the default, is not supported (R28). Client secrets and authorization codes are stored in plaintext (R35).

2026-07-28

  • A route-inventory gate, because the conformance sweep could not see a route upstream ships but never tests (97d1506, 568dbfa). Its first run found live false claims, including R14 above and the phone-number gap now recorded in 03-ROADMAP.md.
  • A discovery-liveness gate that drives every endpoint the discovery documents advertise (476f060) — and immediately found signetauth.com publishing a document naming a different issuer (R20, open, a human decision).
  • cargo fmt is now clean repo-wide and gated (01b20cb). No customer-visible behaviour change.

2026-07-27

  • Every enterprise binary is individually watermarked and attributable (D48). Three layers — overt signed record, covert encrypted copy, structural block ordering. build-enterprise.sh refuses to emit an artifact whose watermark it cannot read back out of the stripped bytes. Proven on real ELF and Mach-O artifacts. Runbook: docs/watermarking.md.
  • Licensing driven end-to-end (D49). First signet licence issued from Warrant org kapable, activated, and verified in a real watermarked binary: valid + enforce = true boots; a tampered signature refuses to boot (exit 1); without enforcement it warns and serves.
  • A user can be deleted — migration 0020 + POST /api/auth/admin/remove-user, closing a data-erasure obligation.
  • Seven better-auth admin-plugin routes added to the certified wire surface (D47). ⚠ Implemented ≠ certified — they are not in compatibility profile v1 until the sweep covers them (D46).
  • The certification scope is published and gated. "Gap 0" now always states what it covers; site/tests/site.test.ts fails the build if it is ever stated unscoped.
  • A mute instance now says so — at boot and per skipped send. Previously a provisioned instance with no [delivery] section returned success for password resets, verification, OTP, magic links and invitations while sending nothing.
  • Signet Cloud runs on Signet — the control plane authenticates its own customers against a Signet instance, clearing the operator's "consume our own services before taking payment" gate.

2026-07-26

  • Offline Warrant licence verification in the engine — Ed25519 signature against a build-baked issuer key plus expiry. No phone-home, so it survives the air-gapped deployment we sell.
  • Signet registered as a Warrant product with free / pro / on-prem-enterprise plans.
  • Two unset secrets that were destroying accounts on Signet Cloud, fixed.
  • Licence lifecycle documented end to end (docs/licensing.md).
  • Repo relicensed to proprietary. It had been MIT OR Apache-2.0 with no LICENSE file — a single publish or clone in that window would have granted irrevocable rights to the engine. Clean only because nothing had ever been distributed.

Enter to open · Esc to close