@yielded/auth-persistence-drizzle 0.1.0-beta.22 → 0.1.0-beta.24

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 (200) hide show
  1. package/README.md +3 -0
  2. package/dist/D1.d.mts +11 -11
  3. package/dist/D1.mjs +2 -1
  4. package/dist/D1.mjs.map +1 -1
  5. package/dist/Libsql.d.mts +40 -56
  6. package/dist/Libsql.mjs +2 -1
  7. package/dist/Libsql.mjs.map +1 -1
  8. package/dist/Mysql2.d.mts +16 -17
  9. package/dist/Pglite.d.mts +18 -18
  10. package/dist/Pglite.mjs +2 -1
  11. package/dist/Pglite.mjs.map +1 -1
  12. package/dist/Postgres.d.mts +24 -24
  13. package/dist/Postgres.mjs +2 -1
  14. package/dist/Postgres.mjs.map +1 -1
  15. package/dist/SqliteBun.d.mts +39 -55
  16. package/dist/SqliteBun.mjs +2 -1
  17. package/dist/SqliteBun.mjs.map +1 -1
  18. package/dist/SqliteDo.d.mts +48 -64
  19. package/dist/SqliteDo.mjs +2 -1
  20. package/dist/SqliteDo.mjs.map +1 -1
  21. package/dist/SqliteNode.d.mts +45 -61
  22. package/dist/SqliteNode.mjs +2 -1
  23. package/dist/SqliteNode.mjs.map +1 -1
  24. package/dist/SqliteWasm.d.mts +48 -64
  25. package/dist/SqliteWasm.mjs +2 -1
  26. package/dist/SqliteWasm.mjs.map +1 -1
  27. package/dist/drizzle/crypto.mjs +1 -1
  28. package/dist/drizzle/d1-emails.d.mts +1 -1
  29. package/dist/drizzle/d1-oauth.d.mts +14 -14
  30. package/dist/drizzle/d1-passkey.d.mts +1 -1
  31. package/dist/drizzle/d1-password-prepared.d.mts +4 -4
  32. package/dist/drizzle/d1-password-prepared.mjs.map +1 -1
  33. package/dist/drizzle/d1-passwords.d.mts +18 -35
  34. package/dist/drizzle/d1-passwords.mjs +78 -290
  35. package/dist/drizzle/d1-passwords.mjs.map +1 -1
  36. package/dist/drizzle/d1-phone.d.mts +1 -1
  37. package/dist/drizzle/d1-proofs.d.mts +2 -2
  38. package/dist/drizzle/d1-sessions.d.mts +3 -3
  39. package/dist/drizzle/d1-sessions.mjs +11 -6
  40. package/dist/drizzle/d1-sessions.mjs.map +1 -1
  41. package/dist/drizzle/d1-totp.d.mts +1 -1
  42. package/dist/drizzle/mysql-password-prepared.d.mts +5 -5
  43. package/dist/drizzle/mysql-password-prepared.mjs.map +1 -1
  44. package/dist/drizzle/mysql-passwords.d.mts +16 -32
  45. package/dist/drizzle/mysql-passwords.mjs.map +1 -1
  46. package/dist/drizzle/mysql-sessions.d.mts +1 -1
  47. package/dist/drizzle/native-sql-table.mjs +135 -0
  48. package/dist/drizzle/native-sql-table.mjs.map +1 -0
  49. package/dist/drizzle/oauth-connected-model.d.mts +20 -281
  50. package/dist/drizzle/oauth-connected-model.mjs +3 -17
  51. package/dist/drizzle/oauth-connected-model.mjs.map +1 -1
  52. package/dist/drizzle/oauth-connected-reference.d.mts +1 -3
  53. package/dist/drizzle/oauth-connected-reference.mjs +4 -53
  54. package/dist/drizzle/oauth-connected-reference.mjs.map +1 -1
  55. package/dist/drizzle/oauth-connected-target.d.mts +3 -990
  56. package/dist/drizzle/oauth-connected-target.mjs +8 -92
  57. package/dist/drizzle/oauth-connected-target.mjs.map +1 -1
  58. package/dist/drizzle/oauth-kernel.mjs +11 -0
  59. package/dist/drizzle/oauth-kernel.mjs.map +1 -0
  60. package/dist/drizzle/oauth-model.d.mts +35 -438
  61. package/dist/drizzle/oauth-model.mjs +6 -44
  62. package/dist/drizzle/oauth-model.mjs.map +1 -1
  63. package/dist/drizzle/oauth-mysql.mjs +2 -2
  64. package/dist/drizzle/oauth-mysql.mjs.map +1 -1
  65. package/dist/drizzle/oauth-proxy.d.mts +10 -0
  66. package/dist/drizzle/oauth-proxy.mjs +54 -0
  67. package/dist/drizzle/oauth-proxy.mjs.map +1 -0
  68. package/dist/drizzle/oauth-target.d.mts +6 -107
  69. package/dist/drizzle/oauth-target.mjs +15 -245
  70. package/dist/drizzle/oauth-target.mjs.map +1 -1
  71. package/dist/drizzle/password-model.d.mts +5 -8
  72. package/dist/drizzle/password-prepared-model.d.mts +5 -5
  73. package/dist/drizzle/password-prepared-model.mjs.map +1 -1
  74. package/dist/drizzle/password-prepared-state.mjs +1 -1
  75. package/dist/drizzle/password-sql.mjs +2 -1
  76. package/dist/drizzle/password-sql.mjs.map +1 -1
  77. package/dist/drizzle/password-target.mjs +13 -2
  78. package/dist/drizzle/password-target.mjs.map +1 -1
  79. package/dist/drizzle/pg-password-prepared.d.mts +5 -5
  80. package/dist/drizzle/pg-password-prepared.mjs.map +1 -1
  81. package/dist/drizzle/pg-passwords.d.mts +16 -32
  82. package/dist/drizzle/pg-passwords.mjs.map +1 -1
  83. package/dist/drizzle/pg-sessions.d.mts +1 -1
  84. package/dist/drizzle/phone-target.d.mts +2 -2
  85. package/dist/drizzle/phone-target.mjs +1 -1
  86. package/dist/drizzle/query-operations.mjs +3 -1
  87. package/dist/drizzle/query-operations.mjs.map +1 -1
  88. package/dist/drizzle/session-sql.mjs +2 -1
  89. package/dist/drizzle/session-sql.mjs.map +1 -1
  90. package/dist/drizzle/sqlite-password-prepared.mjs.map +1 -1
  91. package/dist/drizzle/sqlite-passwords.mjs.map +1 -1
  92. package/dist/drizzle/storage-validation.mjs +10 -198
  93. package/dist/drizzle/storage-validation.mjs.map +1 -1
  94. package/dist/drizzle/totp-target.d.mts +3 -3
  95. package/dist/drizzle/totp-target.mjs +2 -2
  96. package/dist/drizzle/transaction-execution.d.mts +2 -2
  97. package/dist/drizzle/transaction-owner.d.mts +1 -2
  98. package/dist/index.d.mts +3 -2
  99. package/dist/internal/drizzle-postgres-oauth-proxy.d.mts +138 -0
  100. package/dist/internal/drizzle-postgres-oauth-proxy.mjs +29 -0
  101. package/dist/internal/drizzle-postgres-oauth-proxy.mjs.map +1 -0
  102. package/dist/internal/drizzle-postgres.mjs +2 -0
  103. package/dist/internal/drizzle-postgres.mjs.map +1 -1
  104. package/dist/internal/drizzle-sqlite-oauth-proxy.d.mts +146 -0
  105. package/dist/internal/drizzle-sqlite-oauth-proxy.mjs +29 -0
  106. package/dist/internal/drizzle-sqlite-oauth-proxy.mjs.map +1 -0
  107. package/dist/internal/drizzle-sqlite.mjs +2 -0
  108. package/dist/internal/drizzle-sqlite.mjs.map +1 -1
  109. package/package.json +1 -1
  110. package/src/D1.ts +2 -0
  111. package/src/Libsql.ts +2 -0
  112. package/src/Pglite.ts +2 -0
  113. package/src/Postgres.ts +2 -0
  114. package/src/SqliteBun.ts +2 -0
  115. package/src/SqliteDo.ts +2 -0
  116. package/src/SqliteNode.ts +2 -0
  117. package/src/SqliteWasm.ts +2 -0
  118. package/src/drizzle/d1-password-prepared.ts +2 -23
  119. package/src/drizzle/d1-passwords.ts +158 -780
  120. package/src/drizzle/d1-sessions.ts +21 -6
  121. package/src/drizzle/mysql-password-prepared.ts +4 -16
  122. package/src/drizzle/mysql-passwords.ts +5 -20
  123. package/src/drizzle/native-sql-table.ts +259 -0
  124. package/src/drizzle/oauth-connected-model.ts +97 -315
  125. package/src/drizzle/oauth-connected-reference.ts +13 -93
  126. package/src/drizzle/oauth-connected-target.ts +14 -271
  127. package/src/drizzle/oauth-kernel.ts +10 -0
  128. package/src/drizzle/oauth-model.ts +188 -541
  129. package/src/drizzle/oauth-mysql.ts +2 -2
  130. package/src/drizzle/oauth-proxy.ts +101 -0
  131. package/src/drizzle/oauth-target.ts +33 -737
  132. package/src/drizzle/password-model.ts +0 -33
  133. package/src/drizzle/password-prepared-model.ts +0 -18
  134. package/src/drizzle/password-sql.ts +3 -0
  135. package/src/drizzle/password-target.ts +41 -4
  136. package/src/drizzle/pg-password-prepared.ts +4 -16
  137. package/src/drizzle/pg-passwords.ts +5 -20
  138. package/src/drizzle/query-operations.ts +4 -0
  139. package/src/drizzle/session-sql.ts +2 -1
  140. package/src/drizzle/sqlite-password-prepared.ts +4 -52
  141. package/src/drizzle/sqlite-passwords.ts +7 -28
  142. package/src/drizzle/storage-validation.ts +13 -222
  143. package/src/index.ts +2 -8
  144. package/src/internal/drizzle-postgres-oauth-proxy.ts +37 -0
  145. package/src/internal/drizzle-postgres.ts +2 -0
  146. package/src/internal/drizzle-sqlite-oauth-proxy.ts +44 -0
  147. package/src/internal/drizzle-sqlite.ts +2 -0
  148. package/dist/_virtual/_rolldown/runtime.mjs +0 -13
  149. package/dist/drizzle/oauth-accounts.mjs +0 -359
  150. package/dist/drizzle/oauth-accounts.mjs.map +0 -1
  151. package/dist/drizzle/oauth-connected-access.mjs +0 -212
  152. package/dist/drizzle/oauth-connected-access.mjs.map +0 -1
  153. package/dist/drizzle/oauth-connected-collection.mjs +0 -202
  154. package/dist/drizzle/oauth-connected-collection.mjs.map +0 -1
  155. package/dist/drizzle/oauth-connected-custody.mjs +0 -147
  156. package/dist/drizzle/oauth-connected-custody.mjs.map +0 -1
  157. package/dist/drizzle/oauth-connected-flow.mjs +0 -297
  158. package/dist/drizzle/oauth-connected-flow.mjs.map +0 -1
  159. package/dist/drizzle/oauth-connected-input.mjs +0 -122
  160. package/dist/drizzle/oauth-connected-input.mjs.map +0 -1
  161. package/dist/drizzle/oauth-connected-maintenance.mjs +0 -214
  162. package/dist/drizzle/oauth-connected-maintenance.mjs.map +0 -1
  163. package/dist/drizzle/oauth-connected-management.mjs +0 -229
  164. package/dist/drizzle/oauth-connected-management.mjs.map +0 -1
  165. package/dist/drizzle/oauth-connected-settlement.mjs +0 -128
  166. package/dist/drizzle/oauth-connected-settlement.mjs.map +0 -1
  167. package/dist/drizzle/oauth-connected-sign-in.mjs +0 -265
  168. package/dist/drizzle/oauth-connected-sign-in.mjs.map +0 -1
  169. package/dist/drizzle/oauth-connected-state.d.mts +0 -2062
  170. package/dist/drizzle/oauth-connected-state.mjs +0 -430
  171. package/dist/drizzle/oauth-connected-state.mjs.map +0 -1
  172. package/dist/drizzle/oauth-flow.d.mts +0 -22
  173. package/dist/drizzle/oauth-flow.mjs +0 -253
  174. package/dist/drizzle/oauth-flow.mjs.map +0 -1
  175. package/dist/drizzle/oauth-input.mjs +0 -127
  176. package/dist/drizzle/oauth-input.mjs.map +0 -1
  177. package/dist/drizzle/oauth-owner.d.mts +0 -10
  178. package/dist/drizzle/oauth-owner.mjs +0 -10
  179. package/dist/drizzle/oauth-owner.mjs.map +0 -1
  180. package/dist/drizzle/oauth-registration.d.mts +0 -5
  181. package/dist/drizzle/oauth-registration.mjs +0 -374
  182. package/dist/drizzle/oauth-registration.mjs.map +0 -1
  183. package/dist/drizzle/oauth-state.mjs +0 -107
  184. package/dist/drizzle/oauth-state.mjs.map +0 -1
  185. package/src/drizzle/oauth-accounts.ts +0 -805
  186. package/src/drizzle/oauth-connected-access.ts +0 -361
  187. package/src/drizzle/oauth-connected-collection.ts +0 -324
  188. package/src/drizzle/oauth-connected-custody.ts +0 -279
  189. package/src/drizzle/oauth-connected-flow.ts +0 -529
  190. package/src/drizzle/oauth-connected-input.ts +0 -147
  191. package/src/drizzle/oauth-connected-maintenance.ts +0 -346
  192. package/src/drizzle/oauth-connected-management.ts +0 -365
  193. package/src/drizzle/oauth-connected-settlement.ts +0 -251
  194. package/src/drizzle/oauth-connected-sign-in.ts +0 -443
  195. package/src/drizzle/oauth-connected-state.ts +0 -872
  196. package/src/drizzle/oauth-flow.ts +0 -462
  197. package/src/drizzle/oauth-input.ts +0 -201
  198. package/src/drizzle/oauth-owner.ts +0 -29
  199. package/src/drizzle/oauth-registration.ts +0 -640
  200. package/src/drizzle/oauth-state.ts +0 -218
