@aooth/user 0.1.6 → 0.1.8

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.
@@ -1,5 +1,77 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_user_store = require("./user-store-BPZVAboN.cjs");
2
+ const require_federated_identity_store = require("./federated-identity-store-oRjhnR5l.cjs");
3
+ let node_crypto = require("node:crypto");
4
+ //#region src/atscript-db/federated-identity-store.ts
5
+ function isConflict$1(err) {
6
+ return typeof err === "object" && err !== null && err.code === "CONFLICT";
7
+ }
8
+ /**
9
+ * `@atscript/db`-backed {@link FederatedIdentityStore}. Pass the resolved table
10
+ * for the `AoothFederatedIdentity` model shipped at
11
+ * `@aooth/user/atscript-db/federated-model`. The compound-unique
12
+ * `(provider, subject)` index makes `find` an O(1) point read and turns a
13
+ * cross-user re-link attempt into a `CONFLICT` (→ `ALREADY_EXISTS`); `userId`
14
+ * is a plain indexed column, so `listForUser` / `deleteAllForUser` scan it
15
+ * natively.
16
+ */
17
+ var FederatedIdentityStoreAtscriptDb = class extends require_federated_identity_store.FederatedIdentityStore {
18
+ table;
19
+ clock;
20
+ constructor(opts) {
21
+ super();
22
+ this.table = opts.table;
23
+ this.clock = opts.clock ?? Date.now;
24
+ }
25
+ async find(provider, subject) {
26
+ return this.table.findOne({ filter: {
27
+ provider,
28
+ subject
29
+ } });
30
+ }
31
+ async listForUser(userId) {
32
+ return (await this.table.findMany({ filter: { userId } })).toSorted((a, b) => a.linkedAt - b.linkedAt);
33
+ }
34
+ async link(rec) {
35
+ const row = {
36
+ id: (0, node_crypto.randomUUID)(),
37
+ provider: rec.provider,
38
+ subject: rec.subject,
39
+ userId: rec.userId,
40
+ ...require_federated_identity_store.pickDefinedProfile(rec),
41
+ linkedAt: this.clock()
42
+ };
43
+ try {
44
+ await this.table.insertOne(row);
45
+ return row;
46
+ } catch (e) {
47
+ if (isConflict$1(e)) throw new require_federated_identity_store.UserAuthError("ALREADY_EXISTS", `Provider account "${rec.provider}:${rec.subject}" is already linked`);
48
+ throw e;
49
+ }
50
+ }
51
+ async unlink(provider, subject) {
52
+ return (await this.table.deleteMany({
53
+ provider,
54
+ subject
55
+ })).deletedCount > 0;
56
+ }
57
+ async touchLogin(provider, subject, profile) {
58
+ const row = await this.table.findOne({ filter: {
59
+ provider,
60
+ subject
61
+ } });
62
+ if (!row) return;
63
+ const next = {
64
+ ...row,
65
+ lastLoginAt: this.clock()
66
+ };
67
+ if (profile) Object.assign(next, require_federated_identity_store.pickDefinedProfile(profile));
68
+ await this.table.replaceOne(next);
69
+ }
70
+ async deleteAllForUser(userId) {
71
+ return (await this.table.deleteMany({ userId })).deletedCount;
72
+ }
73
+ };
74
+ //#endregion
3
75
  //#region src/atscript-db/index.ts
