Repository navigation
v0.19.0
Minor Changes
-
14ebadf: Account lockout no longer takes away a user's passkeys, and
lockoutSecondsis enforced (GHSA-66gq-4pwg-33jq).- Passkey sign-in is not subject to lockout, and failed passkey assertions no longer count towards it. An assertion cannot be guessed, and because
/loginissues an ephemeral token from an email address alone, anyone could previously submit wrong OTP codes until an account locked and so shut its owner out of every method, passkeys included. POST /loginfor a locked account now continues withloginMethods: ["passkey"]when the account has a passkey, and answers423 account_lockedonly when it has none.- The lock starts at the failure that brings
maxFailuresinside onewindowSecondswindow and lastslockoutSecondsfrom there. Previously the lock lasted only as long as the failures stayed inside the trailing window, whateverlockoutSecondssaid, andretryAfterSecondsreportedlockoutSecondsrather than the time actually left. It is now the time left. - The lockout counter only counts the failure types that lock, so passkey failures recorded before this release stop counting.
An account with no passkey can still be locked by repeated wrong codes, which is what lockout is for. Enrolling a passkey keeps a user able to sign in through a lock someone else triggered.
- Passkey sign-in is not subject to lockout, and failed passkey assertions no longer count towards it. An assertion cannot be guessed, and because
-
a894fa1: Magic link sign-in issues one session per confirmed link, to the attempt that requested it, and a link opened on another device needs the person who opened it to confirm (GHSA-9jg2-jjqq-3346).
- Each link records the
jtiof the ephemeral token that requested it, andGET /magic-link/checkonly looks up links for the polling attempt. Another ephemeral token for the same address cannot collect it. - The first check after the link is confirmed issues the session and marks the link redeemed. Later checks for the same attempt answer
200 { "message": "Success" }without tokens, so a second tab in the same browser still sees the sign-in complete and reads the session it already has. GET /magic-link/verify/{token}consumes the link only when it is opened on the requesting device: proven by the requesting attempt's ephemeral bearer token when the adapter forwards it, otherwise by the IP address and user agent a trusted adapter forwards with its service token. A fingerprint no adapter vouched for does not count, since behind an adapter that does not forward client context every request carries the adapter's own address. Opened anywhere else it answers409 confirmation_requiredwith the requesting platform and time, and leaves the link unused, so a mail scanner or a click on a link someone else requested no longer signs that someone in.- New
POST /magic-link/verify/{token}/confirmconsumes the link after the person who opened it approves. - New auth event
magic_link_confirmation_required. - A request or poll with an ephemeral token that has no
jtiis refused with400. - Migration:
magic_link_tokensgainsattempt_id,requested_device_classandredeemed_at. Links requested before the upgrade can no longer be used; they expire within fifteen minutes.
Breaking for clients: a link opened on a different device now needs a confirmation screen. SDK versions without one show the 409 as an error.
Upgrade the server adapter together with the API. Current adapters (
@seamless-auth/express,@seamless-auth/fastify,@seamless-auth/nextjs, and the Go, Python and Rust adapters) forward the browser's pre-auth token on verify, which proves a link opened in the requesting browser. On an older adapter that proof is missing: Express still forwards the browser's address, so same-browser links keep working there, but@seamless-auth/nextjsforwards no address unlessresolveClientIpis configured, so a link opened in the requesting browser is answered with the confirmation step. - Each link records the
-
266658c: The server refuses to start in development mode on a deployment reachable from outside.
Anything other than
NODE_ENV=productionturns on per-instance signing keys, the/docsexplorer, fallback development secrets and the rate limit and conformance escape hatches. WhenISSUERor anyAPP_ORIGINSentry is not local (localhost,*.localhost,*.test, a single-label name, or a loopback or private address), boot now stops with an error naming each one. The published image setsNODE_ENV=productionand is unaffected; the bundled compose file and the CLI's local stacks are local and keep working.Set
SEAMLESS_ALLOW_INSECURE_DEV=trueto run development mode on such a host on purpose. It logs a warning, and/health/statusreportsdegraded.insecureDevelopmentMode: true.degraded.auditis now optional in that response, since an instance can be degraded for either reason. -
28ce894: The API refuses to leave a deployment with nobody who can administer it.
DELETE /admin/users, a role change throughPATCH /admin/users/{userId}, andDELETE /users/deletenow answer409 { "error": "last_admin" }when they would remove the last active user holdingadmin:write(granted byadmin,admin:writeoradmin:*). The refusal is audited asadmin_last_admin_change_refused. Previously the only way back was a direct database edit, sinceOWNER_EMAILgrants admin only when the account is created. -
e279e81: A response that does not match its declared schema is no longer sent. The API answers
500 { "error": "Internal server error" }instead, the same body as its error handler, and logs the route and the failing paths. Response schemas are what keep responses minimal, so a payload that breaks one is exactly the one that might carry more than it should.A route that declares a single response schema now holds only its
200to it, as the OpenAPI document already said. Error bodies on those routes, such as the404fromGET /users/me, were being checked against the success schema. -
22adadc: Read the service token from
SEAMLESS_SERVICE_TOKEN, the name used across the Seamless ecosystem.SEAMLESS_SERVICE_TOKENis now the preferred env var for the trusted server-adapter and internal bearer secret, and for the secrets that fall back to it (REFRESH_TOKEN_LOOKUP_SECRET,TOTP_SECRET_ENCRYPTION_KEY,OAUTH_STATE_SECRET,DECOY_SUBJECT_SECRET).API_SERVICE_TOKENkeeps working as a fallback, so existing deployments need no change. When only the old name is set, the server logs one info notice at boot naming the preferred variable.- Setting both names to different values stops the server at boot with an error naming both. Setting both to the same value is fine.
validateEnvs.shaccepts either name, and the bundled compose files now passSEAMLESS_SERVICE_TOKEN(falling back toAPI_SERVICE_TOKENfrom the host environment).
-
7c674ef: Tokens are checked more strictly.
- Access and ephemeral tokens must carry
audequal to the issuer, as every token this server signs already does, and atypofaccessorephemeral. Previously the audience was not checked, so the posture doc's claim that it was held only for the adapters. - Internal service tokens must carry an
expno more than an hour (plus a minute of clock skew) away. A service token minted withoutexpused to be accepted forever. The Seamless adapters mint them for 60 seconds and are unaffected; a caller that mints its own must setexp.
- Access and ephemeral tokens must carry
Patch Changes
-
b6e8d7c:
openapi.jsonandadapter-manifest.jsonnow carry the package version. The release version step regenerates both, and the contract test fails when either file drifts from the route definitions or frompackage.json. Both said 0.17.0 through the 0.18.0 release. -
6344f51: The server logs a warning at boot naming each of
REFRESH_TOKEN_LOOKUP_SECRET,TOTP_SECRET_ENCRYPTION_KEY,OAUTH_STATE_SECRETandDECOY_SUBJECT_SECRETthat is unset and so falls back to the service token, with what rotating the service token would then break.docs/production-operations.mddescribes how to separate them safely: set each to the current service token value first, then rotate the service token.The fallback to the service token is deprecated and is removed in v0.20, which requires each secret whose feature is in use. Set them now, to the current service token value, so the upgrade is a no-op.