@rexezuge/identity 0.0.0-stage → 1.0.1
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/LICENSE +21 -0
- package/dist/ResolvedAccount.d.ts +68 -0
- package/dist/ResolvedAccount.d.ts.map +1 -0
- package/dist/ResolvedAccount.js +30 -0
- package/dist/ResolvedAccount.js.map +1 -0
- package/dist/UserDAO.d.ts +88 -0
- package/dist/UserDAO.d.ts.map +1 -0
- package/dist/UserDAO.js +122 -0
- package/dist/UserDAO.js.map +1 -0
- package/dist/UserEmailDAO.d.ts +50 -0
- package/dist/UserEmailDAO.d.ts.map +1 -0
- package/dist/UserEmailDAO.js +49 -0
- package/dist/UserEmailDAO.js.map +1 -0
- package/dist/UserIdentityService.d.ts +91 -0
- package/dist/UserIdentityService.d.ts.map +1 -0
- package/dist/UserIdentityService.js +151 -0
- package/dist/UserIdentityService.js.map +1 -0
- package/dist/accountLookup.d.ts +45 -0
- package/dist/accountLookup.d.ts.map +1 -0
- package/dist/accountLookup.js +107 -0
- package/dist/accountLookup.js.map +1 -0
- package/dist/applyMigrations.d.ts +33 -0
- package/dist/applyMigrations.d.ts.map +1 -0
- package/dist/applyMigrations.js +44 -0
- package/dist/applyMigrations.js.map +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/migrations.d.ts +70 -0
- package/dist/migrations.d.ts.map +1 -0
- package/dist/migrations.js +129 -0
- package/dist/migrations.js.map +1 -0
- package/package.json +39 -3
- package/README.md +0 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Rexezuge
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `users` rows — the stored form of an account.
|
|
3
|
+
*
|
|
4
|
+
* Converged from `backend-data/src/dao/UserDAO.ts` + its `UserRow` (identical in
|
|
5
|
+
* six of the seven identity-carrying repos; AWS and ChordDHT keep only the
|
|
6
|
+
* id/email core).
|
|
7
|
+
*
|
|
8
|
+
* `id` is the stable account key. `email` is the *anchor*: an immutable value
|
|
9
|
+
* that legacy `*_email` foreign keys resolve against, so history stays
|
|
10
|
+
* resolvable across an address change. `current_email` is the address the
|
|
11
|
+
* account signs in with and the one APIs report.
|
|
12
|
+
*/
|
|
13
|
+
export interface UserRow {
|
|
14
|
+
id?: string | null;
|
|
15
|
+
/**
|
|
16
|
+
* The frozen anchor address. Never updated — it is the foreign-key target
|
|
17
|
+
* every legacy `*_email` value resolves against, and rewriting it would
|
|
18
|
+
* cascade the rows that reference it. Read `current_email` for sign-in.
|
|
19
|
+
*/
|
|
20
|
+
email: string;
|
|
21
|
+
/**
|
|
22
|
+
* Mutable sign-in address. Absent on a pre-identity database, where `email`
|
|
23
|
+
* *is* the sign-in address; read it as `current_email ?? email`.
|
|
24
|
+
*/
|
|
25
|
+
current_email?: string | null;
|
|
26
|
+
created_at: number;
|
|
27
|
+
username: string | null;
|
|
28
|
+
updated_at: number | null;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* A resolved account.
|
|
32
|
+
*
|
|
33
|
+
* `id` is the only value that should be used as an identity. `email` is the
|
|
34
|
+
* address the account currently signs in with; `anchorEmail` is the immutable
|
|
35
|
+
* value legacy columns store, and is what a pre-identity row can still be
|
|
36
|
+
* matched on.
|
|
37
|
+
*/
|
|
38
|
+
export interface ResolvedAccount {
|
|
39
|
+
id: string;
|
|
40
|
+
email: string;
|
|
41
|
+
anchorEmail: string;
|
|
42
|
+
username: string | null;
|
|
43
|
+
}
|
|
44
|
+
/** The denormalized subset routes and the permission path use. */
|
|
45
|
+
export type AccountSummary = Pick<ResolvedAccount, 'id' | 'email' | 'username'>;
|
|
46
|
+
/** Shape every `users` read has, whether or not the identity migration has run. */
|
|
47
|
+
export type UserRowLike = Pick<UserRow, 'id' | 'email' | 'current_email' | 'username'> | undefined;
|
|
48
|
+
/** Project a `users` row onto a `ResolvedAccount`. */
|
|
49
|
+
export declare function summarize(row: UserRowLike): ResolvedAccount | null;
|
|
50
|
+
/**
|
|
51
|
+
* The opaque account id.
|
|
52
|
+
*
|
|
53
|
+
* Prefixed so an id is never mistaken for an address in a log line or a URL,
|
|
54
|
+
* and random at 128 bits so accounts are not enumerable.
|
|
55
|
+
*/
|
|
56
|
+
export declare function newAccountId(): string;
|
|
57
|
+
/**
|
|
58
|
+
* Opaque anchor for an account whose login address is already held as another
|
|
59
|
+
* account's anchor.
|
|
60
|
+
*
|
|
61
|
+
* It must be globally unique and must never be a real address: `email` is the
|
|
62
|
+
* anchor every legacy `*_email` foreign key resolves against, so an address
|
|
63
|
+
* parked here could never be re-registered by a different person after its
|
|
64
|
+
* original account moved off it. `.invalid` is reserved by RFC 2606 and can
|
|
65
|
+
* never be delivered to, so it can never authenticate either.
|
|
66
|
+
*/
|
|
67
|
+
export declare function newAnchor(): string;
|
|
68
|
+
//# sourceMappingURL=ResolvedAccount.d.ts.map
|
|
@@ -0,0 +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,GACnB,IAAI,CAAC,OAAO,EAAE,IAAI,GAAG,OAAO,GAAG,eAAe,GAAG,UAAU,CAAC,GAC5D,SAAS,CAAC;AAEd,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"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
/** Project a `users` row onto a `ResolvedAccount`. */
|
|
3
|
+
export function summarize(row) {
|
|
4
|
+
return row?.id
|
|
5
|
+
? { id: row.id, email: (row.current_email ?? row.email).toLowerCase(), anchorEmail: row.email, username: row.username ?? null }
|
|
6
|
+
: null;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* The opaque account id.
|
|
10
|
+
*
|
|
11
|
+
* Prefixed so an id is never mistaken for an address in a log line or a URL,
|
|
12
|
+
* and random at 128 bits so accounts are not enumerable.
|
|
13
|
+
*/
|
|
14
|
+
export function newAccountId() {
|
|
15
|
+
return `usr_${randomUUID().replaceAll('-', '')}`;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Opaque anchor for an account whose login address is already held as another
|
|
19
|
+
* account's anchor.
|
|
20
|
+
*
|
|
21
|
+
* It must be globally unique and must never be a real address: `email` is the
|
|
22
|
+
* anchor every legacy `*_email` foreign key resolves against, so an address
|
|
23
|
+
* parked here could never be re-registered by a different person after its
|
|
24
|
+
* original account moved off it. `.invalid` is reserved by RFC 2606 and can
|
|
25
|
+
* never be delivered to, so it can never authenticate either.
|
|
26
|
+
*/
|
|
27
|
+
export function newAnchor() {
|
|
28
|
+
return `anchor-${randomUUID().replaceAll('-', '')}@users.invalid`;
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=ResolvedAccount.js.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { BaseDAO } from '@rexezuge/d1';
|
|
2
|
+
import type { D1Queryable } from '@rexezuge/d1';
|
|
3
|
+
import type { UserRow } from './ResolvedAccount';
|
|
4
|
+
/**
|
|
5
|
+
* The stored form of a handle: lowercase.
|
|
6
|
+
*
|
|
7
|
+
* Every username read and every username write goes through this, so the two
|
|
8
|
+
* can never disagree about case. That agreement is not cosmetic: `username` has
|
|
9
|
+
* no `COLLATE NOCASE` (SQLite defaults to BINARY), so a stored `Alice2` is a
|
|
10
|
+
* *different value* from `alice2` and a lookup that lowercased its parameter
|
|
11
|
+
* would never find it. Normalising at the write points rather than relying on
|
|
12
|
+
* each caller means a future caller cannot reintroduce an unfindable handle by
|
|
13
|
+
* forgetting.
|
|
14
|
+
*/
|
|
15
|
+
declare function normalizeUsername(username: string): string;
|
|
16
|
+
/**
|
|
17
|
+
* `users` rows. Converged from `backend-data/src/dao/UserDAO.ts` (identical in
|
|
18
|
+
* Durable-DAV, Durable-DAV-Router, Edge-Git, Mail-Otter, Mail-Meow; AWS keeps
|
|
19
|
+
* the id/email core without the username methods).
|
|
20
|
+
*
|
|
21
|
+
* Case rule everywhere in this DAO: lowercase the *parameter*, never the
|
|
22
|
+
* column — `lower(col)` makes the index or primary key unusable. It is only
|
|
23
|
+
* sound because every writer stores lowercase through this DAO's own
|
|
24
|
+
* normalisers.
|
|
25
|
+
*/
|
|
26
|
+
export declare class UserDAO extends BaseDAO {
|
|
27
|
+
constructor(database: D1Queryable);
|
|
28
|
+
/**
|
|
29
|
+
* Create an account, stamped with the identity columns.
|
|
30
|
+
*
|
|
31
|
+
* `INSERT ... ON CONFLICT DO NOTHING` with **no conflict target**, deliberately.
|
|
32
|
+
* `users` carries two unique constraints — the anchor primary key and the
|
|
33
|
+
* unique index on `current_email` — and SQLite applies `DO NOTHING` only to
|
|
34
|
+
* the constraints a named target covers. Naming the anchor therefore left a
|
|
35
|
+
* `current_email` collision as a raised error, which callers cannot tell from
|
|
36
|
+
* a transient D1 failure: it propagated instead of returning `null`, so the
|
|
37
|
+
* opaque-anchor retry never ran and every login request answered 500.
|
|
38
|
+
*
|
|
39
|
+
* Returns the new account's id, or `null` when the insert was a silent no-op —
|
|
40
|
+
* which this DAO establishes by reading the anchor row back and requiring its
|
|
41
|
+
* own generated id to be the one there. The read-back lives here rather than
|
|
42
|
+
* in the caller because a caller that trusted the generated id on a taken
|
|
43
|
+
* anchor would point the address registry at a *phantom* account: the address
|
|
44
|
+
* then resolves to nothing, and the opaque-anchor retry fails the same way.
|
|
45
|
+
* Making the no-op detectable here is what keeps registration from stranding
|
|
46
|
+
* an address forever.
|
|
47
|
+
*/
|
|
48
|
+
createUser(input: {
|
|
49
|
+
anchor: string;
|
|
50
|
+
loginEmail: string;
|
|
51
|
+
now: number;
|
|
52
|
+
}): Promise<string | null>;
|
|
53
|
+
/**
|
|
54
|
+
* @param idOrEmail The stable account key, or an anchor address as the
|
|
55
|
+
* pre-identity fallback. `WHERE id = ? OR email = ?` cannot use either index
|
|
56
|
+
* as a seek, so this stays off the hot path; login resolves through
|
|
57
|
+
* `UserIdentityService` instead.
|
|
58
|
+
*/
|
|
59
|
+
ensureUsername(idOrEmail: string, username: string, now: number): Promise<void>;
|
|
60
|
+
/** @param idOrEmail The stable account key, or an anchor address. */
|
|
61
|
+
setUsername(idOrEmail: string, username: string, now: number): Promise<void>;
|
|
62
|
+
/**
|
|
63
|
+
* Anchor lookup — used for legacy `*_email` values and cascade resolution.
|
|
64
|
+
*
|
|
65
|
+
* This is *not* a login lookup: an account that has changed its address is not
|
|
66
|
+
* findable here by its new one. It is the pre-identity floor and the
|
|
67
|
+
* attribution read.
|
|
68
|
+
*/
|
|
69
|
+
getByEmail(email: string): Promise<UserRow | undefined>;
|
|
70
|
+
/** The account behind a stable key. Null on a pre-identity database. */
|
|
71
|
+
getById(id: string): Promise<UserRow | undefined>;
|
|
72
|
+
/** The account currently signing in with this address. Identity-era only. */
|
|
73
|
+
getByCurrentEmail(email: string): Promise<UserRow | undefined>;
|
|
74
|
+
/**
|
|
75
|
+
* Move an account's sign-in address.
|
|
76
|
+
*
|
|
77
|
+
* The anchor `email` is deliberately untouched: it is what every legacy
|
|
78
|
+
* `*_email` column and foreign key resolves against. Ordering matters — the
|
|
79
|
+
* caller claims the new address first and revokes the old one third, so a
|
|
80
|
+
* failure between steps never leaves the account without a way to sign in.
|
|
81
|
+
*/
|
|
82
|
+
setCurrentEmail(id: string, email: string, now: number): Promise<void>;
|
|
83
|
+
/** Lookup by handle. */
|
|
84
|
+
getByUsernameCi(usernameCi: string): Promise<UserRow | undefined>;
|
|
85
|
+
}
|
|
86
|
+
export { normalizeUsername };
|
|
87
|
+
export type { UserRow };
|
|
88
|
+
//# sourceMappingURL=UserDAO.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"UserDAO.d.ts","sourceRoot":"","sources":["../src/UserDAO.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAEhD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAEjD;;;;;;;;;;GAUG;AACH,iBAAS,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAEnD;AAED;;;;;;;;;GASG;AACH,qBAAa,OAAQ,SAAQ,OAAO;gBACf,QAAQ,EAAE,WAAW;IAIxC;;;;;;;;;;;;;;;;;;;OAmBG;IACU,UAAU,CAAC,KAAK,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAc3G;;;;;OAKG;IACU,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAS5F,qEAAqE;IACxD,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IASzF;;;;;;OAMG;IACU,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC;IAIpE,wEAAwE;IAC3D,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC;IAI9D,6EAA6E;IAChE,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC;IAI3E;;;;;;;OAOG;IACU,eAAe,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAInF,wBAAwB;IACX,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC;CAG/E;AAED,OAAO,EAAE,iBAAiB,EAAE,CAAC;AAC7B,YAAY,EAAE,OAAO,EAAE,CAAC"}
|
package/dist/UserDAO.js
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { BaseDAO } from '@rexezuge/d1';
|
|
2
|
+
import { newAccountId } from './ResolvedAccount';
|
|
3
|
+
/**
|
|
4
|
+
* The stored form of a handle: lowercase.
|
|
5
|
+
*
|
|
6
|
+
* Every username read and every username write goes through this, so the two
|
|
7
|
+
* can never disagree about case. That agreement is not cosmetic: `username` has
|
|
8
|
+
* no `COLLATE NOCASE` (SQLite defaults to BINARY), so a stored `Alice2` is a
|
|
9
|
+
* *different value* from `alice2` and a lookup that lowercased its parameter
|
|
10
|
+
* would never find it. Normalising at the write points rather than relying on
|
|
11
|
+
* each caller means a future caller cannot reintroduce an unfindable handle by
|
|
12
|
+
* forgetting.
|
|
13
|
+
*/
|
|
14
|
+
function normalizeUsername(username) {
|
|
15
|
+
return username.trim().toLowerCase();
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* `users` rows. Converged from `backend-data/src/dao/UserDAO.ts` (identical in
|
|
19
|
+
* Durable-DAV, Durable-DAV-Router, Edge-Git, Mail-Otter, Mail-Meow; AWS keeps
|
|
20
|
+
* the id/email core without the username methods).
|
|
21
|
+
*
|
|
22
|
+
* Case rule everywhere in this DAO: lowercase the *parameter*, never the
|
|
23
|
+
* column — `lower(col)` makes the index or primary key unusable. It is only
|
|
24
|
+
* sound because every writer stores lowercase through this DAO's own
|
|
25
|
+
* normalisers.
|
|
26
|
+
*/
|
|
27
|
+
export class UserDAO extends BaseDAO {
|
|
28
|
+
constructor(database) {
|
|
29
|
+
super(database);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Create an account, stamped with the identity columns.
|
|
33
|
+
*
|
|
34
|
+
* `INSERT ... ON CONFLICT DO NOTHING` with **no conflict target**, deliberately.
|
|
35
|
+
* `users` carries two unique constraints — the anchor primary key and the
|
|
36
|
+
* unique index on `current_email` — and SQLite applies `DO NOTHING` only to
|
|
37
|
+
* the constraints a named target covers. Naming the anchor therefore left a
|
|
38
|
+
* `current_email` collision as a raised error, which callers cannot tell from
|
|
39
|
+
* a transient D1 failure: it propagated instead of returning `null`, so the
|
|
40
|
+
* opaque-anchor retry never ran and every login request answered 500.
|
|
41
|
+
*
|
|
42
|
+
* Returns the new account's id, or `null` when the insert was a silent no-op —
|
|
43
|
+
* which this DAO establishes by reading the anchor row back and requiring its
|
|
44
|
+
* own generated id to be the one there. The read-back lives here rather than
|
|
45
|
+
* in the caller because a caller that trusted the generated id on a taken
|
|
46
|
+
* anchor would point the address registry at a *phantom* account: the address
|
|
47
|
+
* then resolves to nothing, and the opaque-anchor retry fails the same way.
|
|
48
|
+
* Making the no-op detectable here is what keeps registration from stranding
|
|
49
|
+
* an address forever.
|
|
50
|
+
*/
|
|
51
|
+
async createUser(input) {
|
|
52
|
+
const anchor = input.anchor.toLowerCase();
|
|
53
|
+
const loginEmail = input.loginEmail.toLowerCase();
|
|
54
|
+
const id = newAccountId();
|
|
55
|
+
await this.run('INSERT INTO users (email, created_at, id, current_email) VALUES (?, ?, ?, ?) ON CONFLICT DO NOTHING', [
|
|
56
|
+
anchor,
|
|
57
|
+
input.now,
|
|
58
|
+
id,
|
|
59
|
+
loginEmail,
|
|
60
|
+
]);
|
|
61
|
+
const row = await this.getByEmail(anchor);
|
|
62
|
+
return row?.id === id ? id : null;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* @param idOrEmail The stable account key, or an anchor address as the
|
|
66
|
+
* pre-identity fallback. `WHERE id = ? OR email = ?` cannot use either index
|
|
67
|
+
* as a seek, so this stays off the hot path; login resolves through
|
|
68
|
+
* `UserIdentityService` instead.
|
|
69
|
+
*/
|
|
70
|
+
async ensureUsername(idOrEmail, username, now) {
|
|
71
|
+
await this.run('UPDATE users SET username = COALESCE(username, ?), updated_at = COALESCE(updated_at, ?) WHERE id = ? OR email = ?', [
|
|
72
|
+
normalizeUsername(username),
|
|
73
|
+
now,
|
|
74
|
+
idOrEmail,
|
|
75
|
+
idOrEmail,
|
|
76
|
+
]);
|
|
77
|
+
}
|
|
78
|
+
/** @param idOrEmail The stable account key, or an anchor address. */
|
|
79
|
+
async setUsername(idOrEmail, username, now) {
|
|
80
|
+
await this.run('UPDATE users SET username = ?, updated_at = ? WHERE id = ? OR email = ?', [
|
|
81
|
+
normalizeUsername(username),
|
|
82
|
+
now,
|
|
83
|
+
idOrEmail,
|
|
84
|
+
idOrEmail,
|
|
85
|
+
]);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Anchor lookup — used for legacy `*_email` values and cascade resolution.
|
|
89
|
+
*
|
|
90
|
+
* This is *not* a login lookup: an account that has changed its address is not
|
|
91
|
+
* findable here by its new one. It is the pre-identity floor and the
|
|
92
|
+
* attribution read.
|
|
93
|
+
*/
|
|
94
|
+
async getByEmail(email) {
|
|
95
|
+
return this.first('SELECT * FROM users WHERE email = ? LIMIT 1', [email.toLowerCase()]);
|
|
96
|
+
}
|
|
97
|
+
/** The account behind a stable key. Null on a pre-identity database. */
|
|
98
|
+
async getById(id) {
|
|
99
|
+
return this.first('SELECT * FROM users WHERE id = ? LIMIT 1', [id]);
|
|
100
|
+
}
|
|
101
|
+
/** The account currently signing in with this address. Identity-era only. */
|
|
102
|
+
async getByCurrentEmail(email) {
|
|
103
|
+
return this.first('SELECT * FROM users WHERE current_email = ? LIMIT 1', [email.toLowerCase()]);
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Move an account's sign-in address.
|
|
107
|
+
*
|
|
108
|
+
* The anchor `email` is deliberately untouched: it is what every legacy
|
|
109
|
+
* `*_email` column and foreign key resolves against. Ordering matters — the
|
|
110
|
+
* caller claims the new address first and revokes the old one third, so a
|
|
111
|
+
* failure between steps never leaves the account without a way to sign in.
|
|
112
|
+
*/
|
|
113
|
+
async setCurrentEmail(id, email, now) {
|
|
114
|
+
await this.run('UPDATE users SET current_email = ?, updated_at = ? WHERE id = ?', [email.toLowerCase(), now, id]);
|
|
115
|
+
}
|
|
116
|
+
/** Lookup by handle. */
|
|
117
|
+
async getByUsernameCi(usernameCi) {
|
|
118
|
+
return this.first('SELECT * FROM users WHERE username = ? LIMIT 1', [usernameCi.toLowerCase()]);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
export { normalizeUsername };
|
|
122
|
+
//# sourceMappingURL=UserDAO.js.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { BaseDAO } from '@rexezuge/d1';
|
|
2
|
+
import type { D1Queryable } from '@rexezuge/d1';
|
|
3
|
+
/**
|
|
4
|
+
* One known address for an account.
|
|
5
|
+
*
|
|
6
|
+
* `is_verified` gates login: `1` means the address may authenticate the
|
|
7
|
+
* account, `0` means it was changed away from and is retained only so rows
|
|
8
|
+
* written before the change still resolve. A revoked row is re-pointed (not
|
|
9
|
+
* deleted) when a later account legitimately claims the address, so an address
|
|
10
|
+
* is never permanently reserved.
|
|
11
|
+
*
|
|
12
|
+
* Converged from `backend-data/src/dao/UserEmailDAO.ts` (identical in Durable-DAV,
|
|
13
|
+
* Durable-DAV-Router, Edge-Git, Mail-Otter).
|
|
14
|
+
*/
|
|
15
|
+
export interface UserEmailRow {
|
|
16
|
+
email: string;
|
|
17
|
+
user_id: string;
|
|
18
|
+
is_verified: number;
|
|
19
|
+
created_at: number;
|
|
20
|
+
}
|
|
21
|
+
export type EmailClaim = 'claimed' | 'already-claimed';
|
|
22
|
+
export declare class UserEmailDAO extends BaseDAO {
|
|
23
|
+
constructor(database: D1Queryable);
|
|
24
|
+
/**
|
|
25
|
+
* Claim an address for an account.
|
|
26
|
+
*
|
|
27
|
+
* An existing verified row is left alone: the address already belongs to
|
|
28
|
+
* someone, and silently re-pointing it would hand one account's identity to
|
|
29
|
+
* another. Callers check `resolveVerified` first and reject on a hit.
|
|
30
|
+
* An unverified (revoked) row is re-pointed, which releases the address.
|
|
31
|
+
*/
|
|
32
|
+
register(input: {
|
|
33
|
+
email: string;
|
|
34
|
+
userId: string;
|
|
35
|
+
isVerified: boolean;
|
|
36
|
+
now: number;
|
|
37
|
+
}): Promise<EmailClaim>;
|
|
38
|
+
get(email: string): Promise<UserEmailRow | undefined>;
|
|
39
|
+
/** Login resolution: only a verified address identifies an account. */
|
|
40
|
+
resolveVerified(email: string): Promise<UserEmailRow | undefined>;
|
|
41
|
+
listByUserId(userId: string): Promise<UserEmailRow[]>;
|
|
42
|
+
/** Revoke an address for login while keeping it resolvable for attribution. */
|
|
43
|
+
revoke(email: string): Promise<void>;
|
|
44
|
+
/**
|
|
45
|
+
* Revoke every verified address for an account. Used when an account's login
|
|
46
|
+
* address changes, so only the new address can authenticate it.
|
|
47
|
+
*/
|
|
48
|
+
revokeAllVerified(userId: string, exceptEmail: string): Promise<void>;
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=UserEmailDAO.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"UserEmailDAO.d.ts","sourceRoot":"","sources":["../src/UserEmailDAO.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAEhD;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,iBAAiB,CAAC;AAEvD,qBAAa,YAAa,SAAQ,OAAO;gBACpB,QAAQ,EAAE,WAAW;IAIxC;;;;;;;OAOG;IACU,QAAQ,CAAC,KAAK,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,OAAO,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,UAAU,CAAC;IAazG,GAAG,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,SAAS,CAAC;IAOlE,uEAAuE;IAC1D,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,SAAS,CAAC;IAIjE,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC;IAIlE,+EAA+E;IAClE,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIjD;;;OAGG;IACU,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAGnF"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { BaseDAO } from '@rexezuge/d1';
|
|
2
|
+
export class UserEmailDAO extends BaseDAO {
|
|
3
|
+
constructor(database) {
|
|
4
|
+
super(database);
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Claim an address for an account.
|
|
8
|
+
*
|
|
9
|
+
* An existing verified row is left alone: the address already belongs to
|
|
10
|
+
* someone, and silently re-pointing it would hand one account's identity to
|
|
11
|
+
* another. Callers check `resolveVerified` first and reject on a hit.
|
|
12
|
+
* An unverified (revoked) row is re-pointed, which releases the address.
|
|
13
|
+
*/
|
|
14
|
+
async register(input) {
|
|
15
|
+
const email = input.email.toLowerCase();
|
|
16
|
+
const existing = await this.get(email);
|
|
17
|
+
if (existing && existing.is_verified === 1)
|
|
18
|
+
return 'already-claimed';
|
|
19
|
+
await this.run(`INSERT INTO user_emails (email, user_id, is_verified, created_at)
|
|
20
|
+
VALUES (?, ?, ?, ?)
|
|
21
|
+
ON CONFLICT(email) DO UPDATE SET user_id = excluded.user_id, is_verified = excluded.is_verified`, [email, input.userId, input.isVerified ? 1 : 0, input.now]);
|
|
22
|
+
return 'claimed';
|
|
23
|
+
}
|
|
24
|
+
async get(email) {
|
|
25
|
+
// Lowercase the *parameter*: the column is stored lowercased by every
|
|
26
|
+
// writer, so `lower(email) = ?` matched identically while making the
|
|
27
|
+
// primary key unusable.
|
|
28
|
+
return this.first('SELECT * FROM user_emails WHERE email = ? LIMIT 1', [email.toLowerCase()]);
|
|
29
|
+
}
|
|
30
|
+
/** Login resolution: only a verified address identifies an account. */
|
|
31
|
+
async resolveVerified(email) {
|
|
32
|
+
return this.first('SELECT * FROM user_emails WHERE email = ? AND is_verified = 1 LIMIT 1', [email.toLowerCase()]);
|
|
33
|
+
}
|
|
34
|
+
async listByUserId(userId) {
|
|
35
|
+
return this.all('SELECT * FROM user_emails WHERE user_id = ? ORDER BY is_verified DESC, created_at ASC', [userId]);
|
|
36
|
+
}
|
|
37
|
+
/** Revoke an address for login while keeping it resolvable for attribution. */
|
|
38
|
+
async revoke(email) {
|
|
39
|
+
await this.run('UPDATE user_emails SET is_verified = 0 WHERE email = ?', [email.toLowerCase()]);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Revoke every verified address for an account. Used when an account's login
|
|
43
|
+
* address changes, so only the new address can authenticate it.
|
|
44
|
+
*/
|
|
45
|
+
async revokeAllVerified(userId, exceptEmail) {
|
|
46
|
+
await this.run('UPDATE user_emails SET is_verified = 0 WHERE user_id = ? AND email != ?', [userId, exceptEmail.toLowerCase()]);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=UserEmailDAO.js.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { D1Queryable } from '@rexezuge/d1';
|
|
2
|
+
import type { AccountLookupDeps } from './accountLookup';
|
|
3
|
+
import type { ResolvedAccount } from './ResolvedAccount';
|
|
4
|
+
interface UserIdentityEnv {
|
|
5
|
+
DB: D1Queryable;
|
|
6
|
+
}
|
|
7
|
+
export type { UserIdentityEnv };
|
|
8
|
+
/**
|
|
9
|
+
* Injected DAO factories. All optional: each defaults to a real DAO over the
|
|
10
|
+
* same D1 binding, so tests override only what they need.
|
|
11
|
+
*/
|
|
12
|
+
export type UserIdentityDeps = Partial<AccountLookupDeps>;
|
|
13
|
+
/**
|
|
14
|
+
* The account behind an address, plus the address-change operations.
|
|
15
|
+
*
|
|
16
|
+
* Converged from `backend-services/src/identity/UserIdentityService.ts`
|
|
17
|
+
* (identical in Durable-DAV, Durable-DAV-Router, Edge-Git, Mail-Otter; AWS and
|
|
18
|
+
* Mail-Meow keep the resolve core). Canonical names merge the two spellings the
|
|
19
|
+
* repos used — `resolveUserById` (Durable-DAV family) and
|
|
20
|
+
* `resolveAccountById` (AWS) are one method, `resolveAccountById`.
|
|
21
|
+
*
|
|
22
|
+
* The instance is one per request scope: the ownership and permission paths
|
|
23
|
+
* resolve the same caller several times each, and a per-instance address memo
|
|
24
|
+
* costs one query per request rather than one per comparison. Bind it once in
|
|
25
|
+
* the composition root and share it — constructing per consumer is the bug the
|
|
26
|
+
* memo exists to prevent.
|
|
27
|
+
*
|
|
28
|
+
* Resolution itself lives in `accountLookup` (free functions, shared with any
|
|
29
|
+
* account bootstrap); this class adds memoization and the change operations.
|
|
30
|
+
*/
|
|
31
|
+
export declare class UserIdentityService {
|
|
32
|
+
private readonly deps;
|
|
33
|
+
private readonly byEmail;
|
|
34
|
+
constructor(env: UserIdentityEnv, deps?: UserIdentityDeps);
|
|
35
|
+
/**
|
|
36
|
+
* Resolve a sign-in address to its account, or null when unknown.
|
|
37
|
+
*
|
|
38
|
+
* Only a verified address resolves. A revoked address (changed away from) is
|
|
39
|
+
* retained so pre-change rows stay attributable, but it must never
|
|
40
|
+
* authenticate, otherwise a reassigned address would inherit the previous
|
|
41
|
+
* holder's account.
|
|
42
|
+
*/
|
|
43
|
+
resolveAccount(email: string): Promise<ResolvedAccount | null>;
|
|
44
|
+
/** Account id for a sign-in address, or null when unknown. */
|
|
45
|
+
resolveAccountId(email: string): Promise<string | null>;
|
|
46
|
+
/**
|
|
47
|
+
* Account behind a stable key. The inverse direction, for a caller that
|
|
48
|
+
* already holds one (a resource owner) and needs the current address.
|
|
49
|
+
*/
|
|
50
|
+
resolveAccountById(userId: string): Promise<ResolvedAccount | null>;
|
|
51
|
+
/**
|
|
52
|
+
* Resolve-or-create, for a login path that may see a first-time user.
|
|
53
|
+
*
|
|
54
|
+
* Returns null when the address exists but cannot be resolved (a revoked
|
|
55
|
+
* address) — a caller that needs "guaranteed account" must distinguish that
|
|
56
|
+
* from "created now" through its own route, because silently registering on a
|
|
57
|
+
* revoked address would hand the previous holder's history to the new one.
|
|
58
|
+
*/
|
|
59
|
+
resolveOrRegister(email: string, now?: number): Promise<ResolvedAccount | null>;
|
|
60
|
+
/**
|
|
61
|
+
* Every address known for an account, verified ones first.
|
|
62
|
+
*/
|
|
63
|
+
listAddresses(userId: string): Promise<Array<{
|
|
64
|
+
email: string;
|
|
65
|
+
isVerified: boolean;
|
|
66
|
+
}>>;
|
|
67
|
+
/**
|
|
68
|
+
* Point an account at a new sign-in address.
|
|
69
|
+
*
|
|
70
|
+
* The account id, the frozen anchor, and every id-keyed row are untouched:
|
|
71
|
+
* only which address authenticates the account moves. The previous address is
|
|
72
|
+
* revoked rather than deleted, so rows written before the change still resolve
|
|
73
|
+
* to this account, and it is released for a later legitimate holder.
|
|
74
|
+
*
|
|
75
|
+
* Rejects an address already verified for another account. That check is the
|
|
76
|
+
* whole reason this is not simply an `UPDATE`: an unverified change would let
|
|
77
|
+
* anyone claim an address and inherit its history.
|
|
78
|
+
*
|
|
79
|
+
* Order is load-bearing, and it is the one the ops CLI (`change-email.ts`)
|
|
80
|
+
* already gets right: claim first, move `current_email` second, revoke third.
|
|
81
|
+
* Inverting it opens a window in which nothing authenticates if a later step
|
|
82
|
+
* fails.
|
|
83
|
+
*/
|
|
84
|
+
setPrimaryEmail(userId: string, newEmail: string, now?: number): Promise<ResolvedAccount>;
|
|
85
|
+
/**
|
|
86
|
+
* Ops/migration path: attach an address that has already been proven, without
|
|
87
|
+
* making it the sign-in address.
|
|
88
|
+
*/
|
|
89
|
+
linkVerifiedEmail(userId: string, email: string, now?: number): Promise<void>;
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=UserIdentityService.d.ts.map
|
|
@@ -0,0 +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,CAC5B,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,EACb,GAAG,SAAmD,GACrD,OAAO,CAAC,IAAI,CAAC;CAOjB"}
|