@yielded/auth-persistence 0.1.0-beta.22 → 0.1.0-beta.23

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.
Files changed (48) hide show
  1. package/dist/Adapter.d.mts +4 -2
  2. package/dist/Adapter.mjs +3 -1
  3. package/dist/internal/authority-snapshot.mjs +53 -0
  4. package/dist/internal/authority-snapshot.mjs.map +1 -0
  5. package/dist/internal/configuration.d.mts +1 -1
  6. package/dist/internal/configuration.mjs.map +1 -1
  7. package/dist/internal/models/password-model.d.mts +3 -53
  8. package/dist/internal/models/password-model.mjs +0 -2
  9. package/dist/internal/models/password-model.mjs.map +1 -1
  10. package/dist/internal/native-sql-table.d.mts +30 -0
  11. package/dist/internal/native-sql-table.mjs +104 -0
  12. package/dist/internal/native-sql-table.mjs.map +1 -0
  13. package/dist/internal/passkey/target.d.mts +1 -1
  14. package/dist/internal/passkeys.mjs +1 -1
  15. package/dist/internal/password-attempts.d.mts +12 -0
  16. package/dist/internal/password-attempts.mjs +197 -0
  17. package/dist/internal/password-attempts.mjs.map +1 -0
  18. package/dist/internal/password-kernel.d.mts +14 -11
  19. package/dist/internal/password-kernel.mjs +39 -202
  20. package/dist/internal/password-kernel.mjs.map +1 -1
  21. package/dist/internal/persistence.d.mts +2 -0
  22. package/dist/internal/persistence.mjs +4 -4
  23. package/dist/internal/persistence.mjs.map +1 -1
  24. package/dist/internal/session-kernel.d.mts +3 -1
  25. package/dist/internal/session-kernel.mjs +22 -12
  26. package/dist/internal/session-kernel.mjs.map +1 -1
  27. package/dist/internal/sql-persistence.mjs +3 -1
  28. package/dist/internal/sql-persistence.mjs.map +1 -1
  29. package/dist/internal/storage-mapping.mjs +1 -30
  30. package/dist/internal/storage-mapping.mjs.map +1 -1
  31. package/dist/internal/storage-tables.d.mts +0 -43
  32. package/dist/internal/storage-tables.mjs +0 -26
  33. package/dist/internal/storage-tables.mjs.map +1 -1
  34. package/dist/internal/transaction-execution-kernel.d.mts +1 -1
  35. package/dist/internal/transaction-kernel.d.mts +1 -1
  36. package/package.json +1 -1
  37. package/src/Adapter.ts +9 -4
  38. package/src/internal/authority-snapshot.ts +91 -0
  39. package/src/internal/configuration.ts +1 -6
  40. package/src/internal/models/password-model.ts +0 -64
  41. package/src/internal/native-sql-table.ts +164 -0
  42. package/src/internal/password-attempts.ts +466 -0
  43. package/src/internal/password-kernel.ts +102 -623
  44. package/src/internal/persistence.ts +5 -10
  45. package/src/internal/session-kernel.ts +89 -27
  46. package/src/internal/sql-persistence.ts +2 -0
  47. package/src/internal/storage-mapping.ts +0 -29
  48. package/src/internal/storage-tables.ts +0 -15
@@ -19,14 +19,6 @@ export type PasswordAttemptAction = "sign-in" | "change";
19
19
 
20
20
  export type PasswordAttemptState = "pending" | "verified" | "rejected";
21
21
 
22
- export type PasswordRateScopeKind = "action" | "identifier" | "subject";
23
-
24
- export interface PasswordScopeKeys {
25
- readonly action: string;
26
- readonly identifier: string;
27
- readonly subject?: string;
28
- }
29
-
30
22
  export interface PasswordSubjectTable<Subject extends Table, _NativeSubjectId> {
31
23
  readonly table: Subject["table"];
32
24
  readonly id: ColumnKey<Subject>;
@@ -171,40 +163,6 @@ export interface PasswordAttemptTable<Attempt extends Table, NativeSubjectId> {
171
163
  ) => Attempt["insert"];
172
164
  }
173
165
 