@@ -1,6 +1,10 @@
1
1
  /* oxlint-disable no-explicit-any -- D1 plans bridge consumer-owned Drizzle rows and SQL statements. */
2
2
  import type { D1Client } from "@effect/sql-d1/D1Client";
3
- import { NativeDatabase } from "@yielded/auth-persistence/Adapter";
3
+ import {
4
+ NativeDatabase,
5
+ requireStandalone,
6
+ samePasswordCredentialSnapshot as sameSnapshot,
7
+ } from "@yielded/auth-persistence/Adapter";
4
8
  import {
5
9
  coordinateCommit,
6
10
  CurrentCommitJournal,
@@ -12,6 +16,7 @@ import {
12
16
  } from "@yielded/auth/Hooks";
13
17
  import type { LoginIdentifier } from "@yielded/auth/Identity";
14
18
  import {
19
+ EncodedPasswordHash,
15
20
  PasswordUnavailable,
16
21
  PasswordPersistence,
17
22
  type PasswordMutationInput,
@@ -20,10 +25,7 @@ import {
20
25
  snapshotPasswordRequirement,
21
26
  snapshotPasswordRevision,
22
27
  type PasswordAction,
23
- type PasswordAttemptDecision,
24
- type PasswordCredentialSnapshot,
25
28
  type PasswordMutationDecision,
26
- type PasswordAttemptPolicy,
27
29
  } from "@yielded/auth/Password";
28
30
  import { reportPersistenceFailure } from "@yielded/auth/Persistence";
29
31
  import {
@@ -40,18 +42,14 @@ import {
40
42
  } from "@yielded/auth/Sessions";
41
43
  import {
42
44
  and,
43
- count,
44
45
  eq,
45
- gte,
46
+ getTableColumns,
46
47
  inArray,
47
- lte,
48
- notExists,
49
48
  sql,
50
49
  type AnyRelations,
51
50
  type SQL,
52
51
  type Table,
53
52
  } from "drizzle-orm";
54
- import type { EffectDrizzleQueryError } from "drizzle-orm/effect-core";
55
53
  import type { EffectSQLiteD1Database } from "drizzle-orm/effect-d1";
56
54
  import type { AnySQLiteTable } from "drizzle-orm/sqlite-core";
57
55
  import { Cause, DateTime, Effect, Option, Predicate, Redacted, Schema, Context } from "effect";
@@ -67,10 +65,7 @@ import { nativeDatabase } from "./native-database";
67
65
  import {
68
66
  type AnyPasswordRegistrationMapping,
69
67
  type D1PasswordPersistenceMapping,
70
- type PasswordAttemptRecord,
71
- type PasswordRateScopeKind,
72
68
  type PasswordRegistrationMapping,
73
- type PasswordScopeKeys,
74
69
  requiredPasswordConstraints,
75
70
  requiredPasswordRegistrationConstraints,
76
71
  } from "./password-model";
@@ -85,24 +80,14 @@ type PlanPrepare<Method extends (...args: any[]) => any, A> = (
85
80
  ) => PreparedCommit<A>;
86
81
 
87
82
  type Database = EffectSQLiteD1Database<AnyRelations> & { readonly $client: D1Client };
88
- type Mapping = D1PasswordPersistenceMapping<
89
- Table,
90
- Table,
91
- Table,
92
- Table,
93
- Table,
94
- Table,
95
- Table,
96
- Table,
97
- unknown
98
- >;
83
+ type Mapping = D1PasswordPersistenceMapping<Table, Table, Table, Table, Table, unknown>;
99
84
  type RegistrationMapping<Registration> = AnyPasswordRegistrationMapping<Registration>;
100
- type AdapterFailure = PersistenceMappingError | EffectDrizzleQueryError;
101
- const mapAdapter = <A>(effect: Effect.Effect<A, AdapterFailure>) => effect;
102
85
 
103
86
  interface Planned<A> {
104
87
  readonly receipt: A;
105
88
  readonly statements: ReadonlyArray<Statement<any>>;
89
+ /** Final-state guards without `changes()`, repeated after coordinated statements. */
90
+ readonly postconditions?: ReadonlyArray<Statement<any>>;
106
91
  readonly retryable?: (cause: unknown) => boolean;
107
92
  readonly journalGuard?: PreparedCommit<void>;
108
93
  }
@@ -209,11 +194,6 @@ const readEngineNowMillis = Effect.fn("Drizzle.readEngineNowMillis")(function* (
209
194
  );
210
195
  });
211
196
 
212
- const driverValues = (values: object, entries: ReadonlyArray<readonly [string, unknown]>) => ({
213
- ...values,
214
- ...Object.fromEntries(entries),
215
- });
216
-
217
197
  const allocate = <A>(
218
198
  asyncValue: Effect.Effect<A, PersistenceMappingError> | undefined,
219
199
  syncValue: (() => A) | undefined,
@@ -269,44 +249,6 @@ const authorityColumns = (mapping: Mapping) => ({
269
249
  : column(mapping.authorityCredential.table, mapping.authorityCredential.status),
270
250
  });
271
251
 
272
- const attemptColumns = (mapping: Mapping) => ({
273
- moduleId: column(mapping.attempt.table, mapping.attempt.moduleId),
274
- action: column(mapping.attempt.table, mapping.attempt.action),
275
- attemptId: column(mapping.attempt.table, mapping.attempt.attemptId),
276
- identifierNamespace: column(mapping.attempt.table, mapping.attempt.identifierNamespace),
277
- identifierValue: column(mapping.attempt.table, mapping.attempt.identifierValue),
278
- subjectId: column(mapping.attempt.table, mapping.attempt.subjectId),
279
- credentialId: column(mapping.attempt.table, mapping.attempt.credentialId),
280
- securityRevision: column(mapping.attempt.table, mapping.attempt.securityRevision),
281
- credentialRevision: column(mapping.attempt.table, mapping.attempt.credentialRevision),
282
- verifierVersion: column(mapping.attempt.table, mapping.attempt.verifierVersion),
283
- identifierBindingRevision: column(
284
- mapping.attempt.table,
285
- mapping.attempt.identifierBindingRevision,
286
- ),
287
- admittedAt: column(mapping.attempt.table, mapping.attempt.admittedAt),
288
- deadline: column(mapping.attempt.table, mapping.attempt.deadline),
289
- retentionUntil: column(mapping.attempt.table, mapping.attempt.retentionUntil),
290
- state: column(mapping.attempt.table, mapping.attempt.state),
291
- });
292
-
293
- const scopeColumns = (mapping: Mapping) => ({
294
- moduleId: column(mapping.rateScope.table, mapping.rateScope.moduleId),
295
- action: column(mapping.rateScope.table, mapping.rateScope.action),
296
- scopeKind: column(mapping.rateScope.table, mapping.rateScope.scopeKind),
297
- scopeKey: column(mapping.rateScope.table, mapping.rateScope.scopeKey),
298
- });
299
-
300
- const chargeColumns = (mapping: Mapping) => ({
301
- moduleId: column(mapping.charge.table, mapping.charge.moduleId),
302
- action: column(mapping.charge.table, mapping.charge.action),
303
- scopeKind: column(mapping.charge.table, mapping.charge.scopeKind),
304
- scopeKey: column(mapping.charge.table, mapping.charge.scopeKey),
305
- attemptId: column(mapping.charge.table, mapping.charge.attemptId),
306
- occurredAt: column(mapping.charge.table, mapping.charge.occurredAt),
307
- retentionUntil: column(mapping.charge.table, mapping.charge.retentionUntil),
308
- });
309
-
310
252
  const commandColumns = (mapping: Mapping) => ({
311
253
  moduleId: column(mapping.command.table, mapping.command.moduleId),
312
254
  commandId: column(mapping.command.table, mapping.command.commandId),
@@ -360,14 +302,6 @@ const readCredentialNative = Effect.fn("Drizzle.readCredentialNative")(function*
360
302
  .limit(1);
361
303
  });
362
304
 
363
- interface Resolved {
364
- readonly nativeSubjectId: unknown;
365
- readonly subject: any;
366
- readonly identifier: any;
367
- readonly credential?: any;
368
- readonly snapshot?: PasswordCredentialSnapshot;
369
- }
370
-
371
305
  const resolveCredential = Effect.fn("DrizzleD1Password.resolveCredential")(function* (
372
306
  mapping: Mapping,
373
307
  moduleId: string,
@@ -396,137 +330,6 @@ const resolveCredential = Effect.fn("DrizzleD1Password.resolveCredential")(funct
396
330
  return { nativeSubjectId, subject, identifier: identifierRow, credential, snapshot };
397
331
  });
398
332
 
399
- interface ScopeEntry {
400
- readonly kind: PasswordRateScopeKind;
401
- readonly key: string;
402
- readonly limit: number;
403
- readonly windowMillis: number;
404
- }
405
-
406
- const scopeEntries = (keys: PasswordScopeKeys, policy: PasswordAttemptPolicy) => {
407
- const entries: ScopeEntry[] = [
408
- { kind: "action", key: keys.action, ...policy.action },
409
- { kind: "identifier", key: keys.identifier, ...policy.identifier },
410
- ];
411
-
412
- if (keys.subject !== undefined)
413
- entries.push({ kind: "subject", key: keys.subject, ...policy.subject });
414
-
415
- return entries;
416
- };
417
-
418
- const livePendingAttempts = (
419
- mapping: Mapping,
420
- moduleId: string,
421
- action: "sign-in" | "change",
422
- entry: ScopeEntry,
423
- identifier: LoginIdentifier,
424
- nativeSubjectId: unknown | undefined,
425
- activeAt: unknown,
426
- ) => {
427
- const a = attemptColumns(mapping);
428
-
429
- return and(
430
- entry.kind === "action"
431
- ? and(eq(a.moduleId, moduleId), eq(a.action, action))
432
- : entry.kind === "identifier"
433
- ? and(
434
- eq(a.moduleId, moduleId),
435
- eq(a.action, action),
436
- eq(a.identifierNamespace, identifier.namespace),
437
- eq(a.identifierValue, identifier.value),
438
- )
439
- : and(eq(a.moduleId, moduleId), eq(a.action, action), eq(a.subjectId, nativeSubjectId)),
440
- eq(a.state, "pending"),
441
- sql`${a.retentionUntil} > ${activeAt}`,
442
- );
443
- };
444
-
445
- const scopeAdmissionCondition = (
446
- mapping: Mapping,
447
- moduleId: string,
448
- action: "sign-in" | "change",
449
- entry: ScopeEntry,
450
- policy: PasswordAttemptPolicy,
451
- identifier: LoginIdentifier,
452
- nativeSubjectId: unknown | undefined,
453
- ) => {
454
- const c = chargeColumns(mapping);
455
-
456
- const pending = livePendingAttempts(
457
- mapping,
458
- moduleId,
459
- action,
460
- entry,
461
- identifier,
462
- nativeSubjectId,
463
- mapping.d1.engineNow,
464
- );
465
-
466
- return sql`(select count(*) from ${mapping.charge.table} where ${c.moduleId} = ${sql.param(moduleId, c.moduleId)} and ${c.action} = ${sql.param(action, c.action)} and ${c.scopeKind} = ${sql.param(entry.kind, c.scopeKind)} and ${c.scopeKey} = ${sql.param(entry.key, c.scopeKey)} and ${c.occurredAt} >= ${mapping.d1.engineInstantMinus(entry.windowMillis)}) < ${entry.limit} and (select count(*) from ${mapping.attempt.table} where ${pending}) < ${policy.maximumPending}`;
467
- };
468
-
469
- const readScopeAdmission = Effect.fn("DrizzleD1Password.readScopeAdmission")(function* (
470
- mapping: Mapping,
471
- moduleId: string,
472
- action: "sign-in" | "change",
473
- entries: ReadonlyArray<ScopeEntry>,
474
- policy: PasswordAttemptPolicy,
475
- identifier: LoginIdentifier,
476
- nativeSubjectId: unknown | undefined,
477
- now: number,
478
- ) {
479
- const database = yield* CurrentD1PlanningDatabase;
480
-
481
- const c = chargeColumns(mapping);
482
- const admitted: ScopeEntry[] = [];
483
-
484
- for (let index = 0; index < entries.length; index++) {
485
- const entry = entries[index]!;
486
-
487
- const chargeRows = yield* mapAdapter(
488
- database
489
- .select({ total: count() })
490
- .from(mapping.charge.table)
491
- .where(
492
- and(
493
- eq(c.moduleId, moduleId),
494
- eq(c.action, action),
495
- eq(c.scopeKind, entry.kind),
496
- eq(c.scopeKey, entry.key),
497
- gte(c.occurredAt, mapping.encodeInstant(now - entry.windowMillis)),
498
- ),
499
- ),
500
- );
501
-
502
- const pendingRows = yield* mapAdapter(
503
- database
504
- .select({ total: count() })
505
- .from(mapping.attempt.table)
506
- .where(
507
- livePendingAttempts(
508
- mapping,
509
- moduleId,
510
- action,
511
- entry,
512
- identifier,
513
- nativeSubjectId,
514
- mapping.encodeInstant(now),
515
- ),
516
- ),
517
- );
518
-
519
- const admittedHere =
520
- Number(chargeRows[0]?.total ?? 0) < entry.limit &&
521
- Number(pendingRows[0]?.total ?? 0) < policy.maximumPending;
522
-
523
- if (admittedHere) admitted.push(entry);
524
- else if (index === 0) break;
525
- }
526
-
527
- return admitted;
528
- });
529
-
530
333
  const evidenceProofsJson = Schema.encodeSync(
531
334
  Schema.fromJsonString(AuthenticationEvidence.fields.proofs),
532
335
  );
@@ -555,25 +358,6 @@ const assuranceCondition = (
555
358
  const sameIdentifier = (left: LoginIdentifier, right: LoginIdentifier) =>
556
359
  left.namespace === right.namespace && left.value === right.value;
557
360
 
558
- const sameSnapshot = (left: PasswordCredentialSnapshot, right: PasswordCredentialSnapshot) =>
559
- left.moduleId === right.moduleId &&
560
- left.revision.subjectId === right.revision.subjectId &&
561
- left.revision.securityRevision === right.revision.securityRevision &&
562
- left.credentialId === right.credentialId &&
563
- left.credentialRevision === right.credentialRevision &&
564
- left.verifierVersion === right.verifierVersion &&
565
- left.normalization === right.normalization &&
566
- left.identifierVerifiedAtMillis === right.identifierVerifiedAtMillis &&
567
- left.revision.credentials.length === right.revision.credentials.length &&
568
- left.revision.credentials.every((entry) =>
569
- right.revision.credentials.some(
570
- (other) => other.credentialId === entry.credentialId && other.revision === entry.revision,
571
- ),
572
- ) &&
573
- left.identifierBindingRevision === right.identifierBindingRevision &&
574
- sameIdentifier(left.identifier, right.identifier) &&
575
- Redacted.value(left.verifier) === Redacted.value(right.verifier);
576
-
577
361
  const evidenceSatisfiedAt = (
578
362
  evidence: PasswordMutationInput["authorization"]["evidence"],
579
363
  requirement: PasswordMutationInput["authorization"]["requirement"],
@@ -928,34 +712,6 @@ const credentialCurrentCondition = (
928
712
  : sql`exists(select 1 from ${mapping.credential.table} where ${c.moduleId} = ${sql.param(input.moduleId, c.moduleId)} and ${c.subjectId} = ${sql.param(nativeSubjectId, c.subjectId)} and ${c.credentialId} = ${sql.param(input.credential.credentialId, c.credentialId)} and ${c.credentialRevision} = ${sql.param(input.credential.credentialRevision, c.credentialRevision)} and ${c.verifierVersion} = ${sql.param(input.credential.verifierVersion, c.verifierVersion)} and ${c.verifier} = ${sql.param(Redacted.value(input.credential.verifier), c.verifier)} and ${c.normalization} = ${sql.param(input.credential.normalization, c.normalization)})`;
929
713
  };
930
714
 
931
- const admittedSnapshotCondition = (
932
- mapping: Mapping,
933
- resolved: Resolved | undefined,
934
- moduleId: string,
935
- ) => {
936
- const snapshot = resolved?.snapshot;
937
-
938
- if (snapshot === undefined || resolved === undefined) return sql`true`;
939
- const s = subjectColumns(mapping);
940
- const i = identifierColumns(mapping);
941
- const c = credentialColumns(mapping);
942
- const ac = authorityColumns(mapping);
943
-
944
- return and(
945
- sql`exists(select 1 from ${mapping.subject.table} where ${s.id} = ${sql.param(resolved.nativeSubjectId, s.id)} and ${s.status} = ${sql.param(mapping.subject.d1ActiveStatusValue, s.status)} and ${s.securityRevision} = ${sql.param(snapshot.revision.securityRevision, s.securityRevision)})`,
946
- mapping.identifier.d1CurrentCondition({
947
- identifier: snapshot.identifier,
948
- nativeSubjectId: resolved.nativeSubjectId,
949
- }),
950
- sql`exists(select 1 from ${mapping.identifier.table} where ${i.namespace} = ${sql.param(snapshot.identifier.namespace, i.namespace)} and ${i.value} = ${sql.param(snapshot.identifier.value, i.value)} and ${i.subjectId} = ${sql.param(resolved.nativeSubjectId, i.subjectId)} and ${i.bindingRevision} = ${sql.param(snapshot.identifierBindingRevision, i.bindingRevision)})`,
951
- sql`exists(select 1 from ${mapping.credential.table} where ${c.moduleId} = ${sql.param(moduleId, c.moduleId)} and ${c.subjectId} = ${sql.param(resolved.nativeSubjectId, c.subjectId)} and ${c.credentialId} = ${sql.param(snapshot.credentialId, c.credentialId)} and ${c.credentialRevision} = ${sql.param(snapshot.credentialRevision, c.credentialRevision)} and ${c.verifierVersion} = ${sql.param(snapshot.verifierVersion, c.verifierVersion)} and ${c.verifier} = ${sql.param(Redacted.value(snapshot.verifier), c.verifier)} and ${c.normalization} = ${sql.param(snapshot.normalization, c.normalization)})`,
952
- ...snapshot.revision.credentials.map(
953
- (item) =>
954
- sql`exists(select 1 from ${mapping.authorityCredential.table} where ${ac.subjectId} = ${sql.param(resolved.nativeSubjectId, ac.subjectId)} and ${ac.credentialId} = ${sql.param(item.credentialId, ac.credentialId)} and ${ac.revision} = ${sql.param(item.revision, ac.revision)}${ac.status === undefined ? sql`` : sql` and ${ac.status} = ${sql.param(mapping.authorityCredential.d1ActiveStatusValue, ac.status)}`})`,
955
- ),
956
- )!;
957
- };
958
-
959
715
  const mutationStatements = Effect.fn(function* (
960
716
  mapping: Mapping,
961
717
  input: PasswordMutationInput,
@@ -1150,6 +906,7 @@ const compileAddMutation = Effect.fn("DrizzleD1Password.compileAddMutation")(fun
1150
906
  return {
1151
907
  statements,
1152
908
  appliedCondition,
909
+ marker,
1153
910
  retryable: (cause: unknown) => isGuardFailure(cause, marker),
1154
911
  };
1155
912
  });
@@ -1179,357 +936,121 @@ const makePasswordPlans = (
1179
936
  });
1180
937
 
1181
938
  return {
1182
- admitAttempt: <A>(
1183
- input: Parameters<PasswordPersistence["Service"]["admitAttempt"]>[0],
1184
- prepare: PlanPrepare<PasswordPersistence["Service"]["admitAttempt"], A>,
1185
- ) =>
939
+ findCredential: (uncaptured: Parameters<PasswordPersistence["Service"]["findCredential"]>[0]) =>
1186
940
  Effect.gen(function* () {
941
+ if (!validConstraints(mapping)) return yield* unavailable();
942
+ const input = { ...uncaptured, identifier: { ...uncaptured.identifier } };
1187
943
  const database = yield* CurrentD1PlanningDatabase;
944
+ const identifierRow = (yield* readIdentifier(mapping, input.identifier))[0];
1188
945
 
1189
- const attemptId = yield* allocate(mapping.allocateAttemptId, mapping.allocateAttemptIdSync);
1190
-
1191
- return yield* run(
1192
- Effect.gen(function* () {
1193
- const journal = yield* CurrentCommitJournal;
1194
-
1195
- const now = yield* readEngineNowMillis(mapping);
1196
-
1197
- const resolved = yield* resolveCredential(
1198
- mapping,
1199
- input.moduleId,
1200
- input.identifier,
1201
- input.subjectId,
1202
- );
1203
-
1204
- const keys = mapping.scopeKeys({
1205
- moduleId: input.moduleId,
1206
- action: input.action,
1207
- identifier: input.identifier,
1208
- ...(resolved?.snapshot === undefined
1209
- ? {}
1210
- : { subjectId: resolved.snapshot.revision.subjectId }),
1211
- });
1212
-
1213
- const scopes = scopeEntries(keys, input.policy);
1214
-
1215
- const admitted = yield* readScopeAdmission(
1216
- mapping,
1217
- input.moduleId,
1218
- input.action,
1219
- scopes,
1220
- input.policy,
1221
- input.identifier,
1222
- resolved?.nativeSubjectId,
1223
- now,
1224
- );
1225
-
1226
- const allAdmitted = admitted.length === scopes.length;
1227
- const considered = admitted.length === 0 ? scopes.slice(0, 1) : scopes;
1228
-
1229
- const receipt = prepare(
1230
- allAdmitted
1231
- ? {
1232
- _tag: "Admitted" as const,
1233
- attemptId,
1234
- ...(resolved?.snapshot === undefined ? {} : { credential: resolved.snapshot }),
1235
- }
1236
- : { _tag: "Denied" as const },
1237
- journal,
1238
- );
1239
-
1240
- const marker = `effect-auth-password-guard:admit:${attemptId}`;
1241
- const statements: Statement<any>[] = [];
946
+ if (identifierRow === undefined || !mapping.identifier.isCurrent(identifierRow))
947
+ return Option.none();
948
+ const nativeId = identifierRow[mapping.identifier.subjectId];
949
+
950
+ const sc = subjectColumns(mapping),
951
+ ic = identifierColumns(mapping),
952
+ cc = credentialColumns(mapping);
953
+
954
+ // Bind the same native ID through each column's codec, then inspect one coherent snapshot.
955
+ const rows = yield* database
956
+ .select({
957
+ subject: getTableColumns(mapping.subject.table),
958
+ identifier: getTableColumns(mapping.identifier.table),
959
+ credential: getTableColumns(mapping.credential.table),
960
+ })
961
+ .from(mapping.subject.table)
962
+ .innerJoin(
963
+ mapping.identifier.table,
964
+ and(
965
+ eq(ic.namespace, input.identifier.namespace),
966
+ eq(ic.value, input.identifier.value),
967
+ eq(ic.subjectId, nativeId),
968
+ ),
969
+ )
970
+ .innerJoin(
971
+ mapping.credential.table,
972
+ and(eq(cc.moduleId, input.moduleId), eq(cc.subjectId, nativeId)),
973
+ )
974
+ .where(eq(sc.id, nativeId));
1242
975
 
1243
- for (const entry of considered) {
1244
- statements.push(
1245
- yield* statement(
1246
- database
1247
- .insert(mapping.rateScope.table)
1248
- .values(
1249
- mapping.rateScope.encodeInsert({
1250
- moduleId: input.moduleId,
1251
- action: input.action,
1252
- scopeKind: entry.kind,
1253
- scopeKey: entry.key,
1254
- }),
1255
- )
1256
- .onConflictDoNothing(),
1257
- ),
1258
- );
1259
- }
976
+ if (rows.length !== 1) return Option.none();
977
+ const row = rows[0]!;
1260
978
 
1261
- const conditions = considered.map((entry) => {
1262
- const condition = scopeAdmissionCondition(
1263
- mapping,
1264
- input.moduleId,
1265
- input.action,
1266
- entry,
1267
- input.policy,
1268
- input.identifier,
1269
- resolved?.nativeSubjectId,
1270
- );
979
+ if (
980
+ !mapping.subject.isActiveStatus(row.subject[mapping.subject.status]) ||
981
+ !mapping.identifier.isCurrent(row.identifier) ||
982
+ !mapping.subjectId.equals(nativeId, row.subject[mapping.subject.id]) ||
983
+ !mapping.subjectId.equals(nativeId, row.identifier[mapping.identifier.subjectId]) ||
984
+ !mapping.subjectId.equals(nativeId, row.credential[mapping.credential.subjectId])
985
+ )
986
+ return Option.none();
1271
987
 
1272
- return admitted.includes(entry) ? condition : sql`not (${condition})`;
1273
- });
988
+ const credential = yield* mapping.credential
989
+ .decode({ moduleId: input.moduleId, ...row })
990
+ .pipe(Effect.flatMap(snapshotPasswordCredential));
1274
991
 
1275
- statements.push(
1276
- yield* assertion(
1277
- and(
1278
- ...conditions,
1279
- ...(allAdmitted
1280
- ? [admittedSnapshotCondition(mapping, resolved, input.moduleId)]
1281
- : []),
1282
- )!,
1283
- marker,
1284
- ),
1285
- );
1286
- for (const entry of admitted)
1287
- statements.push(
1288
- yield* statement(
1289
- database.insert(mapping.charge.table).values(
1290
- driverValues(
1291
- mapping.charge.encodeInsert({
1292
- moduleId: input.moduleId,
1293
- action: input.action,
1294
- scopeKind: entry.kind,
1295
- scopeKey: entry.key,
1296
- attemptId,
1297
- occurredAtMillis: now,
1298
- retentionUntilMillis: now + entry.windowMillis,
1299
- }),
1300
- [
1301
- [mapping.charge.occurredAt, mapping.d1.engineNow],
1302
- [
1303
- mapping.charge.retentionUntil,
1304
- mapping.d1.engineInstantPlus(entry.windowMillis),
1305
- ],
1306
- ],
1307
- ),
1308
- ),
1309
- ),
1310
- );
1311
- if (allAdmitted) {
1312
- const snapshot = resolved?.snapshot;
1313
-
1314
- const record: PasswordAttemptRecord = {
1315
- moduleId: input.moduleId,
1316
- action: input.action,
1317
- attemptId,
1318
- identifier: input.identifier,
1319
- ...(snapshot === undefined
1320
- ? {}
1321
- : {
1322
- subjectId: snapshot.revision.subjectId,
1323
- credentialId: snapshot.credentialId,
1324
- securityRevision: snapshot.revision.securityRevision,
1325
- credentialRevision: snapshot.credentialRevision,
1326
- verifierVersion: snapshot.verifierVersion,
1327
- identifierBindingRevision: snapshot.identifierBindingRevision,
1328
- }),
1329
- admittedAtMillis: now,
1330
- deadlineMillis: now + input.policy.attemptLifetimeMillis,
1331
- retentionUntilMillis: Math.max(
1332
- now + input.policy.attemptLifetimeMillis,
1333
- ...scopes.map((entry) => now + entry.windowMillis),
1334
- ),
1335
- };
1336
-
1337
- statements.push(
1338
- yield* statement(
1339
- database.insert(mapping.attempt.table).values(
1340
- driverValues(
1341
- mapping.attempt.encodeInsert(record, {
1342
- ...(resolved?.nativeSubjectId === undefined
1343
- ? {}
1344
- : { nativeSubjectId: resolved.nativeSubjectId }),
1345
- state: "pending",
1346
- }),
1347
- [
1348
- [mapping.attempt.admittedAt, mapping.d1.engineNow],
1349
- [
1350
- mapping.attempt.deadline,
1351
- mapping.d1.engineInstantPlus(input.policy.attemptLifetimeMillis),
1352
- ],
1353
- [
1354
- mapping.attempt.retentionUntil,
1355
- mapping.d1.engineInstantPlus(
1356
- Math.max(
1357
- input.policy.attemptLifetimeMillis,
1358
- ...scopes.map((entry) => entry.windowMillis),
1359
- ),
1360
- ),
1361
- ],
1362
- ],
1363
- ),
1364
- ),
1365
- ),
1366
- );
1367
- }
1368
-
1369
- return {
1370
- receipt,
1371
- statements,
1372
- retryable: (cause: unknown) => isGuardFailure(cause, marker),
1373
- };
1374
- }),
1375
- );
992
+ return input.subjectId !== undefined && credential.revision.subjectId !== input.subjectId
993
+ ? Option.none()
994
+ : Option.some(credential);
1376
995
  }),
1377
- settleAttempt: <A>(
1378
- uncaptured: Parameters<PasswordPersistence["Service"]["settleAttempt"]>[0],
1379
- prepare: PlanPrepare<PasswordPersistence["Service"]["settleAttempt"], A>,
1380
- ) =>
1381
- Effect.gen(function* () {
1382
- const database = yield* CurrentD1PlanningDatabase;
1383
-
1384
- const captured =
1385
- uncaptured.captured === undefined
1386
- ? undefined
1387
- : yield* snapshotPasswordCredential(uncaptured.captured);
1388
-
1389
- const input = { ...uncaptured, ...(captured === undefined ? {} : { captured }) };
1390
-
1391
- return yield* run(
1392
- Effect.gen(function* () {
1393
- const journal = yield* CurrentCommitJournal;
1394
-
1395
- const a = attemptColumns(mapping);
996
+ rehashIfCurrent: (input: Parameters<PasswordPersistence["Service"]["rehashIfCurrent"]>[0]) =>
997
+ run(
998
+ Effect.gen(function* () {
999
+ const nextVerifier = Redacted.make(
1000
+ yield* Schema.decodeEffect(EncodedPasswordHash)(Redacted.value(input.nextVerifier)),
1001
+ );
1396
1002
 
1397
- const rows = yield* database
1398
- .select()
1399
- .from(mapping.attempt.table)
1400
- .where(and(eq(a.moduleId, input.moduleId), eq(a.attemptId, input.attemptId)))
1401
- .limit(1);
1003
+ const credential = yield* snapshotPasswordCredential(input.credential);
1004
+ const database = yield* CurrentD1PlanningDatabase;
1005
+ const nativeId = yield* mapping.subjectId.toNative(credential.revision.subjectId);
1006
+ const version = yield* allocate(mapping.allocateRevision, mapping.allocateRevisionSync);
1402
1007
 
1403
- const row = rows[0];
1404
- const engineNow = yield* readEngineNowMillis(mapping);
1008
+ if (version === credential.verifierVersion) return yield* unavailable();
1009
+ const encoded = mapping.credential.encodeVerifier(nextVerifier, version);
1405
1010
 
1406
- const deadline =
1407
- row === undefined
1408
- ? undefined
1409
- : yield* mapping.decodeInstant(row[mapping.attempt.deadline]);
1011
+ const values = {
1012
+ [mapping.credential.verifier]: encoded[mapping.credential.verifier],
1013
+ [mapping.credential.verifierVersion]: encoded[mapping.credential.verifierVersion],
1014
+ };
1410
1015
 
1411
- let resolved: Resolved | undefined;
1412
- let verified = false;
1016
+ if (Object.values(values).some((value) => value === undefined))
1017
+ return yield* unavailable();
1018
+ const c = credentialColumns(mapping);
1413
1019
 
1414
- if (
1415
- row !== undefined &&
1416
- row[mapping.attempt.state] === "pending" &&
1417
- input.outcome === "verified" &&
1418
- input.captured !== undefined &&
1419
- deadline !== undefined &&
1420
- deadline > engineNow
1421
- ) {
1422
- resolved = yield* resolveCredential(
1423
- mapping,
1424
- input.moduleId,
1425
- input.captured.identifier,
1426
- input.captured.revision.subjectId,
1427
- );
1428
- verified =
1429
- input.captured.moduleId === input.moduleId &&
1430
- resolved?.snapshot !== undefined &&
1431
- sameSnapshot(resolved.snapshot, input.captured) &&
1432
- mapping.subjectId.equals(
1433
- row[mapping.attempt.subjectId],
1434
- resolved.nativeSubjectId,
1435
- ) &&
1436
- row[mapping.attempt.identifierNamespace] === input.captured.identifier.namespace &&
1437
- row[mapping.attempt.identifierValue] === input.captured.identifier.value &&
1438
- row[mapping.attempt.credentialId] === input.captured.credentialId &&
1439
- row[mapping.attempt.securityRevision] ===
1440
- input.captured.revision.securityRevision &&
1441
- row[mapping.attempt.credentialRevision] === input.captured.credentialRevision &&
1442
- row[mapping.attempt.verifierVersion] === input.captured.verifierVersion &&
1443
- row[mapping.attempt.identifierBindingRevision] ===
1444
- input.captured.identifierBindingRevision;
1445
- }
1446
- const decision: PasswordAttemptDecision = verified ? "verified" : "rejected";
1447
- const receipt = prepare(decision, journal);
1448
-
1449
- if (row === undefined || row[mapping.attempt.state] !== "pending")
1450
- return { receipt, statements: [] };
1451
- const marker = `effect-auth-password-guard:settle:${input.attemptId}`;
1452
-
1453
- const base = and(
1454
- eq(a.moduleId, input.moduleId),
1455
- eq(a.attemptId, input.attemptId),
1456
- eq(a.state, "pending"),
1457
- )!;
1458
-
1459
- const condition = verified
1460
- ? and(base, sql`${a.deadline} > ${mapping.d1.engineNow}`)!
1461
- : base;
1462
-
1463
- const statements: Statement<any>[] = [
1464
- yield* assertion(
1465
- sql`exists(select 1 from ${mapping.attempt.table} where ${condition})`,
1466
- marker,
1467
- ),
1020
+ return {
1021
+ receipt: undefined,
1022
+ statements: [
1468
1023
  yield* statement(
1469
1024
  database
1470
- .update(mapping.attempt.table)
1471
- .set(updateValues([[mapping.attempt.state, decision]]))
1472
- .where(condition),
1473
- ),
1474
- ];
1475
-
1476
- if (verified && resolved?.snapshot !== undefined) {
1477
- const captured = input.captured!;
1478
- const s = subjectColumns(mapping);
1479
- const i = identifierColumns(mapping);
1480
- const c = credentialColumns(mapping);
1481
-
1482
- const authority = and(
1483
- sql`exists(select 1 from ${mapping.subject.table} where ${s.id} = ${sql.param(resolved.nativeSubjectId, s.id)} and ${s.status} = ${sql.param(mapping.subject.d1ActiveStatusValue, s.status)} and ${s.securityRevision} = ${sql.param(input.captured!.revision.securityRevision, s.securityRevision)})`,
1484
- mapping.identifier.d1CurrentCondition({
1485
- identifier: input.captured!.identifier,
1486
- nativeSubjectId: resolved.nativeSubjectId,
1487
- }),
1488
- sql`exists(select 1 from ${mapping.credential.table} where ${c.moduleId} = ${sql.param(input.moduleId, c.moduleId)} and ${c.subjectId} = ${sql.param(resolved.nativeSubjectId, c.subjectId)} and ${c.credentialId} = ${sql.param(input.captured!.credentialId, c.credentialId)} and ${c.credentialRevision} = ${sql.param(input.captured!.credentialRevision, c.credentialRevision)} and ${c.verifierVersion} = ${sql.param(input.captured!.verifierVersion, c.verifierVersion)} and ${c.verifier} = ${sql.param(Redacted.value(input.captured!.verifier), c.verifier)})`,
1489
- sql`exists(select 1 from ${mapping.identifier.table} where ${i.namespace} = ${sql.param(input.captured!.identifier.namespace, i.namespace)} and ${i.value} = ${sql.param(input.captured!.identifier.value, i.value)} and ${i.subjectId} = ${sql.param(resolved.nativeSubjectId, i.subjectId)} and ${i.bindingRevision} = ${sql.param(input.captured!.identifierBindingRevision, i.bindingRevision)})`,
1490
- )!;
1491
-
1492
- statements.splice(1, 0, yield* assertion(authority, marker));
1493
- if (
1494
- input.rehash !== undefined &&
1495
- input.rehash.expectedVersion === captured.verifierVersion &&
1496
- Redacted.value(input.rehash.expectedVerifier) === Redacted.value(captured.verifier)
1497
- ) {
1498
- const nextVersion = yield* allocate(
1499
- mapping.allocateRevision,
1500
- mapping.allocateRevisionSync,
1501
- );
1502
-
1503
- statements.push(
1504
- yield* statement(
1505
- database
1506
- .update(mapping.credential.table)
1507
- .set(
1508
- mapping.credential.encodeVerifier(input.rehash.nextVerifier, nextVersion),
1509
- )
1510
- .where(
1511
- and(
1512
- eq(c.moduleId, input.moduleId),
1513
- eq(c.subjectId, resolved.nativeSubjectId),
1514
- eq(c.credentialId, captured.credentialId),
1515
- eq(c.credentialRevision, captured.credentialRevision),
1516
- eq(c.verifierVersion, input.rehash.expectedVersion),
1517
- eq(c.verifier, Redacted.value(input.rehash.expectedVerifier)),
1518
- ),
1025
+ .update(mapping.credential.table)
1026
+ .set(values)
1027
+ .where(
1028
+ and(
1029
+ eq(c.moduleId, credential.moduleId),
1030
+ eq(c.subjectId, nativeId),
1031
+ eq(c.credentialId, credential.credentialId),
1032
+ ...(
1033
+ [
1034
+ [c.moduleId, credential.moduleId],
1035
+ [c.subjectId, nativeId],
1036
+ [c.credentialId, credential.credentialId],
1037
+ [c.credentialRevision, credential.credentialRevision],
1038
+ [c.verifierVersion, credential.verifierVersion],
1039
+ [c.verifier, Redacted.value(credential.verifier)],
1040
+ [c.normalization, credential.normalization],
1041
+ ] as const
1042
+ ).map(([column, value]) =>
1043
+ typeof value === "string"
1044
+ ? sql`cast(${column} as blob) = cast(${sql.param(value, column)} as blob)`
1045
+ : eq(column, value),
1519
1046
  ),
1047
+ ),
1520
1048
  ),
1521
- );
1522
- }
1523
- }
1524
-
1525
- return {
1526
- receipt,
1527
- statements,
1528
- retryable: (cause: unknown) => isGuardFailure(cause, marker),
1529
- };
1530
- }),
1531
- );
1532
- }),
1049
+ ),
1050
+ ],
1051
+ };
1052
+ }),
1053
+ ),
1533
1054
  readForSubject: (input: Parameters<PasswordPersistence["Service"]["readForSubject"]>[0]) =>
1534
1055
  Effect.gen(function* () {
1535
1056
  const database = yield* CurrentD1PlanningDatabase;
@@ -1619,10 +1140,14 @@ const makePasswordPlans = (
1619
1140
  nextSecurityRevision,
1620
1141
  });
1621
1142
 
1143
+ if (compiled === undefined)
1144
+ return { receipt: prepare("rejected", journal), statements: [] };
1145
+
1622
1146
  return {
1623
- receipt: prepare(compiled === undefined ? "rejected" : "changed", journal),
1624
- statements: compiled?.statements ?? [],
1625
- ...(compiled?.retryable === undefined ? {} : { retryable: compiled.retryable }),
1147
+ receipt: prepare("changed", journal),
1148
+ statements: compiled.statements,
1149
+ postconditions: [yield* assertion(compiled.appliedCondition, compiled.marker)],
1150
+ retryable: compiled.retryable,
1626
1151
  };
1627
1152
  }),
1628
1153
  );
@@ -1683,15 +1208,6 @@ const makePasswordPlans = (
1683
1208
  "reset-password",
1684
1209
  ),
1685
1210
  ),
1686
- cleanupAttempts: <A>(
1687
- input: Parameters<PasswordPersistence["Service"]["cleanupAttempts"]>[0],
1688
- prepare: PlanPrepare<PasswordPersistence["Service"]["cleanupAttempts"], A>,
1689
- ) =>
1690
- run(
1691
- Effect.gen(function* () {
1692
- return yield* cleanupPlan(mapping, input, prepare);
1693
- }),
1694
- ),
1695
1211
  };
1696
1212
  };
1697
1213
 
@@ -1826,6 +1342,7 @@ const replacementPlan = Effect.fn("Drizzle.replacementPlan")(function* <A>(
1826
1342
  return {
1827
1343
  receipt: passwordReceipt ?? prepare("rejected", journal),
1828
1344
  statements: compiled.statements,
1345
+ ...(compiled.postconditions === undefined ? {} : { postconditions: compiled.postconditions }),
1829
1346
  retryable: (cause: unknown) =>
1830
1347
  isGuardFailure(cause, marker) ||
1831
1348
  isGuardFailure(
@@ -1839,159 +1356,12 @@ const replacementPlan = Effect.fn("Drizzle.replacementPlan")(function* <A>(
1839
1356
  };
1840
1357
  }
1841
1358
 
1842
- return {
1843
- receipt: prepare("changed", journal),
1844
- statements: [
1845
- ...protectedMutation.statements,
1846
- yield* assertion(protectedMutation.appliedCondition, marker),
1847
- ],
1848
- retryable: (cause: unknown) => isGuardFailure(cause, marker),
1849
- };
1850
- });
1851
-
1852
- const cleanupPlan = Effect.fn("Drizzle.cleanupPlan")(function* <A>(
1853
- mapping: Mapping,
1854
- input: { readonly moduleId: string; readonly limit: number },
1855
- prepare: PreparePasswordCommit<{ readonly removed: number; readonly hasMore: boolean }, A>,
1856
- ) {
1857
- const database = yield* CurrentD1PlanningDatabase;
1858
- const journal = yield* CurrentCommitJournal;
1859
-
1860
- const now = yield* readEngineNowMillis(mapping);
1861
- const nativeNow = mapping.encodeInstant(now);
1862
- const a = attemptColumns(mapping);
1863
- const c = chargeColumns(mapping);
1864
- const sc = scopeColumns(mapping);
1865
- let remaining = input.limit;
1866
- let hasMore = false;
1867
-
1868
- const attempts = yield* database
1869
- .select({ attemptId: a.attemptId })
1870
- .from(mapping.attempt.table)
1871
- .where(and(eq(a.moduleId, input.moduleId), lte(a.retentionUntil, nativeNow)))
1872
- .limit(remaining + 1);
1873
-
1874
- if (attempts.length > remaining) hasMore = true;
1875
- const selectedAttempts = attempts.slice(0, remaining);
1876
-
1877
- remaining -= selectedAttempts.length;
1878
-
1879
- const charges = yield* database
1880
- .select({
1881
- action: c.action,
1882
- scopeKind: c.scopeKind,
1883
- scopeKey: c.scopeKey,
1884
- attemptId: c.attemptId,
1885
- })
1886
- .from(mapping.charge.table)
1887
- .where(and(eq(c.moduleId, input.moduleId), lte(c.retentionUntil, nativeNow)))
1888
- .limit(remaining + 1);
1889
-
1890
- if (charges.length > remaining) hasMore = true;
1891
- const selectedCharges = charges.slice(0, remaining);
1892
-
1893
- remaining -= selectedCharges.length;
1894
-
1895
- const scopes = yield* database
1896
- .select({ action: sc.action, scopeKind: sc.scopeKind, scopeKey: sc.scopeKey })
1897
- .from(mapping.rateScope.table)
1898
- .where(
1899
- and(
1900
- eq(sc.moduleId, input.moduleId),
1901
- notExists(
1902
- database
1903
- .select({ one: sql`1` })
1904
- .from(mapping.charge.table)
1905
- .where(
1906
- and(
1907
- eq(c.moduleId, sc.moduleId),
1908
- eq(c.action, sc.action),
1909
- eq(c.scopeKind, sc.scopeKind),
1910
- eq(c.scopeKey, sc.scopeKey),
1911
- ),
1912
- ),
1913
- ),
1914
- ),
1915
- )
1916
- .limit(remaining + 1);
1917
-
1918
- if (scopes.length > remaining) hasMore = true;
1919
- const selectedScopes = scopes.slice(0, remaining);
1920
- const marker = `effect-auth-password-guard:cleanup:${input.moduleId}:${now}`;
1921
- const statements: Statement<any>[] = [];
1922
-
1923
- for (const row of selectedAttempts) {
1924
- const condition = and(
1925
- eq(a.moduleId, input.moduleId),
1926
- eq(a.attemptId, row.attemptId),
1927
- lte(a.retentionUntil, mapping.d1.engineNow),
1928
- )!;
1929
-
1930
- statements.push(
1931
- yield* assertion(
1932
- sql`exists(select 1 from ${mapping.attempt.table} where ${condition})`,
1933
- marker,
1934
- ),
1935
- yield* statement(database.delete(mapping.attempt.table).where(condition)),
1936
- );
1937
- }
1938
- for (const row of selectedCharges) {
1939
- const condition = and(
1940
- eq(c.moduleId, input.moduleId),
1941
- eq(c.action, row.action),
1942
- eq(c.scopeKind, row.scopeKind),
1943
- eq(c.scopeKey, row.scopeKey),
1944
- eq(c.attemptId, row.attemptId),
1945
- lte(c.retentionUntil, mapping.d1.engineNow),
1946
- )!;
1947
-
1948
- statements.push(
1949
- yield* assertion(
1950
- sql`exists(select 1 from ${mapping.charge.table} where ${condition})`,
1951
- marker,
1952
- ),
1953
- yield* statement(database.delete(mapping.charge.table).where(condition)),
1954
- );
1955
- }
1956
- for (const row of selectedScopes) {
1957
- const condition = and(
1958
- eq(sc.moduleId, input.moduleId),
1959
- eq(sc.action, row.action),
1960
- eq(sc.scopeKind, row.scopeKind),
1961
- eq(sc.scopeKey, row.scopeKey),
1962
- notExists(
1963
- database
1964
- .select({ one: sql`1` })
1965
- .from(mapping.charge.table)
1966
- .where(
1967
- and(
1968
- eq(c.moduleId, sc.moduleId),
1969
- eq(c.action, sc.action),
1970
- eq(c.scopeKind, sc.scopeKind),
1971
- eq(c.scopeKey, sc.scopeKey),
1972
- ),
1973
- ),
1974
- ),
1975
- )!;
1976
-
1977
- statements.push(
1978
- yield* assertion(
1979
- sql`exists(select 1 from ${mapping.rateScope.table} where ${condition})`,
1980
- marker,
1981
- ),
1982
- yield* statement(database.delete(mapping.rateScope.table).where(condition)),
1983
- );
1984
- }
1359
+ const applied = yield* assertion(protectedMutation.appliedCondition, marker);
1985
1360
 
1986
1361
  return {
1987
- receipt: prepare(
1988
- {
1989
- removed: selectedAttempts.length + selectedCharges.length + selectedScopes.length,
1990
- hasMore,
1991
- },
1992
- journal,
1993
- ),
1994
- statements,
1362
+ receipt: prepare("changed", journal),
1363
+ statements: [...protectedMutation.statements, applied],
1364
+ postconditions: [applied],
1995
1365
  retryable: (cause: unknown) => isGuardFailure(cause, marker),
1996
1366
  };
1997
1367
  });
@@ -2001,13 +1371,10 @@ export const makeD1PasswordPersistenceServices = Effect.fnUntraced(function* <
2001
1371
  I extends AnySQLiteTable,
2002
1372
  C extends AnySQLiteTable,
2003
1373
  AC extends AnySQLiteTable,
2004
- A extends AnySQLiteTable,
2005
- RS extends AnySQLiteTable,
2006
- CE extends AnySQLiteTable,
2007
1374
  M extends AnySQLiteTable,
2008
1375
  NativeId,
2009
1376
  >(
2010
- mapping: D1PasswordPersistenceMapping<S, I, C, AC, A, RS, CE, M, NativeId>,
1377
+ mapping: D1PasswordPersistenceMapping<S, I, C, AC, M, NativeId>,
2011
1378
  proofMapping?: D1ProofPersistenceMapping<
2012
1379
  any,
2013
1380
  any,
@@ -2033,9 +1400,14 @@ export const makeD1PasswordPersistenceServices = Effect.fnUntraced(function* <
2033
1400
  const hooks = yield* LifecycleHooks;
2034
1401
  const plans = makePasswordPlans(mapping as unknown as Mapping, proofMapping);
2035
1402
 
1403
+ const standalone = Effect.gen(function* () {
1404
+ if (yield* hasCommitScope) return yield* unavailable();
1405
+ yield* requireStandalone(unavailable, database.$client.transactionService);
1406
+ });
1407
+
2036
1408
  const run = <Out, Err, Env>(plan: Effect.Effect<Planned<Out>, Err, Env>) =>
2037
1409
  Effect.gen(function* () {
2038
- if (yield* hasCommitScope) return yield* unavailable();
1410
+ yield* standalone;
2039
1411
 
2040
1412
  return yield* executeStandalone(plan, 2);
2041
1413
  }).pipe(
@@ -2044,10 +1416,13 @@ export const makeD1PasswordPersistenceServices = Effect.fnUntraced(function* <
2044
1416
  );
2045
1417
 
2046
1418
  const service: PasswordPersistence["Service"] = {
2047
- admitAttempt: (input, prepare) =>
2048
- run(plans.admitAttempt(input, prepare)).pipe(translateFailure),
2049
- settleAttempt: (input, prepare) =>
2050
- run(plans.settleAttempt(input, prepare)).pipe(translateFailure),
1419
+ findCredential: (input) =>
1420
+ standalone.pipe(
1421
+ Effect.andThen(plans.findCredential(input)),
1422
+ Effect.provideService(CurrentD1PlanningDatabase, database),
1423
+ translateFailure,
1424
+ ),
1425
+ rehashIfCurrent: (input) => run(plans.rehashIfCurrent(input)).pipe(translateFailure),
2051
1426
  readForSubject: (input) =>
2052
1427
  plans
2053
1428
  .readForSubject(input)
@@ -2077,8 +1452,6 @@ export const makeD1PasswordPersistenceServices = Effect.fnUntraced(function* <
2077
1452
  ),
2078
1453
  resetWithProof: (input, prepare) =>
2079
1454
  run(plans.resetWithProof(input, prepare)).pipe(translateFailure),
2080
- cleanupAttempts: (input, prepare) =>
2081
- run(plans.cleanupAttempts(input, prepare)).pipe(translateFailure),
2082
1455
  };
2083
1456
 
2084
1457
  return { passwordPersistence: service };
@@ -2096,9 +1469,6 @@ export function coordinateD1PasswordPersistence<
2096
1469
  I extends AnySQLiteTable,
2097
1470
  C extends AnySQLiteTable,
2098
1471
  AC extends AnySQLiteTable,
2099
- At extends AnySQLiteTable,
2100
- RS extends AnySQLiteTable,
2101
- CE extends AnySQLiteTable,
2102
1472
  M extends AnySQLiteTable,
2103
1473
  NativeId,
2104
1474
  A,
@@ -2109,7 +1479,7 @@ export function coordinateD1PasswordPersistence<
2109
1479
  >(
2110
1480
  acquire: Effect.Effect<Database, DatabaseError, DatabaseRequirements>,
2111
1481
  options: {
2112
- readonly mapping: D1PasswordPersistenceMapping<S, I, C, AC, At, RS, CE, M, NativeId>;
1482
+ readonly mapping: D1PasswordPersistenceMapping<S, I, C, AC, M, NativeId>;
2113
1483
  readonly proofMapping?:
2114
1484
  | D1ProofPersistenceMapping<any, any, any, any, any, any, any, any, any, any, any, any>
2115
1485
  | undefined;
@@ -2173,10 +1543,17 @@ export function coordinateD1PasswordPersistence<
2173
1543
  );
2174
1544
 
2175
1545
  const service: PasswordPersistence["Service"] = {
2176
- admitAttempt: (input, prepare) =>
2177
- owner.run(run(plans.admitAttempt(input, prepare)).pipe(translateFailure)),
2178
- settleAttempt: (input, prepare) =>
2179
- owner.run(run(plans.settleAttempt(input, prepare)).pipe(translateFailure)),
1546
+ findCredential: (input) =>
1547
+ owner.run(
1548
+ plans
1549
+ .findCredential(input)
1550
+ .pipe(
1551
+ Effect.provideService(CurrentD1PlanningDatabase, database),
1552
+ translateFailure,
1553
+ ),
1554
+ ),
1555
+ rehashIfCurrent: (input) =>
1556
+ owner.run(run(plans.rehashIfCurrent(input)).pipe(translateFailure)),
2180
1557
  readForSubject: (input) =>
2181
1558
  owner.run(
2182
1559
  plans
@@ -2213,8 +1590,6 @@ export function coordinateD1PasswordPersistence<
2213
1590
  ),
2214
1591
  resetWithProof: (input, prepare) =>
2215
1592
  owner.run(run(plans.resetWithProof(input, prepare)).pipe(translateFailure)),
2216
- cleanupAttempts: (input, prepare) =>
2217
- owner.run(run(plans.cleanupAttempts(input, prepare)).pipe(translateFailure)),
2218
1593
  };
2219
1594
 
2220
1595
  const provided = Context.make(PasswordPersistence, service).pipe(
@@ -2229,16 +1604,19 @@ export function coordinateD1PasswordPersistence<
2229
1604
  if (status._tag === "Success" || status.failure._tag !== "CommitPending")
2230
1605
  return yield* unavailable();
2231
1606
  }
2232
- yield* database.$client.batch(statements).pipe(
2233
- Effect.catchCause((cause) =>
2234
- Effect.failCause(
2235
- Cause.map(cause, (error) =>
2236
- everyFailureMatches(cause, mutation?.retryable) ? unavailable() : error,
1607
+ // Re-assert the password's final state after application statements.
1608
+ yield* database.$client
1609
+ .batch([...statements, ...(mutation?.postconditions ?? [])])
1610
+ .pipe(
1611
+ Effect.catchCause((cause) =>
1612
+ Effect.failCause(
1613
+ Cause.map(cause, (error) =>
1614
+ everyFailureMatches(cause, mutation?.retryable) ? unavailable() : error,
1615
+ ),
2237
1616
  ),
2238
1617
  ),
2239
- ),
2240
- translateFailure,
2241
- );
1618
+ translateFailure,
1619
+ );
2242
1620
 
2243
1621
  return value;
2244
1622
  }),