@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.
Files changed (75) hide show
  1. package/README.md +5 -2
  2. package/dist/Adapter.d.mts +2 -2
  3. package/dist/Adapter.mjs +2 -2
  4. package/dist/internal/configuration.d.mts +4 -3
  5. package/dist/internal/configuration.mjs.map +1 -1
  6. package/dist/internal/crypto.mjs +12 -0
  7. package/dist/internal/crypto.mjs.map +1 -0
  8. package/dist/internal/passkey/admission.mjs +19 -15
  9. package/dist/internal/passkey/admission.mjs.map +1 -1
  10. package/dist/internal/passkey/credentials.mjs +4 -4
  11. package/dist/internal/passkey/credentials.mjs.map +1 -1
  12. package/dist/internal/passkey/enrollment.mjs +6 -6
  13. package/dist/internal/passkey/enrollment.mjs.map +1 -1
  14. package/dist/internal/passkey/flow.mjs +7 -7
  15. package/dist/internal/passkey/flow.mjs.map +1 -1
  16. package/dist/internal/passkey/management.mjs +2 -2
  17. package/dist/internal/passkey/management.mjs.map +1 -1
  18. package/dist/internal/passkey/registration-ceremony.mjs +2 -2
  19. package/dist/internal/passkey/registration-ceremony.mjs.map +1 -1
  20. package/dist/internal/passkey/registration-custody.mjs +1 -1
  21. package/dist/internal/passkey/registration-custody.mjs.map +1 -1
  22. package/dist/internal/passkey/registration-write.mjs +12 -12
  23. package/dist/internal/passkey/registration-write.mjs.map +1 -1
  24. package/dist/internal/passkey/state.d.mts +1 -1
  25. package/dist/internal/passkey/state.mjs +8 -9
  26. package/dist/internal/passkey/state.mjs.map +1 -1
  27. package/dist/internal/passkey/target.d.mts +1 -1
  28. package/dist/internal/passkey/target.mjs.map +1 -1
  29. package/dist/internal/passkey/write-state.mjs +13 -13
  30. package/dist/internal/passkey/write-state.mjs.map +1 -1
  31. package/dist/internal/passkey/write-target.mjs.map +1 -1
  32. package/dist/internal/passkey-kernel.d.mts +512 -56
  33. package/dist/internal/passkeys.mjs +32 -14
  34. package/dist/internal/passkeys.mjs.map +1 -1
  35. package/dist/internal/password-kernel.d.mts +1 -1
  36. package/dist/internal/persistence.d.mts +2 -2
  37. package/dist/internal/persistence.mjs +16 -7
  38. package/dist/internal/persistence.mjs.map +1 -1
  39. package/dist/internal/phone-kernel.d.mts +5 -5
  40. package/dist/internal/phone-kernel.mjs +14 -14
  41. package/dist/internal/phone-kernel.mjs.map +1 -1
  42. package/dist/internal/registration.mjs +8 -7
  43. package/dist/internal/registration.mjs.map +1 -1
  44. package/dist/internal/storage-mapping.d.mts +5 -4
  45. package/dist/internal/storage-mapping.mjs +16 -13
  46. package/dist/internal/storage-mapping.mjs.map +1 -1
  47. package/dist/internal/storage-validation.d.mts +13 -3
  48. package/dist/internal/storage-validation.mjs +128 -68
  49. package/dist/internal/storage-validation.mjs.map +1 -1
  50. package/dist/internal/transaction-execution-kernel.d.mts +4 -4
  51. package/dist/internal/transaction-execution-kernel.mjs +7 -5
  52. package/dist/internal/transaction-execution-kernel.mjs.map +1 -1
  53. package/package.json +1 -1
  54. package/src/Adapter.ts +7 -1
  55. package/src/internal/configuration.ts +4 -2
  56. package/src/internal/crypto.ts +15 -0
  57. package/src/internal/passkey/admission.ts +22 -15
  58. package/src/internal/passkey/credentials.ts +4 -4
  59. package/src/internal/passkey/enrollment.ts +7 -7
  60. package/src/internal/passkey/flow.ts +8 -8
  61. package/src/internal/passkey/management.ts +7 -2
  62. package/src/internal/passkey/registration-ceremony.ts +2 -2
  63. package/src/internal/passkey/registration-custody.ts +1 -1
  64. package/src/internal/passkey/registration-write.ts +12 -12
  65. package/src/internal/passkey/state.ts +10 -13
  66. package/src/internal/passkey/target.ts +16 -8
  67. package/src/internal/passkey/write-state.ts +10 -14
  68. package/src/internal/passkey/write-target.ts +11 -3
  69. package/src/internal/passkeys.ts +121 -70
  70. package/src/internal/persistence.ts +28 -11
  71. package/src/internal/phone-kernel.ts +26 -17
  72. package/src/internal/registration.ts +10 -8
  73. package/src/internal/storage-mapping.ts +21 -13
  74. package/src/internal/storage-validation.ts +219 -139
  75. package/src/internal/transaction-execution-kernel.ts +30 -11
