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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/dist/D1.d.mts +4 -4
  2. package/dist/Libsql.d.mts +19 -19
  3. package/dist/Mysql2.d.mts +8 -8
  4. package/dist/Pglite.d.mts +8 -8
  5. package/dist/Postgres.d.mts +6 -6
  6. package/dist/SqliteBun.d.mts +21 -21
  7. package/dist/SqliteDo.d.mts +19 -19
  8. package/dist/SqliteNode.d.mts +17 -17
  9. package/dist/SqliteWasm.d.mts +19 -19
  10. package/dist/drizzle/d1-oauth.d.mts +2 -2
  11. package/dist/drizzle/d1-password-prepared.d.mts +3 -3
  12. package/dist/drizzle/d1-password-prepared.mjs.map +1 -1
  13. package/dist/drizzle/d1-passwords.d.mts +8 -9
  14. package/dist/drizzle/d1-passwords.mjs +103 -173
  15. package/dist/drizzle/d1-passwords.mjs.map +1 -1
  16. package/dist/drizzle/d1-proofs.d.mts +1 -1
  17. package/dist/drizzle/d1-sessions.d.mts +1 -1
  18. package/dist/drizzle/mysql-password-prepared.d.mts +5 -5
  19. package/dist/drizzle/mysql-password-prepared.mjs.map +1 -1
  20. package/dist/drizzle/mysql-passwords.d.mts +9 -9
  21. package/dist/drizzle/mysql-passwords.mjs.map +1 -1
  22. package/dist/drizzle/native-sql-table.mjs +110 -0
  23. package/dist/drizzle/native-sql-table.mjs.map +1 -0
  24. package/dist/drizzle/oauth-connected-target.d.mts +5 -5
  25. package/dist/drizzle/oauth-target.d.mts +1 -1
  26. package/dist/drizzle/password-model.d.mts +5 -7
  27. package/dist/drizzle/password-prepared-model.d.mts +5 -5
  28. package/dist/drizzle/password-prepared-model.mjs.map +1 -1
  29. package/dist/drizzle/password-sql.mjs +2 -1
  30. package/dist/drizzle/password-sql.mjs.map +1 -1
  31. package/dist/drizzle/password-target.mjs +13 -2
  32. package/dist/drizzle/password-target.mjs.map +1 -1
  33. package/dist/drizzle/pg-password-prepared.d.mts +5 -5
  34. package/dist/drizzle/pg-password-prepared.mjs.map +1 -1
  35. package/dist/drizzle/pg-passwords.d.mts +9 -9
  36. package/dist/drizzle/pg-passwords.mjs.map +1 -1
  37. package/dist/drizzle/phone-target.d.mts +1 -1
  38. package/dist/drizzle/session-sql.mjs +2 -1
  39. package/dist/drizzle/session-sql.mjs.map +1 -1
  40. package/dist/drizzle/sqlite-password-prepared.mjs.map +1 -1
  41. package/dist/drizzle/sqlite-passwords.mjs.map +1 -1
  42. package/dist/drizzle/storage-validation.mjs +0 -13
  43. package/dist/drizzle/storage-validation.mjs.map +1 -1
  44. package/dist/drizzle/totp-target.d.mts +1 -1
  45. package/dist/index.d.mts +2 -2
  46. package/dist/internal/drizzle-postgres.mjs +2 -0
  47. package/dist/internal/drizzle-postgres.mjs.map +1 -1
  48. package/dist/internal/drizzle-sqlite.mjs +2 -0
  49. package/dist/internal/drizzle-sqlite.mjs.map +1 -1
  50. package/package.json +1 -1
  51. package/src/drizzle/d1-password-prepared.ts +2 -20
  52. package/src/drizzle/d1-passwords.ts +232 -519
  53. package/src/drizzle/mysql-password-prepared.ts +4 -12
  54. package/src/drizzle/mysql-passwords.ts +5 -15
  55. package/src/drizzle/native-sql-table.ts +229 -0
  56. package/src/drizzle/password-model.ts +0 -20
  57. package/src/drizzle/password-prepared-model.ts +0 -12
  58. package/src/drizzle/password-sql.ts +3 -0
  59. package/src/drizzle/password-target.ts +41 -4
  60. package/src/drizzle/pg-password-prepared.ts +4 -12
  61. package/src/drizzle/pg-passwords.ts +5 -15
  62. package/src/drizzle/session-sql.ts +2 -1
  63. package/src/drizzle/sqlite-password-prepared.ts +4 -48
  64. package/src/drizzle/sqlite-passwords.ts +7 -21
  65. package/src/drizzle/storage-validation.ts +0 -8
  66. package/src/index.ts +0 -4
  67. package/src/internal/drizzle-postgres.ts +2 -0
  68. package/src/internal/drizzle-sqlite.ts +2 -0
@@ -7,8 +7,8 @@ import { D1BatchStatements } from "./D1BatchStatements.mjs";
7
7
  import { CurrentD1PlanningDatabase, makeD1Owner } from "./d1-planning.mjs";
8
8
  import { nativeDatabase } from "./native-database.mjs";
9
9
  import { compileD1ProofCompletionPlan } from "./d1-proofs.mjs";
