@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
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
import { randomId } from "./crypto";
|
|
30
30
|
import { makeEmailKernel, CurrentEmailSql, type EmailSqlDatabase } from "./email-kernel";
|
|
31
31
|
import { PersistenceMappingError } from "./mapping-error";
|
|
32
|
+
import type { NativeSqlTables } from "./native-sql-table";
|
|
32
33
|
import {
|
|
33
34
|
makePasswordKernel,
|
|
34
35
|
CurrentPasswordSql,
|
|
@@ -69,6 +70,7 @@ export interface Backend<T extends object, R> {
|
|
|
69
70
|
readonly describe: (table: T) => StorageTable;
|
|
70
71
|
readonly acquire: Effect.Effect<object, PersistenceConfigurationError, R | SqlClient.SqlClient>;
|
|
71
72
|
readonly operations: QueryOperations;
|
|
73
|
+
readonly nativeTables: (client: SqlClient.SqlClient, database: object) => NativeSqlTables;
|
|
72
74
|
readonly passkeys: (input: {
|
|
73
75
|
readonly storage: MappingInput;
|
|
74
76
|
readonly namespace: string;
|
|
@@ -112,9 +114,9 @@ export const createPersistence = <T extends object, R>(
|
|
|
112
114
|
backend: Backend<T, R>,
|
|
113
115
|
): PersistenceApi<T, R> => {
|
|
114
116
|
const proofKernel = makeProofKernel(backend.operations);
|
|
115
|
-
const passwordKernel = makePasswordKernel(backend.operations, proofKernel);
|
|
117
|
+
const passwordKernel = makePasswordKernel(backend.operations, proofKernel, backend.nativeTables);
|
|
116
118
|
const emailKernel = makeEmailKernel(backend.operations, proofKernel);
|
|
117
|
-
const sessionKernel = makeSessionKernel(backend.operations);
|
|
119
|
+
const sessionKernel = makeSessionKernel(backend.operations, backend.nativeTables);
|
|
118
120
|
const transactionKernel = makeTransactionKernel(backend.operations);
|
|
119
121
|
const executionKernel = makeTransactionExecutionKernel(transactionKernel);
|
|
120
122
|
const phoneKernel = makePhoneKernel(backend.operations, transactionKernel);
|
|
@@ -139,14 +141,7 @@ export const createPersistence = <T extends object, R>(
|
|
|
139
141
|
|
|
140
142
|
const roles: StorageRole[] = ["identifiers", "credentials", "sessions", "sessionFlows"];
|
|
141
143
|
|
|
142
|
-
if (password)
|
|
143
|
-
roles.push(
|
|
144
|
-
"passwords",
|
|
145
|
-
"passwordAttempts",
|
|
146
|
-
"passwordScopes",
|
|
147
|
-
"passwordCharges",
|
|
148
|
-
"passwordCommands",
|
|
149
|
-
);
|
|
144
|
+
if (password) roles.push("passwords", "passwordAttempts", "passwordCommands");
|
|
150
145
|
if (management) roles.push("passwordRegistrations");
|
|
151
146
|
if (email) roles.push("emailCredentials", "emailCommands");
|
|
152
147
|
if (phone) roles.push("phoneState");
|
|
@@ -36,8 +36,10 @@ import {
|
|
|
36
36
|
type SessionStepUpPersistence,
|
|
37
37
|
} from "@yielded/auth/Sessions";
|
|
38
38
|
import { Cause, Context, DateTime, Effect, Option, Schema } from "effect";
|
|
39
|
+
import type { SqlClient } from "effect/sql/SqlClient";
|
|
39
40
|
import type * as SqlError from "effect/sql/SqlError";
|
|
40
41
|
|
|
42
|
+
import { makeAuthoritySnapshot } from "./authority-snapshot";
|
|
41
43
|
import { PersistenceMappingError, isMappedConstraintConflict } from "./mapping-error";
|
|
42
44
|
import type {
|
|
43
45
|
AuthenticationAuthorityMapping,
|
|
@@ -50,6 +52,7 @@ import type {
|
|
|
50
52
|
StatefulSessionMapping,
|
|
51
53
|
} from "./models/session-model";
|
|
52
54
|
import type { SessionStepUpMapping } from "./models/step-up-model";
|
|
55
|
+
import { makeNativeSqlTables, sqlMapping, type NativeSqlTables } from "./native-sql-table";
|
|
53
56
|
/* oxlint-disable no-explicit-any -- existing storage kernels erase foreign table shapes; domain errors remain typed. */
|
|
54
57
|
import type { QueryFailure } from "./query-operations";
|
|
55
58
|
import type { QueryOperations } from "./query-operations";
|
|
@@ -61,7 +64,7 @@ import {
|
|
|
61
64
|
validateStepUpPlan,
|
|
62
65
|
stepUpRotationMatches,
|
|
63
66
|
} from "./step-up-state";
|
|
64
|
-
import
|
|
67
|
+
import { NativeDatabase } from "./transaction-kernel";
|
|
65
68
|
|
|
66
69
|
type CommitMode = "interactive" | "synchronous";
|
|
67
70
|
|
|
@@ -139,7 +142,11 @@ type SqlStepUpRejectAbsence = SessionStepUpInvalid | StaleAuthentication;
|
|
|
139
142
|
|
|
140
143
|
type SqlStepUpReadFailure = AdapterFailure | SqlStepUpRejectAbsence | SessionUnavailable;
|
|
141
144
|
|
|
142
|
-
export const makeSessionKernel = (
|
|
145
|
+
export const makeSessionKernel = (
|
|
146
|
+
operations: QueryOperations,
|
|
147
|
+
nativeTables: (client: SqlClient, database: object) => NativeSqlTables = (client) =>
|
|
148
|
+
makeNativeSqlTables(client),
|
|
149
|
+
) => {
|
|
143
150
|
const { and, eq, gt, lte, inArray, or, sql, column, updateValues } = operations;
|
|
144
151
|
const unavailable = () => SessionUnavailable.make({});
|
|
145
152
|
|
|
@@ -396,15 +403,6 @@ export const makeSessionKernel = (operations: QueryOperations) => {
|
|
|
396
403
|
};
|
|
397
404
|
});
|
|
398
405
|
|
|
399
|
-
const captureIn = Effect.fn("DrizzleSession.capture")(function* (
|
|
400
|
-
mapping: AnyAuthorityMapping,
|
|
401
|
-
subjectId: SubjectId,
|
|
402
|
-
credentialIds: ReadonlyArray<string>,
|
|
403
|
-
locking: boolean,
|
|
404
|
-
) {
|
|
405
|
-
return (yield* captureDetailsIn(mapping, subjectId, credentialIds, locking)).revision;
|
|
406
|
-
});
|
|
407
|
-
|
|
408
406
|
const validateEvidenceIn = Effect.fn("DrizzleSession.validateEvidence")(function* (
|
|
409
407
|
mapping: AnyAuthorityMapping,
|
|
410
408
|
evidence: AuthenticationEvidence,
|
|
@@ -417,6 +415,17 @@ export const makeSessionKernel = (operations: QueryOperations) => {
|
|
|
417
415
|
locking,
|
|
418
416
|
);
|
|
419
417
|
|
|
418
|
+
return yield* validateCapturedEvidence(mapping, evidence, captured);
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
const validateCapturedEvidence = Effect.fnUntraced(function* (
|
|
422
|
+
mapping: AnyAuthorityMapping,
|
|
423
|
+
evidence: AuthenticationEvidence,
|
|
424
|
+
captured: {
|
|
425
|
+
readonly subject: Record<string, unknown>;
|
|
426
|
+
readonly revision: AuthenticationRevision;
|
|
427
|
+
},
|
|
428
|
+
) {
|
|
420
429
|
if (
|
|
421
430
|
captured.revision.securityRevision !== evidence.revision.securityRevision ||
|
|
422
431
|
captured.revision.credentials.length !== evidence.revision.credentials.length ||
|
|
@@ -643,18 +652,28 @@ export const makeSessionKernel = (operations: QueryOperations) => {
|
|
|
643
652
|
);
|
|
644
653
|
const hooks = yield* LifecycleHooks;
|
|
645
654
|
|
|
655
|
+
const root = yield* NativeDatabase;
|
|
656
|
+
const sqlClient = root.$client.withoutTransforms();
|
|
657
|
+
|
|
658
|
+
const tables = yield* sqlMapping(() => nativeTables(sqlClient, root)).pipe(
|
|
659
|
+
Effect.mapError(unavailable),
|
|
660
|
+
);
|
|
661
|
+
|
|
662
|
+
const readSnapshot = makeAuthoritySnapshot(tables, mapping);
|
|
663
|
+
|
|
646
664
|
const service = {
|
|
647
665
|
capture: (subjectId: SubjectId, credentialIds: ReadonlyArray<string>) =>
|
|
648
666
|
safeTransaction(
|
|
649
|
-
|
|
667
|
+
readSnapshot(subjectId, credentialIds).pipe(Effect.map((value) => value.revision)),
|
|
650
668
|
),
|
|
651
669
|
requirements: (evidence: AuthenticationEvidence) =>
|
|
652
670
|
safeTransaction(
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
),
|
|
671
|
+
readSnapshot(
|
|
672
|
+
evidence.revision.subjectId,
|
|
673
|
+
evidence.revision.credentials.map((item) => item.credentialId),
|
|
674
|
+
).pipe(
|
|
675
|
+
Effect.flatMap((captured) => validateCapturedEvidence(mapping, evidence, captured)),
|
|
676
|
+
Effect.map((value) => value.requirement),
|
|
658
677
|
),
|
|
659
678
|
),
|
|
660
679
|
approve: <A>(
|
|
@@ -667,7 +686,20 @@ export const makeSessionKernel = (operations: QueryOperations) => {
|
|
|
667
686
|
mapping.isConstraintConflict,
|
|
668
687
|
Effect.gen(function* () {
|
|
669
688
|
const journal = yield* CurrentCommitJournal;
|
|
670
|
-
|
|
689
|
+
|
|
690
|
+
// With row locks, lock the subject before its credentials in separate
|
|
691
|
+
// reads: a joined FOR UPDATE does not fix that order on every table
|
|
692
|
+
// layout, and a coordinated application may already hold the subject
|
|
693
|
+
// lock. Without row locks one native snapshot is already serialized.
|
|
694
|
+
const { requirement } = yield* options.locking
|
|
695
|
+
? validateEvidenceIn(mapping, input.evidence, true)
|
|
696
|
+
: Effect.flatMap(
|
|
697
|
+
readSnapshot(
|
|
698
|
+
input.evidence.revision.subjectId,
|
|
699
|
+
input.evidence.revision.credentials.map((item) => item.credentialId),
|
|
700
|
+
),
|
|
701
|
+
(captured) => validateCapturedEvidence(mapping, input.evidence, captured),
|
|
702
|
+
);
|
|
671
703
|
|
|
672
704
|
if (input.pending === undefined) {
|
|
673
705
|
const assessed = yield* assessAuthentication(input.evidence, requirement).pipe(
|
|
@@ -720,11 +752,24 @@ export const makeSessionKernel = (operations: QueryOperations) => {
|
|
|
720
752
|
capture: (subjectId, credentialIds) =>
|
|
721
753
|
service
|
|
722
754
|
.capture(subjectId, credentialIds)
|
|
723
|
-
.pipe(
|
|
755
|
+
.pipe(
|
|
756
|
+
Effect.provideService(NativeDatabase, root),
|
|
757
|
+
Effect.provideService(LifecycleHooks, hooks),
|
|
758
|
+
),
|
|
724
759
|
requirements: (evidence) =>
|
|
725
|
-
service
|
|
760
|
+
service
|
|
761
|
+
.requirements(evidence)
|
|
762
|
+
.pipe(
|
|
763
|
+
Effect.provideService(NativeDatabase, root),
|
|
764
|
+
Effect.provideService(LifecycleHooks, hooks),
|
|
765
|
+
),
|
|
726
766
|
approve: (input, prepare) =>
|
|
727
|
-
service
|
|
767
|
+
service
|
|
768
|
+
.approve(input, prepare)
|
|
769
|
+
.pipe(
|
|
770
|
+
Effect.provideService(NativeDatabase, root),
|
|
771
|
+
Effect.provideService(LifecycleHooks, hooks),
|
|
772
|
+
),
|
|
728
773
|
} satisfies AuthenticationAuthority["Service"];
|
|
729
774
|
});
|
|
730
775
|
|
|
@@ -991,6 +1036,13 @@ export const makeSessionKernel = (operations: QueryOperations) => {
|
|
|
991
1036
|
);
|
|
992
1037
|
const hooks = yield* LifecycleHooks;
|
|
993
1038
|
|
|
1039
|
+
const root = yield* NativeDatabase;
|
|
1040
|
+
|
|
1041
|
+
const tables = yield* sqlMapping(() =>
|
|
1042
|
+
nativeTables(root.$client.withoutTransforms(), root),
|
|
1043
|
+
).pipe(Effect.mapError(unavailable));
|
|
1044
|
+
|
|
1045
|
+
const readSnapshot = makeAuthoritySnapshot(tables, mapping);
|
|
994
1046
|
const c = sessionColumns(mapping);
|
|
995
1047
|
|
|
996
1048
|
const persistence = {
|
|
@@ -1006,11 +1058,16 @@ export const makeSessionKernel = (operations: QueryOperations) => {
|
|
|
1006
1058
|
const transaction = yield* CurrentSessionSql;
|
|
1007
1059
|
const journal = yield* CurrentCommitJournal;
|
|
1008
1060
|
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
input.evidence,
|
|
1012
|
-
|
|
1013
|
-
|
|
1061
|
+
// Ordered locks with row locks; one serialized snapshot without (see approve).
|
|
1062
|
+
const { nativeSubjectId, requirement } = yield* options.locking
|
|
1063
|
+
? validateEvidenceIn(mapping, input.evidence, true)
|
|
1064
|
+
: Effect.flatMap(
|
|
1065
|
+
readSnapshot(
|
|
1066
|
+
input.evidence.revision.subjectId,
|
|
1067
|
+
input.evidence.revision.credentials.map((item) => item.credentialId),
|
|
1068
|
+
),
|
|
1069
|
+
(captured) => validateCapturedEvidence(mapping, input.evidence, captured),
|
|
1070
|
+
);
|
|
1014
1071
|
|
|
1015
1072
|
let flowInsert: unknown | undefined;
|
|
1016
1073
|
let pendingExpiresAt: DateTime.Utc | undefined;
|
|
@@ -1403,7 +1460,12 @@ export const makeSessionKernel = (operations: QueryOperations) => {
|
|
|
1403
1460
|
return {
|
|
1404
1461
|
statefulSessionPersistence: {
|
|
1405
1462
|
establish: (input, prepare) =>
|
|
1406
|
-
persistence
|
|
1463
|
+
persistence
|
|
1464
|
+
.establish(input, prepare)
|
|
1465
|
+
.pipe(
|
|
1466
|
+
Effect.provideService(NativeDatabase, root),
|
|
1467
|
+
Effect.provideService(LifecycleHooks, hooks),
|
|
1468
|
+
),
|
|
1407
1469
|
verify: (input) =>
|
|
1408
1470
|
persistence.verify(input).pipe(Effect.provideService(LifecycleHooks, hooks)),
|
|
1409
1471
|
rotate: (input, prepare) =>
|
|
@@ -2,6 +2,7 @@ import { Effect } from "effect";
|
|
|
2
2
|
import { SqlClient } from "effect/sql";
|
|
3
3
|
|
|
4
4
|
import { PersistenceConfigurationError, type PersistenceApi } from "./configuration";
|
|
5
|
+
import { makeNativeSqlTables } from "./native-sql-table";
|
|
5
6
|
import { makeComposedPasskeys } from "./passkeys";
|
|
6
7
|
import { createPersistence } from "./persistence";
|
|
7
8
|
import { makeSqlDatabase, sqlQueryOperations } from "./sql-query";
|
|
@@ -23,6 +24,7 @@ export const AuthPersistence: PersistenceApi<Table> & { readonly table: typeof t
|
|
|
23
24
|
unique: table.unique,
|
|
24
25
|
}),
|
|
25
26
|
operations: sqlQueryOperations,
|
|
27
|
+
nativeTables: (client) => makeNativeSqlTables(client),
|
|
26
28
|
passkeys: makeComposedPasskeys(sqlQueryOperations),
|
|
27
29
|
acquire: Effect.gen(function* () {
|
|
28
30
|
const sql = yield* SqlClient.SqlClient;
|
|
@@ -373,12 +373,6 @@ export const makeMappings = Effect.fnUntraced(function* (input: MappingInput) {
|
|
|
373
373
|
authorityCredential: authorityCredential(),
|
|
374
374
|
constraints: requiredPasswordConstraints,
|
|
375
375
|
isCommandConflict: () => false,
|
|
376
|
-
isRateScopeConflict: () => false,
|
|
377
|
-
scopeKeys: ({ moduleId, action, identifier, subjectId }) => ({
|
|
378
|
-
action: tuple([moduleId, action]),
|
|
379
|
-
identifier: tuple([identifier.namespace, identifier.value]),
|
|
380
|
-
...(subjectId === undefined ? {} : { subject: subjectId }),
|
|
381
|
-
}),
|
|
382
376
|
encodeInstant: instant,
|
|
383
377
|
decodeInstant: readInstant,
|
|
384
378
|
allocateAttemptId: allocate.pipe(Effect.map(PasswordAttemptId.make)),
|
|
@@ -485,29 +479,6 @@ export const makeMappings = Effect.fnUntraced(function* (input: MappingInput) {
|
|
|
485
479
|
state,
|
|
486
480
|
}),
|
|
487
481
|
},
|
|
488
|
-
rateScope: {
|
|
489
|
-
...mapped("passwordScopes"),
|
|
490
|
-
moduleId: "moduleId",
|
|
491
|
-
action: "action",
|
|
492
|
-
scopeKind: "scopeKind",
|
|
493
|
-
scopeKey: "scopeKey",
|
|
494
|
-
encodeInsert: (row) => ({ ...row }),
|
|
495
|
-
},
|
|
496
|
-
charge: {
|
|
497
|
-
...mapped("passwordCharges"),
|
|
498
|
-
moduleId: "moduleId",
|
|
499
|
-
action: "action",
|
|
500
|
-
scopeKind: "scopeKind",
|
|
501
|
-
scopeKey: "scopeKey",
|
|
502
|
-
attemptId: "attemptId",
|
|
503
|
-
occurredAt: "occurredAt",
|
|
504
|
-
retentionUntil: "retentionUntil",
|
|
505
|
-
encodeInsert: (row) => ({
|
|
506
|
-
...row,
|
|
507
|
-
occurredAt: instant(row.occurredAtMillis),
|
|
508
|
-
retentionUntil: instant(row.retentionUntilMillis),
|
|
509
|
-
}),
|
|
510
|
-
},
|
|
511
482
|
command: {
|
|
512
483
|
...mapped("passwordCommands"),
|
|
513
484
|
moduleId: "moduleId",
|
|
@@ -78,21 +78,6 @@ export const storageTables = {
|
|
|
78
78
|
},
|
|
79
79
|
[["moduleId", "attemptId"]],
|
|
80
80
|
),
|
|
81
|
-
passwordScopes: spec({ moduleId: text, action: text, scopeKind: text, scopeKey: text }, [
|
|
82
|
-
["moduleId", "action", "scopeKind", "scopeKey"],
|
|
83
|
-
]),
|
|
84
|
-
passwordCharges: spec(
|
|
85
|
-
{
|
|
86
|
-
moduleId: text,
|
|
87
|
-
action: text,
|
|
88
|
-
scopeKind: text,
|
|
89
|
-
scopeKey: text,
|
|
90
|
-
attemptId: text,
|
|
91
|
-
occurredAt: integer,
|
|
92
|
-
retentionUntil: integer,
|
|
93
|
-
},
|
|
94
|
-
[["moduleId", "action", "scopeKind", "scopeKey", "attemptId"]],
|
|
95
|
-
),
|
|
96
81
|
passwordCommands: spec(
|
|
97
82
|
{
|
|
98
83
|
moduleId: text,
|