@@ -1 +1 @@
1
- {"version":3,"file":"transaction-execution-kernel.mjs","names":[],"sources":["../../src/internal/transaction-execution-kernel.ts"],"sourcesContent":["import {\n coordinateCommit,\n hasCommitScope,\n CurrentCommitJournal,\n type LifecycleHooks,\n type HookConfigurationError,\n} from \"@yielded/auth/Hooks\";\n/* oxlint-disable no-explicit-any -- existing storage kernels erase foreign table shapes; domain errors remain typed. */\n/* oxlint-disable no-explicit-any -- private execution bridge retains exact public driver wrappers. */\nimport { Context, Effect } from \"effect\";\nimport type * as SqlClient from \"effect/sql/SqlClient\";\nimport type * as SqlError from \"effect/sql/SqlError\";\nimport type { Statement } from \"effect/sql/Statement\";\n\nimport { requireStandalone } from \"./standalone\";\nimport {\n NativeDatabase,\n type TransactionNativeDatabase,\n type TransactionOwner,\n type makeTransactionKernel,\n} from \"./transaction-kernel\";\n\nexport interface TransactionTargetConfiguration<Failure> {\n readonly mode: \"interactive\" | \"synchronous\" | \"batch\";\n readonly dialect: \"pg\" | \"mysql\" | \"sqlite\";\n readonly locking: boolean;\n /** The captured database's transaction marker; batch drivers may omit it. */\n readonly standaloneGuard: (\n transactionService: SqlClient.SqlClient[\"transactionService\"] | undefined,\n ) => Effect.Effect<void, Failure>;\n readonly transaction?: <A, E, R>(\n database: any,\n body: (transaction: any) => Effect.Effect<A, E, R>,\n ) => Effect.Effect<A, E | SqlError.SqlError | Failure, R>;\n}\n\nexport type TransactionCoordinatorError<E, Failure> =\n | E\n | Failure\n | HookConfigurationError\n | SqlError.SqlError;\n\nexport interface TransactionBound<Failure> {\n readonly owner: TransactionOwner<Failure>;\n used: boolean;\n poisoned: boolean;\n active: boolean;\n inFlight: number;\n}\n\nexport interface TransactionExecution<Failure, OwnerId, Requirements = LifecycleHooks> {\n readonly bound: boolean;\n readonly active: () => boolean;\n readonly admit: Effect.Effect<void, Failure>;\n readonly poison: () => void;\n readonly run: <A, E, R>(\n body: Effect.Effect<A, E, R>,\n mutation?: boolean,\n ) => Effect.Effect<A, Failure, Exclude<R, OwnerId | CurrentCommitJournal> | Requirements>;\n}\n\nexport const makeTransactionExecutionKernel = (\n transactions: Pick<\n ReturnType<typeof makeTransactionKernel>,\n \"makeTransactionOwner\" | \"reportTransactionFailure\"\n >,\n) => {\n const { makeTransactionOwner, reportTransactionFailure } = transactions;\n\n const sqlClientTransactionStandaloneGuard = <Failure>(\n unavailable: () => Failure,\n transactionService: SqlClient.SqlClient[\"transactionService\"] | undefined,\n ) => requireStandalone(unavailable, transactionService);\n\n const makeTransactionExecution = Effect.fnUntraced(function* <Failure, OwnerId>(\n ownerTag: Context.Key<OwnerId, TransactionOwner<Failure>>,\n configuration: TransactionTargetConfiguration<Failure>,\n unavailable: () => Failure,\n nonce: () => string,\n bound?: TransactionBound<Failure>,\n ): Effect.fn.Return<TransactionExecution<Failure, OwnerId>, never, NativeDatabase> {\n const database = yield* NativeDatabase;\n\n const invariant: (value: unknown) => asserts value = (value) => {\n if (!value) throw unavailable();\n };\n\n return {\n bound: bound !== undefined,\n active: () => bound === undefined || bound.active,\n admit:\n bound !== undefined\n ? Effect.suspend(() => (bound.active ? Effect.void : Effect.fail(unavailable())))\n : Effect.gen(function* () {\n if (yield* hasCommitScope) return yield* Effect.fail(unavailable());\n yield* configuration.standaloneGuard(database.$client.transactionService);\n }),\n poison: () => {\n if (bound !== undefined) bound.poisoned = true;\n },\n run: <A, E, R>(\n body: Effect.Effect<A, E, R>,\n mutation = true,\n ): Effect.Effect<A, Failure, Exclude<R, OwnerId | CurrentCommitJournal> | LifecycleHooks> => {\n return Effect.suspend(() => {\n let entered = false;\n\n const work =\n bound !== undefined\n ? Effect.gen(function* () {\n if (!bound.active) return yield* Effect.fail(unavailable());\n entered = true;\n bound.inFlight++;\n if (!mutation)\n return yield* Effect.provideContext(\n body,\n Context.make(ownerTag, bound.owner).pipe(\n Context.add(CurrentCommitJournal, bound.owner.journal),\n ),\n );\n invariant(!bound.used && !bound.poisoned);\n bound.used = true;\n\n const result = yield* coordinateCommit(\n (journal) =>\n Effect.gen(function* () {\n bound.owner.guards.push(journal.prepare(undefined));\n\n return yield* Effect.provideContext(\n body,\n Context.make(ownerTag, { ...bound.owner, journal }).pipe(\n Context.add(CurrentCommitJournal, journal),\n ),\n );\n }),\n { mode: configuration.mode },\n );\n\n return result.value;\n }).pipe(\n Effect.onExit((exit) =>\n Effect.sync(() => {\n if (entered) bound.inFlight--;\n if (exit._tag === \"Failure\") bound.poisoned = true;\n }),\n ),\n )\n : Effect.gen(function* () {\n if (yield* hasCommitScope) return yield* Effect.fail(unavailable());\n yield* configuration.standaloneGuard(database.$client.transactionService);\n const marker = nonce();\n\n const result = yield* coordinateCommit(\n (journal) => {\n const run = (tx: TransactionNativeDatabase) =>\n Effect.gen(function* () {\n const owner = makeTransactionOwner(tx, journal, marker, unavailable, {\n client: database.$client,\n batch: configuration.mode === \"batch\",\n locking: configuration.locking,\n mysql: configuration.dialect === \"mysql\",\n dialect: configuration.dialect,\n });\n\n const value = yield* Effect.provideContext(\n body,\n Context.make(ownerTag, owner).pipe(\n Context.add(CurrentCommitJournal, journal),\n ),\n );\n\n yield* owner.finish();\n if (configuration.mode === \"batch\")\n yield* tx.$client.batch(owner.statements);\n\n return value;\n });\n\n return configuration.mode === \"batch\"\n ? run(database)\n : configuration.transaction !== undefined\n ? configuration.transaction(database, run)\n : database.transaction(run);\n },\n { mode: configuration.mode },\n );\n\n return result.value;\n });\n\n return work.pipe(\n (operation) => reportTransactionFailure(operation, unavailable),\n Effect.mapError(unavailable),\n Effect.catchDefect(() => Effect.fail(unavailable())),\n );\n });\n },\n };\n });\n\n const coordinateTransactionOwner = <\n Failure,\n OwnerId,\n Resources,\n AllocationError,\n AllocationRequirements,\n Services,\n Transaction,\n A,\n E,\n R,\n >(\n database: TransactionNativeDatabase,\n ownerTag: Context.Key<OwnerId, TransactionOwner<Failure>>,\n configuration: TransactionTargetConfiguration<Failure>,\n allocate: Effect.Effect<Resources, AllocationError, AllocationRequirements>,\n unavailable: () => Failure,\n nonce: () => string,\n services: (execution: TransactionExecution<Failure, OwnerId>, resources: Resources) => Services,\n owner: (\n transaction: Transaction,\n services: Services,\n append: (statement: Statement<any>) => void,\n ) => Effect.Effect<A, E, R>,\n ): Effect.Effect<\n A,\n TransactionCoordinatorError<E, Failure>,\n Exclude<R, CurrentCommitJournal> | LifecycleHooks | AllocationRequirements\n > => {\n return Effect.gen(function* () {\n if (yield* hasCommitScope) return yield* Effect.fail(unavailable());\n yield* configuration.standaloneGuard(database.$client.transactionService);\n\n // Allocation is independent of registration data and always precedes the physical owner.\n const resources = yield* allocate.pipe(\n (operation) => reportTransactionFailure(operation, unavailable),\n Effect.mapError(unavailable),\n Effect.catchDefect(() => Effect.fail(unavailable())),\n );\n\n const marker = nonce();\n\n const result = yield* coordinateCommit(\n (journal) => {\n const run = (tx: TransactionNativeDatabase) =>\n Effect.gen(function* () {\n const state = makeTransactionOwner(tx, journal, marker, unavailable, {\n client: database.$client,\n batch: configuration.mode === \"batch\",\n dialect: configuration.dialect,\n mysql: configuration.dialect === \"mysql\",\n locking: configuration.locking,\n });\n\n const bound: TransactionBound<Failure> = {\n owner: state,\n used: false,\n poisoned: false,\n active: true,\n inFlight: 0,\n };\n\n return yield* Effect.gen(function* () {\n const value = yield* owner(\n tx as Transaction,\n services(\n yield* makeTransactionExecution(\n ownerTag,\n configuration,\n unavailable,\n nonce,\n bound,\n ).pipe(Effect.provideService(NativeDatabase, tx)),\n resources,\n ),\n (statement) => {\n if (!bound.active) throw unavailable();\n state.statements.push(statement);\n },\n );\n\n bound.active = false;\n if (bound.poisoned || bound.inFlight !== 0)\n return yield* Effect.fail(unavailable());\n yield* state.finish();\n if (configuration.mode === \"batch\") yield* tx.$client.batch(state.statements);\n\n return value;\n }).pipe(\n Effect.ensuring(\n Effect.sync(() => {\n bound.active = false;\n }),\n ),\n );\n });\n\n return configuration.mode === \"batch\"\n ? run(database)\n : configuration.transaction !== undefined\n ? configuration.transaction(database, run)\n : database.transaction(run);\n },\n { mode: configuration.mode },\n );\n\n return result.value as A;\n });\n };\n\n return {\n sqlClientTransactionStandaloneGuard,\n makeTransactionExecution,\n coordinateTransactionOwner,\n };\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"transaction-execution-kernel.mjs","names":[],"sources":["../../src/internal/transaction-execution-kernel.ts"],"sourcesContent":["import {\n coordinateCommit,\n hasCommitScope,\n CurrentCommitJournal,\n type LifecycleHooks,\n type HookConfigurationError,\n} from \"@yielded/auth/Hooks\";\n/* oxlint-disable no-explicit-any -- existing storage kernels erase foreign table shapes; domain errors remain typed. */\n/* oxlint-disable no-explicit-any -- private execution bridge retains exact public driver wrappers. */\nimport { Context, Crypto, Effect, type PlatformError } from \"effect\";\nimport type * as SqlClient from \"effect/sql/SqlClient\";\nimport type * as SqlError from \"effect/sql/SqlError\";\nimport type { Statement } from \"effect/sql/Statement\";\n\nimport { requireStandalone } from \"./standalone\";\nimport {\n NativeDatabase,\n type TransactionNativeDatabase,\n type TransactionOwner,\n type makeTransactionKernel,\n} from \"./transaction-kernel\";\n\nexport interface TransactionTargetConfiguration<Failure> {\n readonly mode: \"interactive\" | \"synchronous\" | \"batch\";\n readonly dialect: \"pg\" | \"mysql\" | \"sqlite\";\n readonly locking: boolean;\n /** The captured database's transaction marker; batch drivers may omit it. */\n readonly standaloneGuard: (\n transactionService: SqlClient.SqlClient[\"transactionService\"] | undefined,\n ) => Effect.Effect<void, Failure>;\n readonly transaction?: <A, E, R>(\n database: any,\n body: (transaction: any) => Effect.Effect<A, E, R>,\n ) => Effect.Effect<A, E | SqlError.SqlError | Failure, R>;\n}\n\nexport type TransactionCoordinatorError<E, Failure> =\n | E\n | Failure\n | HookConfigurationError\n | SqlError.SqlError;\n\nexport interface TransactionBound<Failure> {\n readonly owner: TransactionOwner<Failure>;\n used: boolean;\n poisoned: boolean;\n active: boolean;\n inFlight: number;\n}\n\nexport interface TransactionExecution<Failure, OwnerId, Requirements = LifecycleHooks> {\n readonly bound: boolean;\n readonly active: () => boolean;\n readonly admit: Effect.Effect<void, Failure>;\n readonly poison: () => void;\n readonly run: <A, E, R>(\n body: Effect.Effect<A, E, R>,\n mutation?: boolean,\n ) => Effect.Effect<\n A,\n Failure,\n Exclude<Exclude<R, OwnerId | CurrentCommitJournal>, Crypto.Crypto> | Requirements\n >;\n}\n\nexport const makeTransactionExecutionKernel = (\n transactions: Pick<\n ReturnType<typeof makeTransactionKernel>,\n \"makeTransactionOwner\" | \"reportTransactionFailure\"\n >,\n) => {\n const { makeTransactionOwner, reportTransactionFailure } = transactions;\n\n const sqlClientTransactionStandaloneGuard = <Failure>(\n unavailable: () => Failure,\n transactionService: SqlClient.SqlClient[\"transactionService\"] | undefined,\n ) => requireStandalone(unavailable, transactionService);\n\n const makeTransactionExecution = Effect.fnUntraced(function* <Failure, OwnerId>(\n ownerTag: Context.Key<OwnerId, TransactionOwner<Failure>>,\n configuration: TransactionTargetConfiguration<Failure>,\n unavailable: () => Failure,\n nonce: Effect.Effect<string, PlatformError.PlatformError, Crypto.Crypto>,\n bound?: TransactionBound<Failure>,\n ): Effect.fn.Return<\n TransactionExecution<Failure, OwnerId>,\n never,\n NativeDatabase | Crypto.Crypto\n > {\n const database = yield* NativeDatabase;\n const crypto = yield* Crypto.Crypto;\n\n const invariant: (value: unknown) => asserts value = (value) => {\n if (!value) throw unavailable();\n };\n\n return {\n bound: bound !== undefined,\n active: () => bound === undefined || bound.active,\n admit:\n bound !== undefined\n ? Effect.suspend(() => (bound.active ? Effect.void : Effect.fail(unavailable())))\n : Effect.gen(function* () {\n if (yield* hasCommitScope) return yield* Effect.fail(unavailable());\n yield* configuration.standaloneGuard(database.$client.transactionService);\n }),\n poison: () => {\n if (bound !== undefined) bound.poisoned = true;\n },\n run: <A, E, R>(\n body: Effect.Effect<A, E, R>,\n mutation = true,\n ): Effect.Effect<\n A,\n Failure,\n Exclude<Exclude<R, OwnerId | CurrentCommitJournal>, Crypto.Crypto> | LifecycleHooks\n > => {\n return Effect.suspend(() => {\n let entered = false;\n\n const work: Effect.Effect<\n A,\n E | Failure | HookConfigurationError | SqlError.SqlError | PlatformError.PlatformError,\n Exclude<R, OwnerId | CurrentCommitJournal> | LifecycleHooks | Crypto.Crypto\n > =\n bound !== undefined\n ? Effect.gen(function* () {\n if (!bound.active) return yield* Effect.fail(unavailable());\n entered = true;\n bound.inFlight++;\n if (!mutation)\n return yield* Effect.provideContext(\n body,\n Context.make(ownerTag, bound.owner).pipe(\n Context.add(CurrentCommitJournal, bound.owner.journal),\n ),\n );\n invariant(!bound.used && !bound.poisoned);\n bound.used = true;\n\n const result = yield* coordinateCommit(\n (journal) =>\n Effect.gen(function* () {\n bound.owner.guards.push(journal.prepare(undefined));\n\n return yield* Effect.provideContext(\n body,\n Context.make(ownerTag, { ...bound.owner, journal }).pipe(\n Context.add(CurrentCommitJournal, journal),\n ),\n );\n }),\n { mode: configuration.mode },\n );\n\n return result.value;\n }).pipe(\n Effect.onExit((exit) =>\n Effect.sync(() => {\n if (entered) bound.inFlight--;\n if (exit._tag === \"Failure\") bound.poisoned = true;\n }),\n ),\n )\n : Effect.gen(function* () {\n if (yield* hasCommitScope) return yield* Effect.fail(unavailable());\n yield* configuration.standaloneGuard(database.$client.transactionService);\n const marker = yield* nonce;\n\n const result = yield* coordinateCommit(\n (journal) => {\n const run = (tx: TransactionNativeDatabase) =>\n Effect.gen(function* () {\n const owner = makeTransactionOwner(tx, journal, marker, unavailable, {\n client: database.$client,\n batch: configuration.mode === \"batch\",\n locking: configuration.locking,\n mysql: configuration.dialect === \"mysql\",\n dialect: configuration.dialect,\n });\n\n const value = yield* Effect.provideContext(\n body,\n Context.make(ownerTag, owner).pipe(\n Context.add(CurrentCommitJournal, journal),\n ),\n );\n\n yield* owner.finish();\n if (configuration.mode === \"batch\")\n yield* tx.$client.batch(owner.statements);\n\n return value;\n });\n\n return configuration.mode === \"batch\"\n ? run(database)\n : configuration.transaction !== undefined\n ? configuration.transaction(database, run)\n : database.transaction(run);\n },\n { mode: configuration.mode },\n );\n\n return result.value;\n });\n\n return work.pipe(\n Effect.provideService(Crypto.Crypto, crypto),\n (operation) => reportTransactionFailure(operation, unavailable),\n Effect.mapError(unavailable),\n Effect.catchDefect(() => Effect.fail(unavailable())),\n );\n });\n },\n };\n });\n\n const coordinateTransactionOwner = <\n Failure,\n OwnerId,\n Resources,\n AllocationError,\n AllocationRequirements,\n Services,\n Transaction,\n A,\n E,\n R,\n >(\n database: TransactionNativeDatabase,\n ownerTag: Context.Key<OwnerId, TransactionOwner<Failure>>,\n configuration: TransactionTargetConfiguration<Failure>,\n allocate: Effect.Effect<Resources, AllocationError, AllocationRequirements>,\n unavailable: () => Failure,\n nonce: Effect.Effect<string, PlatformError.PlatformError, Crypto.Crypto>,\n services: (execution: TransactionExecution<Failure, OwnerId>, resources: Resources) => Services,\n owner: (\n transaction: Transaction,\n services: Services,\n append: (statement: Statement<any>) => void,\n ) => Effect.Effect<A, E, R>,\n ): Effect.Effect<\n A,\n TransactionCoordinatorError<E, Failure>,\n Exclude<R, CurrentCommitJournal> | LifecycleHooks | AllocationRequirements | Crypto.Crypto\n > => {\n return Effect.gen(function* () {\n if (yield* hasCommitScope) return yield* Effect.fail(unavailable());\n yield* configuration.standaloneGuard(database.$client.transactionService);\n const crypto = yield* Crypto.Crypto;\n\n // Allocation is independent of registration data and always precedes the physical owner.\n const resources = yield* allocate.pipe(\n (operation) => reportTransactionFailure(operation, unavailable),\n Effect.mapError(unavailable),\n Effect.catchDefect(() => Effect.fail(unavailable())),\n );\n\n const marker = yield* nonce.pipe(Effect.mapError(unavailable));\n\n const result = yield* coordinateCommit(\n (journal) => {\n const run = (tx: TransactionNativeDatabase) =>\n Effect.gen(function* () {\n const state = makeTransactionOwner(tx, journal, marker, unavailable, {\n client: database.$client,\n batch: configuration.mode === \"batch\",\n dialect: configuration.dialect,\n mysql: configuration.dialect === \"mysql\",\n locking: configuration.locking,\n });\n\n const bound: TransactionBound<Failure> = {\n owner: state,\n used: false,\n poisoned: false,\n active: true,\n inFlight: 0,\n };\n\n return yield* Effect.gen(function* () {\n const value = yield* owner(\n tx as Transaction,\n services(\n yield* makeTransactionExecution(\n ownerTag,\n configuration,\n unavailable,\n nonce,\n bound,\n ).pipe(Effect.provideService(NativeDatabase, tx)),\n resources,\n ),\n (statement) => {\n if (!bound.active) throw unavailable();\n state.statements.push(statement);\n },\n );\n\n bound.active = false;\n if (bound.poisoned || bound.inFlight !== 0)\n return yield* Effect.fail(unavailable());\n yield* state.finish();\n if (configuration.mode === \"batch\") yield* tx.$client.batch(state.statements);\n\n return value;\n }).pipe(\n Effect.ensuring(\n Effect.sync(() => {\n bound.active = false;\n }),\n ),\n );\n }).pipe(Effect.provideService(Crypto.Crypto, crypto));\n\n return configuration.mode === \"batch\"\n ? run(database)\n : configuration.transaction !== undefined\n ? configuration.transaction(database, run)\n : database.transaction(run);\n },\n { mode: configuration.mode },\n );\n\n return result.value as A;\n });\n };\n\n return {\n sqlClientTransactionStandaloneGuard,\n makeTransactionExecution,\n coordinateTransactionOwner,\n };\n};\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1 +1 @@
1
- {"name":"@yielded/auth-persistence","version":"0.1.0-beta.18","dependencies":{"@noble/hashes":"2.3.0","@yielded/auth":"0.1.0-beta.18","@yielded/auth-crypto":"0.1.0-beta.18"},"devDependencies":{"@effect/platform-node":"4.0.0","@effect/sql-libsql":"4.0.0","@effect/sql-sqlite-node":"4.0.0","@effect/vitest":"4.0.0","effect":"4.0.0","typescript":"7.0.2","vite-plus":"0.3.3"},"peerDependencies":{"effect":"^4.0.0"},"exports":{".":{"types":"./dist/index.d.mts","default":"./dist/index.mjs"},"./Adapter":{"types":"./dist/Adapter.d.mts","default":"./dist/Adapter.mjs"}},"description":"Direct Effect SQL persistence and shared storage contracts for Yielded Auth.","homepage":"https://yielded.dev/auth/","bugs":{"url":"https://github.com/yielded-dev/auth/issues"},"license":"MIT","repository":{"type":"git","url":"git+https://github.com/yielded-dev/auth.git","directory":"packages/auth-persistence"},"files":["dist","src"],"type":"module","sideEffects":[],"publishConfig":{"access":"public"},"scripts":{"build":"vp pack","check":"tsc --noEmit -p tsconfig.json","test":"vp test --passWithNoTests"}}
1
+ {"name":"@yielded/auth-persistence","version":"0.1.0-beta.20","dependencies":{"@yielded/auth":"0.1.0-beta.20"},"devDependencies":{"@effect/platform-node":"4.0.0","@effect/sql-libsql":"4.0.0","@effect/sql-sqlite-node":"4.0.0","@effect/vitest":"4.0.0","effect":"4.0.0","typescript":"7.0.2","vite-plus":"0.3.3"},"peerDependencies":{"effect":"^4.0.0"},"exports":{".":{"types":"./dist/index.d.mts","default":"./dist/index.mjs"},"./Adapter":{"types":"./dist/Adapter.d.mts","default":"./dist/Adapter.mjs"}},"description":"Direct Effect SQL persistence and shared storage contracts for Yielded Auth.","homepage":"https://yielded.dev/auth/","bugs":{"url":"https://github.com/yielded-dev/auth/issues"},"license":"MIT","repository":{"type":"git","url":"git+https://github.com/yielded-dev/auth.git","directory":"packages/auth-persistence"},"files":["dist","src"],"type":"module","sideEffects":[],"publishConfig":{"access":"public"},"scripts":{"build":"vp pack","check":"tsc --noEmit -p tsconfig.json","test":"vp test --passWithNoTests"}}
package/src/Adapter.ts CHANGED
@@ -297,4 +297,10 @@ export type {
297
297
  PersistenceApi,
298
298
  } from "./internal/configuration";
299
299
 
300
- export { validateStorage, type PhysicalStorageTable } from "./internal/storage-validation";
300
+ export {
301
+ validateStorage,
302
+ validateStorageBatch,
303
+ withStorageValidation,
304
+ type PhysicalStorageTable,
305
+ type StorageValidation,
306
+ } from "./internal/storage-validation";
@@ -24,7 +24,7 @@ import type {
24
24
  AuthenticationRequirement,
25
25
  make as makeSessions,
26
26
  } from "@yielded/auth/Sessions";
27
- import { type Context, type Effect, type Layer, Schema } from "effect";
27
+ import { type Context, type Crypto, type Effect, type Layer, Schema } from "effect";
28
28
  import type { SqlClient } from "effect/sql";
29
29
 
30
30
  import type { PersistenceMappingError } from "./mapping-error";
@@ -251,6 +251,7 @@ export interface BoundPersistence<
251
251
  readonly layer: Layer.Layer<
252
252
  Ports<C, Id, A>,
253
253
  PersistenceConfigurationError,
254
+ | Crypto.Crypto
254
255
  | ConfigId<A["namespace"]>
255
256
  | ProvisioningRequirement<A>
256
257
  | PasskeyRequirement<A>
@@ -260,7 +261,8 @@ export interface BoundPersistence<
260
261
  readonly managed: <N, Instant = number>(options: {
261
262
  readonly subjects: SubjectOptions<T, N>;
262
263
  readonly tables?: Partial<Record<Roles<C, Id, A>, T>>;
263
- readonly prefix?: string;
264
+ /** Stable SQL identifier prefix; reuse the deployed prefix for existing tables. */
265
+ readonly prefix: string;
264
266
  readonly timestamps?: TimestampOptions<Instant>;
265
267
  }) => StorageLayout<T, Roles<C, Id, A>>;
266
268
  readonly map: <N, Instant = number>(options: {
@@ -0,0 +1,15 @@
1
+ import { TokenDigest } from "@yielded/auth/Schema";
2
+ import { Crypto, Effect } from "effect";
3
+ import { Base64Url } from "effect/encoding";
4
+
5
+ const encoder = new TextEncoder();
6
+
7
+ export const randomId = Effect.flatMap(Crypto.Crypto, (crypto) =>
8
+ crypto.randomBytes(32).pipe(Effect.map(Base64Url.encode)),
9
+ );
10
+
11
+ /** Persisted UTF-8 SHA-256, encoded as unpadded base64url. */
12
+ export const digest = (value: string) =>
13
+ Effect.flatMap(Crypto.Crypto, (crypto) => crypto.digest("SHA-256", encoder.encode(value))).pipe(
14
+ Effect.map((bytes) => TokenDigest.make(Base64Url.encode(bytes))),
15
+ );
@@ -31,18 +31,19 @@ export const makePasskeyAdmissionKernel = (
31
31
 
32
32
  const chargeRetentionMillis = 86_400_000;
33
33
 
34
- const chargeScopes = (ceremony: PasskeyCeremony, subjectId?: string) => {
34
+ const chargeScopes = Effect.fnUntraced(function* (ceremony: PasskeyCeremony, subjectId?: string) {
35
35
  const scopes: { kind: PasskeyChargeKind; scope: string }[] = [
36
- { kind: "global", scope: key("global", [ceremony.moduleId]) },
36
+ { kind: "global", scope: yield* key("global", [ceremony.moduleId]) },
37
37
  ];
38
38
 
39
- if (subjectId !== undefined) scopes.push({ kind: "subject", scope: subjectScope(subjectId) });
40
- const target = targetScope(ceremony);
39
+ if (subjectId !== undefined)
40
+ scopes.push({ kind: "subject", scope: yield* subjectScope(subjectId) });
41
+ const target = yield* targetScope(ceremony);
41
42
 
42
43
  if (target !== null) scopes.push({ kind: "target", scope: target });
43
44
 
44
45
  return scopes;
45
- };
46
+ });
46
47
 
47
48
  const lockAdmission = Effect.fn("passkey.lockAdmission")(function* (mapping: any) {
48
49
  const owner = yield* CurrentPasskeyTransaction;
@@ -107,7 +108,7 @@ export const makePasskeyAdmissionKernel = (
107
108
  )})`;
108
109
  };
109
110
 
110
- const admissionCondition = (
111
+ const admissionCondition = Effect.fnUntraced(function* (
111
112
  mapping: any,
112
113
  policies: ReadonlyArray<PasskeyMethodPolicy>,
113
114
  ceremony: PasskeyCeremony,
@@ -115,19 +116,23 @@ export const makePasskeyAdmissionKernel = (
115
116
  added: ReadonlyArray<PasskeyChargeKind>,
116
117
  newFlow: boolean,
117
118
  resolvedSubject: boolean,
118
- ) =>
119
- both(
119
+ ) {
120
+ const subject = subjectId === undefined ? undefined : yield* subjectScope(subjectId);
121
+ const scopes = yield* chargeScopes(ceremony, subjectId);
122
+
123
+ return both(
120
124
  ...policies.flatMap((policy) => [
121
125
  sql`${pendingCount(mapping)} + ${newFlow ? 1 : 0} <= ${policy.maximumPending}`,
122
126
  subjectId === undefined
123
127
  ? undefined
124
- : sql`${pendingCount(mapping, subjectScope(subjectId))} + ${newFlow || resolvedSubject ? 1 : 0} <= ${policy.maximumPendingPerSubject}`,
125
- ...chargeScopes(ceremony, subjectId).map(
128
+ : sql`${pendingCount(mapping, subject)} + ${newFlow || resolvedSubject ? 1 : 0} <= ${policy.maximumPendingPerSubject}`,
129
+ ...scopes.map(
126
130
  ({ kind, scope }) =>
127
131
  sql`${chargedCount(mapping, kind, scope, policy.admission[kind].windowMillis)} + ${added.includes(kind) ? 1 : 0} <= ${policy.admission[kind].limit}`,
128
132
  ),
129
133
  ]),
130
134
  );
135
+ });
131
136
 
132
137
  const guardAdmission = (
133
138
  mapping: any,
@@ -135,9 +140,11 @@ export const makePasskeyAdmissionKernel = (
135
140
  ceremony: PasskeyCeremony,
136
141
  subjectId?: string,
137
142
  ) =>
138
- Effect.map(CurrentPasskeyTransaction, (owner) => {
143
+ Effect.gen(function* () {
144
+ const owner = yield* CurrentPasskeyTransaction;
145
+
139
146
  owner.postconditions.push(
140
- admissionCondition(mapping, policies, ceremony, subjectId, [], false, false),
147
+ yield* admissionCondition(mapping, policies, ceremony, subjectId, [], false, false),
141
148
  );
142
149
  });
143
150
 
@@ -161,7 +168,7 @@ export const makePasskeyAdmissionKernel = (
161
168
  columns: mappedColumns(table),
162
169
  });
163
170
 
164
- const scopes = chargeScopes(ceremony, subjectId);
171
+ const scopes = yield* chargeScopes(ceremony, subjectId);
165
172
 
166
173
  invariant(found.rows.length === scopes.length);
167
174
  for (const { kind, scope } of scopes) {
@@ -202,7 +209,7 @@ export const makePasskeyAdmissionKernel = (
202
209
  Effect.flatMap(CurrentPasskeyTransaction, (owner) =>
203
210
  Effect.gen(function* () {
204
211
  const table = mapping.charge;
205
- const scopes = chargeScopes(ceremony, subjectId);
212
+ const scopes = yield* chargeScopes(ceremony, subjectId);
206
213
 
207
214
  const where = equal(table.table, {
208
215
  [table.moduleId]: ceremony.moduleId,
@@ -241,7 +248,7 @@ export const makePasskeyAdmissionKernel = (
241
248
  const earliest = yield* owner.now(clock);
242
249
  const identities: Record<string, unknown>[] = [];
243
250
 
244
- for (const { kind, scope } of chargeScopes(ceremony, subjectId).filter((item) =>
251
+ for (const { kind, scope } of (yield* chargeScopes(ceremony, subjectId)).filter((item) =>
245
252
  kinds.includes(item.kind),
246
253
  )) {
247
254
  const identity = {
@@ -172,7 +172,7 @@ export const makePasskeyCredentialsKernel = (
172
172
  const found = yield* owner.read(
173
173
  descriptor.table,
174
174
  equal(descriptor.table, {
175
- [descriptor.credentialKey]: credentialKey(rpId, protocolCredentialId),
175
+ [descriptor.credentialKey]: yield* credentialKey(rpId, protocolCredentialId),
176
176
  }),
177
177
  { lock: false, observe: false, limit: 1, columns: mappedColumns(descriptor) },
178
178
  );
@@ -257,7 +257,7 @@ export const makePasskeyCredentialsKernel = (
257
257
  ) {
258
258
  const owner = yield* CurrentPasskeyTransaction;
259
259
  const ownership = mapping.credentialOwnership;
260
- const tupleKey = credentialKey(rpId, protocolCredentialId);
260
+ const tupleKey = yield* credentialKey(rpId, protocolCredentialId);
261
261
 
262
262
  const tuple = (yield* owner.read(
263
263
  ownership.table,
@@ -298,7 +298,7 @@ export const makePasskeyCredentialsKernel = (
298
298
  return undefined;
299
299
  let decoded = descriptor.decode(copiedRow(row));
300
300
  const handle = mapping.handleOwnership;
301
- const expectedHandleKey = handleKey(rpId, decoded.userHandle);
301
+ const expectedHandleKey = yield* handleKey(rpId, decoded.userHandle);
302
302
 
303
303
  const handleRow = (yield* owner.read(
304
304
  handle.table,
@@ -452,7 +452,7 @@ export const makePasskeyCredentialsKernel = (
452
452
  );
453
453
  // oxlint-disable-next-line no-restricted-properties -- native SQL column values enter through this untyped driver boundary.
454
454
  userHandle = Schema.decodeUnknownSync(PasskeyUserHandle)(handleRow[handle.userHandle]);
455
- invariant(handleRow[handle.handleKey] === handleKey(input.rpId, userHandle));
455
+ invariant(handleRow[handle.handleKey] === (yield* handleKey(input.rpId, userHandle)));
456
456
  }
457
457
  const credential = mapping.read.credential;
458
458
 
@@ -97,7 +97,7 @@ export const makePasskeyEnrollmentKernel = (
97
97
 
98
98
  const found = yield* owner.read(table.table, where, { limit: 1 });
99
99
  const row = found.rows[0];
100
- const hashed = handleKey(ceremony.profile.rpId, ceremony.context.userHandle);
100
+ const hashed = yield* handleKey(ceremony.profile.rpId, ceremony.context.userHandle);
101
101
 
102
102
  if (row !== undefined)
103
103
  return (
@@ -193,7 +193,7 @@ export const makePasskeyEnrollmentKernel = (
193
193
  action: "enroll-begin",
194
194
  commandId: ceremony.commandId,
195
195
  flowId: ceremony.flowId,
196
- bindingDigest: enrollmentDigest(ceremony),
196
+ bindingDigest: yield* enrollmentDigest(ceremony),
197
197
  revision: ceremony.context.revision,
198
198
  },
199
199
  {
@@ -207,11 +207,11 @@ export const makePasskeyEnrollmentKernel = (
207
207
  )
208
208
  return { _tag: "Rejected" } as const;
209
209
  invariant(
210
- digest(Ceremony.fields.context, ceremony.context) ===
211
- digest(Ceremony.fields.context, {
210
+ (yield* digest(Ceremony.fields.context, ceremony.context)) ===
211
+ (yield* digest(Ceremony.fields.context, {
212
212
  ...ceremony.context,
213
213
  authorization: input.authorization,
214
- }),
214
+ })),
215
215
  );
216
216
  const issued = yield* issueAssertion(mapping, ceremony, input.policy);
217
217
 
@@ -277,7 +277,7 @@ export const makePasskeyEnrollmentKernel = (
277
277
  action: "enroll-complete",
278
278
  commandId: ceremony.commandId,
279
279
  flowId: ceremony.flowId,
280
- bindingDigest: digest(Ceremony, ceremony),
280
+ bindingDigest: yield* digest(Ceremony, ceremony),
281
281
  revision: ceremony.context.revision,
282
282
  },
283
283
  {
@@ -293,7 +293,7 @@ export const makePasskeyEnrollmentKernel = (
293
293
  return yield* reject;
294
294
  const tuple = mapping.read.credentialOwnership;
295
295
  // Credential ownership is RP-global, including retained registration custody.
296
- const key = credentialKeyFor(ceremony.profile.rpId, input.verified.protocolCredentialId);
296
+ const key = yield* credentialKeyFor(ceremony.profile.rpId, input.verified.protocolCredentialId);
297
297
 
298
298
  if (
299
299
  (yield* owner.read(tuple.table, equal(tuple.table, { [tuple.credentialKey]: key }), {
@@ -233,8 +233,8 @@ export const makePasskeyFlowKernel = (
233
233
  const subject = credential?.revision.subjectId ?? knownSubject(ceremony);
234
234
 
235
235
  invariant(
236
- row[table.subjectScope] === (subject === undefined ? null : subjectScope(subject)) &&
237
- row[table.targetScope] === targetScope(ceremony),
236
+ row[table.subjectScope] === (subject === undefined ? null : yield* subjectScope(subject)) &&
237
+ row[table.targetScope] === (yield* targetScope(ceremony)),
238
238
  );
239
239
  if (state === "Pending") {
240
240
  invariant(
@@ -378,12 +378,12 @@ export const makePasskeyFlowKernel = (
378
378
 
379
379
  if (sameCommand.rows.length !== 0 || sameFlow !== undefined)
380
380
  return { _tag: "Rejected" } as const;
381
- const kinds = chargeScopes(ceremony, subjectId).map((item) => item.kind);
381
+ const kinds = (yield* chargeScopes(ceremony, subjectId)).map((item) => item.kind);
382
382
 
383
383
  if (
384
384
  !(yield* owner.check(liveCondition(mapping, ceremony))) ||
385
385
  !(yield* owner.check(
386
- admissionCondition(mapping, [current], ceremony, subjectId, kinds, true, false),
386
+ yield* admissionCondition(mapping, [current], ceremony, subjectId, kinds, true, false),
387
387
  ))
388
388
  )
389
389
  return { _tag: "Rejected" } as const;
@@ -410,8 +410,8 @@ export const makePasskeyFlowKernel = (
410
410
  [table.claimedAt]: null,
411
411
  [table.claimExpiresAt]: null,
412
412
  [table.credentialSnapshot]: null,
413
- [table.subjectScope]: subjectId === undefined ? null : subjectScope(subjectId),
414
- [table.targetScope]: targetScope(ceremony),
413
+ [table.subjectScope]: subjectId === undefined ? null : yield* subjectScope(subjectId),
414
+ [table.targetScope]: yield* targetScope(ceremony),
415
415
  };
416
416
 
417
417
  // Empty-key observations now expect this exact inserted row as well.
@@ -554,7 +554,7 @@ export const makePasskeyFlowKernel = (
554
554
 
555
555
  if (
556
556
  !(yield* owner.check(
557
- admissionCondition(
557
+ yield* admissionCondition(
558
558
  mapping,
559
559
  [read.policy, current],
560
560
  read.ceremony,
@@ -591,7 +591,7 @@ export const makePasskeyFlowKernel = (
591
591
  [table.claimExpiresAt]: mapping.clock.encodeInstant(claim.claimExpiresAtMillis),
592
592
  [table.credentialSnapshot]:
593
593
  input.credential === undefined ? null : credentialStorage.encode(input.credential),
594
- [table.subjectScope]: subjectId === undefined ? null : subjectScope(subjectId),
594
+ [table.subjectScope]: subjectId === undefined ? null : yield* subjectScope(subjectId),
595
595
  },
596
596
  );
597
597
  if (resolved) yield* insertCharges(mapping, read.ceremony, read.policy, ["subject"], subjectId);
@@ -335,7 +335,7 @@ export const makePasskeyManagementKernel = (
335
335
  action: "remove",
336
336
  commandId: input.commandId,
337
337
  flowId: input.commandId,
338
- bindingDigest: removeDigest(mapping.moduleId, input.commandId, input.credential),
338
+ bindingDigest: yield* removeDigest(mapping.moduleId, input.commandId, input.credential),
339
339
  revision: input.credential.revision,
340
340
  },
341
341
  {
@@ -384,7 +384,12 @@ export const makePasskeyManagementKernel = (
384
384
 
385
385
  yield* owner.update(
386
386
  tuple.table,
387
- { [tuple.credentialKey]: credentialKey(credential.rpId, credential.protocolCredentialId) },
387
+ {
388
+ [tuple.credentialKey]: yield* credentialKey(
389
+ credential.rpId,
390
+ credential.protocolCredentialId,
391
+ ),
392
+ },
388
393
  {
389
394
  [tuple.state]: mapping.write.credentialOwnership.removedState,
390
395
  [tuple.version]: owner.marker,
@@ -78,7 +78,7 @@ export const makePasskeyRegistrationCeremonyKernel = (
78
78
  })).rows[0];
79
79
 
80
80
  if (advisory === undefined) return false;
81
- const hashedHandle = handleKey(ceremony.profile.rpId, context.userHandle);
81
+ const hashedHandle = yield* handleKey(ceremony.profile.rpId, context.userHandle);
82
82
 
83
83
  const held = (yield* owner.read(
84
84
  handle.table,
@@ -204,7 +204,7 @@ export const makePasskeyRegistrationCeremonyKernel = (
204
204
  policyStorage.encode(current) !== policyStorage.encode(input.policy) ||
205
205
  !(yield* owner.check(liveCondition(mapping, read.ceremony))) ||
206
206
  !(yield* owner.check(
207
- admissionCondition(
207
+ yield* admissionCondition(
208
208
  mapping,
209
209
  [read.policy, current],
210
210
  read.ceremony,
@@ -26,7 +26,7 @@ export const makePasskeyRegistrationCustodyKernel = (
26
26
  const owner = yield* CurrentPasskeyTransaction;
27
27
  const handle = mapping.handle;
28
28
  const intent = mapping.intent;
29
- const hashed = handleKey(ceremony.profile.rpId, ceremony.context.userHandle);
29
+ const hashed = yield* handleKey(ceremony.profile.rpId, ceremony.context.userHandle);
30
30
 
31
31
  const held = (yield* owner.read(
32
32
  handle.table,
@@ -67,7 +67,7 @@ export const makePasskeyRegistrationWriteKernel = (
67
67
  const { digest, insertCredential, jsonStorage, validRegistration } = writeState;
68
68
  const { both } = transactions;
69
69
 
70
- const registrationData = (mapping: any, registration: any) => {
70
+ const registrationData = Effect.fnUntraced(function* (mapping: any, registration: any) {
71
71
  const codec = jsonStorage(mapping.registration.schema);
72
72
  const encoded = codec.encode(registration);
73
73
 
@@ -76,26 +76,26 @@ export const makePasskeyRegistrationWriteKernel = (
76
76
  return {
77
77
  encoded,
78
78
  registration: codec.decode(encoded),
79
- fingerprint: digest(mapping.registration.schema, registration),
79
+ fingerprint: yield* digest(mapping.registration.schema, registration),
80
80
  };
81
- };
81
+ });
82
82
 
83
- const describe = (mapping: any, registration: any) => {
84
- const data = registrationData(mapping, registration);
83
+ const describe = Effect.fnUntraced(function* (mapping: any, registration: any) {
84
+ const data = yield* registrationData(mapping, registration);
85
85
 
86
86
  const labels = Schema.decodeSync(
87
87
  Schema.Struct({ name: PasskeyLabel, displayName: PasskeyLabel }),
88
88
  )(mapping.registration.describe(data.registration));
89
89
 
90
90
  return { ...data, ...labels };
91
- };
91
+ });
92
92
 
93
93
  const inspectRegistration = Effect.fn("passkey.inspectRegistration")(function* (
94
94
  mapping: any,
95
95
  registration: any,
96
96
  ) {
97
97
  const owner = yield* CurrentPasskeyTransaction;
98
- const value = describe(mapping, registration);
98
+ const value = yield* describe(mapping, registration);
99
99
  const current = yield* readModule(mapping);
100
100
 
101
101
  yield* readPolicyGuards(mapping);
@@ -133,7 +133,7 @@ export const makePasskeyRegistrationWriteKernel = (
133
133
  ceremony.allowedCredentials.length !== 0
134
134
  )
135
135
  return { _tag: "Rejected" } as const;
136
- const value = describe(mapping, input.registration);
136
+ const value = yield* describe(mapping, input.registration);
137
137
 
138
138
  if (
139
139
  value.fingerprint !== ceremony.context.fingerprint ||
@@ -148,7 +148,7 @@ export const makePasskeyRegistrationWriteKernel = (
148
148
 
149
149
  if (!(yield* owner.check(eligible))) return { _tag: "Rejected" } as const;
150
150
  const handle = mapping.handle;
151
- const hashed = handleKey(ceremony.profile.rpId, ceremony.context.userHandle);
151
+ const hashed = yield* handleKey(ceremony.profile.rpId, ceremony.context.userHandle);
152
152
 
153
153
  const absentHandle = yield* owner.read(
154
154
  handle.table,
@@ -259,7 +259,7 @@ export const makePasskeyRegistrationWriteKernel = (
259
259
  const absentCredential = yield* owner.read(
260
260
  tuple.table,
261
261
  equal(tuple.table, {
262
- [tuple.credentialKey]: credentialKey(
262
+ [tuple.credentialKey]: yield* credentialKey(
263
263
  ceremony.profile.rpId,
264
264
  input.verified.protocolCredentialId,
265
265
  ),
@@ -268,7 +268,7 @@ export const makePasskeyRegistrationWriteKernel = (
268
268
  );
269
269
 
270
270
  const handle = mapping.handle;
271
- const hashed = handleKey(ceremony.profile.rpId, ceremony.context.userHandle);
271
+ const hashed = yield* handleKey(ceremony.profile.rpId, ceremony.context.userHandle);
272
272
 
273
273
  const held = (yield* owner.read(
274
274
  handle.table,
@@ -319,7 +319,7 @@ export const makePasskeyRegistrationWriteKernel = (
319
319
  )
320
320
  return yield* reject;
321
321
 
322
- const data = describe(
322
+ const data = yield* describe(
323
323
  mapping,
324
324
  jsonStorage(mapping.registration.schema).decode(row[intent.applicationSnapshot]),
325
325
  );
@@ -1,9 +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 its explicit .js subpaths.
4
- import { sha256 } from "@noble/hashes/sha2.js";
5
- // oxlint-disable-next-line import/extensions -- Noble exposes its explicit .js subpaths.
6
- import { randomBytes } from "@noble/hashes/utils.js";
7
3
  import {
8
4
  PasskeyConfigurationError,
9
5
  PasskeyUnavailable,
@@ -15,9 +11,9 @@ import {
15
11
  PasskeyMethodPolicy,
16
12
  snapshotPasskeySync,
17
13
  } from "@yielded/auth/Passkey";
18
- import { Context, Effect, Schema } from "effect";
19
- import { Base64Url } from "effect/encoding";
14
+ import { Context, Crypto, Effect, Schema } from "effect";
20
15
 
16
+ import { randomId } from "../crypto";
21
17
  import {
22
18
  requiredPasskeyCredentialConstraints,
23
19
  requiredPasskeyPersistenceConstraints,
@@ -48,14 +44,14 @@ export const makePasskeyStateKernel = (
48
44
 
49
45
  const { col, equal, copiedRow, matchesNativeRow } = makeTransactionRows(unavailable);
50
46
 
51
- const nonce = () => Base64Url.encode(randomBytes(32));
47
+ const nonce = randomId;
52
48
 
53
49
  const encoder = new TextEncoder();
54
50
 
55
51
  const decoder = new TextDecoder("utf-8", { fatal: true });
56
52
 
57
53
  /** RP tuples and admission scopes have no collation-dependent string identity. */
58
- const key = (kind: string, fields: ReadonlyArray<string>) => {
54
+ const key = Effect.fnUntraced(function* (kind: string, fields: ReadonlyArray<string>) {
59
55
  const values = ["effect-auth/passkey/" + kind + "/v1", ...fields].map((field) => {
60
56
  const bytes = encoder.encode(field);
61
57
 
@@ -74,10 +70,11 @@ export const makePasskeyStateKernel = (
74
70
  offset += 4 + bytes.length;
75
71
  }
76
72
 
77
- return (
78
- "v1:" + Array.from(sha256(packed), (value) => value.toString(16).padStart(2, "0")).join("")
79
- );
80
- };
73
+ const crypto = yield* Crypto.Crypto;
74
+ const hashed = yield* crypto.digest("SHA-256", packed);
75
+
76
+ return "v1:" + Array.from(hashed, (value) => value.toString(16).padStart(2, "0")).join("");
77
+ });
81
78
 
82
79
  const credentialKey = (rpId: string, id: string) => key("credential", [rpId, id]);
83
80
 
@@ -88,7 +85,7 @@ export const makePasskeyStateKernel = (
88
85
  const targetScope = (ceremony: PasskeyCeremony) => {
89
86
  const context = ceremony.context;
90
87
 
91
- if (!("target" in context)) return null;
88
+ if (!("target" in context)) return Effect.succeed(null);
92
89
  const target = context.target;
93
90
 
94
91
  return key("target", [