10
- import { NativeDatabase } from "@yielded/auth-persistence/Adapter";
11
- import { and, count, eq, gte, inArray, lte, notExists, sql } from "drizzle-orm";
10
+ import { NativeDatabase, requireStandalone, samePasswordCredentialSnapshot } from "@yielded/auth-persistence/Adapter";
11
+ import { and, eq, inArray, lte, sql } from "drizzle-orm";
12
12
  import { Cause, Context, DateTime, Effect, Option, Predicate, Redacted, Schema } from "effect";
13
13
  import { CurrentCommitJournal, HookConfigurationError, LifecycleHooks, coordinateCommit, hasCommitScope } from "@yielded/auth/Hooks";
14
14
  import { AuthenticationEvidence, AuthenticationRequirement, snapshotAuthenticationEvidence } from "@yielded/auth/Sessions";
@@ -16,7 +16,6 @@ import { reportPersistenceFailure } from "@yielded/auth/Persistence";
16
16
  import { ProofUnavailable } from "@yielded/auth/Proofs";
17
17
  import { PasswordPersistence, PasswordUnavailable, snapshotPasswordCredential, snapshotPasswordRequirement, snapshotPasswordRevision } from "@yielded/auth/Password";
18
18
  //#region src/drizzle/d1-passwords.ts
19
- const mapAdapter = (effect) => effect;
20
19
  const unavailable = () => PasswordUnavailable.make({});
21
20
  const everyFailureMatches = (cause, classify) => classify !== void 0 && cause.reasons.length > 0 && cause.reasons.every((reason) => Cause.isFailReason(reason) && classify(Cause.fail(reason.error)));
22
21
  const validConstraints = (mapping) => Object.entries(requiredPasswordConstraints).every(([key, value]) => mapping.constraints[key] === value);
@@ -117,21 +116,6 @@ const attemptColumns = (mapping) => ({
117
116
  retentionUntil: column(mapping.attempt.table, mapping.attempt.retentionUntil),
118
117
  state: column(mapping.attempt.table, mapping.attempt.state)
119
118
  });
120
- const scopeColumns = (mapping) => ({
121
- moduleId: column(mapping.rateScope.table, mapping.rateScope.moduleId),
122
- action: column(mapping.rateScope.table, mapping.rateScope.action),
123
- scopeKind: column(mapping.rateScope.table, mapping.rateScope.scopeKind),
124
- scopeKey: column(mapping.rateScope.table, mapping.rateScope.scopeKey)
125
- });
126
- const chargeColumns = (mapping) => ({
127
- moduleId: column(mapping.charge.table, mapping.charge.moduleId),
128
- action: column(mapping.charge.table, mapping.charge.action),
129
- scopeKind: column(mapping.charge.table, mapping.charge.scopeKind),
130
- scopeKey: column(mapping.charge.table, mapping.charge.scopeKey),
131
- attemptId: column(mapping.charge.table, mapping.charge.attemptId),
132
- occurredAt: column(mapping.charge.table, mapping.charge.occurredAt),
133
- retentionUntil: column(mapping.charge.table, mapping.charge.retentionUntil)
134
- });
135
119
  const commandColumns = (mapping) => ({
136
120
  moduleId: column(mapping.command.table, mapping.command.moduleId),
137
121
  commandId: column(mapping.command.table, mapping.command.commandId),
@@ -180,45 +164,6 @@ const resolveCredential = Effect.fn("DrizzleD1Password.resolveCredential")(funct
180
164
  }).pipe(Effect.flatMap(snapshotPasswordCredential))
181
165
  };
182
166
  });
183
- const scopeEntries = (keys, policy) => {
184
- const entries = [{
185
- kind: "action",
186
- key: keys.action,
187
- ...policy.action
188
- }, {
189
- kind: "identifier",
190
- key: keys.identifier,
191
- ...policy.identifier
192
- }];
193
- if (keys.subject !== void 0) entries.push({
194
- kind: "subject",
195
- key: keys.subject,
196
- ...policy.subject
197
- });
198
- return entries;
199
- };
200
- const livePendingAttempts = (mapping, moduleId, action, entry, identifier, nativeSubjectId, activeAt) => {
201
- const a = attemptColumns(mapping);
202
- return and(entry.kind === "action" ? and(eq(a.moduleId, moduleId), eq(a.action, action)) : entry.kind === "identifier" ? and(eq(a.moduleId, moduleId), eq(a.action, action), eq(a.identifierNamespace, identifier.namespace), eq(a.identifierValue, identifier.value)) : and(eq(a.moduleId, moduleId), eq(a.action, action), eq(a.subjectId, nativeSubjectId)), eq(a.state, "pending"), sql`${a.retentionUntil} > ${activeAt}`);
203
- };
204
- const scopeAdmissionCondition = (mapping, moduleId, action, entry, policy, identifier, nativeSubjectId) => {
205
- const c = chargeColumns(mapping);
206
- const pending = livePendingAttempts(mapping, moduleId, action, entry, identifier, nativeSubjectId, mapping.d1.engineNow);
207
- 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}`;
208
- };
209
- const readScopeAdmission = Effect.fn("DrizzleD1Password.readScopeAdmission")(function* (mapping, moduleId, action, entries, policy, identifier, nativeSubjectId, now) {
210
- const database = yield* CurrentD1PlanningDatabase;
211
- const c = chargeColumns(mapping);
212
- const admitted = [];
213
- for (let index = 0; index < entries.length; index++) {
214
- const entry = entries[index];
215
- const chargeRows = yield* mapAdapter(database.select({ total: count() }).from(mapping.charge.table).where(and(eq(c.moduleId, moduleId), eq(c.action, action), eq(c.scopeKind, entry.kind), eq(c.scopeKey, entry.key), gte(c.occurredAt, mapping.encodeInstant(now - entry.windowMillis)))));
216
- const pendingRows = yield* mapAdapter(database.select({ total: count() }).from(mapping.attempt.table).where(livePendingAttempts(mapping, moduleId, action, entry, identifier, nativeSubjectId, mapping.encodeInstant(now))));
217
- if (Number(chargeRows[0]?.total ?? 0) < entry.limit && Number(pendingRows[0]?.total ?? 0) < policy.maximumPending) admitted.push(entry);
218
- else if (index === 0) break;
219
- }
220
- return admitted;
221
- });
222
167
  const evidenceProofsJson = Schema.encodeSync(Schema.fromJsonString(AuthenticationEvidence.fields.proofs));
223
168
  const alternativesJson = Schema.encodeSync(Schema.fromJsonString(AuthenticationRequirement.fields.alternatives));
224
169
  const assuranceCondition = (mapping, evidence, requirement) => {
@@ -233,7 +178,6 @@ const assuranceCondition = (mapping, evidence, requirement) => {
233
178
  )`;
