@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.
- package/dist/atscript-db.cjs +108 -22
- package/dist/atscript-db.d.cts +69 -12
- package/dist/atscript-db.d.mts +69 -12
- package/dist/atscript-db.mjs +103 -18
- package/dist/{user-store-BaBmH13V.mjs → federated-identity-store-Cmc7jBrw.mjs} +40 -1
- package/dist/federated-identity-store-DEEed8lA.d.cts +378 -0
- package/dist/federated-identity-store-DEEed8lA.d.mts +378 -0
- package/dist/{user-store-BPZVAboN.cjs → federated-identity-store-oRjhnR5l.cjs} +51 -0
- package/dist/index.cjs +301 -220
- package/dist/index.d.cts +114 -72
- package/dist/index.d.mts +114 -72
- package/dist/index.mjs +268 -189
- package/package.json +23 -9
- package/src/atscript-db/federated-identity.as +44 -0
- package/src/atscript-db/federated-identity.as.d.ts +62 -0
- package/src/atscript-db/user-credentials.as +7 -2
- package/src/atscript-db/user-credentials.as.d.ts +62 -0
- package/dist/user-store-B3EStUfT.d.cts +0 -246
- package/dist/user-store-C1lxahSB.d.mts +0 -246
package/dist/atscript-db.cjs
CHANGED
|
@@ -1,5 +1,77 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const
|
|
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
|
|
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(
|
|
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
|
|
22
|
-
|
|
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
|
|
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(
|
|
33
|
-
const patch = {
|
|
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))
|
|
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(
|
|
41
|
-
return (await this.table.deleteMany({
|
|
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 —
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
* `expectedVersion` we thread through
|
|
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(
|
|
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.
|
|
56
|
-
if (!current) throw new
|
|
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(
|
|
144
|
+
if (await this.update(id, {
|
|
60
145
|
...patch,
|
|
61
146
|
expectedVersion: current.version ?? 0
|
|
62
147
|
})) return;
|
|
63
148
|
}
|
|
64
|
-
throw new
|
|
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;
|
package/dist/atscript-db.d.cts
CHANGED
|
@@ -1,5 +1,57 @@
|
|
|
1
|
-
import {
|
|
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(
|
|
49
|
-
|
|
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(
|
|
52
|
-
delete(
|
|
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 —
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
* `expectedVersion` we thread through
|
|
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(
|
|
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 };
|
package/dist/atscript-db.d.mts
CHANGED
|
@@ -1,5 +1,57 @@
|
|
|
1
|
-
import {
|
|
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(
|
|
49
|
-
|
|
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(
|
|
52
|
-
delete(
|
|
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 —
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
* `expectedVersion` we thread through
|
|
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(
|
|
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 };
|
package/dist/atscript-db.mjs
CHANGED
|
@@ -1,4 +1,76 @@
|
|
|
1
|
-
import {
|
|
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(
|
|
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
|
|
21
|
-
|
|
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(
|
|
32
|
-
const patch = {
|
|
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(
|
|
40
|
-
return (await this.table.deleteMany({
|
|
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 —
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
* `expectedVersion` we thread through
|
|
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(
|
|
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.
|
|
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(
|
|
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
|
-
|
|
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 };
|