174
- export interface PasswordRateScopeTable<RateScope extends Table> {
175
- readonly table: RateScope["table"];
176
- readonly moduleId: ColumnKey<RateScope>;
177
- readonly action: ColumnKey<RateScope>;
178
- readonly scopeKind: ColumnKey<RateScope>;
179
- readonly scopeKey: ColumnKey<RateScope>;
180
- readonly encodeInsert: (input: {
181
- readonly moduleId: string;
182
- readonly action: PasswordAttemptAction;
183
- readonly scopeKind: PasswordRateScopeKind;
184
- readonly scopeKey: string;
185
- }) => RateScope["insert"];
186
- }
187
-
188
- export interface PasswordChargeTable<Charge extends Table> {
189
- readonly table: Charge["table"];
190
- readonly moduleId: ColumnKey<Charge>;
191
- readonly action: ColumnKey<Charge>;
192
- readonly scopeKind: ColumnKey<Charge>;
193
- readonly scopeKey: ColumnKey<Charge>;
194
- readonly attemptId: ColumnKey<Charge>;
195
- readonly occurredAt: ColumnKey<Charge>;
196
- readonly retentionUntil: ColumnKey<Charge>;
197
- readonly encodeInsert: (input: {
198
- readonly moduleId: string;
199
- readonly action: PasswordAttemptAction;
200
- readonly scopeKind: PasswordRateScopeKind;
201
- readonly scopeKey: string;
202
- readonly attemptId: PasswordAttemptId;
203
- readonly occurredAtMillis: number;
204
- readonly retentionUntilMillis: number;
205
- }) => Charge["insert"];
206
- }
207
-
208
166
  export interface PasswordCommandTable<Command extends Table> {
209
167
  readonly table: Command["table"];
210
168
  readonly moduleId: ColumnKey<Command>;
@@ -229,8 +187,6 @@ export interface RequiredPasswordConstraints {
229
187
  readonly credentialSubject: "unique(credential.moduleId,credential.subjectId)";
230
188
  readonly credentialId: "unique(credential.moduleId,credential.credentialId)";
231
189
  readonly attempt: "unique(attempt.moduleId,attempt.attemptId)";
232
- readonly rateScope: "unique(rateScope.moduleId,rateScope.action,rateScope.scopeKind,rateScope.scopeKey)";
233
- readonly charge: "unique(charge.moduleId,charge.action,charge.scopeKind,charge.scopeKey,charge.attemptId)";
234
190
  readonly command: "unique(command.moduleId,command.commandId)";
235
191
  }
236
192
 
@@ -240,16 +196,12 @@ export const requiredPasswordConstraints: RequiredPasswordConstraints = {
240
196
  credentialSubject: "unique(credential.moduleId,credential.subjectId)",
241
197
  credentialId: "unique(credential.moduleId,credential.credentialId)",
242
198
  attempt: "unique(attempt.moduleId,attempt.attemptId)",
243
- rateScope: "unique(rateScope.moduleId,rateScope.action,rateScope.scopeKind,rateScope.scopeKey)",
244
- charge: "unique(charge.moduleId,charge.action,charge.scopeKind,charge.scopeKey,charge.attemptId)",
245
199
  command: "unique(command.moduleId,command.commandId)",
246
200
  };
247
201
 
248
202
  export interface PasswordConstraintClassifier {
249
203
  /** Match only the mapped command uniqueness constraint. */
250
204
  readonly isCommandConflict: (cause: unknown) => boolean;
251
- /** Match only the mapped rate-scope uniqueness constraint. */
252
- readonly isRateScopeConflict: (cause: unknown) => boolean;
253
205
  }
254
206
 
255
207
  export interface PasswordD1Clock<Expression extends SqlExpression = SqlExpression> {
@@ -265,8 +217,6 @@ export interface PasswordPersistenceMapping<
265
217
  Credential extends Table,
266
218
  AuthorityCredential extends Table,
267
219
  Attempt extends Table,
268
- RateScope extends Table,
269
- Charge extends Table,
270
220
  Command extends Table,
271
221
  NativeSubjectId,
272
222
  Expression extends SqlExpression = SqlExpression,
@@ -280,17 +230,9 @@ export interface PasswordPersistenceMapping<
280
230
  NativeSubjectId
281
231
  >;
282
232
  readonly attempt: PasswordAttemptTable<Attempt, NativeSubjectId>;
283
- readonly rateScope: PasswordRateScopeTable<RateScope>;
284
- readonly charge: PasswordChargeTable<Charge>;
285
233
  readonly command: PasswordCommandTable<Command>;
286
234
  readonly subjectId: SubjectIdCodec<NativeSubjectId>;
287
235
  readonly constraints: RequiredPasswordConstraints;
288
- readonly scopeKeys: (input: {
289
- readonly moduleId: string;
290
- readonly action: PasswordAttemptAction;
291
- readonly identifier: LoginIdentifier;
292
- readonly subjectId?: string;
293
- }) => PasswordScopeKeys;
294
236
  readonly encodeInstant: (epochMillis: number) => unknown;
295
237
  readonly decodeInstant: (native: unknown) => Effect.Effect<number, PersistenceMappingError>;
296
238
  readonly allocateAttemptId?: Effect.Effect<PasswordAttemptId, PersistenceMappingError>;
@@ -311,8 +253,6 @@ export type D1PasswordPersistenceMapping<
311
253
  Credential extends Table,
312
254
  AuthorityCredential extends Table,
313
255
  Attempt extends Table,
314
- RateScope extends Table,
315
- Charge extends Table,
316
256
  Command extends Table,
317
257
  NativeSubjectId,
318
258
  Expression extends SqlExpression = SqlExpression,
@@ -322,8 +262,6 @@ export type D1PasswordPersistenceMapping<
322
262
  Credential,
323
263
  AuthorityCredential,
324
264
  Attempt,
325
- RateScope,
326
- Charge,
327
265
  Command,
328
266
  NativeSubjectId,
329
267
  Expression
@@ -492,8 +430,6 @@ export type AnyPasswordPersistenceMapping<Expression extends SqlExpression = Sql
492
430
  AnyTableModel,
493
431
  AnyTableModel,
494
432
  AnyTableModel,
495
- AnyTableModel,
496
- AnyTableModel,
497
433
  unknown,
498
434
  Expression
499
435
  >;
@@ -0,0 +1,164 @@
1
+ import { Effect } from "effect";
2
+ import type { SqlClient } from "effect/sql/SqlClient";
3
+ import * as Statement from "effect/sql/Statement";
4
+
5
+ import { PersistenceMappingError } from "./mapping-error";
6
+ import { Table, identifier } from "./sql-table";
7
+
8
+ /** Capture synchronous mapping callbacks without changing SQL execution errors. */
9
+ export const sqlMapping = <A>(thunk: () => A): Effect.Effect<A, PersistenceMappingError> =>
10
+ Effect.try({
11
+ try: thunk,
12
+ catch: (cause) => PersistenceMappingError.make({ operation: "mapping", cause }),
13
+ });
14
+
15
+ /** Physical SQL representation only. Semantic row codecs and policy remain in
16
+ * the owning mapping. Execute assembled statements without name/result transforms. */
17
+ export interface SqlTable {
18
+ readonly name: Statement.Fragment;
19
+ /** Alias reads only; writes continue to target the original physical table. */
20
+ readonly as: (alias: string) => SqlTable;
21
+ readonly column: (key: string) => Statement.Fragment;
22
+ /** Project every column under an ordinal alias, using a distinct prefix per table. */
23
+ readonly fields: (prefix: string) => Statement.Fragment;
24
+ readonly decode: (
25
+ row: Readonly<Record<string, unknown>>,
26
+ prefix: string,
27
+ ) => Record<string, unknown>;
28
+ readonly value: (key: string, value: unknown) => Statement.Fragment;
29
+ /** Complete INSERT prefix; append conflict/returning clauses in the owning operation. */
30
+ readonly insert: (values: Readonly<Record<string, unknown>>) => Statement.Fragment;
31
+ /** Complete UPDATE ... SET prefix; the owning operation supplies its predicate. */
32
+ readonly update: (values: Readonly<Record<string, unknown>>) => Statement.Fragment;
33
+ }
34
+
35
+ export type NativeSqlTables = (table: object) => SqlTable;
36
+
37
+ /** Prefixes are short ASCII identifiers so ordinal aliases cannot be truncated
38
+ * by PostgreSQL. Callers distinguish tables by prefix, never by physical names. */
39
+ export const nativeSqlAlias = (prefix: string, index: number): string => {
40
+ if (!/^[A-Za-z_][A-Za-z0-9_]{0,39}$/.test(prefix))
41
+ throw PersistenceMappingError.make({ operation: "mapping", cause: "Invalid SQL alias prefix" });
42
+
43
+ return `${prefix}${index}`;
44
+ };
45
+
46
+ const directTable = (client: SqlClient, physical: object): SqlTable => {
47
+ if (!(physical instanceof Table))
48
+ throw PersistenceMappingError.make({ operation: "mapping", cause: "Expected a SQL table" });
49
+
50
+ const quote = (name: string) => {
51
+ if (name.includes("\0"))
52
+ throw PersistenceMappingError.make({ operation: "mapping", cause: "NUL in SQL identifier" });
53
+
54
+ return client.literal(identifier(name));
55
+ };
56
+
57
+ const name =
58
+ physical.schema === undefined
59
+ ? quote(physical.name)
60
+ : client`${quote(physical.schema)}.${quote(physical.name)}`;
61
+
62
+ const columns = Object.entries(physical.columns);
63
+
64
+ const getColumn = (key: string) => {
65
+ const column = Object.hasOwn(physical.columns, key) ? physical.columns[key] : undefined;
66
+
67
+ if (column === undefined)
68
+ throw PersistenceMappingError.make({ operation: "column", cause: "Missing SQL column" });
69
+
70
+ return column;
71
+ };
72
+
73
+ const value = (key: string, input: unknown): Statement.Fragment => {
74
+ const mapped = getColumn(key);
75
+
76
+ if (Statement.isFragment(input)) return input;
77
+ const encoded = input === null ? null : mapped.mapToDriverValue(input);
78
+
79
+ return Statement.fragment([
80
+ Statement.parameter(
81
+ typeof encoded === "boolean"
82
+ ? client.onDialectOrElse({ sqlite: () => Number(encoded), orElse: () => encoded })
83
+ : encoded,
84
+ ),
85
+ ]);
86
+ };
87
+
88
+ const entries = (values: Readonly<Record<string, unknown>>) =>
89
+ Object.entries(values).filter(
90
+ ([key, input]) => Object.hasOwn(physical.columns, key) && input !== undefined,
91
+ );
92
+
93
+ const bind = (alias?: string): SqlTable => {
94
+ const reference = alias === undefined ? name : quote(alias);
95
+ const column = (key: string) => client`${reference}.${quote(getColumn(key).options.name)}`;
96
+
97
+ return {
98
+ name: alias === undefined ? name : client`${name} AS ${reference}`,
99
+ as: bind,
100
+ column,
101
+ value,
102
+ fields: (prefix) =>
103
+ client.join(
104
+ ", ",
105
+ false,
106
+ )(
107
+ columns.map(
108
+ ([key], index) => client`${column(key)} AS ${quote(nativeSqlAlias(prefix, index))}`,
109
+ ),
110
+ ),
111
+ decode: (row, prefix) =>
112
+ Object.fromEntries(
113
+ columns.map(([key, field], index) => {
114
+ const alias = nativeSqlAlias(prefix, index);
115
+
116
+ if (!Object.hasOwn(row, alias))
117
+ throw PersistenceMappingError.make({
118
+ operation: "decode",
119
+ cause: "Missing SQL projection field",
120
+ });
121
+
122
+ return [key, field.decode(row[alias])];
123
+ }),
124
+ ),
125
+ insert: (values) => {
126
+ const fields = entries(values);
127
+
128
+ if (fields.length === 0) return client`INSERT INTO ${name} DEFAULT VALUES`;
129
+
130
+ return client`INSERT INTO ${name} (${client.join(", ", false)(fields.map(([key]) => quote(getColumn(key).options.name)))}) VALUES (${client.join(", ", false)(fields.map(([key, input]) => value(key, input)))})`;
131
+ },
132
+ update: (values) => {
133
+ const fields = entries(values);
134
+
135
+ if (fields.length === 0)
136
+ throw PersistenceMappingError.make({ operation: "mapping", cause: "Empty SQL update" });
137
+
138
+ return client`UPDATE ${name} SET ${client.join(", ", false)(fields.map(([key, input]) => client`${quote(getColumn(key).options.name)} = ${value(key, input)}`))}`;
139
+ },
140
+ };
141
+ };
142
+
143
+ return bind();
144
+ };
145
+
146
+ /** Cache physical metadata only; insert/update materialize values and hooks at
147
+ * each call. Construction acquires no client and owns no transaction. */
148
+ export const makeNativeSqlTables = (
149
+ client: SqlClient,
150
+ convert: (client: SqlClient, table: object) => SqlTable = directTable,
151
+ ): NativeSqlTables => {
152
+ const tables = new WeakMap<object, SqlTable>();
153
+
154
+ return (table) => {
155
+ const existing = tables.get(table);
156
+
157
+ if (existing !== undefined) return existing;
158
+ const mapped = convert(client, table);
159
+
160
+ tables.set(table, mapped);
161
+
162
+ return mapped;
163
+ };
164
+ };