@yielded/auth-persistence 0.1.0-beta.17 → 0.1.0-beta.19
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 +5 -4
- 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/email-kernel.d.mts +7 -7
- 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 +705 -249
- package/dist/internal/passkeys.mjs +32 -14
- package/dist/internal/passkeys.mjs.map +1 -1
- package/dist/internal/password-kernel.d.mts +6 -6
- package/dist/internal/password-kernel.mjs +1 -1
- package/dist/internal/password-kernel.mjs.map +1 -1
- package/dist/internal/persistence.d.mts +2 -2
- package/dist/internal/persistence.mjs +18 -9
- package/dist/internal/persistence.mjs.map +1 -1
- package/dist/internal/phone-kernel.d.mts +7 -7
- 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/session-kernel.mjs +2 -1
- package/dist/internal/session-kernel.mjs.map +1 -1
- package/dist/internal/sql-query.mjs +1 -1
- package/dist/internal/sql-query.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 +5 -3
- 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/password-kernel.ts +2 -2
- package/src/internal/persistence.ts +30 -13
- package/src/internal/phone-kernel.ts +26 -17
- package/src/internal/registration.ts +10 -8
- package/src/internal/session-kernel.ts +3 -2
- package/src/internal/sql-query.ts +3 -2
- 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
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,
|
|
@@ -1168,9 +1168,9 @@ export const makePasswordKernel = <
|
|
|
1168
1168
|
moduleId: input.moduleId,
|
|
1169
1169
|
action: input.action,
|
|
1170
1170
|
identifier: input.identifier,
|
|
1171
|
-
...(resolved === undefined
|
|
1171
|
+
...(resolved?.snapshot === undefined
|
|
1172
1172
|
? {}
|
|
1173
|
-
: { subjectId: resolved.snapshot
|
|
1173
|
+
: { subjectId: resolved.snapshot.revision.subjectId }),
|
|
1174
1174
|
});
|
|
1175
1175
|
|
|
1176
1176
|
const scopes = scopeEntries(keys, input.policy);
|
|
@@ -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);
|
|
@@ -403,7 +418,7 @@ export const createPersistence = <T extends object, R>(
|
|
|
403
418
|
locking: dialect === "pg",
|
|
404
419
|
standaloneGuard: standalone(() => PasswordUnavailable.make({})),
|
|
405
420
|
insertIfAbsent: (query) => query.onConflictDoNothing(),
|
|
406
|
-
proof: proofConfiguration,
|
|
421
|
+
...(proofConfiguration === undefined ? {} : { proof: proofConfiguration }),
|
|
407
422
|
})
|
|
408
423
|
.pipe(
|
|
409
424
|
Effect.provideService(CurrentPasswordSql, native),
|
|
@@ -439,7 +454,7 @@ export const createPersistence = <T extends object, R>(
|
|
|
439
454
|
mode: "interactive",
|
|
440
455
|
locking: dialect === "pg",
|
|
441
456
|
standaloneGuard: standalone(() => EmailUnavailable.make({})),
|
|
442
|
-
proof: proofConfiguration,
|
|
457
|
+
...(proofConfiguration === undefined ? {} : { proof: proofConfiguration }),
|
|
443
458
|
})
|
|
444
459
|
.pipe(
|
|
445
460
|
Effect.provideService(CurrentEmailSql, native),
|
|
@@ -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
|
);
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import { randomId } from "@yielded/auth-crypto";
|
|
2
1
|
import { coordinateCommit, hasCommitScope, LifecycleHooks } from "@yielded/auth/Hooks";
|
|
3
2
|
import type { LoginIdentifier } from "@yielded/auth/Identity";
|
|
4
3
|
import { PasswordUnavailable } from "@yielded/auth/Password";
|
|
5
4
|
import { reportPersistenceFailure } from "@yielded/auth/Persistence";
|
|
6
5
|
import type { SubjectId } from "@yielded/auth/Schema";
|
|
7
6
|
import { SecurityRevision } from "@yielded/auth/Sessions";
|
|
8
|
-
import { type Context, Effect, Schema } from "effect";
|
|
7
|
+
import { type Context, Crypto, Effect, Schema } from "effect";
|
|
9
8
|
|
|
10
9
|
import { PersistenceConfigurationError } from "./configuration";
|
|
10
|
+
import { randomId } from "./crypto";
|
|
11
11
|
import { CurrentPasswordSql } from "./password-kernel";
|
|
12
12
|
import type { QueryOperations } from "./query-operations";
|
|
13
13
|
import type { PasswordRegistrationAuthority } from "./registration-contract";
|
|
@@ -25,7 +25,7 @@ type CreateSubject = (input: {
|
|
|
25
25
|
* always suppress; a public request ID never recovers a private password intent.
|
|
26
26
|
*/
|
|
27
27
|
export const makeRegistrationAuthority = Effect.fn("makeRegistrationAuthority")(function* <R>(
|
|
28
|
-
mappings: ReturnType<typeof makeMappings
|
|
28
|
+
mappings: Effect.Success<ReturnType<typeof makeMappings>>,
|
|
29
29
|
operations: QueryOperations,
|
|
30
30
|
standalone: Effect.Effect<void, PasswordUnavailable>,
|
|
31
31
|
provisioning: Context.Key<R, object>,
|
|
@@ -33,10 +33,11 @@ export const makeRegistrationAuthority = Effect.fn("makeRegistrationAuthority")(
|
|
|
33
33
|
): Effect.fn.Return<
|
|
34
34
|
PasswordRegistrationAuthority<unknown>,
|
|
35
35
|
PersistenceConfigurationError,
|
|
36
|
-
LifecycleHooks | CurrentPasswordSql | R
|
|
36
|
+
LifecycleHooks | CurrentPasswordSql | Crypto.Crypto | R
|
|
37
37
|
> {
|
|
38
38
|
const database = yield* CurrentPasswordSql;
|
|
39
39
|
const hooks = yield* LifecycleHooks;
|
|
40
|
+
const crypto = yield* Crypto.Crypto;
|
|
40
41
|
// Core has already decoded registration with the selected strategy's Schema;
|
|
41
42
|
// the dynamic strategy table erases only that heterogeneous callback signature.
|
|
42
43
|
const creators = (yield* provisioning) as Readonly<Record<string, CreateSubject>>;
|
|
@@ -129,7 +130,7 @@ export const makeRegistrationAuthority = Effect.fn("makeRegistrationAuthority")(
|
|
|
129
130
|
mapping.identifier.encodeInitialInsert(
|
|
130
131
|
input.identifier,
|
|
131
132
|
nativeId,
|
|
132
|
-
SecurityRevision.make(randomId
|
|
133
|
+
SecurityRevision.make(yield* randomId),
|
|
133
134
|
),
|
|
134
135
|
)
|
|
135
136
|
.onConflictDoNothing()
|
|
@@ -139,8 +140,8 @@ export const makeRegistrationAuthority = Effect.fn("makeRegistrationAuthority")(
|
|
|
139
140
|
// subject too, then persist a non-authorizing receipt in a new transaction.
|
|
140
141
|
if (bound.length !== 1) return yield* new IdentifierTaken({});
|
|
141
142
|
|
|
142
|
-
const credentialId = randomId
|
|
143
|
-
const credentialRevision = SecurityRevision.make(randomId
|
|
143
|
+
const credentialId = yield* randomId;
|
|
144
|
+
const credentialRevision = SecurityRevision.make(yield* randomId);
|
|
144
145
|
|
|
145
146
|
yield* tx.insert(mapping.credential.table).values(
|
|
146
147
|
mapping.credential.encodeInsert({
|
|
@@ -148,7 +149,7 @@ export const makeRegistrationAuthority = Effect.fn("makeRegistrationAuthority")(
|
|
|
148
149
|
subjectId: nativeId,
|
|
149
150
|
credentialId,
|
|
150
151
|
credentialRevision,
|
|
151
|
-
verifierVersion: SecurityRevision.make(randomId
|
|
152
|
+
verifierVersion: SecurityRevision.make(yield* randomId),
|
|
152
153
|
replacement: input.replacement,
|
|
153
154
|
}),
|
|
154
155
|
);
|
|
@@ -169,6 +170,7 @@ export const makeRegistrationAuthority = Effect.fn("makeRegistrationAuthority")(
|
|
|
169
170
|
return committed.value;
|
|
170
171
|
}).pipe(
|
|
171
172
|
Effect.provideService(LifecycleHooks, hooks),
|
|
173
|
+
Effect.provideService(Crypto.Crypto, crypto),
|
|
172
174
|
(work) => reportPersistenceFailure(work, Schema.is(PasswordUnavailable)),
|
|
173
175
|
Effect.mapError(() => PasswordUnavailable.make({})),
|
|
174
176
|
),
|
|
@@ -466,7 +466,7 @@ export const makeSessionKernel = (operations: QueryOperations) => {
|
|
|
466
466
|
const ensureDirectFlowAvailable = Effect.fn("DrizzleSession.ensureFlowAvailable")(function* (
|
|
467
467
|
mapping: AnyAuthorityMapping &
|
|
468
468
|
AnyFlowMapping & {
|
|
469
|
-
readonly pending?: PendingAuthenticationTables<any, any, any, any>["pending"];
|
|
469
|
+
readonly pending?: PendingAuthenticationTables<any, any, any, any>["pending"] | undefined;
|
|
470
470
|
},
|
|
471
471
|
evidence: AuthenticationEvidence,
|
|
472
472
|
nativeSubjectId: unknown,
|
|
@@ -1389,10 +1389,11 @@ export const makeSessionKernel = (operations: QueryOperations) => {
|
|
|
1389
1389
|
);
|
|
1390
1390
|
|
|
1391
1391
|
const extra = decoded[input.limit];
|
|
1392
|
+
const nextCursor = extra === undefined ? undefined : sessions.at(-1)?.sessionId;
|
|
1392
1393
|
|
|
1393
1394
|
return {
|
|
1394
1395
|
sessions,
|
|
1395
|
-
...(
|
|
1396
|
+
...(nextCursor === undefined ? {} : { nextCursor }),
|
|
1396
1397
|
};
|
|
1397
1398
|
}),
|
|
1398
1399
|
),
|
|
@@ -107,7 +107,7 @@ interface QueryState {
|
|
|
107
107
|
readonly on: Fragment;
|
|
108
108
|
}>;
|
|
109
109
|
readonly selection?: Selection;
|
|
110
|
-
readonly where?: Fragment;
|
|
110
|
+
readonly where?: Fragment | undefined;
|
|
111
111
|
readonly values?: Row;
|
|
112
112
|
readonly returning?: Selection | true;
|
|
113
113
|
readonly order?: ReadonlyArray<Fragment>;
|
|
@@ -258,7 +258,8 @@ export const makeSqlDatabase = Effect.fnUntraced(function* (dialect: Dialect) {
|
|
|
258
258
|
|
|
259
259
|
const database: SqlDatabase = {
|
|
260
260
|
$client: client,
|
|
261
|
-
select: (selection?: Selection) =>
|
|
261
|
+
select: (selection?: Selection) =>
|
|
262
|
+
query({ operation: "select", ...(selection === undefined ? {} : { selection }) }),
|
|
262
263
|
insert: (table: Table) => query({ operation: "insert", table }),
|
|
263
264
|
update: (table: Table) => query({ operation: "update", table }),
|
|
264
265
|
delete: (table: Table) => query({ operation: "delete", table }),
|