@yielded/auth-persistence 0.1.0-beta.18 → 0.1.0-beta.20
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/README.md +5 -2
- package/dist/Adapter.d.mts +2 -2
- package/dist/Adapter.mjs +2 -2
- package/dist/internal/configuration.d.mts +4 -3
- package/dist/internal/configuration.mjs.map +1 -1
- package/dist/internal/crypto.mjs +12 -0
- package/dist/internal/crypto.mjs.map +1 -0
- package/dist/internal/passkey/admission.mjs +19 -15
- package/dist/internal/passkey/admission.mjs.map +1 -1
- package/dist/internal/passkey/credentials.mjs +4 -4
- package/dist/internal/passkey/credentials.mjs.map +1 -1
- package/dist/internal/passkey/enrollment.mjs +6 -6
- package/dist/internal/passkey/enrollment.mjs.map +1 -1
- package/dist/internal/passkey/flow.mjs +7 -7
- package/dist/internal/passkey/flow.mjs.map +1 -1
- package/dist/internal/passkey/management.mjs +2 -2
- package/dist/internal/passkey/management.mjs.map +1 -1
- package/dist/internal/passkey/registration-ceremony.mjs +2 -2
- package/dist/internal/passkey/registration-ceremony.mjs.map +1 -1
- package/dist/internal/passkey/registration-custody.mjs +1 -1
- package/dist/internal/passkey/registration-custody.mjs.map +1 -1
- package/dist/internal/passkey/registration-write.mjs +12 -12
- package/dist/internal/passkey/registration-write.mjs.map +1 -1
- package/dist/internal/passkey/state.d.mts +1 -1
- package/dist/internal/passkey/state.mjs +8 -9
- package/dist/internal/passkey/state.mjs.map +1 -1
- package/dist/internal/passkey/target.d.mts +1 -1
- package/dist/internal/passkey/target.mjs.map +1 -1
- package/dist/internal/passkey/write-state.mjs +13 -13
- package/dist/internal/passkey/write-state.mjs.map +1 -1
- package/dist/internal/passkey/write-target.mjs.map +1 -1
- package/dist/internal/passkey-kernel.d.mts +512 -56
- package/dist/internal/passkeys.mjs +32 -14
- package/dist/internal/passkeys.mjs.map +1 -1
- package/dist/internal/password-kernel.d.mts +1 -1
- package/dist/internal/persistence.d.mts +2 -2
- package/dist/internal/persistence.mjs +16 -7
- package/dist/internal/persistence.mjs.map +1 -1
- package/dist/internal/phone-kernel.d.mts +5 -5
- package/dist/internal/phone-kernel.mjs +14 -14
- package/dist/internal/phone-kernel.mjs.map +1 -1
- package/dist/internal/registration.mjs +8 -7
- package/dist/internal/registration.mjs.map +1 -1
- package/dist/internal/storage-mapping.d.mts +5 -4
- package/dist/internal/storage-mapping.mjs +16 -13
- package/dist/internal/storage-mapping.mjs.map +1 -1
- package/dist/internal/storage-validation.d.mts +13 -3
- package/dist/internal/storage-validation.mjs +128 -68
- package/dist/internal/storage-validation.mjs.map +1 -1
- package/dist/internal/transaction-execution-kernel.d.mts +4 -4
- package/dist/internal/transaction-execution-kernel.mjs +7 -5
- package/dist/internal/transaction-execution-kernel.mjs.map +1 -1
- package/package.json +1 -1
- package/src/Adapter.ts +7 -1
- package/src/internal/configuration.ts +4 -2
- package/src/internal/crypto.ts +15 -0
- package/src/internal/passkey/admission.ts +22 -15
- package/src/internal/passkey/credentials.ts +4 -4
- package/src/internal/passkey/enrollment.ts +7 -7
- package/src/internal/passkey/flow.ts +8 -8
- package/src/internal/passkey/management.ts +7 -2
- package/src/internal/passkey/registration-ceremony.ts +2 -2
- package/src/internal/passkey/registration-custody.ts +1 -1
- package/src/internal/passkey/registration-write.ts +12 -12
- package/src/internal/passkey/state.ts +10 -13
- package/src/internal/passkey/target.ts +16 -8
- package/src/internal/passkey/write-state.ts +10 -14
- package/src/internal/passkey/write-target.ts +11 -3
- package/src/internal/passkeys.ts +121 -70
- package/src/internal/persistence.ts +28 -11
- package/src/internal/phone-kernel.ts +26 -17
- package/src/internal/registration.ts +10 -8
- package/src/internal/storage-mapping.ts +21 -13
- package/src/internal/storage-validation.ts +219 -139
- package/src/internal/transaction-execution-kernel.ts +30 -11
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
snapshotPasskeySync,
|
|
27
27
|
} from "@yielded/auth/Passkey";
|
|
28
28
|
import { SubjectId } from "@yielded/auth/Schema";
|
|
29
|
-
import { Effect, Schema } from "effect";
|
|
29
|
+
import { type Crypto, Effect, Schema } from "effect";
|
|
30
30
|
import type { Statement } from "effect/sql/Statement";
|
|
31
31
|
|
|
32
32
|
import type { PersistenceMappingError } from "../mapping-error";
|
|
@@ -130,7 +130,7 @@ export const makePasskeyTargetKernel = (
|
|
|
130
130
|
): Effect.fn.Return<
|
|
131
131
|
PasskeyExecution,
|
|
132
132
|
PasskeyConfigurationError,
|
|
133
|
-
LifecycleHooks | NativeDatabase
|
|
133
|
+
LifecycleHooks | NativeDatabase | Crypto.Crypto
|
|
134
134
|
> {
|
|
135
135
|
const hooks = yield* LifecycleHooks;
|
|
136
136
|
|
|
@@ -437,7 +437,7 @@ export const makePasskeyTargetKernel = (
|
|
|
437
437
|
): Effect.Effect<
|
|
438
438
|
PasskeyCredentialServices,
|
|
439
439
|
PasskeyConfigurationError | PersistenceMappingError,
|
|
440
|
-
RSetup | NativeDatabase
|
|
440
|
+
RSetup | NativeDatabase | Crypto.Crypto
|
|
441
441
|
> =>
|
|
442
442
|
Effect.gen(function* () {
|
|
443
443
|
const mapping = yield* capturedMapping(source, "read", configuration);
|
|
@@ -455,7 +455,7 @@ export const makePasskeyTargetKernel = (
|
|
|
455
455
|
): Effect.Effect<
|
|
456
456
|
PasskeyEnrollmentContextServices,
|
|
457
457
|
PasskeyConfigurationError | PersistenceMappingError,
|
|
458
|
-
RSetup | NativeDatabase
|
|
458
|
+
RSetup | NativeDatabase | Crypto.Crypto
|
|
459
459
|
> =>
|
|
460
460
|
Effect.gen(function* () {
|
|
461
461
|
const mapping = yield* capturedMapping(source, "context", configuration);
|
|
@@ -473,7 +473,7 @@ export const makePasskeyTargetKernel = (
|
|
|
473
473
|
): Effect.Effect<
|
|
474
474
|
PasskeyPersistenceServices,
|
|
475
475
|
PasskeyConfigurationError | PersistenceMappingError,
|
|
476
|
-
RSetup | LifecycleHooks | NativeDatabase
|
|
476
|
+
RSetup | LifecycleHooks | NativeDatabase | Crypto.Crypto
|
|
477
477
|
> =>
|
|
478
478
|
Effect.gen(function* () {
|
|
479
479
|
const mapping = yield* capturedMapping(source, "assertion", configuration);
|
|
@@ -490,7 +490,7 @@ export const makePasskeyTargetKernel = (
|
|
|
490
490
|
): Effect.Effect<
|
|
491
491
|
PasskeyRegistrationCeremonyServices,
|
|
492
492
|
PasskeyConfigurationError | PersistenceMappingError,
|
|
493
|
-
RSetup | LifecycleHooks | NativeDatabase
|
|
493
|
+
RSetup | LifecycleHooks | NativeDatabase | Crypto.Crypto
|
|
494
494
|
> =>
|
|
495
495
|
Effect.gen(function* () {
|
|
496
496
|
const mapping = yield* capturedMapping(source, "registration", configuration);
|
|
@@ -508,7 +508,11 @@ export const makePasskeyTargetKernel = (
|
|
|
508
508
|
services: PasskeyPersistenceServices,
|
|
509
509
|
append: (statement: Statement<unknown>) => void,
|
|
510
510
|
) => Effect.Effect<A, E, R>,
|
|
511
|
-
): Effect.Effect<
|
|
511
|
+
): Effect.Effect<
|
|
512
|
+
A,
|
|
513
|
+
PasskeyCoordinatorError<E>,
|
|
514
|
+
R | RSetup | LifecycleHooks | NativeDatabase | Crypto.Crypto
|
|
515
|
+
> =>
|
|
512
516
|
Effect.flatMap(capturedMapping(source, "assertion", configuration), (mapping) =>
|
|
513
517
|
Effect.flatMap(LifecycleHooks, (hooks) =>
|
|
514
518
|
coordinateTransactionOwner(
|
|
@@ -545,7 +549,11 @@ export const makePasskeyTargetKernel = (
|
|
|
545
549
|
services: PasskeyRegistrationCeremonyServices,
|
|
546
550
|
append: (statement: Statement<unknown>) => void,
|
|
547
551
|
) => Effect.Effect<A, E, R>,
|
|
548
|
-
): Effect.Effect<
|
|
552
|
+
): Effect.Effect<
|
|
553
|
+
A,
|
|
554
|
+
PasskeyCoordinatorError<E>,
|
|
555
|
+
R | RSetup | LifecycleHooks | NativeDatabase | Crypto.Crypto
|
|
556
|
+
> =>
|
|
549
557
|
Effect.flatMap(capturedMapping(source, "registration", configuration), (mapping) =>
|
|
550
558
|
Effect.flatMap(LifecycleHooks, (hooks) =>
|
|
551
559
|
coordinateTransactionOwner(
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
/* oxlint-disable no-explicit-any -- existing mapped-row bridge; public adapters preserve table, ID, and Effect types. */
|
|
2
2
|
|
|
3
|
-
// oxlint-disable-next-line import/extensions -- Noble exposes explicit .js subpaths.
|
|
4
|
-
import { sha256 } from "@noble/hashes/sha2.js";
|
|
5
3
|
import {
|
|
6
4
|
type PasskeyActionAuthorization,
|
|
7
5
|
type PasskeyCeremony,
|
|
@@ -18,11 +16,11 @@ import {
|
|
|
18
16
|
PasskeyManagementPolicy,
|
|
19
17
|
snapshotPasskeySync,
|
|
20
18
|
} from "@yielded/auth/Passkey";
|
|
21
|
-
import {
|
|
19
|
+
import { type SubjectId } from "@yielded/auth/Schema";
|
|
22
20
|
import { SessionInvalidationWindow } from "@yielded/auth/Sessions";
|
|
23
21
|
import { DateTime, Effect, Schema } from "effect";
|
|
24
|
-
import { Base64Url } from "effect/encoding";
|
|
25
22
|
|
|
23
|
+
import { digest as digestText } from "../crypto";
|
|
26
24
|
import type { PersistenceMappingError } from "../mapping-error";
|
|
27
25
|
import type { QueryOperations } from "../query-operations";
|
|
28
26
|
import type { makeTransactionKernel } from "../transaction-kernel";
|
|
@@ -85,10 +83,7 @@ export const makePasskeyWriteStateKernel = (
|
|
|
85
83
|
const digest = <S extends Schema.Codec<unknown, unknown, never, never>>(
|
|
86
84
|
schema: S,
|
|
87
85
|
value: S["Type"],
|
|
88
|
-
) =>
|
|
89
|
-
TokenDigest.make(
|
|
90
|
-
Base64Url.encode(sha256(new TextEncoder().encode(jsonStorage(schema).encode(value)))),
|
|
91
|
-
);
|
|
86
|
+
) => digestText(jsonStorage(schema).encode(value));
|
|
92
87
|
|
|
93
88
|
const currentSubject = Effect.fn("passkey.currentWriteSubject")(function* (
|
|
94
89
|
mapping: any,
|
|
@@ -205,7 +200,7 @@ export const makePasskeyWriteStateKernel = (
|
|
|
205
200
|
read.subjectIds.equals(table.decodeSubjectId(copiedRow(row)), subject.nativeId),
|
|
206
201
|
);
|
|
207
202
|
const ownership = read.credentialOwnership;
|
|
208
|
-
const key = credentialKey(decoded.rpId, decoded.protocolCredentialId);
|
|
203
|
+
const key = yield* credentialKey(decoded.rpId, decoded.protocolCredentialId);
|
|
209
204
|
|
|
210
205
|
const tuple = (yield* owner.read(
|
|
211
206
|
ownership.table,
|
|
@@ -226,7 +221,7 @@ export const makePasskeyWriteStateKernel = (
|
|
|
226
221
|
row[table.credentialKey] === key,
|
|
227
222
|
);
|
|
228
223
|
const handle = read.handleOwnership;
|
|
229
|
-
const hashedHandle = handleKey(decoded.rpId, decoded.userHandle);
|
|
224
|
+
const hashedHandle = yield* handleKey(decoded.rpId, decoded.userHandle);
|
|
230
225
|
|
|
231
226
|
const held = (yield* owner.read(
|
|
232
227
|
handle.table,
|
|
@@ -470,9 +465,10 @@ export const makePasskeyWriteStateKernel = (
|
|
|
470
465
|
}
|
|
471
466
|
owner.postconditions.push(mapping.invalidation.postcondition(input));
|
|
472
467
|
const flow = mapping.flow;
|
|
468
|
+
const scope = yield* subjectScope(subject.subjectId);
|
|
473
469
|
|
|
474
470
|
const where = both(
|
|
475
|
-
equal(flow.table, { [flow.subjectScope]:
|
|
471
|
+
equal(flow.table, { [flow.subjectScope]: scope }),
|
|
476
472
|
sql`${col(flow.table, flow.state)} in (${flow.states.Pending}, ${flow.states.Claimed})`,
|
|
477
473
|
);
|
|
478
474
|
|
|
@@ -485,7 +481,7 @@ export const makePasskeyWriteStateKernel = (
|
|
|
485
481
|
for (const observation of owner.observations)
|
|
486
482
|
if (observation.table === flow.table)
|
|
487
483
|
observation.rows = observation.rows.map((row) =>
|
|
488
|
-
row[flow.subjectScope] ===
|
|
484
|
+
row[flow.subjectScope] === scope &&
|
|
489
485
|
[flow.states.Pending, flow.states.Claimed].includes(row[flow.state])
|
|
490
486
|
? { ...row, [flow.state]: flow.states.Rejected, [flow.version]: owner.marker }
|
|
491
487
|
: row,
|
|
@@ -559,8 +555,8 @@ export const makePasskeyWriteStateKernel = (
|
|
|
559
555
|
});
|
|
560
556
|
|
|
561
557
|
const input = { subjectId: subject.nativeId, credential, summary: safe, marker: owner.marker };
|
|
562
|
-
const tupleKey = credentialKey(credential.rpId, credential.protocolCredentialId);
|
|
563
|
-
const hashedHandle = handleKey(credential.rpId, credential.userHandle);
|
|
558
|
+
const tupleKey = yield* credentialKey(credential.rpId, credential.protocolCredentialId);
|
|
559
|
+
const hashedHandle = yield* handleKey(credential.rpId, credential.userHandle);
|
|
564
560
|
const tuple = read.credentialOwnership;
|
|
565
561
|
|
|
566
562
|
const absent = yield* owner.read(
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
import * as M from "@yielded/auth/Passkey";
|
|
11
11
|
import { SubjectId } from "@yielded/auth/Schema";
|
|
12
12
|
import { SessionInvalidationWindow } from "@yielded/auth/Sessions";
|
|
13
|
-
import { Effect, Layer, Schema } from "effect";
|
|
13
|
+
import { type Crypto, Effect, Layer, Schema } from "effect";
|
|
14
14
|
import type { Statement } from "effect/sql/Statement";
|
|
15
15
|
|
|
16
16
|
import type { PasskeyMappingSource } from "../models/passkey-model";
|
|
@@ -371,7 +371,11 @@ export const makePasskeyWriteTargetKernel = (
|
|
|
371
371
|
services: PasskeyManagementServices,
|
|
372
372
|
append: (statement: Statement<unknown>) => void,
|
|
373
373
|
) => Effect.Effect<A, E, R>,
|
|
374
|
-
): Effect.Effect<
|
|
374
|
+
): Effect.Effect<
|
|
375
|
+
A,
|
|
376
|
+
PasskeyCoordinatorError<E>,
|
|
377
|
+
R | RSetup | LifecycleHooks | NativeDatabase | Crypto.Crypto
|
|
378
|
+
> =>
|
|
375
379
|
Effect.gen(function* () {
|
|
376
380
|
const mapping = yield* writeMapping(source, configuration, false);
|
|
377
381
|
const hooks = yield* LifecycleHooks;
|
|
@@ -406,7 +410,11 @@ export const makePasskeyWriteTargetKernel = (
|
|
|
406
410
|
services: PasskeyRegistrationServices<Value>,
|
|
407
411
|
append: (statement: Statement<unknown>) => void,
|
|
408
412
|
) => Effect.Effect<A, E, R>,
|
|
409
|
-
): Effect.Effect<
|
|
413
|
+
): Effect.Effect<
|
|
414
|
+
A,
|
|
415
|
+
PasskeyCoordinatorError<E>,
|
|
416
|
+
R | RSetup | LifecycleHooks | NativeDatabase | Crypto.Crypto
|
|
417
|
+
> =>
|
|
410
418
|
Effect.gen(function* () {
|
|
411
419
|
const mapping = yield* writeMapping(source, configuration, true);
|
|
412
420
|
const hooks = yield* LifecycleHooks;
|
package/src/internal/passkeys.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { digest } from "@yielded/auth-crypto";
|
|
2
1
|
import {
|
|
3
2
|
PasskeyCredentials,
|
|
4
3
|
PasskeyCredential,
|
|
@@ -19,6 +18,7 @@ import {
|
|
|
19
18
|
type MappingInput,
|
|
20
19
|
type PasskeyFeature,
|
|
21
20
|
} from "./configuration";
|
|
21
|
+
import { digest } from "./crypto";
|
|
22
22
|
import { PersistenceMappingError } from "./mapping-error";
|
|
23
23
|
import {
|
|
24
24
|
requiredPasskeyCredentialConstraints,
|
|
@@ -54,6 +54,13 @@ const ModuleRow = Schema.Struct({
|
|
|
54
54
|
policyRevision: Schema.String,
|
|
55
55
|
});
|
|
56
56
|
|
|
57
|
+
const ConfiguredModuleRow = Schema.Struct({
|
|
58
|
+
moduleId: Schema.String,
|
|
59
|
+
active: Schema.Boolean,
|
|
60
|
+
policyRevision: Schema.String,
|
|
61
|
+
admitted: Schema.Literals(["present", "missing"]),
|
|
62
|
+
});
|
|
63
|
+
|
|
57
64
|
const credentialData = PasskeyCredential.mapFields(
|
|
58
65
|
({ revision: _revision, active: _active, ...fields }) => fields,
|
|
59
66
|
);
|
|
@@ -390,7 +397,21 @@ export const makeComposedPasskeys = (
|
|
|
390
397
|
const client = yield* SqlClient.SqlClient;
|
|
391
398
|
|
|
392
399
|
const policies = yield* Effect.forEach(features, (feature) =>
|
|
393
|
-
Effect.
|
|
400
|
+
Effect.gen(function* () {
|
|
401
|
+
const policy = yield* feature.policy;
|
|
402
|
+
const encoded = yield* Schema.encodeEffect(policyJson)(policy);
|
|
403
|
+
|
|
404
|
+
const policyRevision = yield* digest(
|
|
405
|
+
yield* Schema.encodeEffect(seedJson)({
|
|
406
|
+
policy,
|
|
407
|
+
...(feature.managementPolicy === undefined
|
|
408
|
+
? {}
|
|
409
|
+
: { management: feature.managementPolicy }),
|
|
410
|
+
}),
|
|
411
|
+
);
|
|
412
|
+
|
|
413
|
+
return { feature, policy, encoded, policyRevision };
|
|
414
|
+
}),
|
|
394
415
|
);
|
|
395
416
|
|
|
396
417
|
const m = yield* Effect.try({
|
|
@@ -413,82 +434,112 @@ export const makeComposedPasskeys = (
|
|
|
413
434
|
};
|
|
414
435
|
|
|
415
436
|
const native = yield* CurrentProofSql;
|
|
437
|
+
const module = m.table("passkeyModules");
|
|
438
|
+
const columns = getTableColumns(module);
|
|
439
|
+
const admission = m.table("passkeyAdmissions");
|
|
440
|
+
const ac = getTableColumns(admission);
|
|
441
|
+
|
|
442
|
+
// Read live configuration on every acquisition. Matching modules and
|
|
443
|
+
// admission ownership need no initialization writes or advisory lock.
|
|
444
|
+
const configured = yield* native
|
|
445
|
+
.select({
|
|
446
|
+
moduleId: columns.moduleId,
|
|
447
|
+
active: columns.active,
|
|
448
|
+
policyRevision: columns.policyRevision,
|
|
449
|
+
// Predicate SQL preserves qualifiers through single-table projections.
|
|
450
|
+
admitted: sql`case when exists(select 1 from ${admission}
|
|
451
|
+
where ${and(eq(ac.authorityScope, namespace), eq(ac.moduleId, columns.moduleId))})
|
|
452
|
+
then 'present' else 'missing' end`,
|
|
453
|
+
})
|
|
454
|
+
.from(module)
|
|
455
|
+
.where(
|
|
456
|
+
inArray(
|
|
457
|
+
columns.moduleId,
|
|
458
|
+
policies.map(({ feature }) => feature.moduleId),
|
|
459
|
+
),
|
|
460
|
+
)
|
|
461
|
+
.pipe(
|
|
462
|
+
Effect.flatMap(Schema.decodeUnknownEffect(Schema.Array(ConfiguredModuleRow))),
|
|
463
|
+
Effect.map((rows) =>
|
|
464
|
+
policies.every(({ feature, policyRevision }) =>
|
|
465
|
+
rows.some(
|
|
466
|
+
(row) =>
|
|
467
|
+
row.moduleId === feature.moduleId &&
|
|
468
|
+
row.active &&
|
|
469
|
+
row.admitted === "present" &&
|
|
470
|
+
row.policyRevision === policyRevision,
|
|
471
|
+
),
|
|
472
|
+
),
|
|
473
|
+
),
|
|
474
|
+
);
|
|
416
475
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
const encoded
|
|
423
|
-
|
|
424
|
-
const policyRevision = digest(
|
|
425
|
-
yield* Schema.encodeEffect(seedJson)({
|
|
426
|
-
policy,
|
|
427
|
-
...(feature.managementPolicy === undefined
|
|
428
|
-
? {}
|
|
429
|
-
: { management: feature.managementPolicy }),
|
|
430
|
-
}),
|
|
431
|
-
);
|
|
432
|
-
|
|
433
|
-
const module = m.table("passkeyModules");
|
|
434
|
-
const columns = getTableColumns(module);
|
|
435
|
-
|
|
436
|
-
yield* native
|
|
437
|
-
.insert(module)
|
|
438
|
-
.values({ moduleId: feature.moduleId, active: true, policy: encoded, policyRevision })
|
|
439
|
-
.onConflictDoNothing();
|
|
440
|
-
|
|
441
|
-
const rows = yield* native
|
|
442
|
-
.select()
|
|
443
|
-
.from(module)
|
|
444
|
-
.where(eq(columns.moduleId, feature.moduleId));
|
|
445
|
-
|
|
446
|
-
const row = yield* Schema.decodeUnknownEffect(ModuleRow)(rows[0]);
|
|
447
|
-
|
|
448
|
-
if (!row.active)
|
|
449
|
-
return yield* configurationError(`Passkey module ${feature.moduleId} is disabled`);
|
|
450
|
-
if (row.policyRevision !== policyRevision) {
|
|
451
|
-
const previous = yield* Schema.decodeEffect(policyJson)(row.policy);
|
|
452
|
-
|
|
453
|
-
if (policy.generation <= previous.generation)
|
|
454
|
-
return yield* configurationError(
|
|
455
|
-
`Increase the passkey policy generation to change ${feature.moduleId}`,
|
|
456
|
-
);
|
|
476
|
+
if (!configured)
|
|
477
|
+
yield* client.withTransaction(
|
|
478
|
+
Effect.gen(function* () {
|
|
479
|
+
if (dialect === "pg")
|
|
480
|
+
yield* client`select pg_advisory_xact_lock(hashtext(${`${namespace}/passkeys`}))`;
|
|
481
|
+
for (const { feature, policy, encoded, policyRevision } of policies) {
|
|
457
482
|
yield* native
|
|
458
|
-
.
|
|
459
|
-
.
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
const updated = yield* native
|
|
483
|
+
.insert(module)
|
|
484
|
+
.values({
|
|
485
|
+
moduleId: feature.moduleId,
|
|
486
|
+
active: true,
|
|
487
|
+
policy: encoded,
|
|
488
|
+
policyRevision,
|
|
489
|
+
})
|
|
490
|
+
.onConflictDoNothing();
|
|
491
|
+
|
|
492
|
+
const rows = yield* native
|
|
469
493
|
.select()
|
|
470
494
|
.from(module)
|
|
471
495
|
.where(eq(columns.moduleId, feature.moduleId));
|
|
472
496
|
|
|
473
|
-
const
|
|
497
|
+
const row = yield* Schema.decodeUnknownEffect(ModuleRow)(rows[0]);
|
|
474
498
|
|
|
475
|
-
if (!
|
|
476
|
-
return yield* configurationError(
|
|
477
|
-
|
|
478
|
-
);
|
|
499
|
+
if (!row.active)
|
|
500
|
+
return yield* configurationError(`Passkey module ${feature.moduleId} is disabled`);
|
|
501
|
+
if (row.policyRevision !== policyRevision) {
|
|
502
|
+
const previous = yield* Schema.decodeEffect(policyJson)(row.policy);
|
|
503
|
+
|
|
504
|
+
if (policy.generation <= previous.generation)
|
|
505
|
+
return yield* configurationError(
|
|
506
|
+
`Increase the passkey policy generation to change ${feature.moduleId}`,
|
|
507
|
+
);
|
|
508
|
+
yield* native
|
|
509
|
+
.update(module)
|
|
510
|
+
.set({ policy: encoded, policyRevision })
|
|
511
|
+
.where(
|
|
512
|
+
and(
|
|
513
|
+
eq(columns.moduleId, feature.moduleId),
|
|
514
|
+
eq(columns.policyRevision, row.policyRevision),
|
|
515
|
+
eq(columns.active, true),
|
|
516
|
+
),
|
|
517
|
+
);
|
|
518
|
+
|
|
519
|
+
const updated = yield* native
|
|
520
|
+
.select()
|
|
521
|
+
.from(module)
|
|
522
|
+
.where(eq(columns.moduleId, feature.moduleId));
|
|
523
|
+
|
|
524
|
+
const applied = yield* Schema.decodeUnknownEffect(ModuleRow)(updated[0]);
|
|
525
|
+
|
|
526
|
+
if (!applied.active || applied.policyRevision !== policyRevision)
|
|
527
|
+
return yield* configurationError(
|
|
528
|
+
`Passkey policy changed while configuring ${feature.moduleId}`,
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
yield* native
|
|
532
|
+
.insert(m.table("passkeyAdmissions"))
|
|
533
|
+
.values({
|
|
534
|
+
authorityScope: namespace,
|
|
535
|
+
moduleId: feature.moduleId,
|
|
536
|
+
version: policyRevision,
|
|
537
|
+
ownerMarker: policyRevision,
|
|
538
|
+
})
|
|
539
|
+
.onConflictDoNothing();
|
|
479
540
|
}
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
.values({
|
|
483
|
-
authorityScope: namespace,
|
|
484
|
-
moduleId: feature.moduleId,
|
|
485
|
-
version: policyRevision,
|
|
486
|
-
ownerMarker: policyRevision,
|
|
487
|
-
})
|
|
488
|
-
.onConflictDoNothing();
|
|
489
|
-
}
|
|
490
|
-
}),
|
|
491
|
-
);
|
|
541
|
+
}),
|
|
542
|
+
);
|
|
492
543
|
|
|
493
544
|
const credential = yield* makeTargetPasskeyCredentials<ReadMapping, never>(
|
|
494
545
|
m.read,
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { digest, randomId } from "@yielded/auth-crypto";
|
|
2
1
|
import { EmailAddressPersistence, EmailUnavailable } from "@yielded/auth/Email";
|
|
3
2
|
import { LifecycleHooks } from "@yielded/auth/Hooks";
|
|
4
3
|
import type { PasskeyConfig } from "@yielded/auth/Passkey";
|
|
@@ -7,7 +6,7 @@ import { hooksLayer } from "@yielded/auth/Persistence";
|
|
|
7
6
|
import { PhoneAdmission, PhoneSignInTargets, PhoneOtpUnavailable } from "@yielded/auth/PhoneOtp";
|
|
8
7
|
import { ProofPersistence, ProofUnavailable } from "@yielded/auth/Proofs";
|
|
9
8
|
import { AuthenticationAuthority, SessionUnavailable } from "@yielded/auth/Sessions";
|
|
10
|
-
import { Context, Effect, Layer, Schema } from "effect";
|
|
9
|
+
import { Context, type Crypto, Effect, Layer, Schema } from "effect";
|
|
11
10
|
import { SqlClient } from "effect/sql";
|
|
12
11
|
|
|
13
12
|
import {
|
|
@@ -27,6 +26,7 @@ import {
|
|
|
27
26
|
type PasskeyRequirement,
|
|
28
27
|
type MappingInput,
|
|
29
28
|
} from "./configuration";
|
|
29
|
+
import { randomId } from "./crypto";
|
|
30
30
|
import { makeEmailKernel, CurrentEmailSql, type EmailSqlDatabase } from "./email-kernel";
|
|
31
31
|
import { PersistenceMappingError } from "./mapping-error";
|
|
32
32
|
import {
|
|
@@ -52,7 +52,11 @@ import {
|
|
|
52
52
|
type StorageRole,
|
|
53
53
|
type StorageTable,
|
|
54
54
|
} from "./storage-tables";
|
|
55
|
-
import {
|
|
55
|
+
import {
|
|
56
|
+
validateStorageBatch,
|
|
57
|
+
withStorageValidation,
|
|
58
|
+
type StorageValidation,
|
|
59
|
+
} from "./storage-validation";
|
|
56
60
|
import { makeTransactionExecutionKernel } from "./transaction-execution-kernel";
|
|
57
61
|
import {
|
|
58
62
|
makeTransactionKernel,
|
|
@@ -74,7 +78,12 @@ export interface Backend<T extends object, R> {
|
|
|
74
78
|
}) => Effect.Effect<
|
|
75
79
|
Context.Context<never>,
|
|
76
80
|
PersistenceConfigurationError,
|
|
77
|
-
|
|
81
|
+
| PasskeyConfig
|
|
82
|
+
| LifecycleHooks
|
|
83
|
+
| SqlClient.SqlClient
|
|
84
|
+
| NativeDatabase
|
|
85
|
+
| CurrentProofSql
|
|
86
|
+
| Crypto.Crypto
|
|
78
87
|
>;
|
|
79
88
|
}
|
|
80
89
|
|
|
@@ -208,8 +217,10 @@ export const createPersistence = <T extends object, R>(
|
|
|
208
217
|
if (idColumn === undefined || idColumn.type === "boolean")
|
|
209
218
|
throw configError("Subject ID must map a text or integer column");
|
|
210
219
|
|
|
211
|
-
const prefix =
|
|
212
|
-
|
|
220
|
+
const prefix = options.prefix;
|
|
221
|
+
|
|
222
|
+
if (managed && (prefix === undefined || !/^[A-Za-z_][A-Za-z0-9_]{0,100}$/.test(prefix)))
|
|
223
|
+
throw configError("Managed persistence requires an explicit SQL table prefix");
|
|
213
224
|
|
|
214
225
|
const schema: Partial<Record<StorageRole, T>> = { ...options.tables };
|
|
215
226
|
|
|
@@ -310,6 +321,8 @@ export const createPersistence = <T extends object, R>(
|
|
|
310
321
|
return yield* configError(
|
|
311
322
|
"Persistence configuration belongs to a different Auth definition",
|
|
312
323
|
);
|
|
324
|
+
const validations: StorageValidation[] = [];
|
|
325
|
+
|
|
313
326
|
for (const role of roles) {
|
|
314
327
|
const table = (storage.schema as Partial<Record<StorageRole, T>>)[role];
|
|
315
328
|
|
|
@@ -321,12 +334,14 @@ export const createPersistence = <T extends object, R>(
|
|
|
321
334
|
if (columns[name] === undefined)
|
|
322
335
|
return yield* configError(`Missing ${role}.${name} column`);
|
|
323
336
|
}
|
|
324
|
-
|
|
337
|
+
validations.push({ table: description, required: storageTables[role].unique });
|
|
325
338
|
}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
339
|
+
validations.push({
|
|
340
|
+
table: backend.describe(storage.subjects.table as T),
|
|
341
|
+
required: [[storage.subjects.id]],
|
|
342
|
+
});
|
|
343
|
+
yield* validateStorageBatch(dialect, validations);
|
|
344
|
+
const mappings = yield* makeMappings(storage);
|
|
330
345
|
|
|
331
346
|
const standalone = <E>(error: () => E) =>
|
|
332
347
|
requireStandalone(error, client.transactionService);
|
|
@@ -564,6 +579,7 @@ export const createPersistence = <T extends object, R>(
|
|
|
564
579
|
Context.Context<never>,
|
|
565
580
|
PersistenceConfigurationError,
|
|
566
581
|
| PasskeyRequirement<A>
|
|
582
|
+
| Crypto.Crypto
|
|
567
583
|
| LifecycleHooks
|
|
568
584
|
| SqlClient.SqlClient
|
|
569
585
|
| NativeDatabase
|
|
@@ -582,6 +598,7 @@ export const createPersistence = <T extends object, R>(
|
|
|
582
598
|
// The checked capability metadata above determines exactly these service keys.
|
|
583
599
|
return context as Context.Context<Ports<C, Id, A>>;
|
|
584
600
|
}).pipe(
|
|
601
|
+
withStorageValidation,
|
|
585
602
|
Effect.mapError((error) =>
|
|
586
603
|
Schema.is(PersistenceConfigurationError)(error)
|
|
587
604
|
? error
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { digest, randomId } from "@yielded/auth-crypto";
|
|
2
1
|
import {
|
|
3
2
|
PhoneCustody,
|
|
4
3
|
PhoneCommandId,
|
|
@@ -14,6 +13,7 @@ import type { SubjectId } from "@yielded/auth/Schema";
|
|
|
14
13
|
import { AuthenticationRequirement, SecurityRevision } from "@yielded/auth/Sessions";
|
|
15
14
|
import { Context, Effect, Option, Schema } from "effect";
|
|
16
15
|
|
|
16
|
+
import { digest, randomId } from "./crypto";
|
|
17
17
|
/* oxlint-disable no-explicit-any -- existing storage kernels erase foreign table shapes; domain errors remain typed. */
|
|
18
18
|
import type { SqlExpression as SQL } from "./query-operations";
|
|
19
19
|
import type { QueryOperations, SqlFragment, SqlColumn } from "./query-operations";
|
|
@@ -100,7 +100,7 @@ export const makePhoneKernel = <
|
|
|
100
100
|
moduleId: mapping.moduleId,
|
|
101
101
|
record: value,
|
|
102
102
|
}),
|
|
103
|
-
[mapping.state.version]: randomId
|
|
103
|
+
[mapping.state.version]: yield* randomId,
|
|
104
104
|
};
|
|
105
105
|
|
|
106
106
|
if (observed.rows.length === 0) {
|
|
@@ -128,7 +128,7 @@ export const makePhoneKernel = <
|
|
|
128
128
|
mapping: any,
|
|
129
129
|
phoneNumber: string,
|
|
130
130
|
) {
|
|
131
|
-
const key = scope(mapping.moduleId, "custody", phoneNumber),
|
|
131
|
+
const key = yield* scope(mapping.moduleId, "custody", phoneNumber),
|
|
132
132
|
observation = yield* stateRead(mapping, key);
|
|
133
133
|
|
|
134
134
|
const custody =
|
|
@@ -327,7 +327,7 @@ export const makePhoneKernel = <
|
|
|
327
327
|
now = yield* owner.now(mapping),
|
|
328
328
|
policy = mapping.admission;
|
|
329
329
|
|
|
330
|
-
const key = scope(mapping.moduleId, "admission", input.action + "/" + input.requestId),
|
|
330
|
+
const key = yield* scope(mapping.moduleId, "admission", input.action + "/" + input.requestId),
|
|
331
331
|
receipt = yield* stateRead(mapping, key);
|
|
332
332
|
|
|
333
333
|
if (receipt.rows.length !== 0) {
|
|
@@ -338,7 +338,7 @@ export const makePhoneKernel = <
|
|
|
338
338
|
|
|
339
339
|
return (
|
|
340
340
|
saved.fingerprint === input.fingerprint &&
|
|
341
|
-
saved.network === digest(input.networkKey) &&
|
|
341
|
+
saved.network === (yield* digest(input.networkKey)) &&
|
|
342
342
|
saved.accepted === true &&
|
|
343
343
|
saved.expiresAtMillis > now
|
|
344
344
|
);
|
|
@@ -347,11 +347,20 @@ export const makePhoneKernel = <
|
|
|
347
347
|
const window = Math.floor(now / policy.windowMillis),
|
|
348
348
|
entries = [
|
|
349
349
|
{
|
|
350
|
-
key: scope(
|
|
350
|
+
key: yield* scope(
|
|
351
|
+
mapping.moduleId,
|
|
352
|
+
"network",
|
|
353
|
+
input.action + "/" + (yield* digest(input.networkKey)),
|
|
354
|
+
),
|
|
351
355
|
limit: input.action === "request" ? policy.networkRequests : policy.networkAttempts,
|
|
352
356
|
},
|
|
353
357
|
...(input.action === "request"
|
|
354
|
-
? [
|
|
358
|
+
? [
|
|
359
|
+
{
|
|
360
|
+
key: yield* scope(mapping.moduleId, "messages", "global"),
|
|
361
|
+
limit: policy.maximumMessages,
|
|
362
|
+
},
|
|
363
|
+
]
|
|
355
364
|
: []),
|
|
356
365
|
].sort((a, b) => a.key.localeCompare(b.key));
|
|
357
366
|
|
|
@@ -383,7 +392,7 @@ export const makePhoneKernel = <
|
|
|
383
392
|
key,
|
|
384
393
|
{
|
|
385
394
|
fingerprint: input.fingerprint,
|
|
386
|
-
network: digest(input.networkKey),
|
|
395
|
+
network: yield* digest(input.networkKey),
|
|
387
396
|
accepted,
|
|
388
397
|
expiresAtMillis: now + Math.min(policy.requestRetentionMillis, input.replayLifetimeMillis),
|
|
389
398
|
},
|
|
@@ -446,7 +455,7 @@ export const makePhoneKernel = <
|
|
|
446
455
|
)
|
|
447
456
|
return { decision: rejected };
|
|
448
457
|
|
|
449
|
-
const commandKey = scope(mapping.moduleId, "command", input.commandId),
|
|
458
|
+
const commandKey = yield* scope(mapping.moduleId, "command", input.commandId),
|
|
450
459
|
command = yield* stateRead(mapping, commandKey);
|
|
451
460
|
|
|
452
461
|
if (command.rows.length !== 0) return { decision: rejected };
|
|
@@ -564,10 +573,10 @@ export const makePhoneKernel = <
|
|
|
564
573
|
? mapping.subjectIds.toSubject(mapping.subjectIds.allocate())
|
|
565
574
|
: captured.target.revision!.subjectId,
|
|
566
575
|
nativeId = mapping.subjectIds.toNative(subjectId),
|
|
567
|
-
securityRevision = SecurityRevision.make(randomId
|
|
568
|
-
credentialRevision = SecurityRevision.make(randomId
|
|
569
|
-
custodyRevision = SecurityRevision.make(randomId
|
|
570
|
-
credentialId = captured.target.custody?.credentialId ?? randomId
|
|
576
|
+
securityRevision = SecurityRevision.make(yield* randomId),
|
|
577
|
+
credentialRevision = SecurityRevision.make(yield* randomId),
|
|
578
|
+
custodyRevision = SecurityRevision.make(yield* randomId),
|
|
579
|
+
credentialId = captured.target.custody?.credentialId ?? (yield* randomId);
|
|
571
580
|
|
|
572
581
|
invariant(mapping.subjectIds.toSubject(nativeId) === subjectId);
|
|
573
582
|
|
|
@@ -647,12 +656,12 @@ export const makePhoneKernel = <
|
|
|
647
656
|
yield* owner.update(
|
|
648
657
|
i.table,
|
|
649
658
|
{ [i.namespace]: "phone", [i.value]: old.phoneNumber },
|
|
650
|
-
{ [i.status]: i.encodeStatus(false), [i.revision]: randomId
|
|
659
|
+
{ [i.status]: i.encodeStatus(false), [i.revision]: yield* randomId },
|
|
651
660
|
);
|
|
652
661
|
yield* owner.update(
|
|
653
662
|
c.table,
|
|
654
663
|
{ [c.id]: old.credentialId, [c.subjectId]: nativeId },
|
|
655
|
-
{ [c.status]: c.encodeStatus(false), [c.revision]: randomId
|
|
664
|
+
{ [c.status]: c.encodeStatus(false), [c.revision]: yield* randomId },
|
|
656
665
|
);
|
|
657
666
|
yield* stateWrite(
|
|
658
667
|
mapping,
|
|
@@ -660,8 +669,8 @@ export const makePhoneKernel = <
|
|
|
660
669
|
{
|
|
661
670
|
...old,
|
|
662
671
|
state: "retired",
|
|
663
|
-
custodyRevision: SecurityRevision.make(randomId
|
|
664
|
-
credentialRevision: SecurityRevision.make(randomId
|
|
672
|
+
custodyRevision: SecurityRevision.make(yield* randomId),
|
|
673
|
+
credentialRevision: SecurityRevision.make(yield* randomId),
|
|
665
674
|
},
|
|
666
675
|
captured.source.observation,
|
|
667
676
|
);
|