234
179
  };
235
180
  const sameIdentifier = (left, right) => left.namespace === right.namespace && left.value === right.value;
236
- const sameSnapshot = (left, right) => left.moduleId === right.moduleId && left.revision.subjectId === right.revision.subjectId && left.revision.securityRevision === right.revision.securityRevision && left.credentialId === right.credentialId && left.credentialRevision === right.credentialRevision && left.verifierVersion === right.verifierVersion && left.normalization === right.normalization && left.identifierVerifiedAtMillis === right.identifierVerifiedAtMillis && left.revision.credentials.length === right.revision.credentials.length && left.revision.credentials.every((entry) => right.revision.credentials.some((other) => other.credentialId === entry.credentialId && other.revision === entry.revision)) && left.identifierBindingRevision === right.identifierBindingRevision && sameIdentifier(left.identifier, right.identifier) && Redacted.value(left.verifier) === Redacted.value(right.verifier);
237
181
  const evidenceSatisfiedAt = (evidence, requirement, now) => {
238
182
  if (evidence.proofs.length > 64 || evidence.revision.credentials.length > 64) return false;
239
183
  const revisions = /* @__PURE__ */ new Map();
@@ -325,7 +269,7 @@ const readAuthority = Effect.fn("DrizzleD1Password.readAuthority")(function* (ma
325
269
  identifier,
326
270
  credential
327
271
  });
328
- if (!sameSnapshot(snapshot, input.credential)) return void 0;
272
+ if (!samePasswordCredentialSnapshot(snapshot, input.credential)) return void 0;
329
273
  identifierCondition = identifierCurrentCondition(mapping, input, nativeSubjectId, identifier[mapping.identifier.verifiedAt]);
330
274
  const ic = identifierColumns(mapping);
331
275
  identifierCondition = and(identifierCondition, sql`exists(select 1 from ${mapping.identifier.table} where ${ic.namespace} = ${sql.param(input.credential.identifier.namespace, ic.namespace)} and ${ic.value} = ${sql.param(input.credential.identifier.value, ic.value)} and ${ic.subjectId} = ${sql.param(nativeSubjectId, ic.subjectId)} and ${ic.bindingRevision} = ${sql.param(input.credential.identifierBindingRevision, ic.bindingRevision)})`);
@@ -366,7 +310,7 @@ const admittedSnapshotCondition = (mapping, resolved, moduleId) => {
366
310
  return and(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)})`, mapping.identifier.d1CurrentCondition({
367
311
  identifier: snapshot.identifier,
368
312
  nativeSubjectId: resolved.nativeSubjectId
369
- }), 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)})`, 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)})`, ...snapshot.revision.credentials.map((item) => 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 === void 0 ? sql`` : sql` and ${ac.status} = ${sql.param(mapping.authorityCredential.d1ActiveStatusValue, ac.status)}`})`));
313
+ }), 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)} and ${Predicate.isNullish(resolved.identifier[mapping.identifier.verifiedAt]) ? sql`${i.verifiedAt} is null` : sql`${i.verifiedAt} = ${sql.param(resolved.identifier[mapping.identifier.verifiedAt], i.verifiedAt)}`})`, 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)})`, ...snapshot.revision.credentials.map((item) => 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 === void 0 ? sql`` : sql` and ${ac.status} = ${sql.param(mapping.authorityCredential.d1ActiveStatusValue, ac.status)}`})`));
370
314
  };
371
315
  const mutationStatements = Effect.fn(function* (mapping, input, nativeSubjectId, revisions, now) {
372
316
  const database = yield* CurrentD1PlanningDatabase;
@@ -440,6 +384,7 @@ const compileAddMutation = Effect.fn("DrizzleD1Password.compileAddMutation")(fun
440
384
  yield* assertion(appliedCondition, marker)
441
385
  ],
442
386
  appliedCondition,
387
+ marker,
443
388
  retryable: (cause) => isGuardFailure(cause, marker)
444
389
  };
445
390
  });
@@ -449,52 +394,31 @@ const makePasswordPlans = (mapping, proofMapping) => {
449
394
  return yield* plan;
450
395
  });
451
396
  return {
452
- admitAttempt: (input, prepare) => Effect.gen(function* () {
453
- const database = yield* CurrentD1PlanningDatabase;
397
+ prepareAttempt: (uncaptured) => Effect.gen(function* () {
398
+ if (!validConstraints(mapping)) return yield* unavailable();
399
+ const input = Object.freeze({
400
+ ...uncaptured,
401
+ identifier: Object.freeze({ ...uncaptured.identifier })
402
+ });
403
+ const resolved = yield* resolveCredential(mapping, input.moduleId, input.identifier, input.subjectId);
404
+ const snapshot = resolved?.snapshot;
454
405
  const attemptId = yield* allocate(mapping.allocateAttemptId, mapping.allocateAttemptIdSync);
455
- return yield* run(Effect.gen(function* () {
456
- const journal = yield* CurrentCommitJournal;
457
- const now = yield* readEngineNowMillis(mapping);
458
- const resolved = yield* resolveCredential(mapping, input.moduleId, input.identifier, input.subjectId);
459
- const keys = mapping.scopeKeys({
460
- moduleId: input.moduleId,
461
- action: input.action,
462
- identifier: input.identifier,
463
- ...resolved?.snapshot === void 0 ? {} : { subjectId: resolved.snapshot.revision.subjectId }
464
- });
465
- const scopes = scopeEntries(keys, input.policy);
466
- const admitted = yield* readScopeAdmission(mapping, input.moduleId, input.action, scopes, input.policy, input.identifier, resolved?.nativeSubjectId, now);
467
- const allAdmitted = admitted.length === scopes.length;
468
- const considered = admitted.length === 0 ? scopes.slice(0, 1) : scopes;
469
- const receipt = prepare(allAdmitted ? {
470
- _tag: "Admitted",
471
- attemptId,
472
- ...resolved?.snapshot === void 0 ? {} : { credential: resolved.snapshot }
473
- } : { _tag: "Denied" }, journal);
474
- const marker = `effect-auth-password-guard:admit:${attemptId}`;
475
- const statements = [];
476
- for (const entry of considered) statements.push(yield* statement(database.insert(mapping.rateScope.table).values(mapping.rateScope.encodeInsert({
477
- moduleId: input.moduleId,
478
- action: input.action,
479
- scopeKind: entry.kind,
480
- scopeKey: entry.key
481
- })).onConflictDoNothing()));
482
- const conditions = considered.map((entry) => {
483
- const condition = scopeAdmissionCondition(mapping, input.moduleId, input.action, entry, input.policy, input.identifier, resolved?.nativeSubjectId);
484
- return admitted.includes(entry) ? condition : sql`not (${condition})`;
485
- });
486
- statements.push(yield* assertion(and(...conditions, ...allAdmitted ? [admittedSnapshotCondition(mapping, resolved, input.moduleId)] : []), marker));
487
- for (const entry of admitted) statements.push(yield* statement(database.insert(mapping.charge.table).values(driverValues(mapping.charge.encodeInsert({
488
- moduleId: input.moduleId,
489
- action: input.action,
490
- scopeKind: entry.kind,
491
- scopeKey: entry.key,
492
- attemptId,
493
- occurredAtMillis: now,
494
- retentionUntilMillis: now + entry.windowMillis
495
- }), [[mapping.charge.occurredAt, mapping.d1.engineNow], [mapping.charge.retentionUntil, mapping.d1.engineInstantPlus(entry.windowMillis)]]))));
496
- if (allAdmitted) {
497
- const snapshot = resolved?.snapshot;
406
+ let used = false;
407
+ return Object.freeze({
408
+ ...snapshot === void 0 ? {} : { credential: snapshot },
409
+ claim: Effect.suspend(() => {
410
+ if (used) return Effect.fail(unavailable());
411
+ used = true;
412
+ return Effect.void;
413
+ }),
414
+ admit: (prepare) => run(Effect.gen(function* () {
415
+ const database = yield* CurrentD1PlanningDatabase;
416
+ const journal = yield* CurrentCommitJournal;
417
+ const now = yield* readEngineNowMillis(mapping);
418
+ const marker = `effect-auth-password-guard:admit:${attemptId}`;
419
+ const a = attemptColumns(mapping);
420
+ const key = and(eq(a.moduleId, input.moduleId), eq(a.attemptId, attemptId));
421
+ const current = admittedSnapshotCondition(mapping, resolved, input.moduleId);
498
422
  const record = {
499
423
  moduleId: input.moduleId,
500
424
  action: input.action,
@@ -509,24 +433,33 @@ const makePasswordPlans = (mapping, proofMapping) => {
509
433
  identifierBindingRevision: snapshot.identifierBindingRevision
510
434
  },
511
435
  admittedAtMillis: now,
512
- deadlineMillis: now + input.policy.attemptLifetimeMillis,
513
- retentionUntilMillis: Math.max(now + input.policy.attemptLifetimeMillis, ...scopes.map((entry) => now + entry.windowMillis))
436
+ deadlineMillis: now + input.attemptLifetimeMillis,
437
+ retentionUntilMillis: now + input.attemptLifetimeMillis
514
438
  };
515
- statements.push(yield* statement(database.insert(mapping.attempt.table).values(driverValues(mapping.attempt.encodeInsert(record, {
516
- ...resolved?.nativeSubjectId === void 0 ? {} : { nativeSubjectId: resolved.nativeSubjectId },
517
- state: "pending"
518
- }), [
519
- [mapping.attempt.admittedAt, mapping.d1.engineNow],
520
- [mapping.attempt.deadline, mapping.d1.engineInstantPlus(input.policy.attemptLifetimeMillis)],
521
- [mapping.attempt.retentionUntil, mapping.d1.engineInstantPlus(Math.max(input.policy.attemptLifetimeMillis, ...scopes.map((entry) => entry.windowMillis)))]
522
- ]))));
523
- }
524
- return {
525
- receipt,
526
- statements,
527
- retryable: (cause) => isGuardFailure(cause, marker)
528
- };
529
- }));
439
+ const receipt = prepare({
440
+ _tag: "Admitted",
441
+ attemptId,
442
+ ...snapshot === void 0 ? {} : { credential: snapshot }
443
+ }, journal);
444
+ const admitted = and(current, sql`exists(select 1 from ${mapping.attempt.table} where ${key} and ${a.state} = ${"pending"} and ${a.deadline} > ${mapping.d1.engineNow})`);
445
+ return {
446
+ receipt,
447
+ statements: [
448
+ yield* assertion(and(current, sql`not exists(select 1 from ${mapping.attempt.table} where ${key})`), marker),
449
+ yield* statement(database.insert(mapping.attempt.table).values(driverValues(mapping.attempt.encodeInsert(record, {
450
+ ...resolved?.nativeSubjectId === void 0 ? {} : { nativeSubjectId: resolved.nativeSubjectId },
451
+ state: "pending"
452
+ }), [
453
+ [mapping.attempt.admittedAt, mapping.d1.engineNow],
454
+ [mapping.attempt.deadline, mapping.d1.engineInstantPlus(input.attemptLifetimeMillis)],
455
+ [mapping.attempt.retentionUntil, mapping.d1.engineInstantPlus(input.attemptLifetimeMillis)]
456
+ ]))),
457
+ yield* assertion(and(sql`changes() = 1`, admitted), marker)
458
+ ],
459
+ postconditions: [yield* assertion(admitted, marker)]
460
+ };
461
+ }))
462
+ });
530
463
  }),
531
464
  settleAttempt: (uncaptured, prepare) => Effect.gen(function* () {
532
465
  const database = yield* CurrentD1PlanningDatabase;
@@ -545,7 +478,7 @@ const makePasswordPlans = (mapping, proofMapping) => {
545
478
  let verified = false;
546
479
  if (row !== void 0 && row[mapping.attempt.state] === "pending" && input.outcome === "verified" && input.captured !== void 0 && deadline !== void 0 && deadline > engineNow) {
547
480
  resolved = yield* resolveCredential(mapping, input.moduleId, input.captured.identifier, input.captured.revision.subjectId);
548
- verified = input.captured.moduleId === input.moduleId && resolved?.snapshot !== void 0 && sameSnapshot(resolved.snapshot, input.captured) && mapping.subjectId.equals(row[mapping.attempt.subjectId], resolved.nativeSubjectId) && row[mapping.attempt.identifierNamespace] === input.captured.identifier.namespace && row[mapping.attempt.identifierValue] === input.captured.identifier.value && row[mapping.attempt.credentialId] === input.captured.credentialId && row[mapping.attempt.securityRevision] === input.captured.revision.securityRevision && row[mapping.attempt.credentialRevision] === input.captured.credentialRevision && row[mapping.attempt.verifierVersion] === input.captured.verifierVersion && row[mapping.attempt.identifierBindingRevision] === input.captured.identifierBindingRevision;
481
+ verified = input.captured.moduleId === input.moduleId && resolved?.snapshot !== void 0 && samePasswordCredentialSnapshot(resolved.snapshot, input.captured) && mapping.subjectId.equals(row[mapping.attempt.subjectId], resolved.nativeSubjectId) && row[mapping.attempt.identifierNamespace] === input.captured.identifier.namespace && row[mapping.attempt.identifierValue] === input.captured.identifier.value && row[mapping.attempt.credentialId] === input.captured.credentialId && row[mapping.attempt.securityRevision] === input.captured.revision.securityRevision && row[mapping.attempt.credentialRevision] === input.captured.credentialRevision && row[mapping.attempt.verifierVersion] === input.captured.verifierVersion && row[mapping.attempt.identifierBindingRevision] === input.captured.identifierBindingRevision;
549
482
  }
550
483
  const decision = verified ? "verified" : "rejected";
551
484
  const receipt = prepare(decision, journal);
@@ -555,26 +488,34 @@ const makePasswordPlans = (mapping, proofMapping) => {
555
488
  };
556
489
  const marker = `effect-auth-password-guard:settle:${input.attemptId}`;
557
490
  const base = and(eq(a.moduleId, input.moduleId), eq(a.attemptId, input.attemptId), eq(a.state, "pending"));
558
- const condition = verified ? and(base, sql`${a.deadline} > ${mapping.d1.engineNow}`) : base;
491
+ const attemptSnapshot = verified && resolved?.snapshot !== void 0 ? and(eq(a.action, row[mapping.attempt.action]), eq(a.subjectId, resolved.nativeSubjectId), eq(a.identifierNamespace, input.captured.identifier.namespace), eq(a.identifierValue, input.captured.identifier.value), eq(a.credentialId, input.captured.credentialId), eq(a.securityRevision, input.captured.revision.securityRevision), eq(a.credentialRevision, input.captured.credentialRevision), eq(a.verifierVersion, input.captured.verifierVersion), eq(a.identifierBindingRevision, input.captured.identifierBindingRevision), eq(a.deadline, row[mapping.attempt.deadline])) : sql`true`;
492
+ const condition = verified ? and(base, attemptSnapshot, sql`${a.deadline} > ${mapping.d1.engineNow}`) : base;
559
493
  const statements = [yield* assertion(sql`exists(select 1 from ${mapping.attempt.table} where ${condition})`, marker), yield* statement(database.update(mapping.attempt.table).set(updateValues([[mapping.attempt.state, decision]])).where(condition))];
494
+ let finalAuthority = sql`true`;
560
495
  if (verified && resolved?.snapshot !== void 0) {
561
496
  const captured = input.captured;
562
- const s = subjectColumns(mapping);
563
- const i = identifierColumns(mapping);
564
497
  const c = credentialColumns(mapping);
565
- const authority = and(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)})`, mapping.identifier.d1CurrentCondition({
566
- identifier: input.captured.identifier,
567
- nativeSubjectId: resolved.nativeSubjectId
568
- }), 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)})`, 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)})`);
569
- statements.splice(1, 0, yield* assertion(authority, marker));
498
+ finalAuthority = admittedSnapshotCondition(mapping, resolved, input.moduleId);
499
+ statements.splice(1, 0, yield* assertion(finalAuthority, marker));
570
500
  if (input.rehash !== void 0 && input.rehash.expectedVersion === captured.verifierVersion && Redacted.value(input.rehash.expectedVerifier) === Redacted.value(captured.verifier)) {
571
501
  const nextVersion = yield* allocate(mapping.allocateRevision, mapping.allocateRevisionSync);
572
- statements.push(yield* statement(database.update(mapping.credential.table).set(mapping.credential.encodeVerifier(input.rehash.nextVerifier, nextVersion)).where(and(eq(c.moduleId, input.moduleId), eq(c.subjectId, resolved.nativeSubjectId), eq(c.credentialId, captured.credentialId), eq(c.credentialRevision, captured.credentialRevision), eq(c.verifierVersion, input.rehash.expectedVersion), eq(c.verifier, Redacted.value(input.rehash.expectedVerifier))))));
502
+ statements.push(yield* assertion(sql`changes() = 1`, marker), yield* statement(database.update(mapping.credential.table).set(mapping.credential.encodeVerifier(input.rehash.nextVerifier, nextVersion)).where(and(eq(c.moduleId, input.moduleId), eq(c.subjectId, resolved.nativeSubjectId), eq(c.credentialId, captured.credentialId), eq(c.credentialRevision, captured.credentialRevision), eq(c.verifierVersion, input.rehash.expectedVersion), eq(c.verifier, Redacted.value(input.rehash.expectedVerifier))))));
503
+ finalAuthority = admittedSnapshotCondition(mapping, {
504
+ ...resolved,
505
+ snapshot: {
506
+ ...resolved.snapshot,
507
+ verifierVersion: nextVersion,
508
+ verifier: input.rehash.nextVerifier
509
+ }
510
+ }, input.moduleId);
573
511
  }
