@ultimat3/auth 23.0.0 → 25.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +95 -46
- package/README.md +245 -93
- package/package.json +5 -5
- package/src/adapter.ts +49 -18
- package/src/api-key-resolver.ts +14 -9
- package/src/api-keys.ts +61 -14
- package/src/auth.ts +56 -53
- package/src/builtin-adapter-violation.ts +43 -0
- package/src/builtin-adapter.ts +77 -9
- package/src/client-scope.ts +8 -2
- package/src/directory.ts +2 -13
- package/src/email.ts +1 -1
- package/src/errors.ts +80 -48
- package/src/guards.ts +2 -2
- package/src/id-token.ts +2 -3
- package/src/index.ts +57 -30
- package/src/jwks.ts +18 -7
- package/src/kdf-gate.ts +6 -6
- package/src/limiter-install.ts +1 -1
- package/src/login-attempt.ts +70 -0
- package/src/memory-adapter.ts +73 -7
- package/src/mfa-challenge.ts +151 -0
- package/src/mfa-secret.ts +91 -0
- package/src/mfa.ts +14 -28
- package/src/oauth-builtins.ts +6 -0
- package/src/oauth-cookie.ts +17 -7
- package/src/oauth-discovery.ts +24 -5
- package/src/oauth-errors.ts +4 -84
- package/src/oauth-exchange.ts +13 -7
- package/src/oauth-grant-context.ts +106 -0
- package/src/oauth-login.ts +78 -23
- package/src/oauth-profile.ts +48 -11
- package/src/oauth-registry.ts +16 -1
- package/src/oauth-route.ts +75 -22
- package/src/oauth.ts +4 -3
- package/src/policy-bridge.ts +51 -8
- package/src/privileges.ts +46 -18
- package/src/rate-limit-postgres.ts +113 -101
- package/src/rate-limit.ts +50 -16
- package/src/revocation.ts +25 -25
- package/src/session.ts +69 -37
- package/src/tables.ts +8 -10
- package/src/tokens.ts +2 -5
- package/src/verify.ts +4 -3
package/CLAUDE.md
CHANGED
|
@@ -5,14 +5,14 @@ Tier 2. Produces the `Actor`; produces nothing else. Authorization is `@ultimat3
|
|
|
5
5
|
| Rule | |
|
|
6
6
|
|---|---|
|
|
7
7
|
| Deps | `@ultimat3/core`, `@ultimat3/schema`, `@ultimat3/db`. No external deps. |
|
|
8
|
-
| Never import | `@ultimat3/policy`, `@ultimat3/http` (tier 2 consumers), `@ultimat3/mail` (
|
|
8
|
+
| Never import | `@ultimat3/policy`, `@ultimat3/http` (tier 2 consumers), `@ultimat3/mail` (tier 4, upward) |
|
|
9
9
|
| Policy seam | `PolicyActorFields` in `policy-bridge.ts` mirrors policy's shape structurally |
|
|
10
10
|
| Http seam | `RequestLike` / `CookieJar` in `session.ts` and `AuthRouteDescriptor` in `oauth-route.ts`; http binds to them, not the reverse |
|
|
11
11
|
| Mail seam | injected `MailSender` port in `verify.ts`; the app wires `@ultimat3/mail`'s `send` |
|
|
12
12
|
| Better Auth | binds through `AuthAdapter`. It is an adapter, never a dependency. |
|
|
13
13
|
| Errors | `AuthError` from `errors.ts`; never `throw new Error` |
|
|
14
14
|
| Time | take a `Clock`. No `Date.now()` anywhere in this package. |
|
|
15
|
-
| Secrets | compare with `timingSafeEqual` (from `@ultimat3/core
|
|
15
|
+
| Secrets | compare with `timingSafeEqual` (imported from `@ultimat3/core` — never re-exported here, same implementation `@ultimat3/storage` uses); store `sha256Hex`. Never `===` on a secret. |
|
|
16
16
|
|
|
17
17
|
## Non-negotiables
|
|
18
18
|
|
|
@@ -41,19 +41,29 @@ Tier 2. Produces the `Actor`; produces nothing else. Authorization is `@ultimat3
|
|
|
41
41
|
- **The limiter's table is bounded, and the eviction order is the guarantee.** Every bucket carries
|
|
42
42
|
`forgetAtMs`; `policy.maxKeys` (`DEFAULT_MAX_AUTH_LIMIT_KEYS`) is the backstop, and a **live
|
|
43
43
|
lockout outranks its own deadline** in the comparator. Never reduce that sort to recency.
|
|
44
|
-
- **`AuthLimiter` is async on every member
|
|
45
|
-
`
|
|
46
|
-
|
|
47
|
-
`
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
`
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
44
|
+
- **`AuthLimiter` is async on every member, declares the policy it enforces, and is a
|
|
45
|
+
RESERVATION.** `reserve(key)` counts the attempt in the step that admits it (refused, counting
|
|
46
|
+
nothing, while locked); `refund(reservation)` gives ONE back and lifts a lockout the window no
|
|
47
|
+
longer justifies; `recordSuccess(key)` clears the key. There is no check-then-record pair: N
|
|
48
|
+
concurrent guesses all passed the check. `assertAuthLimiterPolicy` (once, in `defineAuth`)
|
|
49
|
+
refuses a per-process limiter under `scope: 'shared'` (`X_AUTH_LIMITER_NOT_SHARED`) and different
|
|
50
|
+
`maxAttempts`/`windowMs`/`lockoutMs` (`X_AUTH_LIMITER_POLICY_MISMATCH`). `maxKeys` is not
|
|
51
|
+
compared. Nothing here reads the environment to guess a replica count.
|
|
52
|
+
- **Every credential check runs through `openLoginAttempt`** (`login-attempt.ts`): account → ip →
|
|
53
|
+
org reserved in that fixed order BEFORE the KDF or the TOTP compare. A failure calls nothing (the
|
|
54
|
+
reservations are the record); `release()` for no verdict (a shed KDF, a store that is down, a
|
|
55
|
+
bucket that refused) or a factor proven with another still owed; `succeed()` clears the account
|
|
56
|
+
and refunds the shared two.
|
|
57
|
+
- **`postgresAuthLimiter` is the shared limiter: ONE row per key in `x_auth_lockouts`, holding the
|
|
58
|
+
instants still inside the window** (`attempts_ms bigint[]` — a sliding window). The take is one
|
|
59
|
+
`insert … on conflict do update` reading the window off `x_auth_lockouts.<column>`, never a CTE
|
|
60
|
+
and never a second table: only the row the statement locks is re-read after a wait. `admitted`
|
|
61
|
+
is a column because `returning` cannot see the old row; no row back is never an admission. A
|
|
62
|
+
live lockout is never rewritten. `PgExecutor` (`@ultimat3/core`'s, re-exported) is structural:
|
|
63
|
+
the pool is the host's.
|
|
54
64
|
- **`configureAuthLimiters` is the HOST's install point and takes a FACTORY**, called with the
|
|
55
65
|
RESOLVED policy once per bucket; the comparison still runs on what comes back. Precedence:
|
|
56
|
-
`config.limiter` → the installed factory → `
|
|
66
|
+
`config.limiter` → the installed factory → `memoryAuthLimiter`. `installedAuthLimiter` and
|
|
57
67
|
`installedLimiterCount()` are NOT in `src/index.ts`. `purgeAuthLimits()` sweeps only the
|
|
58
68
|
**widest** window among the built limiters (retained one per `windowMs` in a `Map`), on the
|
|
59
69
|
limiter's own clock. `AuthLimiter.purgeExpired` is optional.
|
|
@@ -76,6 +86,9 @@ Tier 2. Produces the `Actor`; produces nothing else. Authorization is `@ultimat3
|
|
|
76
86
|
`alg: none` are refused in `decodeJwtHeader`. It checks `nbf` and `azp` too.
|
|
77
87
|
- **`resolveGrants` is a seam, never a group-to-role table**, called on EVERY login. Absent leaves
|
|
78
88
|
the row alone. A user created with no roles and no org logs a warning.
|
|
89
|
+
Its second argument (`OAuthGrantContext`, `oauth-grant-context.ts`, #670) is the token set, the
|
|
90
|
+
whole verified id-token payload and a LAZY, once-per-login, `sub`-checked `userinfo()` — read
|
|
91
|
+
through `oauth-profile.ts`'s `userinfoBody`, never a second userinfo reader.
|
|
79
92
|
- **`verifySession` writes at most once per `idleSlideMs`** (default `idleTtlMs / 20`). Never cache
|
|
80
93
|
the USER row — `authenticate` re-reads it every request so a revoked role bites on the next one.
|
|
81
94
|
In `observed`, `null` is an ANSWER (clears the stored value) and `undefined` is silence.
|
|
@@ -90,22 +103,29 @@ Tier 2. Produces the `Actor`; produces nothing else. Authorization is `@ultimat3
|
|
|
90
103
|
- `takeVerification(purpose, identifier, tokenHash)` consumes **only on a hash match**, in one
|
|
91
104
|
conditional statement (`consumed_at is null` on the UPDATE and its subselect,
|
|
92
105
|
`order by created_at desc limit 1`); `consumeVerification` still compares in constant time.
|
|
106
|
+
`consumed_at` is the adapter's `Clock`, bound, never `now()`.
|
|
93
107
|
- **Foreign text in a `cause:` goes through `renderCauseValue`, in a `fix:` through
|
|
94
108
|
`renderFixLiteral`, and in a shell `fix:` through `renderFixShellArg`.** Foreign here:
|
|
95
|
-
`providerDetail()`'s return, `claims.iss`, `accountLocked`'s `key
|
|
109
|
+
`providerDetail()`'s return, `claims.iss`, `accountLocked`'s `key` (an `org:` key is never
|
|
110
|
+
rendered at all — it was looked up from the caller's address), and every OAuth endpoint URL
|
|
96
111
|
in a command position (`jwks.ts`, `oauth-profile.ts`, `oauth-exchange.ts`,
|
|
97
112
|
`oauth-discovery.ts`) — the registry is filled by an issuer's own discovery document. A LINE
|
|
98
113
|
that would not run degrades to prose: `jwks.ts`'s `readTheKeySet(tail)` decides with core's
|
|
99
114
|
`isFixShellSafe`. `oauth-discovery.ts` still owes that repair (it keeps `auth` on
|
|
100
115
|
`FIX_SHELL_ARG_PINS`). `${provider}` is registry-validated boot config, not foreign.
|
|
101
|
-
-
|
|
116
|
+
- Cookies are read through `@ultimat3/core`'s `readCookie` (`readSessionCookie`, the oauth
|
|
117
|
+
handshake), which never throws on a malformed value (`decodeURIComponent('%')`).
|
|
118
|
+
They are WRITTEN through its `serializeSetCookie`, never a literal (`bun run set-cookie-literals`,
|
|
119
|
+
`X_SET_COOKIE_HAND_BUILT`); `cookie-golden.test.ts` pins the bytes.
|
|
102
120
|
- A token endpoint's HTTP 200 is not success — read `error`.
|
|
103
121
|
- Link by address only when the provider **and** the local account both verified it
|
|
104
122
|
(`link: 'verified-email'`, default; `'never'` is the only other value).
|
|
105
123
|
- **The OAuth route paths are not configurable.** `oauth-paths.ts` imports nothing and is read by
|
|
106
124
|
both `oauth-errors.ts` and `oauth-route.ts`; every "start over" fix is `restartAt(provider)`.
|
|
107
125
|
- The routes are **descriptors** (`AuthRouteDescriptor`), never mounted handlers.
|
|
108
|
-
- The callback answers failure as **coded JSON
|
|
126
|
+
- The callback answers failure as **coded JSON** — a fixed body per code (`code`, `title`, `docs`,
|
|
127
|
+
one `cause`, `fix: x errors explain <CODE> --json`); the authored cause and fix are the
|
|
128
|
+
`auth.oauth.refused` log line — and success redirects to a fixed `successPath` —
|
|
109
129
|
never `?next=` (`nextAfterSignIn` in `@ultimat3/http` is the one open-redirect check).
|
|
110
130
|
- The handshake cookie is cleared on **every** callback outcome.
|
|
111
131
|
- Refresh is **not implemented**, so the framework **stores no provider token**: `accountFor`
|
|
@@ -118,47 +138,70 @@ Tier 2. Produces the `Actor`; produces nothing else. Authorization is `@ultimat3
|
|
|
118
138
|
an uncoded adapter throw logs `auth.oauth.uncoded_failure`, a rejecting `OAuthFetch` logs
|
|
119
139
|
`auth.oauth.token_fetch_failed`, and the `POST /token` body is read as
|
|
120
140
|
`providerDetail(response, 'coded-only')` (that request carries `client_secret`).
|
|
121
|
-
- **`x_users.mfa_secret` is
|
|
122
|
-
|
|
141
|
+
- **`x_users.mfa_secret` is SEALED** (`mfa-secret.ts`, purpose `auth:x_users.mfa_secret`):
|
|
142
|
+
`saveTotpSecret` writes, `openTotpSecret` reads and REFUSES a value core's `isSealed` does not
|
|
143
|
+
recognise (`X_MFA_SECRET_UNSEALED`, fix `x auth seal-mfa --json`). No plaintext fallback, ever —
|
|
144
|
+
`sealMfaSecrets` is the one-shot — each write a compare-and-set (`replaceMfaSecret`), a row that
|
|
145
|
+
changed underneath is `skipped` — and `x doctor` counts what is left. Whether a value is sealed is
|
|
146
|
+
`isSealed`'s to say, never a `like` in an adapter (`listUsersWithMfaSecret` returns them all).
|
|
123
147
|
- **`providerJwks` memoises only the DEFAULT client**; a caller supplying options gets its own.
|
|
124
|
-
- **A JWKS refresh is single-flighted through core's `
|
|
125
|
-
`deadlineMs = timeoutMs * 2`**; eviction frees the key, never the work, and a `
|
|
148
|
+
- **A JWKS refresh is single-flighted through core's `singleFlight` with
|
|
149
|
+
`deadlineMs = timeoutMs * 2`**; eviction frees the key, never the work, and a `generationFence`
|
|
126
150
|
generation check (read, never `guard`) stops a superseded refresh overwriting the cache.
|
|
127
151
|
`schedule` is injectable.
|
|
128
152
|
- **A success clears the ACCOUNT bucket and nothing else** — clearing the address bucket made the
|
|
129
|
-
limiter inert against stuffing.
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
153
|
+
limiter inert against stuffing. A password proven with a second factor still owed clears
|
|
154
|
+
NOTHING: `completeMfa` is what clears, or a repeat of the first leg wipes the wrong codes.
|
|
155
|
+
- **`MemoryAuthAdapter.createUser` and `updateUser` enforce `x_users.id`, `email` and `external_id`
|
|
156
|
+
uniqueness** (`authUniqueViolation`, `X_AUTH_WRITE_FAILED`), NULLS DISTINCT like Postgres;
|
|
157
|
+
`PostgresAuthAdapter` answers the same code through `builtin-adapter-violation.ts`.
|
|
158
|
+
**`MemoryAuthAdapter` takes a `Clock`** (default `systemClock`) and stamps every instant from it.
|
|
133
159
|
`adapter-parity.test.ts` pins both.
|
|
134
|
-
-
|
|
135
|
-
`
|
|
136
|
-
|
|
137
|
-
|
|
160
|
+
- **`listUsersByOrg` orders by byte**: `collate "C"` in Postgres, a code-point compare in memory —
|
|
161
|
+
never `localeCompare`, never UTF-16 `<` (`list-users-order.live.test.ts`, on an `en_US` db).
|
|
162
|
+
- Every `AuthAdapter` member is REQUIRED — a missing one is a compile error at `defineAuth`, and
|
|
163
|
+
no caller in this package refuses at runtime with `X_NOT_IMPLEMENTED`.
|
|
164
|
+
- **An api key is its owner's credential.** `verifyApiKey` re-reads the owner (after the secret
|
|
165
|
+
matched, never before) and refuses a missing or disabled one as the same `X_API_KEY_INVALID`; a
|
|
166
|
+
key with `userId === null` has no owner and is unchanged. `disableUser` revokes the user's live
|
|
167
|
+
keys beside their sessions, and logs before its first write.
|
|
168
|
+
- **A key's scopes only ever shrink** (`apiKeyScopes`, `policy-bridge.ts`): `*` and `<res>:*` are
|
|
169
|
+
refused at `issueApiKey` (`X_CONFIG_INVALID`) and dropped from a stored row; an OWNED key keeps
|
|
170
|
+
only what its owner's grants cover. Roles are policy's to expand, so the default grants are the
|
|
171
|
+
row's `permissions` + `scopes` (`directGrants`) and a role-based app passes
|
|
172
|
+
`apiKeyResolver(store, { grantsOf })`. Never union a key with the owner's roles.
|
|
138
173
|
- **`apiKeyResolver(() => store)` is the ONE token → caller mapping** (`api-key-resolver.ts`):
|
|
139
174
|
`verifyApiKey` → `apiKeyActor`, `X_API_KEY_INVALID` → `null`, everything else rethrown. Its
|
|
140
175
|
`ApiKeyCaller` is structurally http's `BearerCaller` and mcp's `ResolvedToken` — declared here
|
|
141
176
|
because tier 2 imports neither. The store is a thunk (a mount is declared before boot installs
|
|
142
|
-
the database client). No second in-memory key store: `
|
|
177
|
+
the database client). No second in-memory key store: `MemoryAuthAdapter` IS an `ApiKeyStore`.
|
|
143
178
|
- Rotate the session id on any privilege change (`rotateSession`, called by `updatePrivileges` in
|
|
144
|
-
`privileges.ts`), never patch the row.
|
|
179
|
+
`privileges.ts`), never patch the row. A `passwordHash` change also ends every OTHER session of
|
|
180
|
+
that user (`deleteOtherSessions`), or all of them when the caller passed no session of theirs.
|
|
181
|
+
**A new id, never a new lifetime**: rotation carries `createdAt` and `absoluteExpiresAt` over
|
|
182
|
+
(`createSession` clamps both, so they only shorten), and the rotated cookie's `Max-Age` is
|
|
183
|
+
`remainingMaxAgeSeconds` — `session-rotation.test.ts` pins both adapters.
|
|
145
184
|
- **Every argon2 call goes through `kdfGate()`** — width 8, queue 64, `X_OVERLOADED` past it
|
|
146
|
-
(borrowed from http, in `AUTH_BORROWED_ERROR_CODES`). The pool is core's `
|
|
185
|
+
(borrowed from http, in `AUTH_BORROWED_ERROR_CODES`). The pool is core's `flightGate`, the
|
|
147
186
|
refusal auth's own through core's `overflow:` seam. `configureKdfGate()` is the ONE install point
|
|
148
187
|
and deliberately not a `defineAuth` key.
|
|
149
|
-
- **MFA
|
|
150
|
-
|
|
151
|
-
`
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
`auth.
|
|
188
|
+
- **MFA's second leg is `completeMfa(auth, challenge, code)`** (`mfa-challenge.ts`). `login()`
|
|
189
|
+
ends in `X_MFA_REQUIRED` whose `meta.challenge` is a `seal()` value (purpose
|
|
190
|
+
`auth:mfa-challenge`: user id, nonce, expiry on the server's clock, `MFA_CHALLENGE_TTL_MS`) —
|
|
191
|
+
`meta`, never `cause`, and never a bare user id: a second leg keyed by one is unauthenticated by
|
|
192
|
+
construction. The code is reserved against the same three buckets before it is compared, and a
|
|
193
|
+
spent step is refused through `auth.totpReplay` (checked and remembered with no `await`
|
|
194
|
+
between). It needs the app's master key (`X_SEAL_KEY_MISSING`). No route ships: the handler
|
|
195
|
+
decides how the challenge travels. Anything that is not six digits is a RECOVERY CODE: hashed
|
|
196
|
+
(`recoveryCodeHash`) and consumed by `adapter.consumeRecoveryCode` — one conditional UPDATE, so
|
|
197
|
+
single-use is storage's guarantee. There is no read-then-write redemption helper.
|
|
155
198
|
- **`mfa.required` is the literal `false`, and `defineAuth` refuses a `true`** (`X_CONFIG_INVALID`):
|
|
156
199
|
enforcing it would lock out every un-enrolled user. `mfa.issuer` is read by
|
|
157
200
|
`enrolTotp(auth, { account })`.
|
|
158
201
|
- **A TOTP secret that decodes to zero bytes is NO key**: `verifyTotp` returns the generic failure
|
|
159
202
|
(`{ ok: false, step: null }`), `totpCode` and `enrolTotp` throw `X_MFA_SECRET_INVALID`. The secret
|
|
160
203
|
never reaches `cause:`/`fix:`. No length floor beyond one byte.
|
|
161
|
-
- **`
|
|
204
|
+
- **`memoryTotpReplayGuard`'s table is bounded and the eviction ORDER is the guarantee**: subjects
|
|
162
205
|
whose steps are all below the drift floor are forgotten first; past `DEFAULT_MAX_TOTP_SUBJECTS`,
|
|
163
206
|
evict by newest spent step ascending, least-recently-seen tie-break. `maxSubjects` is normalised
|
|
164
207
|
first (`boundedSubjects`): anything not a positive finite integer takes the default.
|
|
@@ -173,8 +216,10 @@ Tier 2. Produces the `Actor`; produces nothing else. Authorization is `@ultimat3
|
|
|
173
216
|
| `policy-bridge.ts` | the one funnel: identity → `Actor`, all four `ActorKind`s |
|
|
174
217
|
| `session.ts` | two expiries, rotation, revocation, device list, the cookie |
|
|
175
218
|
| `adapter.ts` | the seam; `builtin-adapter.ts` (Postgres) + `memory-adapter.ts` |
|
|
176
|
-
| `
|
|
177
|
-
| `rate-limit
|
|
219
|
+
| `builtin-adapter-violation.ts` | a Postgres unique violation on an auth table → `authUniqueViolation` |
|
|
220
|
+
| `rate-limit.ts` | per-ip, per-account and per-org buckets as reservations, lockout, scope check, `loginFailed()` |
|
|
221
|
+
| `login-attempt.ts` | one attempt's reservations across the three buckets, taken before the check and settled after it |
|
|
222
|
+
| `rate-limit-postgres.ts` | the SHARED limiter: one table, one row per key, a one-statement take, over a structural `PgExecutor` |
|
|
178
223
|
| `limiter-install.ts` | the host's one install point for that limiter — the factory, what it built, and the purge over it |
|
|
179
224
|
| `oauth.ts` | `OAuthProvider`, PKCE, `beginOAuth`, the callback gate. No I/O, no env |
|
|
180
225
|
| `oauth-builtins.ts` | the three shipped IdPs, as data. Imports only the type, so no cycle |
|
|
@@ -182,22 +227,25 @@ Tier 2. Produces the `Actor`; produces nothing else. Authorization is `@ultimat3
|
|
|
182
227
|
| `oauth-discovery.ts` | `/.well-known/openid-configuration` → an `OAuthProvider`. One `fetch` |
|
|
183
228
|
| `jwks.ts` | `crypto.subtle` signature verification, cached by `kid`, one shared in-flight refresh. No dependency |
|
|
184
229
|
| `workload.ts` | a workload JWT (K8s SA / SPIFFE / IMDS / RFC 8693) → a `ServiceIdentity` |
|
|
185
|
-
| `revocation.ts` | per-user, per-org and before-an-instant sweeps; `disableUser` |
|
|
230
|
+
| `revocation.ts` | per-user, per-org and before-an-instant sweeps; `disableUser` (sessions and api keys) |
|
|
231
|
+
| `mfa-challenge.ts` | the sealed challenge `X_MFA_REQUIRED` carries, and `completeMfa` |
|
|
232
|
+
| `mfa-secret.ts` | the TOTP secret at rest: seal on write, open on read, the one-shot that seals older rows |
|
|
186
233
|
| `directory.ts` | `describeUser` (allow-list projection), `listOrgUsers`, external-id lookup |
|
|
187
|
-
| `privileges.ts` | `updatePrivileges` — the grant,
|
|
234
|
+
| `privileges.ts` | `updatePrivileges` — the grant, the rotation it requires, and the sessions a changed password ends |
|
|
188
235
|
| `oauth-cookie.ts` | the handshake's home between the two legs: seal, open, the cookie |
|
|
189
236
|
| `oauth-exchange.ts` | `oauthCredentials` + the one POST to the token endpoint |
|
|
190
237
|
| `id-token.ts` | id token → claims this handshake may believe |
|
|
191
238
|
| `id-token-fixture.ts` | the one string-input JWT builder the OAuth tests share. Off `index.ts` |
|
|
192
239
|
| `auth-fixture.ts` | the KDF parameters, the password and the `AuthError` catcher the three `auth*.test.ts` suites share. Off `index.ts` |
|
|
193
|
-
| `oauth-profile.ts` | claims or userinfo → one `OAuthProfile` |
|
|
240
|
+
| `oauth-profile.ts` | claims or userinfo → one `OAuthProfile`; `userinfoBody`, the one userinfo reader |
|
|
241
|
+
| `oauth-grant-context.ts` | `resolveGrants`' second argument: tokens, every id-token claim, a lazy subject-checked `userinfo()` |
|
|
194
242
|
| `oauth-login.ts` | profile → account link → session. `completeOAuthLogin` is the entry point |
|
|
195
243
|
| `oauth-login-fixture.ts` | the adapter, clock and profile the three `oauth-login*` suites share. Off `index.ts` |
|
|
196
244
|
| `oauth-paths.ts` | the one declaration of where the two routes live. Imports nothing |
|
|
197
245
|
| `errors.ts` | the codes this package owns and borrows, their titles, the one `registerErrorCodes()` call, `AuthError`, and every non-OAuth factory |
|
|
198
|
-
| `oauth-errors.ts` | the OAuth
|
|
246
|
+
| `oauth-errors.ts` | the OAuth refusals more than one OAuth module raises, and `restartAt`. A refusal with one thrower lives beside it; declares no code and registers nothing |
|
|
199
247
|
| `oauth-route.ts` | `oauthLogin(auth)` — the redirect out and the callback back |
|
|
200
|
-
| `kdf-gate.ts` | the one bound on concurrent argon2 work, and the `X_OVERLOADED` past it — core's `
|
|
248
|
+
| `kdf-gate.ts` | the one bound on concurrent argon2 work, and the `X_OVERLOADED` past it — core's `flightGate` with auth's own refusal injected |
|
|
201
249
|
| `email.ts` | `normaliseEmail` — the one normalisation an address gets before it is an identity key |
|
|
202
250
|
| `json.ts` | reading untrusted JSON: `isRecord`, and a base64url JWT segment as an object or `null` |
|
|
203
251
|
| `sign-out.ts` | `signOutHeaders({ session? })` — the expired session cookie plus `Clear-Site-Data: "cache", "storage"` (`SIGN_OUT_CLEAR_SITE_DATA`), never `"cookies"`. A PWA's offline cache does not survive a sign-out, by decision (a cached private page IS the previous principal's data); the page boot's rescope wipe is the second line. Secure contexts only |
|
|
@@ -215,7 +263,8 @@ Gotchas:
|
|
|
215
263
|
this package **owns**, unconditionally, and lists the borrowed two in `AUTH_BORROWED_ERROR_CODES`
|
|
216
264
|
without a title. A `hasErrorCode()` guard would suppress the `X_ERROR_CODE_DUPLICATE` that is
|
|
217
265
|
supposed to fire when two packages claim one code.
|
|
218
|
-
- Tests run against `
|
|
266
|
+
- Tests run against `MemoryAuthAdapter`; the one ADAPTER test that boots a database is
|
|
267
|
+
`adapter-parity-identity.test.ts` (PGlite). The two other exceptions are
|
|
219
268
|
`postgresAuthLimiter`'s, and they are exceptions in the shape the repo already has: the
|
|
220
269
|
scripted-executor twin (`rate-limit-postgres.test.ts`) proves the protocol with no server,
|
|
221
270
|
and `rate-limit-postgres.live.test.ts` is `describe.skip` without `TEST_DATABASE_URL` — the
|