4
76
  function isConflict(err) {
5
77
  return typeof err === "object" && err !== null && err.code === "CONFLICT";
@@ -7,62 +79,76 @@ function isConflict(err) {
7
79
  /**
8
80
  * `@atscript/db`-backed `UserStore`. Pass the resolved table for the
9
81
  * `AoothUserCredentials` (or a `.as` interface extending it) shipped at the
10
- * `@aooth/user/atscript-db/model.as` subpath.
82
+ * `@aooth/user/atscript-db/model.as` subpath. Identity reads use the
83
+ * `@meta.id` PK (`id`) plus the unique `username` / `email` indexes; writes key
84
+ * on `id`.
11
85
  */
12
- var UsersStoreAtscriptDb = class extends require_user_store.UserStore {
86
+ var UsersStoreAtscriptDb = class extends require_federated_identity_store.UserStore {
13
87
  table;
14
88
  constructor(opts) {
15
89
  super();
16
90
  this.table = opts.table;
17
91
  }
18
- async exists(username) {
19
- return await this.table.count({ filter: { username } }) > 0;
92
+ async exists(handle) {
93
+ return await this.table.count({ filter: { username: handle } }) > 0;
94
+ }
95
+ async findById(id) {
96
+ return await this.table.findOne({ filter: { id } });
97
+ }
98
+ async findByHandle(handle) {
99
+ const byUsername = await this.table.findOne({ filter: { username: handle } });
100
+ if (byUsername) return byUsername;
101
+ return await this.table.findOne({ filter: { email: handle } });
20
102
  }
21
- async findByUsername(username) {
22
- return await this.table.findOne({ filter: { username } }) ?? null;
103
+ async findByIdentifier(value) {
104
+ const byId = await this.table.findOne({ filter: { id: value } });
105
+ if (byId) return byId;
106
+ return this.findByHandle(value);
23
107
  }
24
108
  async create(data) {
25
109
  try {
26
110
  await this.table.insertOne(data);
27
111
  } catch (e) {
28
- if (isConflict(e)) throw new require_user_store.UserAuthError("ALREADY_EXISTS", `User "${data.username}" already exists`);
112
+ if (isConflict(e)) throw new require_federated_identity_store.UserAuthError("ALREADY_EXISTS", `User "${data.username}" already exists`);
29
113
  throw e;
30
114
  }
31
115
  }
32
- async update(username, update) {
33
- const patch = { username };
116
+ async update(id, update) {
117
+ const patch = { id };
34
118
  if (update.set) Object.assign(patch, update.set);
35
- if (update.inc) for (const [path, amount] of Object.entries(update.inc)) require_user_store.setAtPath(patch, path, { $inc: amount });
119
+ if (update.inc) for (const [path, amount] of Object.entries(update.inc)) require_federated_identity_store.setAtPath(patch, path, { $inc: amount });
36
120
  if (Object.keys(patch).length <= 1) return true;
37
121
  if (update.expectedVersion !== void 0) patch.$cas = { version: update.expectedVersion };
38
122
  return (await this.table.updateOne(patch)).matchedCount > 0;
39
123
  }
40
- async delete(username) {
41
- return (await this.table.deleteMany({ username })).deletedCount > 0;
124
+ async delete(id) {
125
+ return (await this.table.deleteMany({ id })).deletedCount > 0;
42
126
  }
43
127
  /**
44
128
  * Inline retry loop rather than delegating to @atscript/db's
45
129
  * `withOptimisticRetry`: that helper expects the mutator to always return a
46
130
  * patch object, but our contract lets the mutator return `null` (the
47
- * race-loser detects nothing to do — see `UserService.consumeBackupCode`).
48
- * Bridging would need a sentinel exception. The version-bump + $cas
49
- * atomicity still happen at the atscript-db table layer via the
50
- * `expectedVersion` we thread through `update()`.
131
+ * race-loser detects nothing to do — used by callers whose mutator opts
132
+ * out of a write after re-reading state). Bridging would need a sentinel
133
+ * exception. The version-bump + $cas atomicity still happen at the
134
+ * atscript-db table layer via the `expectedVersion` we thread through
135
+ * `update()`.
51
136
  */
52
- async withCas(username, mutator, opts) {
137
+ async withCas(id, mutator, opts) {
53
138
  const maxAttempts = opts?.maxAttempts ?? 2;
54
139
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
55
- const current = await this.findByUsername(username);
56
- if (!current) throw new require_user_store.UserAuthError("NOT_FOUND");
140
+ const current = await this.findById(id);
141
+ if (!current) throw new require_federated_identity_store.UserAuthError("NOT_FOUND");
57
142
  const patch = mutator(current);
58
143
  if (patch === null) return;
59
- if (await this.update(username, {
144
+ if (await this.update(id, {
60
145
  ...patch,
61
146
  expectedVersion: current.version ?? 0
62
147
  })) return;
63
148
  }
64
- throw new require_user_store.UserAuthError("CAS_EXHAUSTED");
149
+ throw new require_federated_identity_store.UserAuthError("CAS_EXHAUSTED");
65
150
  }
66
151
  };
67
152
  //#endregion
153
+ exports.FederatedIdentityStoreAtscriptDb = FederatedIdentityStoreAtscriptDb;
68
154
  exports.UsersStoreAtscriptDb = UsersStoreAtscriptDb;
@@ -1,5 +1,57 @@
1
- import { S as UserCredentials, n as WithCasOptions, t as UserStore, w as UserStoreUpdate } from "./user-store-B3EStUfT.cjs";
1
+ import { D as UserCredentials, i as NewFederatedIdentity, k as UserStoreUpdate, n as FederatedIdentityStore, o as UserStore, r as FederatedProfileSnapshot, s as WithCasOptions, t as FederatedIdentity } from "./federated-identity-store-DEEed8lA.cjs";
2
2
 
3
+ //#region src/atscript-db/federated-identity-store.d.ts
4
+ /**
5
+ * Structural surface of `AtscriptDbTable` covering exactly the methods this
6
+ * adapter calls — kept loose so `@aooth/user` doesn't pull `@atscript/db` types
7
+ * into its public surface. Consumers pass `db.getTable(AoothFederatedIdentity)`
8
+ * directly and TypeScript matches by-shape. Mirrors `AuthCredentialTable` from
9
+ * `@aooth/auth/atscript-db`.
10
+ */
11
+ interface FederatedIdentityTable {
12
+ insertOne(row: FederatedIdentity): Promise<{
13
+ insertedId: unknown;
14
+ }>;
15
+ findOne(query: {
16
+ filter: Record<string, unknown>;
17
+ }): Promise<FederatedIdentity | null>;
18
+ findMany(query: {
19
+ filter?: Record<string, unknown>;
20
+ }): Promise<FederatedIdentity[]>;
21
+ replaceOne(row: FederatedIdentity): Promise<{
22
+ matchedCount: number;
23
+ modifiedCount: number;
24
+ }>;
25
+ deleteMany(filter: Record<string, unknown>): Promise<{
26
+ deletedCount: number;
27
+ }>;
28
+ }
29
+ interface FederatedIdentityStoreAtscriptDbOptions {
30
+ table: FederatedIdentityTable;
31
+ /** Injectable clock for `linkedAt` / `lastLoginAt`. Defaults to `Date.now`. */
32
+ clock?: () => number;
33
+ }
34
+ /**
35
+ * `@atscript/db`-backed {@link FederatedIdentityStore}. Pass the resolved table
36
+ * for the `AoothFederatedIdentity` model shipped at
37
+ * `@aooth/user/atscript-db/federated-model`. The compound-unique
38
+ * `(provider, subject)` index makes `find` an O(1) point read and turns a
39
+ * cross-user re-link attempt into a `CONFLICT` (→ `ALREADY_EXISTS`); `userId`
40
+ * is a plain indexed column, so `listForUser` / `deleteAllForUser` scan it
41
+ * natively.
42
+ */
43
+ declare class FederatedIdentityStoreAtscriptDb extends FederatedIdentityStore {
44
+ private readonly table;
45
+ private readonly clock;
46
+ constructor(opts: FederatedIdentityStoreAtscriptDbOptions);
47
+ find(provider: string, subject: string): Promise<FederatedIdentity | null>;
48
+ listForUser(userId: string): Promise<FederatedIdentity[]>;
49
+ link(rec: NewFederatedIdentity): Promise<FederatedIdentity>;
50
+ unlink(provider: string, subject: string): Promise<boolean>;
51
+ touchLogin(provider: string, subject: string, profile?: FederatedProfileSnapshot): Promise<void>;
52
+ deleteAllForUser(userId: string): Promise<number>;
53
+ }
54
+ //#endregion
3
55
  //#region src/atscript-db/index.d.ts
4
56
  /**
5
57
  * Persisted row shape — `UserCredentials` plus the consumer's custom user
@@ -40,26 +92,31 @@ interface UsersStoreAtscriptDbOptions<TUserCustom extends object> {
40
92
  /**
41
93
  * `@atscript/db`-backed `UserStore`. Pass the resolved table for the
42
94
  * `AoothUserCredentials` (or a `.as` interface extending it) shipped at the
43
- * `@aooth/user/atscript-db/model.as` subpath.
95
+ * `@aooth/user/atscript-db/model.as` subpath. Identity reads use the
96
+ * `@meta.id` PK (`id`) plus the unique `username` / `email` indexes; writes key
97
+ * on `id`.
44
98
  */
45
99
  declare class UsersStoreAtscriptDb<TUserCustom extends object = object> extends UserStore<TUserCustom> {
46
100
  protected table: AuthUserTable<TUserCustom>;
47
101
  constructor(opts: UsersStoreAtscriptDbOptions<TUserCustom>);
48
- exists(username: string): Promise<boolean>;
49
- findByUsername(username: string): Promise<(UserCredentials & TUserCustom) | null>;
102
+ exists(handle: string): Promise<boolean>;
103
+ findById(id: string): Promise<(UserCredentials & TUserCustom) | null>;
104
+ findByHandle(handle: string): Promise<(UserCredentials & TUserCustom) | null>;
105
+ findByIdentifier(value: string): Promise<(UserCredentials & TUserCustom) | null>;
50
106
  create(data: UserCredentials & TUserCustom): Promise<void>;
51
- update(username: string, update: UserStoreUpdate): Promise<boolean>;
52
- delete(username: string): Promise<boolean>;
107
+ update(id: string, update: UserStoreUpdate): Promise<boolean>;
108
+ delete(id: string): Promise<boolean>;
53
109
  /**
54
110
  * Inline retry loop rather than delegating to @atscript/db's
55
111
  * `withOptimisticRetry`: that helper expects the mutator to always return a
56
112
  * patch object, but our contract lets the mutator return `null` (the
57
- * race-loser detects nothing to do — see `UserService.consumeBackupCode`).
58
- * Bridging would need a sentinel exception. The version-bump + $cas
59
- * atomicity still happen at the atscript-db table layer via the
60
- * `expectedVersion` we thread through `update()`.
113
+ * race-loser detects nothing to do — used by callers whose mutator opts
114
+ * out of a write after re-reading state). Bridging would need a sentinel
115
+ * exception. The version-bump + $cas atomicity still happen at the
116
+ * atscript-db table layer via the `expectedVersion` we thread through
117
+ * `update()`.
61
118
  */
62
- withCas(username: string, mutator: (current: UserCredentials & TUserCustom) => UserStoreUpdate | null, opts?: WithCasOptions): Promise<void>;
119
+ withCas(id: string, mutator: (current: UserCredentials & TUserCustom) => UserStoreUpdate | null, opts?: WithCasOptions): Promise<void>;
63
120
  }
64
121
  //#endregion
65
- export { AuthUserTable, UserCredentialsRow, UsersStoreAtscriptDb };
122
+ export { AuthUserTable, FederatedIdentityStoreAtscriptDb, type FederatedIdentityTable, UserCredentialsRow, UsersStoreAtscriptDb };
@@ -1,5 +1,57 @@
1
- import { S as UserCredentials, n as WithCasOptions, t as UserStore, w as UserStoreUpdate } from "./user-store-C1lxahSB.mjs";
1
+ import { D as UserCredentials, i as NewFederatedIdentity, k as UserStoreUpdate, n as FederatedIdentityStore, o as UserStore, r as FederatedProfileSnapshot, s as WithCasOptions, t as FederatedIdentity } from "./federated-identity-store-DEEed8lA.mjs";
2
2
 
3
+ //#region src/atscript-db/federated-identity-store.d.ts
4
+ /**
5
+ * Structural surface of `AtscriptDbTable` covering exactly the methods this
6
+ * adapter calls — kept loose so `@aooth/user` doesn't pull `@atscript/db` types
7
+ * into its public surface. Consumers pass `db.getTable(AoothFederatedIdentity)`
8
+ * directly and TypeScript matches by-shape. Mirrors `AuthCredentialTable` from
9
+ * `@aooth/auth/atscript-db`.
10
+ */
11
+ interface FederatedIdentityTable {
12
+ insertOne(row: FederatedIdentity): Promise<{
13
+ insertedId: unknown;
14
+ }>;
15
+ findOne(query: {
16
+ filter: Record<string, unknown>;
17
+ }): Promise<FederatedIdentity | null>;
18
+ findMany(query: {
19
+ filter?: Record<string, unknown>;
20
+ }): Promise<FederatedIdentity[]>;
21
+ replaceOne(row: FederatedIdentity): Promise<{
22
+ matchedCount: number;
23
+ modifiedCount: number;
24
+ }>;
25
+ deleteMany(filter: Record<string, unknown>): Promise<{
26
+ deletedCount: number;
27
+ }>;
28
+ }
29
+ interface FederatedIdentityStoreAtscriptDbOptions {
30
+ table: FederatedIdentityTable;
31
+ /** Injectable clock for `linkedAt` / `lastLoginAt`. Defaults to `Date.now`. */
32
+ clock?: () => number;
33
+ }
34
+ /**
35
+ * `@atscript/db`-backed {@link FederatedIdentityStore}. Pass the resolved table
36
+ * for the `AoothFederatedIdentity` model shipped at
37
+ * `@aooth/user/atscript-db/federated-model`. The compound-unique
38
+ * `(provider, subject)` index makes `find` an O(1) point read and turns a
39
+ * cross-user re-link attempt into a `CONFLICT` (→ `ALREADY_EXISTS`); `userId`
40
+ * is a plain indexed column, so `listForUser` / `deleteAllForUser` scan it
41
+ * natively.
42
+ */
43
+ declare class FederatedIdentityStoreAtscriptDb extends FederatedIdentityStore {
44
+ private readonly table;
45
+ private readonly clock;
46
+ constructor(opts: FederatedIdentityStoreAtscriptDbOptions);
47
+ find(provider: string, subject: string): Promise<FederatedIdentity | null>;
48
+ listForUser(userId: string): Promise<FederatedIdentity[]>;
49
+ link(rec: NewFederatedIdentity): Promise<FederatedIdentity>;
50
+ unlink(provider: string, subject: string): Promise<boolean>;
51
+ touchLogin(provider: string, subject: string, profile?: FederatedProfileSnapshot): Promise<void>;
52
+ deleteAllForUser(userId: string): Promise<number>;
53
+ }
54
+ //#endregion
3
55
  //#region src/atscript-db/index.d.ts
4
56
  /**
5
57
  * Persisted row shape — `UserCredentials` plus the consumer's custom user
@@ -40,26 +92,31 @@ interface UsersStoreAtscriptDbOptions<TUserCustom extends object> {
40
92
  /**
41
93
  * `@atscript/db`-backed `UserStore`. Pass the resolved table for the
42
94
  * `AoothUserCredentials` (or a `.as` interface extending it) shipped at the
43
- * `@aooth/user/atscript-db/model.as` subpath.
95
+ * `@aooth/user/atscript-db/model.as` subpath. Identity reads use the
96
+ * `@meta.id` PK (`id`) plus the unique `username` / `email` indexes; writes key
97
+ * on `id`.
44
98
  */
45
99
  declare class UsersStoreAtscriptDb<TUserCustom extends object = object> extends UserStore<TUserCustom> {
46
100
  protected table: AuthUserTable<TUserCustom>;
47
101
  constructor(opts: UsersStoreAtscriptDbOptions<TUserCustom>);
48
- exists(username: string): Promise<boolean>;
49
- findByUsername(username: string): Promise<(UserCredentials & TUserCustom) | null>;
102
+ exists(handle: string): Promise<boolean>;
103
+ findById(id: string): Promise<(UserCredentials & TUserCustom) | null>;
104
+ findByHandle(handle: string): Promise<(UserCredentials & TUserCustom) | null>;
105
+ findByIdentifier(value: string): Promise<(UserCredentials & TUserCustom) | null>;
50
106
  create(data: UserCredentials & TUserCustom): Promise<void>;
51
- update(username: string, update: UserStoreUpdate): Promise<boolean>;
52
- delete(username: string): Promise<boolean>;
107
+ update(id: string, update: UserStoreUpdate): Promise<boolean>;
108
+ delete(id: string): Promise<boolean>;
53
109
  /**
54
110
  * Inline retry loop rather than delegating to @atscript/db's
55
111
  * `withOptimisticRetry`: that helper expects the mutator to always return a
56
112
  * patch object, but our contract lets the mutator return `null` (the
57
- * race-loser detects nothing to do — see `UserService.consumeBackupCode`).
58
- * Bridging would need a sentinel exception. The version-bump + $cas
59
- * atomicity still happen at the atscript-db table layer via the
60
- * `expectedVersion` we thread through `update()`.
113
+ * race-loser detects nothing to do — used by callers whose mutator opts
114
+ * out of a write after re-reading state). Bridging would need a sentinel
115
+ * exception. The version-bump + $cas atomicity still happen at the
116
+ * atscript-db table layer via the `expectedVersion` we thread through
117
+ * `update()`.
61
118
  */
62
- withCas(username: string, mutator: (current: UserCredentials & TUserCustom) => UserStoreUpdate | null, opts?: WithCasOptions): Promise<void>;
119
+ withCas(id: string, mutator: (current: UserCredentials & TUserCustom) => UserStoreUpdate | null, opts?: WithCasOptions): Promise<void>;
63
120
  }
64
121
  //#endregion
65
- export { AuthUserTable, UserCredentialsRow, UsersStoreAtscriptDb };
122
+ export { AuthUserTable, FederatedIdentityStoreAtscriptDb, type FederatedIdentityTable, UserCredentialsRow, UsersStoreAtscriptDb };
@@ -1,4 +1,76 @@
1
- import { c as setAtPath, l as UserAuthError, t as UserStore } from "./user-store-BaBmH13V.mjs";
1
+ import { d as UserAuthError, n as pickDefinedProfile, r as UserStore, t as FederatedIdentityStore, u as setAtPath } from "./federated-identity-store-Cmc7jBrw.mjs";
2
+ import { randomUUID } from "node:crypto";
3
+ //#region src/atscript-db/federated-identity-store.ts
4
+ function isConflict$1(err) {
5
+ return typeof err === "object" && err !== null && err.code === "CONFLICT";
6
+ }
7
+ /**
8
+ * `@atscript/db`-backed {@link FederatedIdentityStore}. Pass the resolved table
9
+ * for the `AoothFederatedIdentity` model shipped at
10
+ * `@aooth/user/atscript-db/federated-model`. The compound-unique
11
+ * `(provider, subject)` index makes `find` an O(1) point read and turns a
12
+ * cross-user re-link attempt into a `CONFLICT` (→ `ALREADY_EXISTS`); `userId`
13
+ * is a plain indexed column, so `listForUser` / `deleteAllForUser` scan it
14
+ * natively.
15
+ */
16
+ var FederatedIdentityStoreAtscriptDb = class extends FederatedIdentityStore {
17
+ table;
18
+ clock;
19
+ constructor(opts) {
20
+ super();
21
+ this.table = opts.table;
22
+ this.clock = opts.clock ?? Date.now;
23
+ }
24
+ async find(provider, subject) {
25
+ return this.table.findOne({ filter: {
26
+ provider,
27
+ subject
28
+ } });
29
+ }
30
+ async listForUser(userId) {
31
+ return (await this.table.findMany({ filter: { userId } })).toSorted((a, b) => a.linkedAt - b.linkedAt);
32
+ }
33
+ async link(rec) {
34
+ const row = {
35
+ id: randomUUID(),
36
+ provider: rec.provider,
37
+ subject: rec.subject,
38
+ userId: rec.userId,
39
+ ...pickDefinedProfile(rec),
40
+ linkedAt: this.clock()
41
+ };
42
+ try {
43
+ await this.table.insertOne(row);
44
+ return row;
45
+ } catch (e) {
46
+ if (isConflict$1(e)) throw new UserAuthError("ALREADY_EXISTS", `Provider account "${rec.provider}:${rec.subject}" is already linked`);
47
+ throw e;
48
+ }
49
+ }
50
+ async unlink(provider, subject) {
51
+ return (await this.table.deleteMany({
52
+ provider,
53
+ subject
54
+ })).deletedCount > 0;
55
+ }
56
+ async touchLogin(provider, subject, profile) {
57
+ const row = await this.table.findOne({ filter: {
58
+ provider,
59
+ subject
60
+ } });
61
+ if (!row) return;
62
+ const next = {
63
+ ...row,
64
+ lastLoginAt: this.clock()
65
+ };
66
+ if (profile) Object.assign(next, pickDefinedProfile(profile));
67
+ await this.table.replaceOne(next);
68
+ }
69
+ async deleteAllForUser(userId) {
70
+ return (await this.table.deleteMany({ userId })).deletedCount;
71
+ }
72
+ };
73
+ //#endregion
2
74
  //#region src/atscript-db/index.ts
3
75
  function isConflict(err) {
4
76
  return typeof err === "object" && err !== null && err.code === "CONFLICT";
@@ -6,7 +78,9 @@ function isConflict(err) {
6
78
  /**
7
79
  * `@atscript/db`-backed `UserStore`. Pass the resolved table for the
8
80
  * `AoothUserCredentials` (or a `.as` interface extending it) shipped at the
9
- * `@aooth/user/atscript-db/model.as` subpath.
81
+ * `@aooth/user/atscript-db/model.as` subpath. Identity reads use the
82
+ * `@meta.id` PK (`id`) plus the unique `username` / `email` indexes; writes key
83
+ * on `id`.
10
84
  */
11
85
  var UsersStoreAtscriptDb = class extends UserStore {
12
86
  table;
@@ -14,11 +88,21 @@ var UsersStoreAtscriptDb = class extends UserStore {
14
88
  super();
15
89
  this.table = opts.table;
16
90
  }
17
- async exists(username) {
18
- return await this.table.count({ filter: { username } }) > 0;
91
+ async exists(handle) {
92
+ return await this.table.count({ filter: { username: handle } }) > 0;
93
+ }
94
+ async findById(id) {
95
+ return await this.table.findOne({ filter: { id } });
96
+ }
97
+ async findByHandle(handle) {
98
+ const byUsername = await this.table.findOne({ filter: { username: handle } });
99
+ if (byUsername) return byUsername;
100
+ return await this.table.findOne({ filter: { email: handle } });
19
101
  }
20
- async findByUsername(username) {
21
- return await this.table.findOne({ filter: { username } }) ?? null;
102
+ async findByIdentifier(value) {
103
+ const byId = await this.table.findOne({ filter: { id: value } });
104
+ if (byId) return byId;
105
+ return this.findByHandle(value);
22
106
  }
23
107
  async create(data) {
24
108
  try {
@@ -28,34 +112,35 @@ var UsersStoreAtscriptDb = class extends UserStore {
28
112
  throw e;
29
113
  }
30
114
  }
31
- async update(username, update) {
32
- const patch = { username };
115
+ async update(id, update) {
116
+ const patch = { id };
33
117
  if (update.set) Object.assign(patch, update.set);
34
118
  if (update.inc) for (const [path, amount] of Object.entries(update.inc)) setAtPath(patch, path, { $inc: amount });
35
119
  if (Object.keys(patch).length <= 1) return true;
36
120
  if (update.expectedVersion !== void 0) patch.$cas = { version: update.expectedVersion };
37
121
  return (await this.table.updateOne(patch)).matchedCount > 0;
38
122
  }
39
- async delete(username) {
40
- return (await this.table.deleteMany({ username })).deletedCount > 0;
123
+ async delete(id) {
124
+ return (await this.table.deleteMany({ id })).deletedCount > 0;
41
125
  }
42
126
  /**
43
127
  * Inline retry loop rather than delegating to @atscript/db's
44
128
  * `withOptimisticRetry`: that helper expects the mutator to always return a
45
129
  * patch object, but our contract lets the mutator return `null` (the
46
- * race-loser detects nothing to do — see `UserService.consumeBackupCode`).
47
- * Bridging would need a sentinel exception. The version-bump + $cas
48
- * atomicity still happen at the atscript-db table layer via the
49
- * `expectedVersion` we thread through `update()`.
130
+ * race-loser detects nothing to do — used by callers whose mutator opts
131
+ * out of a write after re-reading state). Bridging would need a sentinel
132
+ * exception. The version-bump + $cas atomicity still happen at the
133
+ * atscript-db table layer via the `expectedVersion` we thread through
134
+ * `update()`.
50
135
  */
51
- async withCas(username, mutator, opts) {
136
+ async withCas(id, mutator, opts) {
52
137
  const maxAttempts = opts?.maxAttempts ?? 2;
53
138
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
54
- const current = await this.findByUsername(username);
139
+ const current = await this.findById(id);
55
140
  if (!current) throw new UserAuthError("NOT_FOUND");
56
141
  const patch = mutator(current);
57
142
  if (patch === null) return;
58
- if (await this.update(username, {
143
+ if (await this.update(id, {
59
144
  ...patch,
60
145
  expectedVersion: current.version ?? 0
61
146
  })) return;
@@ -64,4 +149,4 @@ var UsersStoreAtscriptDb = class extends UserStore {
64
149
  }
65
150
  };
66
151
  //#endregion
67
- export { UsersStoreAtscriptDb };
152
+ export { FederatedIdentityStoreAtscriptDb, UsersStoreAtscriptDb };
@@ -15,6 +15,8 @@ const defaultMessages = {
15
15
  CAS_EXHAUSTED: "Update conflict — please retry"
16
16
  };
17
17
  var UserAuthError = class extends Error {
18
+ type;
19
+ details;
18
20
  name = "UserAuthError";
19
21
  constructor(type, message, details) {
20
22
  super(message ?? defaultMessages[type]);
@@ -92,6 +94,43 @@ function incrementAtPath(obj, path, amount) {
92
94
  }
93
95
  //#endregion
94
96
  //#region src/store/user-store.ts
97
+ /**
98
+ * Storage seam for user credentials, keyed by the stable surrogate **`id`**
99
+ * (the token subject). Reads come in three flavours:
100
+ *
101
+ * - `findById` — strict, by the surrogate id; the canonical identity read used
102
+ * by authenticated flows that resolve the session subject (`getUserId()`).
103
+ * - `findByHandle` — deterministic LOGIN resolver (`username` then `email`).
104
+ * - `findByIdentifier` — permissive internal/admin/recovery lookup (`id`, then
105
+ * `username`, then `email`).
106
+ *
107
+ * Writes (`update`/`delete`/`withCas`) all key on the surrogate `id`.
108
+ */
95
109
  var UserStore = class {};
96
110
  //#endregion
97
- export { maskEmail as a, setAtPath as c, incrementAtPath as i, UserAuthError as l, deepMerge as n, maskMfaValue as o, generateSecureRandom as r, maskPhone as s, UserStore as t };
111
+ //#region src/store/federated-identity-store.ts
112
+ /**
113
+ * Copy only the DEFINED display fields — so a `touchLogin` / `link` with a
114
+ * partial profile (e.g. Apple omitting the email on a repeat login) never
115
+ * overwrites a stored value with `undefined`. Shared by every
116
+ * {@link FederatedIdentityStore} impl.
117
+ */
118
+ function pickDefinedProfile(src) {
119
+ const out = {};
120
+ if (src.email !== void 0) out.email = src.email;
121
+ if (src.emailVerified !== void 0) out.emailVerified = src.emailVerified;
122
+ if (src.displayName !== void 0) out.displayName = src.displayName;
123
+ if (src.avatarUrl !== void 0) out.avatarUrl = src.avatarUrl;
124
+ return out;
125
+ }
126
+ /**
127
+ * Storage seam for the account-linking table (RFC IDP.md §3.3). The stable
128
+ * lookup key is the composite `(provider, subject)`; a user may own many rows
129
+ * (one per linked provider account), so `userId` reads return a list.
130
+ *
131
+ * In-memory + atscript-db implementations ship alongside; the abstract surface
132
+ * keeps the federated-login core (`@aooth/idp`, phase 2) storage-agnostic.
133
+ */
134
+ var FederatedIdentityStore = class {};
135
+ //#endregion
136
+ export { generateSecureRandom as a, maskMfaValue as c, UserAuthError as d, deepMerge as i, maskPhone as l, pickDefinedProfile as n, incrementAtPath as o, UserStore as r, maskEmail as s, FederatedIdentityStore as t, setAtPath as u };