574
512
  }
513
+ const settled = and(finalAuthority, sql`exists(select 1 from ${mapping.attempt.table} where ${a.moduleId} = ${sql.param(input.moduleId, a.moduleId)} and ${a.attemptId} = ${sql.param(input.attemptId, a.attemptId)} and ${a.state} = ${sql.param(decision, a.state)} and ${attemptSnapshot}${verified ? sql` and ${a.deadline} > ${mapping.d1.engineNow}` : sql``})`);
514
+ statements.push(yield* assertion(and(sql`changes() = 1`, settled), marker));
575
515
  return {
576
516
  receipt,
577
517
  statements,
518
+ postconditions: [yield* assertion(settled, marker)],
578
519
  retryable: (cause) => isGuardFailure(cause, marker)
579
520
  };
580
521
  }));
@@ -617,10 +558,15 @@ const makePasswordPlans = (mapping, proofMapping) => {
617
558
  verifierVersion,
618
559
  nextSecurityRevision
619
560
  });
561
+ if (compiled === void 0) return {
562
+ receipt: prepare("rejected", journal),
563
+ statements: []
564
+ };
620
565
  return {
621
- receipt: prepare(compiled === void 0 ? "rejected" : "changed", journal),
622
- statements: compiled?.statements ?? [],
623
- ...compiled?.retryable === void 0 ? {} : { retryable: compiled.retryable }
566
+ receipt: prepare("changed", journal),
567
+ statements: compiled.statements,
568
+ postconditions: [yield* assertion(compiled.appliedCondition, compiled.marker)],
569
+ retryable: compiled.retryable
624
570
  };
