@rexezuge/identity 1.0.6 → 1.0.7
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
CHANGED
|
@@ -24,12 +24,12 @@ const account = await resolveAccount(deps, 'user@example.com'); // ResolvedAccou
|
|
|
24
24
|
|
|
25
25
|
## 2. DAOs
|
|
26
26
|
|
|
27
|
-
| Export
|
|
28
|
-
|
|
|
29
|
-
| `UserDAO`
|
|
30
|
-
| `UserEmailDAO`
|
|
31
|
-
| `newAccountId` / `newAnchor`
|
|
32
|
-
| `summarize` / `AccountSummary` | safe-to-log account projection
|
|
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
33
|
|
|
34
34
|
```ts
|
|
35
35
|
import { UserDAO, UserEmailDAO } from '@rexezuge/identity';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ResolvedAccount.d.ts","sourceRoot":"","sources":["../src/ResolvedAccount.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,OAAO;IACtB,EAAE,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB;;;;OAIG;IACH,KAAK,EAAE,MAAM,CAAC;IACd;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAED,kEAAkE;AAClE,MAAM,MAAM,cAAc,GAAG,IAAI,CAAC,eAAe,EAAE,IAAI,GAAG,OAAO,GAAG,UAAU,CAAC,CAAC;AAEhF,mFAAmF;AACnF,MAAM,MAAM,WAAW,
|
|
1
|
+
{"version":3,"file":"ResolvedAccount.d.ts","sourceRoot":"","sources":["../src/ResolvedAccount.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,OAAO;IACtB,EAAE,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB;;;;OAIG;IACH,KAAK,EAAE,MAAM,CAAC;IACd;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAED,kEAAkE;AAClE,MAAM,MAAM,cAAc,GAAG,IAAI,CAAC,eAAe,EAAE,IAAI,GAAG,OAAO,GAAG,UAAU,CAAC,CAAC;AAEhF,mFAAmF;AACnF,MAAM,MAAM,WAAW,GAAG,IAAI,CAAC,OAAO,EAAE,IAAI,GAAG,OAAO,GAAG,eAAe,GAAG,UAAU,CAAC,GAAG,SAAS,CAAC;AAEnG,sDAAsD;AACtD,wBAAgB,SAAS,CAAC,GAAG,EAAE,WAAW,GAAG,eAAe,GAAG,IAAI,CAIlE;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,IAAI,MAAM,CAErC;AAED;;;;;;;;;GASG;AACH,wBAAgB,SAAS,IAAI,MAAM,CAElC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ResolvedAccount.js","sourceRoot":"","sources":["../src/ResolvedAccount.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"ResolvedAccount.js","sourceRoot":"","sources":["../src/ResolvedAccount.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAqDzC,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 = Pick<UserRow, 'id' | 'email' | 'current_email' | 'username'> | 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"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"UserIdentityService.d.ts","sourceRoot":"","sources":["../src/UserIdentityService.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAIhD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAEzD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAIzD,UAAU,eAAe;IACvB,EAAE,EAAE,WAAW,CAAC;CACjB;AAED,YAAY,EAAE,eAAe,EAAE,CAAC;AAEhC;;;GAGG;AACH,MAAM,MAAM,gBAAgB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAAC;AAE1D;;;;;;;;;;;;;;;;;GAiBG;AACH,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,QAAQ,CAAC,IAAI,CAA8B;IAEnD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA6C;gBAElD,GAAG,EAAE,eAAe,EAAE,IAAI,GAAE,gBAAqB;IAQpE;;;;;;;OAOG;IACU,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC;IAS3E,8DAA8D;IACjD,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAKpE;;;OAGG;IACU,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC;IAMhF;;;;;;;OAOG;IACU,iBAAiB,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,SAAmD,GAAG,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC;IAYtI;;OAEG;IACU,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IAMlG;;;;;;;;;;;;;;;;OAgBG;IACU,eAAe,CAC1B,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,GAAG,SAAmD,GACrD,OAAO,CAAC,eAAe,CAAC;IAsB3B;;;OAGG;IACU,iBAAiB,
|
|
1
|
+
{"version":3,"file":"UserIdentityService.d.ts","sourceRoot":"","sources":["../src/UserIdentityService.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAIhD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAEzD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAIzD,UAAU,eAAe;IACvB,EAAE,EAAE,WAAW,CAAC;CACjB;AAED,YAAY,EAAE,eAAe,EAAE,CAAC;AAEhC;;;GAGG;AACH,MAAM,MAAM,gBAAgB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAAC;AAE1D;;;;;;;;;;;;;;;;;GAiBG;AACH,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,QAAQ,CAAC,IAAI,CAA8B;IAEnD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA6C;gBAElD,GAAG,EAAE,eAAe,EAAE,IAAI,GAAE,gBAAqB;IAQpE;;;;;;;OAOG;IACU,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC;IAS3E,8DAA8D;IACjD,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAKpE;;;OAGG;IACU,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC;IAMhF;;;;;;;OAOG;IACU,iBAAiB,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,SAAmD,GAAG,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC;IAYtI;;OAEG;IACU,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IAMlG;;;;;;;;;;;;;;;;OAgBG;IACU,eAAe,CAC1B,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,GAAG,SAAmD,GACrD,OAAO,CAAC,eAAe,CAAC;IAsB3B;;;OAGG;IACU,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,SAAmD,GAAG,OAAO,CAAC,IAAI,CAAC;CAOrI"}
|
|
@@ -1 +1 @@
|
|
|
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
|
+
{"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(userId: string, email: string, now = TimestampUtil.getCurrentUnixTimestampInSeconds()): 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"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rexezuge/identity",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.7",
|
|
4
4
|
"private": false,
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -25,9 +25,9 @@
|
|
|
25
25
|
"dist"
|
|
26
26
|
],
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@rexezuge/
|
|
29
|
-
"@rexezuge/
|
|
30
|
-
"@rexezuge/
|
|
28
|
+
"@rexezuge/errors": "1.0.7",
|
|
29
|
+
"@rexezuge/shared": "1.0.7",
|
|
30
|
+
"@rexezuge/d1": "1.0.7"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"@types/node": "26.6.4",
|