@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.
- package/dist/Adapter.d.mts +4 -2
- package/dist/Adapter.mjs +3 -1
- package/dist/internal/authority-snapshot.mjs +53 -0
- package/dist/internal/authority-snapshot.mjs.map +1 -0
- package/dist/internal/configuration.d.mts +1 -1
- package/dist/internal/configuration.mjs.map +1 -1
- package/dist/internal/models/password-model.d.mts +3 -53
- package/dist/internal/models/password-model.mjs +0 -2
- package/dist/internal/models/password-model.mjs.map +1 -1
- package/dist/internal/native-sql-table.d.mts +30 -0
- package/dist/internal/native-sql-table.mjs +104 -0
- package/dist/internal/native-sql-table.mjs.map +1 -0
- package/dist/internal/passkey/target.d.mts +1 -1
- package/dist/internal/passkeys.mjs +1 -1
- package/dist/internal/password-attempts.d.mts +12 -0
- package/dist/internal/password-attempts.mjs +197 -0
- package/dist/internal/password-attempts.mjs.map +1 -0
- package/dist/internal/password-kernel.d.mts +14 -11
- package/dist/internal/password-kernel.mjs +39 -202
- package/dist/internal/password-kernel.mjs.map +1 -1
- package/dist/internal/persistence.d.mts +2 -0
- package/dist/internal/persistence.mjs +4 -4
- package/dist/internal/persistence.mjs.map +1 -1
- package/dist/internal/session-kernel.d.mts +3 -1
- package/dist/internal/session-kernel.mjs +22 -12
- package/dist/internal/session-kernel.mjs.map +1 -1
- package/dist/internal/sql-persistence.mjs +3 -1
- package/dist/internal/sql-persistence.mjs.map +1 -1
- package/dist/internal/storage-mapping.mjs +1 -30
- package/dist/internal/storage-mapping.mjs.map +1 -1
- package/dist/internal/storage-tables.d.mts +0 -43
- package/dist/internal/storage-tables.mjs +0 -26
- package/dist/internal/storage-tables.mjs.map +1 -1
- package/dist/internal/transaction-execution-kernel.d.mts +1 -1
- package/dist/internal/transaction-kernel.d.mts +1 -1
- package/package.json +1 -1
- package/src/Adapter.ts +9 -4
- package/src/internal/authority-snapshot.ts +91 -0
- package/src/internal/configuration.ts +1 -6
- package/src/internal/models/password-model.ts +0 -64
- package/src/internal/native-sql-table.ts +164 -0
- package/src/internal/password-attempts.ts +466 -0
- package/src/internal/password-kernel.ts +102 -623
- package/src/internal/persistence.ts +5 -10
- package/src/internal/session-kernel.ts +89 -27
- package/src/internal/sql-persistence.ts +2 -0
- package/src/internal/storage-mapping.ts +0 -29
- 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
|
+
};
|