625
571
  }));
626
572
  }),
@@ -695,12 +641,15 @@ const replacementPlan = Effect.fn("Drizzle.replacementPlan")(function* (mapping,
695
641
  return {
696
642
  receipt: passwordReceipt ?? prepare("rejected", journal),
697
643
  statements: compiled.statements,
644
+ ...compiled.postconditions === void 0 ? {} : { postconditions: compiled.postconditions },
698
645
  retryable: (cause) => isGuardFailure(cause, marker) || isGuardFailure(cause, `effect-auth-proof-guard:complete:${input.completion.input.continuationId}`) || isGuardFailure(cause, `effect-auth-proof-guard:protected:${input.completion.input.continuationId}`)
699
646
  };
700
647
  }
648
+ const applied = yield* assertion(protectedMutation.appliedCondition, marker);
701
649
  return {
702
650
  receipt: prepare("changed", journal),
703
- statements: [...protectedMutation.statements, yield* assertion(protectedMutation.appliedCondition, marker)],
651
+ statements: [...protectedMutation.statements, applied],
652
+ postconditions: [applied],
704
653
  retryable: (cause) => isGuardFailure(cause, marker)
705
654
  };
706
655
  });
@@ -710,47 +659,18 @@ const cleanupPlan = Effect.fn("Drizzle.cleanupPlan")(function* (mapping, input,
710
659
  const now = yield* readEngineNowMillis(mapping);
711
660
  const nativeNow = mapping.encodeInstant(now);
712
661
  const a = attemptColumns(mapping);
713
- const c = chargeColumns(mapping);
714
- const sc = scopeColumns(mapping);
715
- let remaining = input.limit;
716
- let hasMore = false;
717
- const attempts = yield* database.select({ attemptId: a.attemptId }).from(mapping.attempt.table).where(and(eq(a.moduleId, input.moduleId), lte(a.retentionUntil, nativeNow))).limit(remaining + 1);
718
- if (attempts.length > remaining) hasMore = true;
719
- const selectedAttempts = attempts.slice(0, remaining);
720
- remaining -= selectedAttempts.length;
721
- const charges = yield* database.select({
722
- action: c.action,
723
- scopeKind: c.scopeKind,
724
- scopeKey: c.scopeKey,
725
- attemptId: c.attemptId
726
- }).from(mapping.charge.table).where(and(eq(c.moduleId, input.moduleId), lte(c.retentionUntil, nativeNow))).limit(remaining + 1);
727
- if (charges.length > remaining) hasMore = true;
728
- const selectedCharges = charges.slice(0, remaining);
729
- remaining -= selectedCharges.length;
730
- const scopes = yield* database.select({
731
- action: sc.action,
732
- scopeKind: sc.scopeKind,
733
- scopeKey: sc.scopeKey
734
- }).from(mapping.rateScope.table).where(and(eq(sc.moduleId, input.moduleId), notExists(database.select({ one: sql`1` }).from(mapping.charge.table).where(and(eq(c.moduleId, sc.moduleId), eq(c.action, sc.action), eq(c.scopeKind, sc.scopeKind), eq(c.scopeKey, sc.scopeKey)))))).limit(remaining + 1);
735
- if (scopes.length > remaining) hasMore = true;
736
- const selectedScopes = scopes.slice(0, remaining);
662
+ const attempts = yield* database.select({ attemptId: a.attemptId }).from(mapping.attempt.table).where(and(eq(a.moduleId, input.moduleId), lte(a.retentionUntil, nativeNow))).limit(input.limit + 1);
663
+ const selectedAttempts = attempts.slice(0, input.limit);
664
+ const hasMore = attempts.length > selectedAttempts.length;
737
665
  const marker = `effect-auth-password-guard:cleanup:${input.moduleId}:${now}`;
738
666
  const statements = [];
739
667
  for (const row of selectedAttempts) {
740
668
  const condition = and(eq(a.moduleId, input.moduleId), eq(a.attemptId, row.attemptId), lte(a.retentionUntil, mapping.d1.engineNow));
741
669
  statements.push(yield* assertion(sql`exists(select 1 from ${mapping.attempt.table} where ${condition})`, marker), yield* statement(database.delete(mapping.attempt.table).where(condition)));
742
670
  }
743
- for (const row of selectedCharges) {
744
- const condition = and(eq(c.moduleId, input.moduleId), eq(c.action, row.action), eq(c.scopeKind, row.scopeKind), eq(c.scopeKey, row.scopeKey), eq(c.attemptId, row.attemptId), lte(c.retentionUntil, mapping.d1.engineNow));
745
- statements.push(yield* assertion(sql`exists(select 1 from ${mapping.charge.table} where ${condition})`, marker), yield* statement(database.delete(mapping.charge.table).where(condition)));
746
- }
747
- for (const row of selectedScopes) {
748
- const condition = and(eq(sc.moduleId, input.moduleId), eq(sc.action, row.action), eq(sc.scopeKind, row.scopeKind), eq(sc.scopeKey, row.scopeKey), notExists(database.select({ one: sql`1` }).from(mapping.charge.table).where(and(eq(c.moduleId, sc.moduleId), eq(c.action, sc.action), eq(c.scopeKind, sc.scopeKind), eq(c.scopeKey, sc.scopeKey)))));
749
- statements.push(yield* assertion(sql`exists(select 1 from ${mapping.rateScope.table} where ${condition})`, marker), yield* statement(database.delete(mapping.rateScope.table).where(condition)));
750
- }
751
671
  return {
752
672
  receipt: prepare({
753
- removed: selectedAttempts.length + selectedCharges.length + selectedScopes.length,
673
+ removed: selectedAttempts.length,
754
674
  hasMore
755
675
  }, journal),
756
676
  statements,
@@ -765,12 +685,19 @@ const makeD1PasswordPersistenceServices = Effect.fnUntraced(function* (mapping,
765
685
  }).pipe(Effect.provideServiceEffect(NativeDatabase, nativeDatabase(Database)), Effect.mapError(unavailable));
766
686
  const hooks = yield* LifecycleHooks;
767
687
  const plans = makePasswordPlans(mapping, proofMapping);
768
- const run = (plan) => Effect.gen(function* () {
688
+ const standalone = Effect.gen(function* () {
769
689
  if (yield* hasCommitScope) return yield* unavailable();
690
+ yield* requireStandalone(unavailable, database.$client.transactionService);
691
+ });
692
+ const run = (plan) => Effect.gen(function* () {
693
+ yield* standalone;
770
694
  return yield* executeStandalone(plan, 2);
771
695
  }).pipe(Effect.provideService(CurrentD1PlanningDatabase, database), Effect.provideService(LifecycleHooks, hooks));
772
696
  return { passwordPersistence: {
773
- admitAttempt: (input, prepare) => run(plans.admitAttempt(input, prepare)).pipe(translateFailure),
697
+ prepareAttempt: (input) => standalone.pipe(Effect.andThen(plans.prepareAttempt(input)), Effect.map((planned) => Object.freeze({
698
+ ...planned.credential === void 0 ? {} : { credential: planned.credential },
699
+ admit: (prepare) => planned.claim.pipe(Effect.andThen(run(planned.admit(prepare))), translateFailure)
700
+ })), Effect.provideService(CurrentD1PlanningDatabase, database), translateFailure),
774
701
  settleAttempt: (input, prepare) => run(plans.settleAttempt(input, prepare)).pipe(translateFailure),
775
702
  readForSubject: (input) => plans.readForSubject(input).pipe(Effect.provideService(CurrentD1PlanningDatabase, database), Effect.provideService(LifecycleHooks, hooks), translateFailure),
776
703
  recoveryTarget: (input) => plans.recoveryTarget(input).pipe(Effect.provideService(CurrentD1PlanningDatabase, database), Effect.provideService(LifecycleHooks, hooks), translateFailure),
@@ -814,7 +741,10 @@ function coordinateD1PasswordPersistence(acquire, options, body) {
814
741
  return guarded.receipt;
815
742
  }), { mode: "batch" }).pipe(Effect.map((result) => result.value), Effect.provideService(CurrentD1PlanningDatabase, database), Effect.provideService(LifecycleHooks, hooks));
816
743
  const provided = Context.make(PasswordPersistence, {
817
- admitAttempt: (input, prepare) => owner.run(run(plans.admitAttempt(input, prepare)).pipe(translateFailure)),
744
+ prepareAttempt: (input) => owner.run(plans.prepareAttempt(input).pipe(Effect.map((planned) => Object.freeze({
745
+ ...planned.credential === void 0 ? {} : { credential: planned.credential },
746
+ admit: (prepare) => owner.run(planned.claim.pipe(Effect.andThen(run(planned.admit(prepare))), translateFailure))
747
+ })), Effect.provideService(CurrentD1PlanningDatabase, database), translateFailure)),
818
748
  settleAttempt: (input, prepare) => owner.run(run(plans.settleAttempt(input, prepare)).pipe(translateFailure)),
819
749
  readForSubject: (input) => owner.run(plans.readForSubject(input).pipe(Effect.provideService(CurrentD1PlanningDatabase, database), Effect.provideService(LifecycleHooks, hooks), translateFailure)),
820
750
  recoveryTarget: (input) => owner.run(plans.recoveryTarget(input).pipe(Effect.provideService(CurrentD1PlanningDatabase, database), Effect.provideService(LifecycleHooks, hooks), translateFailure)),
@@ -829,7 +759,7 @@ function coordinateD1PasswordPersistence(acquire, options, body) {
829
759
  const status = yield* Effect.result(mutation.journalGuard.read);
830
760
  if (status._tag === "Success" || status.failure._tag !== "CommitPending") return yield* unavailable();
831
761
  }
832
- yield* database.$client.batch(statements).pipe(Effect.catchCause((cause) => Effect.failCause(Cause.map(cause, (error) => everyFailureMatches(cause, mutation?.retryable) ? unavailable() : error))), translateFailure);
762
+ yield* database.$client.batch([...statements, ...mutation?.postconditions ?? []]).pipe(Effect.catchCause((cause) => Effect.failCause(Cause.map(cause, (error) => everyFailureMatches(cause, mutation?.retryable) ? unavailable() : error))), translateFailure);
833
763
  return value;
834
764
  }), { mode: "batch" }).pipe(Effect.provideService(LifecycleHooks, hooks))).value;
835
765
  }).pipe(Effect.provideService(NativeDatabase, database)));