@ultimat3/auth
Advanced tools
+18
-0
@@ -101,2 +101,9 @@ # @ultimat3/auth — agent notes | ||
| `createAuthLimiter` can keep bounding itself; a limiter with no table declares nothing. | ||
| **What the seam RETAINS is one limiter per window, `As of 2026-08-23`** — a `Map` keyed by | ||
| `windowMs`, because the widest window is the only thing a purge reads. It was a list appended to | ||
| on every `installedAuthLimiter` call, two per `defineAuth`, trimmed by nothing: a process that | ||
| redefines auth (`x dev`'s reload, a test file, a host building one `Auth` per app) held every | ||
| limiter it ever built and the store behind each. `installedLimiterCount()` is the only | ||
| observation of that growth — a purge sweeps exactly one limiter however many are held — and it | ||
| is not exported from `src/index.ts`. | ||
@@ -272,2 +279,13 @@ - **`normaliseEmail` is the ONE normalisation, it lives ABOVE the `AuthAdapter` seam, and no | ||
| rows, and the second was unreachable forever. `adapter-parity.test.ts` pins both halves. | ||
| **A NULL is not a value to either index, `As of 2026-08-23`**: a Postgres unique index is NULLS | ||
| DISTINCT, so `external_id` constrains only the rows that carry one — the check was | ||
| `!== undefined`, and `oauth-login.ts` binds `grants.externalId ?? null` for every first-time | ||
| OAuth user, so the SECOND such signup on a `MemoryAdapter` app failed with `X_AUTH_WRITE_FAILED` | ||
| against a constraint production does not have. | ||
| - **`MemoryAdapter` takes a `Clock`, and every instant it stamps comes from it** — | ||
| `new MemoryAdapter(clock)`, defaulting to `systemClock`, so the no-argument construction every | ||
| test already writes is unchanged. `takeVerification` stamped `consumedAt` with the record's | ||
| own `createdAt` — the moment the link was ISSUED — where `BuiltinAdapter` writes | ||
| `consumed_at = now()`, the moment it was REDEEMED: a redemption an hour later and one a second | ||
| later recorded the identical instant, and a frozen test clock could not move either. | ||
| - The new `AuthAdapter` members are OPTIONAL (`findUserByExternalId`, `listUsersByOrg`, | ||
@@ -274,0 +292,0 @@ `deleteSessionsForUser`, `deleteSessionsForOrg`, `deleteSessionsCreatedBefore`). A required |
+4
-4
| { | ||
| "name": "@ultimat3/auth", | ||
| "version": "10.0.0", | ||
| "version": "11.0.0", | ||
| "description": "Sessions, passwords, OAuth, MFA and api keys — resolved to one Actor", | ||
@@ -34,6 +34,6 @@ "license": "MIT", | ||
| "dependencies": { | ||
| "@ultimat3/core": "10.0.0", | ||
| "@ultimat3/db": "10.0.0", | ||
| "@ultimat3/schema": "10.0.0" | ||
| "@ultimat3/core": "11.0.0", | ||
| "@ultimat3/db": "11.0.0", | ||
| "@ultimat3/schema": "11.0.0" | ||
| } | ||
| } |
+1
-1
@@ -448,3 +448,3 @@ # @ultimat3/auth 🔐 | ||
| | `BuiltinAdapter` | Postgres via `@ultimat3/db`; takes an injected `DbClient` | | ||
| | `MemoryAdapter` | `x new` before a database exists, and every test in this package | | ||
| | `MemoryAdapter` | `x new` before a database exists, and every test in this package. `new MemoryAdapter(clock)` — `systemClock` by default — stamps every instant it writes | | ||
| | your own | implement `AuthAdapter`; DDL in `tables.ts` shows what the columns mean | | ||
@@ -451,0 +451,0 @@ |
@@ -27,4 +27,16 @@ // Single responsibility: the ONE ambient install point for where failed credential attempts are | ||
| let factory: AuthLimiterFactory | undefined; | ||
| /** Every limiter this process built through the factory above, so a purge can reach them. */ | ||
| let built: AuthLimiter[] = []; | ||
| /** | ||
| * The limiters a purge can still reach, ONE per distinct window. | ||
| * | ||
| * A list appended to per call is a leak with no ceiling: `installedAuthLimiter` runs twice per | ||
| * `defineAuth` — the account/IP bucket and the tenant bucket — and nothing trimmed it, so a | ||
| * process that redefines auth (`x dev`'s reload, a test file, a host building one `Auth` per app) | ||
| * held every limiter it ever built, and the store behind each, for its whole life. | ||
| * | ||
| * Keyed by `windowMs` because that is the only thing `purgeAuthLimits` reads: every limiter here | ||
| * writes the same two tables, so one per window is all a sweep can distinguish. The FIRST of a | ||
| * window is kept, which is the limiter the old list already swept — a second install clears the | ||
| * map, so nothing here outlives the pool it was built on either way. | ||
| */ | ||
| let built = new Map<number, AuthLimiter>(); | ||
@@ -41,3 +53,3 @@ /** | ||
| factory = next; | ||
| built = []; | ||
| built = new Map(); | ||
| } | ||
@@ -48,3 +60,3 @@ | ||
| factory = undefined; | ||
| built = []; | ||
| built = new Map(); | ||
| } | ||
@@ -61,6 +73,16 @@ | ||
| const limiter = factory(policy); | ||
| built.push(limiter); | ||
| if (!built.has(policy.windowMs)) built.set(policy.windowMs, limiter); | ||
| return limiter; | ||
| } | ||
| /** | ||
| * How many limiters a purge can still reach. Deliberately NOT exported from `src/index.ts`: it | ||
| * exists because unbounded retention has no other observation — `purgeAuthLimits` sweeps exactly | ||
| * one limiter however many were held, so no assertion about behaviour could see the growth. The | ||
| * same shape as `@ultimat3/realtime`'s `droppedChannelFrames`. | ||
| */ | ||
| export function installedLimiterCount(): number { | ||
| return built.size; | ||
| } | ||
| /** A limiter that keeps rows somebody else has to delete. The memory limiter sweeps itself. */ | ||
@@ -89,3 +111,3 @@ type PurgingAuthLimiter = AuthLimiter & { purgeExpired(): Promise<number> }; | ||
| let widest: PurgingAuthLimiter | undefined; | ||
| for (const limiter of built) { | ||
| for (const limiter of built.values()) { | ||
| if (!canPurge(limiter)) continue; | ||
@@ -92,0 +114,0 @@ if (widest === undefined || limiter.policy.windowMs > widest.policy.windowMs) widest = limiter; |
@@ -5,2 +5,3 @@ // Single responsibility: an in-memory `AuthAdapter`. It is the driver `x new` uses before a | ||
| import { type Clock, systemClock } from '@ultimat3/core'; | ||
| import type { | ||
@@ -25,2 +26,3 @@ AuthAccount, | ||
| readonly name = 'memory'; | ||
| readonly #clock: Clock; | ||
| readonly #users = new Map<string, AuthUser>(); | ||
@@ -33,2 +35,11 @@ readonly #sessions = new Map<string, AuthSession>(); | ||
| /** | ||
| * The clock every instant this adapter stamps comes from — one argument, because a stamp is a | ||
| * fact about WHEN a call happened and a test that cannot move it can only assert a range. | ||
| * Defaults to `systemClock`, so `new MemoryAdapter()` is what it always was. | ||
| */ | ||
| constructor(clock: Clock = systemClock) { | ||
| this.#clock = clock; | ||
| } | ||
| /** | ||
| * Exact match, because `BuiltinAdapter` issues `where email = $1` against a plain `text ... | ||
@@ -65,3 +76,13 @@ * unique` column and nothing folds case there. Normalising here instead made this the ONE | ||
| } | ||
| if (input.externalId !== undefined && existing.externalId === input.externalId) { | ||
| // `!= null` in one predicate, spelled out: a Postgres unique index is NULLS DISTINCT, so | ||
| // `external_id text unique` constrains only the rows that CARRY a value and admits | ||
| // unlimited NULLs. `!== undefined` alone made a second account with no external id collide | ||
| // with the first — and `oauth-login.ts` hands over `grants.externalId ?? null` for every | ||
| // first-time OAuth user, so the second such signup failed against a constraint production | ||
| // does not have. | ||
| if ( | ||
| input.externalId !== undefined && | ||
| input.externalId !== null && | ||
| existing.externalId === input.externalId | ||
| ) { | ||
| throw authUniqueViolation('createUser', 'x_users', 'external_id'); | ||
@@ -223,3 +244,6 @@ } | ||
| if (!timingSafeEqual(tokenHash, record.tokenHash)) return null; | ||
| const consumed: AuthVerification = { ...record, consumedAt: new Date(record.createdAt) }; | ||
| // The moment it was REDEEMED, which is what `consumed_at = now()` writes on the Postgres | ||
| // side. This was `new Date(record.createdAt)` — the moment it was ISSUED — so every window | ||
| // measured from the stamp read a redemption as having happened at issue time. | ||
| const consumed: AuthVerification = { ...record, consumedAt: this.#clock.now() }; | ||
| this.#verifications.set(key, consumed); | ||
@@ -226,0 +250,0 @@ return consumed; |
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
383012
1.11%6464
0.69%+ Added
+ Added
+ Added
- Removed
- Removed
- Removed
Updated
Updated
Updated