@rexezuge/identity 1.0.1 → 1.0.3
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/README.md +61 -0
- package/dist/ResolvedAccount.js.map +1 -1
- package/dist/UserDAO.js.map +1 -1
- package/dist/UserEmailDAO.js.map +1 -1
- package/dist/UserIdentityService.js.map +1 -1
- package/dist/accountLookup.js.map +1 -1
- package/dist/applyMigrations.js.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/migrations.js.map +1 -1
- package/package.json +5 -4
package/README.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# @rexezuge/identity
|
|
2
|
+
|
|
3
|
+
Account layer with address resolution, user DAOs, and canonical identity migrations.
|
|
4
|
+
|
|
5
|
+
Depends on `@rexezuge/d1` (persistence + statement splitter), `@rexezuge/errors`, and `@rexezuge/shared` (email normalization).
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @rexezuge/identity
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## 1. The invariant: the address is not the account
|
|
14
|
+
|
|
15
|
+
A login address can move between accounts, so resolution goes through the verified `user_emails` registry — never the mutable address string — and a revoked row resolves to nothing. The only safe fall-through is a database whose identity migration has not run yet (`isMissingSchemaError`); every other D1 failure rethrows, because a transient error answered with the wrong account is a full cross-account read.
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { resolveAccount, type AccountLookupDeps } from '@rexezuge/identity';
|
|
19
|
+
|
|
20
|
+
const account = await resolveAccount(deps, 'user@example.com'); // ResolvedAccount | null
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
`UserIdentityService` is the stateful service over the same rules (register, verify, resolve); the free functions exist so account bootstrap and the service share one implementation.
|
|
24
|
+
|
|
25
|
+
## 2. DAOs
|
|
26
|
+
|
|
27
|
+
| Export | Role |
|
|
28
|
+
| --- | --- |
|
|
29
|
+
| `UserDAO` | `users` table: frozen anchor id, lookup by id/anchor/email |
|
|
30
|
+
| `UserEmailDAO` | `user_emails` registry: claims, verification, revocation |
|
|
31
|
+
| `newAccountId` / `newAnchor` | ID generation for new accounts |
|
|
32
|
+
| `summarize` / `AccountSummary` | safe-to-log account projection |
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
import { UserDAO, UserEmailDAO } from '@rexezuge/identity';
|
|
36
|
+
|
|
37
|
+
const users = new UserDAO(env.DB);
|
|
38
|
+
const emails = new UserEmailDAO(env.DB);
|
|
39
|
+
await users.getById(account.id);
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## 3. Migrations
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
import { applyIdentityMigrations } from '@rexezuge/identity';
|
|
46
|
+
|
|
47
|
+
await applyIdentityMigrations(env.DB); // fresh install: users.sql
|
|
48
|
+
await applyIdentityMigrations(env.DB, { upgrade: true }); // legacy DB: + upgrade-user-identity.sql (rerunnable)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Executed through `@rexezuge/d1`'s splitter — one `batch()` per file, matching D1's own per-file apply, so a failure names its file. `IDENTITY_MIGRATIONS` / `IDENTITY_MIGRATION_SQL` carry the same pair as data for repos that apply migrations through their own runner. The upgrade backfills a verified registry row for every anchor, which is what lets pre-identity databases resolve addresses the old way until they migrate.
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Verifying this package
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
pnpm --filter @rexezuge/identity exec tsc -p tsconfig.json
|
|
59
|
+
pnpm --filter @rexezuge/identity typecheck
|
|
60
|
+
pnpm --filter @rexezuge/identity test
|
|
61
|
+
```
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ResolvedAccount.js","sourceRoot":"","sources":["../src/ResolvedAccount.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAuDzC,sDAAsD;AACtD,MAAM,UAAU,SAAS,CAAC,GAAgB;IACxC,OAAO,GAAG,EAAE,EAAE;QACZ,CAAC,CAAC,EAAE,EAAE,EAAE,GAAG,CAAC,EAAE,EAAE,KAAK,EAAE,CAAC,GAAG,CAAC,aAAa,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC,WAAW,EAAE,EAAE,WAAW,EAAE,GAAG,CAAC,KAAK,EAAE,QAAQ,EAAE,GAAG,CAAC,QAAQ,IAAI,IAAI,EAAE;QAC/H,CAAC,CAAC,IAAI,CAAC;AACX,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,YAAY;IAC1B,OAAO,OAAO,UAAU,EAAE,CAAC,UAAU,CAAC,GAAG,EAAE,EAAE,CAAC,EAAE,CAAC;AACnD,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,SAAS;IACvB,OAAO,UAAU,UAAU,EAAE,CAAC,UAAU,CAAC,GAAG,EAAE,EAAE,CAAC,gBAAgB,CAAC;AACpE,CAAC"}
|
|
1
|
+
{"version":3,"file":"ResolvedAccount.js","sourceRoot":"","sources":["../src/ResolvedAccount.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAuDzC,sDAAsD;AACtD,MAAM,UAAU,SAAS,CAAC,GAAgB;IACxC,OAAO,GAAG,EAAE,EAAE;QACZ,CAAC,CAAC,EAAE,EAAE,EAAE,GAAG,CAAC,EAAE,EAAE,KAAK,EAAE,CAAC,GAAG,CAAC,aAAa,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC,WAAW,EAAE,EAAE,WAAW,EAAE,GAAG,CAAC,KAAK,EAAE,QAAQ,EAAE,GAAG,CAAC,QAAQ,IAAI,IAAI,EAAE;QAC/H,CAAC,CAAC,IAAI,CAAC;AACX,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,YAAY;IAC1B,OAAO,OAAO,UAAU,EAAE,CAAC,UAAU,CAAC,GAAG,EAAE,EAAE,CAAC,EAAE,CAAC;AACnD,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,SAAS;IACvB,OAAO,UAAU,UAAU,EAAE,CAAC,UAAU,CAAC,GAAG,EAAE,EAAE,CAAC,gBAAgB,CAAC;AACpE,CAAC","sourcesContent":["import { randomUUID } from 'node:crypto';\n\n/**\n * `users` rows — the stored form of an account.\n *\n * Converged from `backend-data/src/dao/UserDAO.ts` + its `UserRow` (identical in\n * six of the seven identity-carrying repos; AWS and ChordDHT keep only the\n * id/email core).\n *\n * `id` is the stable account key. `email` is the *anchor*: an immutable value\n * that legacy `*_email` foreign keys resolve against, so history stays\n * resolvable across an address change. `current_email` is the address the\n * account signs in with and the one APIs report.\n */\nexport interface UserRow {\n id?: string | null;\n /**\n * The frozen anchor address. Never updated — it is the foreign-key target\n * every legacy `*_email` value resolves against, and rewriting it would\n * cascade the rows that reference it. Read `current_email` for sign-in.\n */\n email: string;\n /**\n * Mutable sign-in address. Absent on a pre-identity database, where `email`\n * *is* the sign-in address; read it as `current_email ?? email`.\n */\n current_email?: string | null;\n created_at: number;\n username: string | null;\n updated_at: number | null;\n}\n\n/**\n * A resolved account.\n *\n * `id` is the only value that should be used as an identity. `email` is the\n * address the account currently signs in with; `anchorEmail` is the immutable\n * value legacy columns store, and is what a pre-identity row can still be\n * matched on.\n */\nexport interface ResolvedAccount {\n id: string;\n email: string;\n anchorEmail: string;\n username: string | null;\n}\n\n/** The denormalized subset routes and the permission path use. */\nexport type AccountSummary = Pick<ResolvedAccount, 'id' | 'email' | 'username'>;\n\n/** Shape every `users` read has, whether or not the identity migration has run. */\nexport type UserRowLike =\n | Pick<UserRow, 'id' | 'email' | 'current_email' | 'username'>\n | undefined;\n\n/** Project a `users` row onto a `ResolvedAccount`. */\nexport function summarize(row: UserRowLike): ResolvedAccount | null {\n return row?.id\n ? { id: row.id, email: (row.current_email ?? row.email).toLowerCase(), anchorEmail: row.email, username: row.username ?? null }\n : null;\n}\n\n/**\n * The opaque account id.\n *\n * Prefixed so an id is never mistaken for an address in a log line or a URL,\n * and random at 128 bits so accounts are not enumerable.\n */\nexport function newAccountId(): string {\n return `usr_${randomUUID().replaceAll('-', '')}`;\n}\n\n/**\n * Opaque anchor for an account whose login address is already held as another\n * account's anchor.\n *\n * It must be globally unique and must never be a real address: `email` is the\n * anchor every legacy `*_email` foreign key resolves against, so an address\n * parked here could never be re-registered by a different person after its\n * original account moved off it. `.invalid` is reserved by RFC 2606 and can\n * never be delivered to, so it can never authenticate either.\n */\nexport function newAnchor(): string {\n return `anchor-${randomUUID().replaceAll('-', '')}@users.invalid`;\n}\n"]}
|
package/dist/UserDAO.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"UserDAO.js","sourceRoot":"","sources":["../src/UserDAO.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAEvC,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAGjD;;;;;;;;;;GAUG;AACH,SAAS,iBAAiB,CAAC,QAAgB;IACzC,OAAO,QAAQ,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;AACvC,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,OAAO,OAAQ,SAAQ,OAAO;IAClC,YAAmB,QAAqB;QACtC,KAAK,CAAC,QAAQ,CAAC,CAAC;IAClB,CAAC;IAED;;;;;;;;;;;;;;;;;;;OAmBG;IACI,KAAK,CAAC,UAAU,CAAC,KAA0D;QAChF,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC;QAC1C,MAAM,UAAU,GAAG,KAAK,CAAC,UAAU,CAAC,WAAW,EAAE,CAAC;QAClD,MAAM,EAAE,GAAG,YAAY,EAAE,CAAC;QAC1B,MAAM,IAAI,CAAC,GAAG,CAAC,qGAAqG,EAAE;YACpH,MAAM;YACN,KAAK,CAAC,GAAG;YACT,EAAE;YACF,UAAU;SACX,CAAC,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;QAC1C,OAAO,GAAG,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;IACpC,CAAC;IAED;;;;;OAKG;IACI,KAAK,CAAC,cAAc,CAAC,SAAiB,EAAE,QAAgB,EAAE,GAAW;QAC1E,MAAM,IAAI,CAAC,GAAG,CAAC,mHAAmH,EAAE;YAClI,iBAAiB,CAAC,QAAQ,CAAC;YAC3B,GAAG;YACH,SAAS;YACT,SAAS;SACV,CAAC,CAAC;IACL,CAAC;IAED,qEAAqE;IAC9D,KAAK,CAAC,WAAW,CAAC,SAAiB,EAAE,QAAgB,EAAE,GAAW;QACvE,MAAM,IAAI,CAAC,GAAG,CAAC,yEAAyE,EAAE;YACxF,iBAAiB,CAAC,QAAQ,CAAC;YAC3B,GAAG;YACH,SAAS;YACT,SAAS;SACV,CAAC,CAAC;IACL,CAAC;IAED;;;;;;OAMG;IACI,KAAK,CAAC,UAAU,CAAC,KAAa;QACnC,OAAO,IAAI,CAAC,KAAK,CAAU,6CAA6C,EAAE,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC;IACnG,CAAC;IAED,wEAAwE;IACjE,KAAK,CAAC,OAAO,CAAC,EAAU;QAC7B,OAAO,IAAI,CAAC,KAAK,CAAU,0CAA0C,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;IAC/E,CAAC;IAED,6EAA6E;IACtE,KAAK,CAAC,iBAAiB,CAAC,KAAa;QAC1C,OAAO,IAAI,CAAC,KAAK,CAAU,qDAAqD,EAAE,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC;IAC3G,CAAC;IAED;;;;;;;OAOG;IACI,KAAK,CAAC,eAAe,CAAC,EAAU,EAAE,KAAa,EAAE,GAAW;QACjE,MAAM,IAAI,CAAC,GAAG,CAAC,iEAAiE,EAAE,CAAC,KAAK,CAAC,WAAW,EAAE,EAAE,GAAG,EAAE,EAAE,CAAC,CAAC,CAAC;IACpH,CAAC;IAED,wBAAwB;IACjB,KAAK,CAAC,eAAe,CAAC,UAAkB;QAC7C,OAAO,IAAI,CAAC,KAAK,CAAU,gDAAgD,EAAE,CAAC,UAAU,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC;IAC3G,CAAC;CACF;AAED,OAAO,EAAE,iBAAiB,EAAE,CAAC"}
|
|
1
|
+
{"version":3,"file":"UserDAO.js","sourceRoot":"","sources":["../src/UserDAO.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAEvC,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAGjD;;;;;;;;;;GAUG;AACH,SAAS,iBAAiB,CAAC,QAAgB;IACzC,OAAO,QAAQ,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;AACvC,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,OAAO,OAAQ,SAAQ,OAAO;IAClC,YAAmB,QAAqB;QACtC,KAAK,CAAC,QAAQ,CAAC,CAAC;IAClB,CAAC;IAED;;;;;;;;;;;;;;;;;;;OAmBG;IACI,KAAK,CAAC,UAAU,CAAC,KAA0D;QAChF,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC;QAC1C,MAAM,UAAU,GAAG,KAAK,CAAC,UAAU,CAAC,WAAW,EAAE,CAAC;QAClD,MAAM,EAAE,GAAG,YAAY,EAAE,CAAC;QAC1B,MAAM,IAAI,CAAC,GAAG,CAAC,qGAAqG,EAAE;YACpH,MAAM;YACN,KAAK,CAAC,GAAG;YACT,EAAE;YACF,UAAU;SACX,CAAC,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;QAC1C,OAAO,GAAG,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;IACpC,CAAC;IAED;;;;;OAKG;IACI,KAAK,CAAC,cAAc,CAAC,SAAiB,EAAE,QAAgB,EAAE,GAAW;QAC1E,MAAM,IAAI,CAAC,GAAG,CAAC,mHAAmH,EAAE;YAClI,iBAAiB,CAAC,QAAQ,CAAC;YAC3B,GAAG;YACH,SAAS;YACT,SAAS;SACV,CAAC,CAAC;IACL,CAAC;IAED,qEAAqE;IAC9D,KAAK,CAAC,WAAW,CAAC,SAAiB,EAAE,QAAgB,EAAE,GAAW;QACvE,MAAM,IAAI,CAAC,GAAG,CAAC,yEAAyE,EAAE;YACxF,iBAAiB,CAAC,QAAQ,CAAC;YAC3B,GAAG;YACH,SAAS;YACT,SAAS;SACV,CAAC,CAAC;IACL,CAAC;IAED;;;;;;OAMG;IACI,KAAK,CAAC,UAAU,CAAC,KAAa;QACnC,OAAO,IAAI,CAAC,KAAK,CAAU,6CAA6C,EAAE,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC;IACnG,CAAC;IAED,wEAAwE;IACjE,KAAK,CAAC,OAAO,CAAC,EAAU;QAC7B,OAAO,IAAI,CAAC,KAAK,CAAU,0CAA0C,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;IAC/E,CAAC;IAED,6EAA6E;IACtE,KAAK,CAAC,iBAAiB,CAAC,KAAa;QAC1C,OAAO,IAAI,CAAC,KAAK,CAAU,qDAAqD,EAAE,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC;IAC3G,CAAC;IAED;;;;;;;OAOG;IACI,KAAK,CAAC,eAAe,CAAC,EAAU,EAAE,KAAa,EAAE,GAAW;QACjE,MAAM,IAAI,CAAC,GAAG,CAAC,iEAAiE,EAAE,CAAC,KAAK,CAAC,WAAW,EAAE,EAAE,GAAG,EAAE,EAAE,CAAC,CAAC,CAAC;IACpH,CAAC;IAED,wBAAwB;IACjB,KAAK,CAAC,eAAe,CAAC,UAAkB;QAC7C,OAAO,IAAI,CAAC,KAAK,CAAU,gDAAgD,EAAE,CAAC,UAAU,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC;IAC3G,CAAC;CACF;AAED,OAAO,EAAE,iBAAiB,EAAE,CAAC","sourcesContent":["import { BaseDAO } from '@rexezuge/d1';\nimport type { D1Queryable } from '@rexezuge/d1';\nimport { newAccountId } from './ResolvedAccount';\nimport type { UserRow } from './ResolvedAccount';\n\n/**\n * The stored form of a handle: lowercase.\n *\n * Every username read and every username write goes through this, so the two\n * can never disagree about case. That agreement is not cosmetic: `username` has\n * no `COLLATE NOCASE` (SQLite defaults to BINARY), so a stored `Alice2` is a\n * *different value* from `alice2` and a lookup that lowercased its parameter\n * would never find it. Normalising at the write points rather than relying on\n * each caller means a future caller cannot reintroduce an unfindable handle by\n * forgetting.\n */\nfunction normalizeUsername(username: string): string {\n return username.trim().toLowerCase();\n}\n\n/**\n * `users` rows. Converged from `backend-data/src/dao/UserDAO.ts` (identical in\n * Durable-DAV, Durable-DAV-Router, Edge-Git, Mail-Otter, Mail-Meow; AWS keeps\n * the id/email core without the username methods).\n *\n * Case rule everywhere in this DAO: lowercase the *parameter*, never the\n * column — `lower(col)` makes the index or primary key unusable. It is only\n * sound because every writer stores lowercase through this DAO's own\n * normalisers.\n */\nexport class UserDAO extends BaseDAO {\n public constructor(database: D1Queryable) {\n super(database);\n }\n\n /**\n * Create an account, stamped with the identity columns.\n *\n * `INSERT ... ON CONFLICT DO NOTHING` with **no conflict target**, deliberately.\n * `users` carries two unique constraints — the anchor primary key and the\n * unique index on `current_email` — and SQLite applies `DO NOTHING` only to\n * the constraints a named target covers. Naming the anchor therefore left a\n * `current_email` collision as a raised error, which callers cannot tell from\n * a transient D1 failure: it propagated instead of returning `null`, so the\n * opaque-anchor retry never ran and every login request answered 500.\n *\n * Returns the new account's id, or `null` when the insert was a silent no-op —\n * which this DAO establishes by reading the anchor row back and requiring its\n * own generated id to be the one there. The read-back lives here rather than\n * in the caller because a caller that trusted the generated id on a taken\n * anchor would point the address registry at a *phantom* account: the address\n * then resolves to nothing, and the opaque-anchor retry fails the same way.\n * Making the no-op detectable here is what keeps registration from stranding\n * an address forever.\n */\n public async createUser(input: { anchor: string; loginEmail: string; now: number }): Promise<string | null> {\n const anchor = input.anchor.toLowerCase();\n const loginEmail = input.loginEmail.toLowerCase();\n const id = newAccountId();\n await this.run('INSERT INTO users (email, created_at, id, current_email) VALUES (?, ?, ?, ?) ON CONFLICT DO NOTHING', [\n anchor,\n input.now,\n id,\n loginEmail,\n ]);\n const row = await this.getByEmail(anchor);\n return row?.id === id ? id : null;\n }\n\n /**\n * @param idOrEmail The stable account key, or an anchor address as the\n * pre-identity fallback. `WHERE id = ? OR email = ?` cannot use either index\n * as a seek, so this stays off the hot path; login resolves through\n * `UserIdentityService` instead.\n */\n public async ensureUsername(idOrEmail: string, username: string, now: number): Promise<void> {\n await this.run('UPDATE users SET username = COALESCE(username, ?), updated_at = COALESCE(updated_at, ?) WHERE id = ? OR email = ?', [\n normalizeUsername(username),\n now,\n idOrEmail,\n idOrEmail,\n ]);\n }\n\n /** @param idOrEmail The stable account key, or an anchor address. */\n public async setUsername(idOrEmail: string, username: string, now: number): Promise<void> {\n await this.run('UPDATE users SET username = ?, updated_at = ? WHERE id = ? OR email = ?', [\n normalizeUsername(username),\n now,\n idOrEmail,\n idOrEmail,\n ]);\n }\n\n /**\n * Anchor lookup — used for legacy `*_email` values and cascade resolution.\n *\n * This is *not* a login lookup: an account that has changed its address is not\n * findable here by its new one. It is the pre-identity floor and the\n * attribution read.\n */\n public async getByEmail(email: string): Promise<UserRow | undefined> {\n return this.first<UserRow>('SELECT * FROM users WHERE email = ? LIMIT 1', [email.toLowerCase()]);\n }\n\n /** The account behind a stable key. Null on a pre-identity database. */\n public async getById(id: string): Promise<UserRow | undefined> {\n return this.first<UserRow>('SELECT * FROM users WHERE id = ? LIMIT 1', [id]);\n }\n\n /** The account currently signing in with this address. Identity-era only. */\n public async getByCurrentEmail(email: string): Promise<UserRow | undefined> {\n return this.first<UserRow>('SELECT * FROM users WHERE current_email = ? LIMIT 1', [email.toLowerCase()]);\n }\n\n /**\n * Move an account's sign-in address.\n *\n * The anchor `email` is deliberately untouched: it is what every legacy\n * `*_email` column and foreign key resolves against. Ordering matters — the\n * caller claims the new address first and revokes the old one third, so a\n * failure between steps never leaves the account without a way to sign in.\n */\n public async setCurrentEmail(id: string, email: string, now: number): Promise<void> {\n await this.run('UPDATE users SET current_email = ?, updated_at = ? WHERE id = ?', [email.toLowerCase(), now, id]);\n }\n\n /** Lookup by handle. */\n public async getByUsernameCi(usernameCi: string): Promise<UserRow | undefined> {\n return this.first<UserRow>('SELECT * FROM users WHERE username = ? LIMIT 1', [usernameCi.toLowerCase()]);\n }\n}\n\nexport { normalizeUsername };\nexport type { UserRow };\n"]}
|
package/dist/UserEmailDAO.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"UserEmailDAO.js","sourceRoot":"","sources":["../src/UserEmailDAO.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAwBvC,MAAM,OAAO,YAAa,SAAQ,OAAO;IACvC,YAAmB,QAAqB;QACtC,KAAK,CAAC,QAAQ,CAAC,CAAC;IAClB,CAAC;IAED;;;;;;;OAOG;IACI,KAAK,CAAC,QAAQ,CAAC,KAA0E;QAC9F,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC;QACxC,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QACvC,IAAI,QAAQ,IAAI,QAAQ,CAAC,WAAW,KAAK,CAAC;YAAE,OAAO,iBAAiB,CAAC;QACrE,MAAM,IAAI,CAAC,GAAG,CACZ;;uGAEiG,EACjG,CAAC,KAAK,EAAE,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,CAC3D,CAAC;QACF,OAAO,SAAS,CAAC;IACnB,CAAC;IAEM,KAAK,CAAC,GAAG,CAAC,KAAa;QAC5B,sEAAsE;QACtE,qEAAqE;QACrE,wBAAwB;QACxB,OAAO,IAAI,CAAC,KAAK,CAAe,mDAAmD,EAAE,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC;IAC9G,CAAC;IAED,uEAAuE;IAChE,KAAK,CAAC,eAAe,CAAC,KAAa;QACxC,OAAO,IAAI,CAAC,KAAK,CAAe,uEAAuE,EAAE,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC;IAClI,CAAC;IAEM,KAAK,CAAC,YAAY,CAAC,MAAc;QACtC,OAAO,IAAI,CAAC,GAAG,CAAe,uFAAuF,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC;IACnI,CAAC;IAED,+EAA+E;IACxE,KAAK,CAAC,MAAM,CAAC,KAAa;QAC/B,MAAM,IAAI,CAAC,GAAG,CAAC,wDAAwD,EAAE,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC;IAClG,CAAC;IAED;;;OAGG;IACI,KAAK,CAAC,iBAAiB,CAAC,MAAc,EAAE,WAAmB;QAChE,MAAM,IAAI,CAAC,GAAG,CAAC,yEAAyE,EAAE,CAAC,MAAM,EAAE,WAAW,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC;IACjI,CAAC;CACF"}
|
|
1
|
+
{"version":3,"file":"UserEmailDAO.js","sourceRoot":"","sources":["../src/UserEmailDAO.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAwBvC,MAAM,OAAO,YAAa,SAAQ,OAAO;IACvC,YAAmB,QAAqB;QACtC,KAAK,CAAC,QAAQ,CAAC,CAAC;IAClB,CAAC;IAED;;;;;;;OAOG;IACI,KAAK,CAAC,QAAQ,CAAC,KAA0E;QAC9F,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC;QACxC,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QACvC,IAAI,QAAQ,IAAI,QAAQ,CAAC,WAAW,KAAK,CAAC;YAAE,OAAO,iBAAiB,CAAC;QACrE,MAAM,IAAI,CAAC,GAAG,CACZ;;uGAEiG,EACjG,CAAC,KAAK,EAAE,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,CAC3D,CAAC;QACF,OAAO,SAAS,CAAC;IACnB,CAAC;IAEM,KAAK,CAAC,GAAG,CAAC,KAAa;QAC5B,sEAAsE;QACtE,qEAAqE;QACrE,wBAAwB;QACxB,OAAO,IAAI,CAAC,KAAK,CAAe,mDAAmD,EAAE,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC;IAC9G,CAAC;IAED,uEAAuE;IAChE,KAAK,CAAC,eAAe,CAAC,KAAa;QACxC,OAAO,IAAI,CAAC,KAAK,CAAe,uEAAuE,EAAE,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC;IAClI,CAAC;IAEM,KAAK,CAAC,YAAY,CAAC,MAAc;QACtC,OAAO,IAAI,CAAC,GAAG,CAAe,uFAAuF,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC;IACnI,CAAC;IAED,+EAA+E;IACxE,KAAK,CAAC,MAAM,CAAC,KAAa;QAC/B,MAAM,IAAI,CAAC,GAAG,CAAC,wDAAwD,EAAE,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC;IAClG,CAAC;IAED;;;OAGG;IACI,KAAK,CAAC,iBAAiB,CAAC,MAAc,EAAE,WAAmB;QAChE,MAAM,IAAI,CAAC,GAAG,CAAC,yEAAyE,EAAE,CAAC,MAAM,EAAE,WAAW,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC;IACjI,CAAC;CACF","sourcesContent":["import { BaseDAO } from '@rexezuge/d1';\nimport type { D1Queryable } from '@rexezuge/d1';\n\n/**\n * One known address for an account.\n *\n * `is_verified` gates login: `1` means the address may authenticate the\n * account, `0` means it was changed away from and is retained only so rows\n * written before the change still resolve. A revoked row is re-pointed (not\n * deleted) when a later account legitimately claims the address, so an address\n * is never permanently reserved.\n *\n * Converged from `backend-data/src/dao/UserEmailDAO.ts` (identical in Durable-DAV,\n * Durable-DAV-Router, Edge-Git, Mail-Otter).\n */\nexport interface UserEmailRow {\n email: string;\n user_id: string;\n is_verified: number;\n created_at: number;\n}\n\nexport type EmailClaim = 'claimed' | 'already-claimed';\n\nexport class UserEmailDAO extends BaseDAO {\n public constructor(database: D1Queryable) {\n super(database);\n }\n\n /**\n * Claim an address for an account.\n *\n * An existing verified row is left alone: the address already belongs to\n * someone, and silently re-pointing it would hand one account's identity to\n * another. Callers check `resolveVerified` first and reject on a hit.\n * An unverified (revoked) row is re-pointed, which releases the address.\n */\n public async register(input: { email: string; userId: string; isVerified: boolean; now: number }): Promise<EmailClaim> {\n const email = input.email.toLowerCase();\n const existing = await this.get(email);\n if (existing && existing.is_verified === 1) return 'already-claimed';\n await this.run(\n `INSERT INTO user_emails (email, user_id, is_verified, created_at)\n VALUES (?, ?, ?, ?)\n ON CONFLICT(email) DO UPDATE SET user_id = excluded.user_id, is_verified = excluded.is_verified`,\n [email, input.userId, input.isVerified ? 1 : 0, input.now],\n );\n return 'claimed';\n }\n\n public async get(email: string): Promise<UserEmailRow | undefined> {\n // Lowercase the *parameter*: the column is stored lowercased by every\n // writer, so `lower(email) = ?` matched identically while making the\n // primary key unusable.\n return this.first<UserEmailRow>('SELECT * FROM user_emails WHERE email = ? LIMIT 1', [email.toLowerCase()]);\n }\n\n /** Login resolution: only a verified address identifies an account. */\n public async resolveVerified(email: string): Promise<UserEmailRow | undefined> {\n return this.first<UserEmailRow>('SELECT * FROM user_emails WHERE email = ? AND is_verified = 1 LIMIT 1', [email.toLowerCase()]);\n }\n\n public async listByUserId(userId: string): Promise<UserEmailRow[]> {\n return this.all<UserEmailRow>('SELECT * FROM user_emails WHERE user_id = ? ORDER BY is_verified DESC, created_at ASC', [userId]);\n }\n\n /** Revoke an address for login while keeping it resolvable for attribution. */\n public async revoke(email: string): Promise<void> {\n await this.run('UPDATE user_emails SET is_verified = 0 WHERE email = ?', [email.toLowerCase()]);\n }\n\n /**\n * Revoke every verified address for an account. Used when an account's login\n * address changes, so only the new address can authenticate it.\n */\n public async revokeAllVerified(userId: string, exceptEmail: string): Promise<void> {\n await this.run('UPDATE user_emails SET is_verified = 0 WHERE user_id = ? AND email != ?', [userId, exceptEmail.toLowerCase()]);\n }\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"UserIdentityService.js","sourceRoot":"","sources":["../src/UserIdentityService.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"UserIdentityService.js","sourceRoot":"","sources":["../src/UserIdentityService.ts"],"names":[],"mappings":"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","sourcesContent":["import { BadRequestError } from '@rexezuge/errors';\nimport type { D1Queryable } from '@rexezuge/d1';\nimport { isValidEmailFormat } from '@rexezuge/shared';\nimport { TimestampUtil } from '@rexezuge/shared';\nimport { registerAccount, resolveAccount } from './accountLookup';\nimport type { AccountLookupDeps } from './accountLookup';\nimport { newAnchor, summarize } from './ResolvedAccount';\nimport type { ResolvedAccount } from './ResolvedAccount';\nimport { UserDAO } from './UserDAO';\nimport { UserEmailDAO } from './UserEmailDAO';\n\ninterface UserIdentityEnv {\n DB: D1Queryable;\n}\n\nexport type { UserIdentityEnv };\n\n/**\n * Injected DAO factories. All optional: each defaults to a real DAO over the\n * same D1 binding, so tests override only what they need.\n */\nexport type UserIdentityDeps = Partial<AccountLookupDeps>;\n\n/**\n * The account behind an address, plus the address-change operations.\n *\n * Converged from `backend-services/src/identity/UserIdentityService.ts`\n * (identical in Durable-DAV, Durable-DAV-Router, Edge-Git, Mail-Otter; AWS and\n * Mail-Meow keep the resolve core). Canonical names merge the two spellings the\n * repos used — `resolveUserById` (Durable-DAV family) and\n * `resolveAccountById` (AWS) are one method, `resolveAccountById`.\n *\n * The instance is one per request scope: the ownership and permission paths\n * resolve the same caller several times each, and a per-instance address memo\n * costs one query per request rather than one per comparison. Bind it once in\n * the composition root and share it — constructing per consumer is the bug the\n * memo exists to prevent.\n *\n * Resolution itself lives in `accountLookup` (free functions, shared with any\n * account bootstrap); this class adds memoization and the change operations.\n */\nexport class UserIdentityService {\n private readonly deps: Required<AccountLookupDeps>;\n\n private readonly byEmail = new Map<string, ResolvedAccount | null>();\n\n public constructor(env: UserIdentityEnv, deps: UserIdentityDeps = {}) {\n this.deps = {\n userDAO: () => Promise.resolve(new UserDAO(env.DB)),\n userEmailDAO: () => Promise.resolve(new UserEmailDAO(env.DB)),\n ...deps,\n };\n }\n\n /**\n * Resolve a sign-in address to its account, or null when unknown.\n *\n * Only a verified address resolves. A revoked address (changed away from) is\n * retained so pre-change rows stay attributable, but it must never\n * authenticate, otherwise a reassigned address would inherit the previous\n * holder's account.\n */\n public async resolveAccount(email: string): Promise<ResolvedAccount | null> {\n const key = email.trim().toLowerCase();\n if (!isValidEmailFormat(key)) return null;\n if (this.byEmail.has(key)) return this.byEmail.get(key) ?? null;\n const resolved = await resolveAccount(this.deps, key);\n this.byEmail.set(key, resolved);\n return resolved;\n }\n\n /** Account id for a sign-in address, or null when unknown. */\n public async resolveAccountId(email: string): Promise<string | null> {\n const account = await this.resolveAccount(email);\n return account?.id ?? null;\n }\n\n /**\n * Account behind a stable key. The inverse direction, for a caller that\n * already holds one (a resource owner) and needs the current address.\n */\n public async resolveAccountById(userId: string): Promise<ResolvedAccount | null> {\n const userDAO = await this.deps.userDAO();\n const row = await userDAO.getById(userId);\n return summarize(row);\n }\n\n /**\n * Resolve-or-create, for a login path that may see a first-time user.\n *\n * Returns null when the address exists but cannot be resolved (a revoked\n * address) — a caller that needs \"guaranteed account\" must distinguish that\n * from \"created now\" through its own route, because silently registering on a\n * revoked address would hand the previous holder's history to the new one.\n */\n public async resolveOrRegister(email: string, now = TimestampUtil.getCurrentUnixTimestampInSeconds()): Promise<ResolvedAccount | null> {\n const existing = await this.resolveAccount(email);\n if (existing) return existing;\n const key = email.trim().toLowerCase();\n // The anchor is the address itself whenever it is free. When it is already\n // held as another account's anchor — its previous holder moved off it — the\n // opaque-anchor retry is what lets a released address be used again.\n const account = (await registerAccount(this.deps, key, key, now)) ?? (await registerAccount(this.deps, key, newAnchor(), now));\n this.byEmail.set(key, account);\n return account;\n }\n\n /**\n * Every address known for an account, verified ones first.\n */\n public async listAddresses(userId: string): Promise<Array<{ email: string; isVerified: boolean }>> {\n const emailDAO = await this.deps.userEmailDAO();\n const rows = await emailDAO.listByUserId(userId);\n return rows.map((row) => ({ email: row.email, isVerified: row.is_verified === 1 }));\n }\n\n /**\n * Point an account at a new sign-in address.\n *\n * The account id, the frozen anchor, and every id-keyed row are untouched:\n * only which address authenticates the account moves. The previous address is\n * revoked rather than deleted, so rows written before the change still resolve\n * to this account, and it is released for a later legitimate holder.\n *\n * Rejects an address already verified for another account. That check is the\n * whole reason this is not simply an `UPDATE`: an unverified change would let\n * anyone claim an address and inherit its history.\n *\n * Order is load-bearing, and it is the one the ops CLI (`change-email.ts`)\n * already gets right: claim first, move `current_email` second, revoke third.\n * Inverting it opens a window in which nothing authenticates if a later step\n * fails.\n */\n public async setPrimaryEmail(\n userId: string,\n newEmail: string,\n now = TimestampUtil.getCurrentUnixTimestampInSeconds(),\n ): Promise<ResolvedAccount> {\n const email = newEmail.trim().toLowerCase();\n if (!isValidEmailFormat(email)) throw new BadRequestError('Invalid email address');\n const userDAO = await this.deps.userDAO();\n const emailDAO = await this.deps.userEmailDAO();\n const row = await userDAO.getById(userId);\n if (!row?.id) throw new BadRequestError('User not found');\n const current = (row.current_email ?? row.email).toLowerCase();\n if (current === email) {\n return { id: row.id, email, anchorEmail: row.email, username: row.username ?? null };\n }\n const holder = await emailDAO.resolveVerified(email);\n if (holder && holder.user_id !== row.id) throw new BadRequestError('Email is already in use');\n await emailDAO.register({ email, userId: row.id, isVerified: true, now });\n await userDAO.setCurrentEmail(row.id, email, now);\n // Revoke every other verified address, so only the new one authenticates.\n await emailDAO.revokeAllVerified(row.id, email);\n this.byEmail.delete(current);\n this.byEmail.set(email, { id: row.id, email, anchorEmail: row.email, username: row.username ?? null });\n return { id: row.id, email, anchorEmail: row.email, username: row.username ?? null };\n }\n\n /**\n * Ops/migration path: attach an address that has already been proven, without\n * making it the sign-in address.\n */\n public async linkVerifiedEmail(\n userId: string,\n email: string,\n now = TimestampUtil.getCurrentUnixTimestampInSeconds(),\n ): Promise<void> {\n const address = email.trim().toLowerCase();\n if (!isValidEmailFormat(address)) throw new BadRequestError('Invalid email address');\n const emailDAO = await this.deps.userEmailDAO();\n const outcome = await emailDAO.register({ email: address, userId, isVerified: true, now });\n if (outcome === 'already-claimed') throw new BadRequestError('Email is already in use');\n }\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"accountLookup.js","sourceRoot":"","sources":["../src/accountLookup.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"accountLookup.js","sourceRoot":"","sources":["../src/accountLookup.ts"],"names":[],"mappings":"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","sourcesContent":["import { isMissingSchemaError } from '@rexezuge/d1';\nimport { summarize } from './ResolvedAccount';\nimport type { ResolvedAccount } from './ResolvedAccount';\nimport { UserDAO } from './UserDAO';\nimport { UserEmailDAO } from './UserEmailDAO';\n\n/**\n * Injected DAO factories. All optional: each defaults to a real DAO over the\n * same D1 binding, so tests override only what they need.\n */\nexport interface AccountLookupDeps {\n userDAO: () => Promise<UserDAO>;\n userEmailDAO: () => Promise<UserEmailDAO>;\n}\n\n/**\n * Address -> account resolution, as free functions so the service and any\n * higher-level account bootstrap share one implementation instead of\n * re-deriving the query.\n *\n * Converged from `backend-services/src/user/accountLookup.ts` (identical in\n * Durable-DAV, Durable-DAV-Router, Edge-Git, Mail-Otter).\n */\nexport async function resolveAccount(deps: AccountLookupDeps, email: string): Promise<ResolvedAccount | null> {\n const normalized = email.trim().toLowerCase();\n if (!normalized) return null;\n const userDAO = await deps.userDAO();\n // A known address is authoritative: the registry says which account it belongs\n // to, and a *revoked* row (the account moved off this address) must not\n // resolve at all. Falling through to the legacy lookups in that case would let\n // a reassigned address keep authenticating the previous holder's account.\n //\n // Only a *missing registry* falls through, and \"missing\" means one specific\n // thing: the identity migration has not run. The previous\n // `.catch(() => null)` answered that question for **every** failure, so a\n // transient D1 error on this one statement made the code proceed to the anchor\n // lookup below — `getByEmail`, which is an attribution lookup whose key is a\n // re-registrable string, not an identity. An address whose account had moved\n // off it (so its `user_emails` row is revoked, and correctly refuses to\n // resolve) would then be resolved against the *previous holder's* anchor, and\n // that account's id was installed as the caller's identity: full access to\n // someone else's rows from one failed `SELECT`.\n //\n // So the degradation is narrowed to `isMissingSchemaError`, and everything\n // else rethrows.\n const registered = await deps\n .userEmailDAO()\n .then((dao) => dao.get(normalized))\n .catch((error: unknown) => {\n if (isMissingSchemaError(error)) return null;\n throw error;\n });\n if (registered) {\n return registered.is_verified === 1 ? summarize(await userDAO.getById(registered.user_id)) : null;\n }\n // No registry row at all. Safe to fall through: the identity migration\n // backfilled a verified row for every anchor, so a row-less address is either\n // new or on a pre-identity database, where the address *is* the anchor.\n //\n // No `.catch` on either: a failed `users` read is an outage, and reporting it\n // as \"no such account\" would turn a D1 blip into a 404 and a spurious\n // re-registration attempt.\n const byCurrent = await userDAO.getByCurrentEmail(normalized);\n const legacy = byCurrent ?? (await userDAO.getByEmail(normalized));\n return summarize(legacy);\n}\n\n/** Account id for a sign-in address, or null when unknown. */\nexport async function resolveUserId(deps: AccountLookupDeps, email: string): Promise<string | null> {\n const account = await resolveAccount(deps, email);\n return account?.id ?? null;\n}\n\n/**\n * Insert an account and claim its sign-in address.\n *\n * The anchor is the address itself whenever it is free, which keeps new rows\n * shaped like the pre-identity ones. It falls back to an opaque anchor only when\n * the address is already held as another account's anchor — i.e. its previous\n * holder moved off it — so a released address is never permanently unusable.\n *\n * Returns null when the insert could not produce a *new* account holding this\n * address, which is the caller's signal to retry with a different anchor.\n *\n * The read-back is load-bearing, not defensive. `createUser` is\n * `ON CONFLICT(email) DO NOTHING`, so when the anchor is already taken the\n * insert is silently a no-op and the generated `id` belongs to nobody. Trusting\n * it would (a) re-point the address registry at a phantom account that resolves\n * to nothing, permanently stranding the address, and (b) on the opaque-anchor\n * retry, hand the caller an account whose address the registry refuses to\n * re-point because the phantom row already marked it verified. Re-reading the\n * anchor row and requiring *our* id to be the one there makes the no-op\n * detectable, so the retry gets a genuinely new account.\n */\nexport async function registerAccount(\n deps: AccountLookupDeps,\n loginEmail: string,\n anchor: string,\n now: number,\n): Promise<ResolvedAccount | null> {\n const userDAO = await deps.userDAO();\n const created = await userDAO.createUser({ anchor, loginEmail, now });\n // `null` is the detected no-op: the anchor was already held. The caller's\n // signal to retry with an opaque anchor — see `newAnchor`.\n if (created === null) return null;\n // Claim the sign-in address *before* resolving, otherwise the fresh account is\n // invisible to the registry and registration looks like a failure. An address\n // already verified for another account is left alone by `register`, and the\n // resolve below then reports that account instead.\n await deps\n .userEmailDAO()\n .then((dao) => dao.register({ email: loginEmail, userId: created, isVerified: true, now }))\n .catch((error: unknown) => {\n // Pre-identity registry only, per the same rule `resolveAccount` follows: a\n // missing table is tolerated, a failed write is not. Swallowing the second\n // would return an account the address registry has never heard of, which\n // resolves to nothing on the next login.\n if (isMissingSchemaError(error)) return;\n throw error;\n });\n return resolveAccount(deps, loginEmail);\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"applyMigrations.js","sourceRoot":"","sources":["../src/applyMigrations.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AAEpD,OAAO,EAAE,qBAAqB,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAEnE;;;;GAIG;AACH,MAAM,sBAAsB,GAAG;IAC7B,WAAW,EAAE,YAAY;IACzB,2BAA2B,EAAE,qBAAqB;CAC1C,CAAC;AAIX,oCAAoC;AACpC,MAAM,gBAAgB,GAAqC,CAAC,WAAW,CAAC,CAAC;AAEzE,gFAAgF;AAChF,MAAM,kBAAkB,GAAqC,CAAC,WAAW,EAAE,2BAA2B,CAAC,CAAC;AAExG;;;;;;;;;;;GAWG;AACH,KAAK,UAAU,uBAAuB,CAAC,EAAe,EAAE,UAAiC,EAAE;IACzF,MAAM,KAAK,GAAG,OAAO,CAAC,OAAO,KAAK,IAAI,CAAC,CAAC,CAAC,kBAAkB,CAAC,CAAC,CAAC,gBAAgB,CAAC;IAC/E,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,UAAU,GAAG,oBAAoB,CAAC,sBAAsB,CAAC,IAAI,CAAC,CAAC,CAAC;QACtE,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC;YAAE,SAAS;QACtC,IAAI,EAAE,CAAC,KAAK,EAAE,CAAC;YACb,MAAM,EAAE,CAAC,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;YACzD,SAAS;QACX,CAAC;QACD,KAAK,MAAM,GAAG,IAAI,UAAU,EAAE,CAAC;YAC7B,MAAM,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,CAAC;QAC9B,CAAC;IACH,CAAC;AACH,CAAC;AAED,OAAO,EAAE,uBAAuB,EAAE,sBAAsB,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,CAAC"}
|
|
1
|
+
{"version":3,"file":"applyMigrations.js","sourceRoot":"","sources":["../src/applyMigrations.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AAEpD,OAAO,EAAE,qBAAqB,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAEnE;;;;GAIG;AACH,MAAM,sBAAsB,GAAG;IAC7B,WAAW,EAAE,YAAY;IACzB,2BAA2B,EAAE,qBAAqB;CAC1C,CAAC;AAIX,oCAAoC;AACpC,MAAM,gBAAgB,GAAqC,CAAC,WAAW,CAAC,CAAC;AAEzE,gFAAgF;AAChF,MAAM,kBAAkB,GAAqC,CAAC,WAAW,EAAE,2BAA2B,CAAC,CAAC;AAExG;;;;;;;;;;;GAWG;AACH,KAAK,UAAU,uBAAuB,CAAC,EAAe,EAAE,UAAiC,EAAE;IACzF,MAAM,KAAK,GAAG,OAAO,CAAC,OAAO,KAAK,IAAI,CAAC,CAAC,CAAC,kBAAkB,CAAC,CAAC,CAAC,gBAAgB,CAAC;IAC/E,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,UAAU,GAAG,oBAAoB,CAAC,sBAAsB,CAAC,IAAI,CAAC,CAAC,CAAC;QACtE,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC;YAAE,SAAS;QACtC,IAAI,EAAE,CAAC,KAAK,EAAE,CAAC;YACb,MAAM,EAAE,CAAC,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;YACzD,SAAS;QACX,CAAC;QACD,KAAK,MAAM,GAAG,IAAI,UAAU,EAAE,CAAC;YAC7B,MAAM,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,CAAC;QAC9B,CAAC;IACH,CAAC;AACH,CAAC;AAED,OAAO,EAAE,uBAAuB,EAAE,sBAAsB,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,CAAC","sourcesContent":["import { executableStatements } from '@rexezuge/d1';\nimport type { D1Queryable } from '@rexezuge/d1';\nimport { UPGRADE_USER_IDENTITY, USERS_SCHEMA } from './migrations';\n\n/**\n * The identity migrations this package ships, by filename. `IDENTITY_MIGRATIONS`\n * in `migrations.ts` carries the same pair with the reasoning; this map is the\n * executable form.\n */\nconst IDENTITY_MIGRATION_SQL = {\n 'users.sql': USERS_SCHEMA,\n 'upgrade-user-identity.sql': UPGRADE_USER_IDENTITY,\n} as const;\n\ntype IdentityMigrationName = keyof typeof IDENTITY_MIGRATION_SQL;\n\n/** The full fresh-install order. */\nconst FRESH_MIGRATIONS: readonly IdentityMigrationName[] = ['users.sql'];\n\n/** A legacy database applies the upgrade after the fresh schema is in place. */\nconst UPGRADE_MIGRATIONS: readonly IdentityMigrationName[] = ['users.sql', 'upgrade-user-identity.sql'];\n\n/**\n * Apply the identity migrations to a D1 binding.\n *\n * Split and executed through `@rexezuge/d1` — the same statement splitter the\n * test helpers use — so a kit migration and an integration suite can never\n * disagree about where one statement ends. One `batch()` per file, matching\n * D1's own per-file apply, so a failure names its file.\n *\n * `upgrade: false` (the default) applies only the fresh-install schema. Pass\n * `upgrade: true` for a database whose `users` table predates the identity\n * columns; the upgrade file is rerunnable, so applying both is safe.\n */\nasync function applyIdentityMigrations(db: D1Queryable, options: { upgrade?: boolean } = {}): Promise<void> {\n const names = options.upgrade === true ? UPGRADE_MIGRATIONS : FRESH_MIGRATIONS;\n for (const name of names) {\n const statements = executableStatements(IDENTITY_MIGRATION_SQL[name]);\n if (statements.length === 0) continue;\n if (db.batch) {\n await db.batch(statements.map((sql) => db.prepare(sql)));\n continue;\n }\n for (const sql of statements) {\n await db.prepare(sql).run();\n }\n }\n}\n\nexport { applyIdentityMigrations, IDENTITY_MIGRATION_SQL, FRESH_MIGRATIONS, UPGRADE_MIGRATIONS };\nexport type { IdentityMigrationName };\n"]}
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAEjF,OAAO,EAAE,uBAAuB,EAAE,sBAAsB,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAE1H,OAAO,EAAE,mBAAmB,EAAE,qBAAqB,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AACxF,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAEvE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAE9C,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAEjF,OAAO,EAAE,uBAAuB,EAAE,sBAAsB,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAE1H,OAAO,EAAE,mBAAmB,EAAE,qBAAqB,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AACxF,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAEvE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAE9C,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC","sourcesContent":["/**\n * `@rexezuge/identity` — the account layer: the address is not the account.\n *\n * Converged from the seven repos that authenticate users (all but\n * ChordDHT-Tracker, whose callers are certificates). Durable-DAV's invariants\n * are the canonical ones because they are the strictest: the frozen anchor,\n * the verified-only registry, and the fail-closed resolution order.\n */\n\nexport { registerAccount, resolveAccount, resolveUserId } from './accountLookup';\nexport type { AccountLookupDeps } from './accountLookup';\nexport { applyIdentityMigrations, IDENTITY_MIGRATION_SQL, FRESH_MIGRATIONS, UPGRADE_MIGRATIONS } from './applyMigrations';\nexport type { IdentityMigrationName } from './applyMigrations';\nexport { IDENTITY_MIGRATIONS, UPGRADE_USER_IDENTITY, USERS_SCHEMA } from './migrations';\nexport { newAccountId, newAnchor, summarize } from './ResolvedAccount';\nexport type { AccountSummary, ResolvedAccount, UserRow, UserRowLike } from './ResolvedAccount';\nexport { UserDAO } from './UserDAO';\nexport { UserEmailDAO } from './UserEmailDAO';\nexport type { EmailClaim, UserEmailRow } from './UserEmailDAO';\nexport { UserIdentityService } from './UserIdentityService';\nexport type { UserIdentityDeps, UserIdentityEnv } from './UserIdentityService';\n"]}
|
package/dist/migrations.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"migrations.js","sourceRoot":"","sources":["../src/migrations.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,MAAM,YAAY,GAAG;;;;;;;;;;;;;;;;;;;;;;;CAuBpB,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,qBAAqB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoC7B,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,mBAAmB,GAAiD;IACxE,EAAE,IAAI,EAAE,gBAAgB,EAAE,GAAG,EAAE,YAAY,EAAE;IAC7C,EAAE,IAAI,EAAE,gCAAgC,EAAE,GAAG,EAAE,qBAAqB,EAAE;CACvE,CAAC;AAEF,OAAO,EAAE,mBAAmB,EAAE,qBAAqB,EAAE,YAAY,EAAE,CAAC"}
|
|
1
|
+
{"version":3,"file":"migrations.js","sourceRoot":"","sources":["../src/migrations.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,MAAM,YAAY,GAAG;;;;;;;;;;;;;;;;;;;;;;;CAuBpB,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,qBAAqB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoC7B,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,mBAAmB,GAAiD;IACxE,EAAE,IAAI,EAAE,gBAAgB,EAAE,GAAG,EAAE,YAAY,EAAE;IAC7C,EAAE,IAAI,EAAE,gCAAgC,EAAE,GAAG,EAAE,qBAAqB,EAAE;CACvE,CAAC;AAEF,OAAO,EAAE,mBAAmB,EAAE,qBAAqB,EAAE,YAAY,EAAE,CAAC","sourcesContent":["/**\n * The canonical `users` / `user_emails` schema.\n *\n * Converged from migration `0001_init.sql` + `0004_user_identity.sql` across the\n * identity-carrying repos, expressed as the *final* shape so a fresh database\n * never has to be upgraded: `users.id` is the account key from the start, and\n * `user_emails` exists from the start.\n *\n * The invariants that make an address change safe (Durable-DAV's design, which\n * is the strictest in the family and the one every other repo's upgrade is\n * migrating toward):\n *\n * - `users.id` is an opaque, unique account key — never an address.\n * - `users.email` is the **frozen anchor**: immutable, the foreign-key target\n * every legacy `*_email` value resolves against, so history survives a change.\n * - `users.current_email` is the mutable sign-in address; unique, so an address\n * can never be claimed by two accounts.\n * - `users.username` is a separately-mutable public handle, not the identity.\n * - `user_emails` is the address registry: `is_verified = 1` may authenticate;\n * a changed-from address stays at `0` (attributable, non-authenticating) and\n * is re-pointable by a later claimant.\n *\n * The anchor design is forced on D1: it honours neither\n * `PRAGMA foreign_keys = off` nor `legacy_alter_table`, and `defer_foreign_keys`\n * does not suppress `ON DELETE CASCADE`, so a live reference cannot be repointed\n * without losing rows. A fresh install just starts with the anchor in place.\n *\n * A repo with legacy tables that reference `users(email)` applies this file's\n * statements and then adds its own additive backfill (the converged pattern is\n * in `upgrade-user-identity.sql`), repointing each legacy column through\n * `user_emails` rather than through the anchor.\n */\nconst USERS_SCHEMA = `\n-- The account. The anchor (email) is immutable; current_email is the login.\nCREATE TABLE IF NOT EXISTS users (\n id TEXT NOT NULL PRIMARY KEY,\n email TEXT NOT NULL UNIQUE,\n current_email TEXT NOT NULL UNIQUE,\n username TEXT,\n created_at INTEGER NOT NULL,\n updated_at INTEGER\n);\n\nCREATE UNIQUE INDEX IF NOT EXISTS idx_users_id ON users(id);\nCREATE UNIQUE INDEX IF NOT EXISTS idx_users_current_email ON users(current_email);\n\n-- The address registry: which account an address authenticates.\nCREATE TABLE IF NOT EXISTS user_emails (\n email TEXT PRIMARY KEY,\n user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,\n is_verified INTEGER NOT NULL DEFAULT 0,\n created_at INTEGER NOT NULL\n);\n\nCREATE INDEX IF NOT EXISTS idx_user_emails_user ON user_emails(user_id);\n`;\n\n/**\n * The additive upgrade for a database whose `users` predates the identity\n * columns — converged from `0004_user_identity.sql` (Durable-DAV) with the\n * table-specific phases (dav_volumes, namespaces) generalized into a\n * documented extension point, because every repo's legacy tables differ.\n *\n * Run order per phase matches Durable-DAV's, and each backfill is guarded so\n * the file is rerunnable: `users.id` is only filled where still missing, and\n * every registry insert is `INSERT OR IGNORE`.\n *\n * Why the address stays in `users` at all (the anchor question, answered once):\n * SQLite rewrites a child's foreign key clause when the parent is renamed and\n * drops the parent by cascading, so the reference from a legacy\n * `owner_email`-style column cannot be repointed at `users.id` without losing\n * rows. Keeping `email` as a frozen anchor sidesteps the rebuild entirely:\n * this migration is purely additive.\n *\n * A consumer with its own legacy tables appends one additive phase per table:\n * add the `*_user_id` column with `REFERENCES users(id) ON DELETE CASCADE`\n * (`NO ACTION` would *restrict* where the existing `owner_email` cascade\n * removed rows — the two references must cascade together), backfill it by\n * resolving the stored address through `user_emails`, and index it.\n */\nconst UPGRADE_USER_IDENTITY = `\n-- Phase 1: stable account key.\n-- SQLite cannot add a PRIMARY KEY column, so the id is a plain column with a\n-- unique index. A unique index is a valid foreign key parent, which is all the\n-- user_id references need.\nALTER TABLE users ADD COLUMN id TEXT;\n\nUPDATE users SET id = 'usr_' || lower(hex(randomblob(16))) WHERE id IS NULL;\n\nCREATE UNIQUE INDEX IF NOT EXISTS idx_users_id ON users(id);\n\n-- Phase 2: mutable sign-in address.\n-- 'email' stays as the frozen anchor; 'current_email' is what the account signs\n-- in with and what the API reports. Uniqueness is enforced here, so an address\n-- can never be claimed by two accounts.\nALTER TABLE users ADD COLUMN current_email TEXT;\n\nUPDATE users SET current_email = lower(email) WHERE current_email IS NULL;\n\nCREATE UNIQUE INDEX IF NOT EXISTS idx_users_current_email ON users(current_email);\n\n-- Phase 3: address registry.\n-- Login resolution consults is_verified = 1 only. Backfilled from the frozen\n-- anchor address of every existing account, lowercased so a legacy mixed-case\n-- row still yields exactly one login identity.\nCREATE TABLE IF NOT EXISTS user_emails (\n email TEXT PRIMARY KEY,\n user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,\n is_verified INTEGER NOT NULL DEFAULT 0,\n created_at INTEGER NOT NULL\n);\n\nCREATE INDEX IF NOT EXISTS idx_user_emails_user ON user_emails(user_id);\n\nINSERT OR IGNORE INTO user_emails (email, user_id, is_verified, created_at)\nSELECT lower(email), id, 1, created_at FROM users;\n`;\n\n/**\n * Every migration this package ships, in apply order.\n *\n * `upgrade-user-identity.sql` is *not* applied on a fresh database — the\n * fresh-install path is `users.sql` alone, and the upgrade exists only for a\n * legacy one.\n */\nconst IDENTITY_MIGRATIONS: ReadonlyArray<{ name: string; sql: string }> = [\n { name: '0001_users.sql', sql: USERS_SCHEMA },\n { name: '0002_upgrade_user_identity.sql', sql: UPGRADE_USER_IDENTITY },\n];\n\nexport { IDENTITY_MIGRATIONS, UPGRADE_USER_IDENTITY, USERS_SCHEMA };\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rexezuge/identity",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"private": false,
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -25,15 +25,16 @@
|
|
|
25
25
|
"dist"
|
|
26
26
|
],
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@rexezuge/d1": "1.0.
|
|
29
|
-
"@rexezuge/errors": "1.0.
|
|
30
|
-
"@rexezuge/shared": "1.0.
|
|
28
|
+
"@rexezuge/d1": "1.0.3",
|
|
29
|
+
"@rexezuge/errors": "1.0.3",
|
|
30
|
+
"@rexezuge/shared": "1.0.3"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"@types/node": "26.6.4",
|
|
34
34
|
"typescript": "6.0.3",
|
|
35
35
|
"vitest": "4.1.11"
|
|
36
36
|
},
|
|
37
|
+
"description": "Account layer with address resolution, user DAOs, and canonical identity migrations.",
|
|
37
38
|
"scripts": {
|
|
38
39
|
"build": "tsc -p tsconfig.json",
|
|
39
40
|
"typecheck": "tsc -p tsconfig.test.json --noEmit",
|