@yielded/auth-persistence 0.1.0-beta.21 → 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 (48) hide show
  1. package/dist/Adapter.d.mts +4 -2
  2. package/dist/Adapter.mjs +3 -1
  3. package/dist/internal/authority-snapshot.mjs +53 -0
  4. package/dist/internal/authority-snapshot.mjs.map +1 -0
  5. package/dist/internal/configuration.d.mts +1 -1
  6. package/dist/internal/configuration.mjs.map +1 -1
  7. package/dist/internal/models/password-model.d.mts +3 -53
  8. package/dist/internal/models/password-model.mjs +0 -2
  9. package/dist/internal/models/password-model.mjs.map +1 -1
  10. package/dist/internal/native-sql-table.d.mts +30 -0
  11. package/dist/internal/native-sql-table.mjs +104 -0
  12. package/dist/internal/native-sql-table.mjs.map +1 -0
  13. package/dist/internal/passkey/target.d.mts +1 -1
  14. package/dist/internal/passkeys.mjs +1 -1
  15. package/dist/internal/password-attempts.d.mts +12 -0
  16. package/dist/internal/password-attempts.mjs +197 -0
  17. package/dist/internal/password-attempts.mjs.map +1 -0
  18. package/dist/internal/password-kernel.d.mts +15 -12
  19. package/dist/internal/password-kernel.mjs +39 -202
  20. package/dist/internal/password-kernel.mjs.map +1 -1
  21. package/dist/internal/persistence.d.mts +2 -0
  22. package/dist/internal/persistence.mjs +4 -4
  23. package/dist/internal/persistence.mjs.map +1 -1
  24. package/dist/internal/session-kernel.d.mts +3 -1
  25. package/dist/internal/session-kernel.mjs +22 -12
  26. package/dist/internal/session-kernel.mjs.map +1 -1
  27. package/dist/internal/sql-persistence.mjs +3 -1
  28. package/dist/internal/sql-persistence.mjs.map +1 -1
  29. package/dist/internal/storage-mapping.mjs +1 -30
  30. package/dist/internal/storage-mapping.mjs.map +1 -1
  31. package/dist/internal/storage-tables.d.mts +0 -43
  32. package/dist/internal/storage-tables.mjs +0 -26
  33. package/dist/internal/storage-tables.mjs.map +1 -1
  34. package/dist/internal/transaction-execution-kernel.d.mts +1 -1
  35. package/dist/internal/transaction-kernel.d.mts +1 -1
  36. package/package.json +1 -1
  37. package/src/Adapter.ts +9 -4
  38. package/src/internal/authority-snapshot.ts +91 -0
  39. package/src/internal/configuration.ts +1 -6
  40. package/src/internal/models/password-model.ts +0 -64
  41. package/src/internal/native-sql-table.ts +164 -0
  42. package/src/internal/password-attempts.ts +466 -0
  43. package/src/internal/password-kernel.ts +102 -623
  44. package/src/internal/persistence.ts +5 -10
  45. package/src/internal/session-kernel.ts +89 -27
  46. package/src/internal/sql-persistence.ts +2 -0
  47. package/src/internal/storage-mapping.ts +0 -29
  48. package/src/internal/storage-tables.ts +0 -15
@@ -1,6 +1,10 @@
1
1
  import { PersistenceMappingError } from "./mapping-error.mjs";
2
+ import { makeNativeSqlTables, sqlMapping } from "./native-sql-table.mjs";
3
+ import { NativeDatabase } from "./transaction-kernel.mjs";
4
+ import { CurrentPasswordPreparedTransaction, PasswordPreparedPostconditions } from "./PasswordPreparedPostconditions.mjs";
2
5
  import { CurrentProofSql } from "./proof-kernel.mjs";
3
6
  import { requiredPasswordConstraints } from "./models/password-model.mjs";
7
+ import { makePasswordAttempts, samePasswordCredentialSnapshot } from "./password-attempts.mjs";
4
8
  import { Cause, Context, DateTime, Effect, Option, Redacted, Schema } from "effect";
5
9
  import { CurrentCommitJournal, HookConfigurationError, LifecycleHooks, coordinateCommit, hasCommitScope } from "@yielded/auth/Hooks";
6
10
  import { snapshotAuthenticationEvidence } from "@yielded/auth/Sessions";
@@ -9,8 +13,8 @@ import { ProofUnavailable } from "@yielded/auth/Proofs";
9
13
  import { PasswordPersistence, PasswordUnavailable, snapshotPasswordCredential, snapshotPasswordRequirement, snapshotPasswordRevision } from "@yielded/auth/Password";
10
14
  //#region src/internal/password-kernel.ts
11
15
  var CurrentPasswordSql = class extends Context.Service()("effect-auth/CurrentPasswordSql") {};
12
- const makePasswordKernel = (operations, proofs) => {
13
- const { and, eq, gt, gte, inArray, lte, notExists, sql, column, updateValues } = operations;
16
+ const makePasswordKernel = (operations, proofs, nativeTables = (client) => makeNativeSqlTables(client)) => {
17
+ const { and, eq, inArray, lte, column, updateValues } = operations;
14
18
  const { completeProofPlanIn } = proofs;
15
19
  const unavailable = () => PasswordUnavailable.make({});
16
20
  const translateFailure = (effect) => reportPersistenceFailure(effect, (error) => Schema.is(PasswordUnavailable)(error) || Schema.is(ProofUnavailable)(error) || Schema.is(HookConfigurationError)(error)).pipe(Effect.catchCause((cause) => Effect.failCause(Cause.map(cause, () => unavailable()))));
@@ -60,21 +64,6 @@ const makePasswordKernel = (operations, proofs) => {
60
64
  retentionUntil: column(mapping.attempt.table, mapping.attempt.retentionUntil),
61
65
  state: column(mapping.attempt.table, mapping.attempt.state)
62
66
  });
63
- const scopeColumns = (mapping) => ({
64
- moduleId: column(mapping.rateScope.table, mapping.rateScope.moduleId),
65
- action: column(mapping.rateScope.table, mapping.rateScope.action),
66
- scopeKind: column(mapping.rateScope.table, mapping.rateScope.scopeKind),
67
- scopeKey: column(mapping.rateScope.table, mapping.rateScope.scopeKey)
68
- });
69
- const chargeColumns = (mapping) => ({
70
- moduleId: column(mapping.charge.table, mapping.charge.moduleId),
71
- action: column(mapping.charge.table, mapping.charge.action),
72
- scopeKind: column(mapping.charge.table, mapping.charge.scopeKind),
73
- scopeKey: column(mapping.charge.table, mapping.charge.scopeKey),
74
- attemptId: column(mapping.charge.table, mapping.charge.attemptId),
75
- occurredAt: column(mapping.charge.table, mapping.charge.occurredAt),
76
- retentionUntil: column(mapping.charge.table, mapping.charge.retentionUntil)
77
- });
78
67
  const commandColumns = (mapping) => ({
79
68
  moduleId: column(mapping.command.table, mapping.command.moduleId),
80
69
  commandId: column(mapping.command.table, mapping.command.commandId),
@@ -106,7 +95,6 @@ const makePasswordKernel = (operations, proofs) => {
106
95
  return allocate(mode, asyncValue ?? mapping.allocateRevision, syncValue ?? mapping.allocateRevisionSync);
107
96
  };
108
97
  const sameIdentifier = (left, right) => left.namespace === right.namespace && left.value === right.value;
109
- const sameCredentialSnapshot = (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);
110
98
  const evidenceSatisfiedAt = (evidence, requirement, now) => {
111
99
  if (evidence.proofs.length > 64 || evidence.revision.credentials.length > 64) return false;
112
100
  const revisions = /* @__PURE__ */ new Map();
@@ -197,54 +185,6 @@ const makePasswordKernel = (operations, proofs) => {
197
185
  }).pipe(Effect.flatMap(snapshotPasswordCredential))
198
186
  };
199
187
  });
200
- const scopeEntries = (keys, policy) => {
201
- const values = [{
202
- kind: "action",
203
- key: keys.action,
204
- ...policy.action
205
- }, {
206
- kind: "identifier",
207
- key: keys.identifier,
208
- ...policy.identifier
209
- }];
210
- if (keys.subject !== void 0) values.push({
211
- kind: "subject",
212
- key: keys.subject,
213
- ...policy.subject
214
- });
215
- return values;
216
- };
217
- const lockScope = Effect.fn("DrizzlePassword.lockScope")(function* (mapping, configuration, moduleId, action, entry) {
218
- const database = yield* CurrentPasswordSql;
219
- const query = database.insert(mapping.rateScope.table).values(mapping.rateScope.encodeInsert({
220
- moduleId,
221
- action,
222
- scopeKind: entry.kind,
223
- scopeKey: entry.key
224
- }));
225
- yield* configuration.insertIfAbsent(query, mapping.rateScope.scopeKey, entry.key);
226
- const c = scopeColumns(mapping);
227
- yield* selectRows(database.select().from(mapping.rateScope.table).where(and(eq(c.moduleId, moduleId), eq(c.action, action), eq(c.scopeKind, entry.kind), eq(c.scopeKey, entry.key))).limit(1), configuration.locking);
228
- });
229
- const scopeAdmits = Effect.fn("DrizzlePassword.scopeAdmits")(function* (mapping, moduleId, action, entry, policy, identifier, nativeSubjectId, now) {
230
- const database = yield* CurrentPasswordSql;
231
- const c = chargeColumns(mapping);
232
- if ((yield* selectRows(database.select({ attemptId: c.attemptId }).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)))).limit(entry.limit), true)).length >= entry.limit) return false;
233
- const a = attemptColumns(mapping);
234
- const pendingWhere = 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"), gt(a.retentionUntil, mapping.encodeInstant(now)));
235
- return (yield* selectRows(database.select({ attemptId: a.attemptId }).from(mapping.attempt.table).where(pendingWhere).limit(policy.maximumPending), true)).length < policy.maximumPending;
236
- });
237
- const insertCharge = Effect.fn("Drizzle.insertCharge")(function* (mapping, moduleId, action, entry, attemptId, now) {
238
- return yield* (yield* CurrentPasswordSql).insert(mapping.charge.table).values(mapping.charge.encodeInsert({
239
- moduleId,
240
- action,
241
- scopeKind: entry.kind,
242
- scopeKey: entry.key,
243
- attemptId,
244
- occurredAtMillis: now,
245
- retentionUntilMillis: now + entry.windowMillis
246
- }));
247
- });
248
188
  const owned = (database, mapping, configuration, body) => {
249
189
  if (!validConstraints(mapping)) return Effect.fail(unavailable());
250
190
  const run = coordinateCommit(() => database.transaction((transaction) => body.pipe(Effect.provideService(CurrentPasswordSql, transaction), Effect.provideService(CurrentProofSql, transaction))), { mode: configuration.mode }).pipe(Effect.map((result) => result.value));
@@ -254,6 +194,15 @@ const makePasswordKernel = (operations, proofs) => {
254
194
  return yield* run;
255
195
  }) : run;
256
196
  };
197
+ const registerFinal = Effect.fnUntraced(function* (check) {
198
+ const postconditions = yield* Effect.serviceOption(PasswordPreparedPostconditions);
199
+ if (Option.isNone(postconditions)) return;
200
+ const postcondition = Effect.gen(function* () {
201
+ const transaction = yield* CurrentPasswordPreparedTransaction;
202
+ if (!(yield* check.pipe(Effect.provideService(CurrentPasswordSql, transaction)))) return yield* unavailable();
203
+ }).pipe(translateFailure);
204
+ if (!postconditions.value.register(postcondition)) return yield* unavailable();
205
+ });
257
206
  const safeRead = (database, configuration, body) => Effect.gen(function* () {
258
207
  if (configuration.coordinated !== true) {
259
208
  if (yield* hasCommitScope) return yield* unavailable();
@@ -317,7 +266,7 @@ const makePasswordKernel = (operations, proofs) => {
317
266
  identifier,
318
267
  credential
319
268
  });
320
- if (!sameCredentialSnapshot(snapshot, input.credential)) return void 0;
269
+ if (!samePasswordCredentialSnapshot(snapshot, input.credential)) return void 0;
321
270
  }
322
271
  const currentRequirement = yield* mapping.subject.decodeActionRequirement(row, action);
323
272
  const now = yield* nowMillis;
@@ -383,10 +332,9 @@ const makePasswordKernel = (operations, proofs) => {
383
332
  }));
384
333
  yield* database.update(mapping.subject.table).set(updateValues([[mapping.subject.securityRevision, nextSecurityRevision]])).where(and(eq(subjectColumns(mapping).id, authority.nativeSubjectId), eq(subjectColumns(mapping).securityRevision, input.expectedRevision.securityRevision)));
385
334
  yield* insertCommand(mapping, input, "add-password", commandNow ?? authority.now);
386
- const inserted = (yield* readPasswordCredential(mapping, input.moduleId, authority.nativeSubjectId, false))[0];
387
- const authorityInserted = (yield* database.select().from(mapping.authorityCredential.table).where(and(eq(authorityCredentialColumns(mapping).subjectId, authority.nativeSubjectId), eq(authorityCredentialColumns(mapping).credentialId, credentialId), eq(authorityCredentialColumns(mapping).revision, credentialRevision))).limit(1))[0];
388
- const subjectUpdated = (yield* database.select().from(mapping.subject.table).where(and(eq(subjectColumns(mapping).id, authority.nativeSubjectId), eq(subjectColumns(mapping).securityRevision, nextSecurityRevision))).limit(1))[0];
389
- if (inserted === void 0 || inserted[mapping.credential.credentialId] !== credentialId || inserted[mapping.credential.credentialRevision] !== credentialRevision || inserted[mapping.credential.verifierVersion] !== verifierVersion || inserted[mapping.credential.verifier] !== Redacted.value(input.replacement.verifier) || inserted[mapping.credential.normalization] !== input.replacement.normalization || authorityInserted === void 0 || subjectUpdated === void 0 || !(yield* mutationPostconditions(mapping, input, authority.nativeSubjectId, subjectUpdated, authorityInserted, commandNow ?? authority.now))) return yield* unavailable();
335
+ const applied = checkMutationApplied(mapping, input, authority.nativeSubjectId, revisions, commandNow ?? authority.now);
336
+ if (!(yield* applied)) return yield* unavailable();
337
+ yield* registerFinal(applied);
390
338
  return true;
391
339
  });
392
340
  const makeSqlPasswordPersistence = Effect.fn("makeSqlPasswordPersistence")(function* (mapping, configuration) {
@@ -396,96 +344,11 @@ const makePasswordKernel = (operations, proofs) => {
396
344
  proof: configuration.proof?.mapping
397
345
  }) ?? Effect.void).pipe(Effect.mapError(unavailable));
398
346
  const hooks = yield* LifecycleHooks;
347
+ if (!validConstraints(mapping)) return yield* unavailable();
348
+ const root = yield* NativeDatabase;
349
+ const attempts = yield* makePasswordAttempts(yield* sqlMapping(() => nativeTables(root.$client.withoutTransforms(), root)).pipe(translateFailure), mapping, configuration);
399
350
  return PasswordPersistence.of({
400
- admitAttempt: (input, prepare) => Effect.gen(function* () {
401
- const attemptId = yield* allocate(configuration.mode, mapping.allocateAttemptId, mapping.allocateAttemptIdSync);
402
- return yield* owned(database, mapping, configuration, Effect.gen(function* () {
403
- const transaction = yield* CurrentPasswordSql;
404
- const journal = yield* CurrentCommitJournal;
405
- const resolved = yield* resolveCredential(mapping, input.moduleId, input.identifier, input.subjectId, configuration.locking);
406
- const keys = mapping.scopeKeys({
407
- moduleId: input.moduleId,
408
- action: input.action,
409
- identifier: input.identifier,
410
- ...resolved?.snapshot === void 0 ? {} : { subjectId: resolved.snapshot.revision.subjectId }
411
- });
412
- const scopes = scopeEntries(keys, input.policy);
413
- const admitted = [];
414
- yield* lockScope(mapping, configuration, input.moduleId, input.action, scopes[0]);
415
- let currentNow = yield* nowMillis;
416
- if (yield* scopeAdmits(mapping, input.moduleId, input.action, scopes[0], input.policy, input.identifier, resolved?.nativeSubjectId, currentNow)) admitted.push(scopes[0]);
417
- if (admitted.length === 1) for (const entry of scopes.slice(1)) {
418
- yield* lockScope(mapping, configuration, input.moduleId, input.action, entry);
419
- currentNow = yield* nowMillis;
420
- if (yield* scopeAdmits(mapping, input.moduleId, input.action, entry, input.policy, input.identifier, resolved?.nativeSubjectId, currentNow)) admitted.push(entry);
421
- }
422
- const now = yield* nowMillis;
423
- for (const entry of admitted) yield* insertCharge(mapping, input.moduleId, input.action, entry, attemptId, now);
424
- if (admitted.length !== scopes.length) return prepare({ _tag: "Denied" }, journal);
425
- const snapshot = resolved?.snapshot;
426
- const deadline = now + input.policy.attemptLifetimeMillis;
427
- const retentionUntil = Math.max(deadline, ...scopes.map((entry) => now + entry.windowMillis));
428
- yield* transaction.insert(mapping.attempt.table).values(mapping.attempt.encodeInsert({
429
- moduleId: input.moduleId,
430
- action: input.action,
431
- attemptId,
432
- identifier: input.identifier,
433
- ...snapshot === void 0 ? {} : {
434
- subjectId: snapshot.revision.subjectId,
435
- credentialId: snapshot.credentialId,
436
- securityRevision: snapshot.revision.securityRevision,
437
- credentialRevision: snapshot.credentialRevision,
438
- verifierVersion: snapshot.verifierVersion,
439
- identifierBindingRevision: snapshot.identifierBindingRevision
440
- },
441
- admittedAtMillis: now,
442
- deadlineMillis: deadline,
443
- retentionUntilMillis: retentionUntil
444
- }, {
445
- ...resolved?.nativeSubjectId === void 0 ? {} : { nativeSubjectId: resolved.nativeSubjectId },
446
- state: "pending"
447
- }));
448
- return prepare({
449
- _tag: "Admitted",
450
- attemptId,
451
- ...snapshot === void 0 ? {} : { credential: snapshot }
452
- }, journal);
453
- }));
454
- }).pipe(Effect.provideService(CurrentPasswordSql, database), Effect.provideService(LifecycleHooks, hooks), translateFailure),
455
- settleAttempt: (uncaptured, prepare) => Effect.gen(function* () {
456
- const captured = uncaptured.captured === void 0 ? void 0 : yield* snapshotPasswordCredential(uncaptured.captured);
457
- const input = {
458
- ...uncaptured,
459
- ...captured === void 0 ? {} : { captured }
460
- };
461
- return yield* owned(database, mapping, configuration, Effect.gen(function* () {
462
- const transaction = yield* CurrentPasswordSql;
463
- const journal = yield* CurrentCommitJournal;
464
- const c = attemptColumns(mapping);
465
- let row = (yield* selectRows(transaction.select().from(mapping.attempt.table).where(and(eq(c.moduleId, input.moduleId), eq(c.attemptId, input.attemptId))).limit(1), false))[0];
466
- if (row === void 0 || row[mapping.attempt.state] !== "pending") return prepare("rejected", journal);
467
- let decision = "rejected";
468
- let current;
469
- let attemptMatches = false;
470
- if (input.outcome === "verified" && input.captured !== void 0) {
471
- current = yield* resolveCredential(mapping, input.moduleId, input.captured.identifier, input.captured.revision.subjectId, configuration.locking);
472
- attemptMatches = input.captured.moduleId === input.moduleId && row[mapping.attempt.identifierNamespace] === input.captured.identifier.namespace && row[mapping.attempt.identifierValue] === input.captured.identifier.value && mapping.subjectId.equals(row[mapping.attempt.subjectId], current?.nativeSubjectId) && 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;
473
- }
474
- row = (yield* selectRows(transaction.select().from(mapping.attempt.table).where(and(eq(c.moduleId, input.moduleId), eq(c.attemptId, input.attemptId))).limit(1), configuration.locking))[0];
475
- if (row === void 0 || row[mapping.attempt.state] !== "pending") return prepare("rejected", journal);
476
- const now = yield* nowMillis;
477
- const deadline = yield* mapping.decodeInstant(row[mapping.attempt.deadline]);
478
- if (attemptMatches && current?.snapshot !== void 0 && input.captured !== void 0 && deadline > now && sameCredentialSnapshot(current.snapshot, input.captured)) decision = "verified";
479
- const prepared = prepare(decision, journal);
480
- yield* transaction.update(mapping.attempt.table).set(updateValues([[mapping.attempt.state, decision]])).where(and(eq(c.moduleId, input.moduleId), eq(c.attemptId, input.attemptId), eq(c.state, "pending")));
481
- if (decision === "verified" && input.rehash !== void 0 && current?.snapshot !== void 0) {
482
- const cc = credentialColumns(mapping);
483
- const nextVersion = yield* allocate(configuration.mode, mapping.allocateRevision, mapping.allocateRevisionSync);
484
- yield* transaction.update(mapping.credential.table).set(mapping.credential.encodeVerifier(input.rehash.nextVerifier, nextVersion)).where(and(eq(cc.moduleId, input.moduleId), eq(cc.subjectId, current.nativeSubjectId), eq(cc.credentialId, current.snapshot.credentialId), eq(cc.credentialRevision, current.snapshot.credentialRevision), eq(cc.verifierVersion, input.rehash.expectedVersion), eq(cc.verifier, Redacted.value(input.rehash.expectedVerifier))));
485
- }
486
- return prepared;
487
- }));
488
- }).pipe(Effect.provideService(CurrentPasswordSql, database), Effect.provideService(LifecycleHooks, hooks), translateFailure),
351
+ ...attempts,
489
352
  readForSubject: (input) => safeRead(database, configuration, Effect.gen(function* () {
490
353
  const transaction = yield* CurrentPasswordSql;
491
354
  const locked = yield* lockSubject(mapping, input.subjectId, false);
@@ -565,11 +428,14 @@ const makePasswordKernel = (operations, proofs) => {
565
428
  if (!(yield* currentExpectedCredential(mapping, input, authority.nativeSubjectId))) return prepare("rejected", journal);
566
429
  let passwordReceipt;
567
430
  yield* completeProofPlanIn(configuration.proof.mapping, configuration.proof.configuration, input.completion, Effect.gen(function* () {
568
- return yield* writeReplacement(mapping, input, authority.nativeSubjectId, {
431
+ const revisions = {
569
432
  credentialRevision,
570
433
  verifierVersion,
571
434
  nextSecurityRevision
572
- }, authority.now);
435
+ };
436
+ const applied = yield* writeReplacement(mapping, input, authority.nativeSubjectId, revisions, authority.now);
437
+ if (applied) yield* registerReplacement(mapping, input, authority.nativeSubjectId, revisions, authority.now);
438
+ return applied;
573
439
  }), (decision) => {
574
440
  passwordReceipt = prepare(decision === "completed" ? "changed" : "rejected", journal);
575
441
  return decision;
@@ -582,54 +448,20 @@ const makePasswordKernel = (operations, proofs) => {
582
448
  const journal = yield* CurrentCommitJournal;
583
449
  const now = yield* nowMillis;
584
450
  const nativeNow = mapping.encodeInstant(now);
585
- let remaining = input.limit;
586
- let hasMore = false;
587
451
  const a = attemptColumns(mapping);
588
- const c = chargeColumns(mapping);
589
- const attempts = yield* selectRows(transaction.select({ attemptId: a.attemptId }).from(mapping.attempt.table).where(and(eq(a.moduleId, input.moduleId), lte(a.retentionUntil, nativeNow))).limit(remaining + 1), false);
590
- if (attempts.length > remaining) hasMore = true;
591
- const attemptCandidates = attempts.slice(0, remaining);
592
- remaining -= attemptCandidates.length;
593
- const charges = yield* selectRows(transaction.select({
594
- action: c.action,
595
- scopeKind: c.scopeKind,
596
- scopeKey: c.scopeKey,
597
- attemptId: c.attemptId
598
- }).from(mapping.charge.table).where(and(eq(c.moduleId, input.moduleId), lte(c.retentionUntil, nativeNow))).limit(remaining + 1), false);
599
- if (charges.length > remaining) hasMore = true;
600
- const chargeCandidates = charges.slice(0, remaining);
601
- remaining -= chargeCandidates.length;
602
- const sc = scopeColumns(mapping);
603
- const scopes = yield* selectRows(transaction.select({
604
- action: sc.action,
605
- scopeKind: sc.scopeKind,
606
- scopeKey: sc.scopeKey
607
- }).from(mapping.rateScope.table).where(and(eq(sc.moduleId, input.moduleId), notExists(transaction.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), remaining === 0 ? false : configuration.locking);
608
- if (scopes.length > remaining) hasMore = true;
609
- const selectedScopes = scopes.slice(0, remaining);
610
- remaining -= selectedScopes.length;
611
- const selectedCharges = [];
612
- for (const candidate of chargeCandidates) {
613
- const locked = yield* selectRows(transaction.select({
614
- action: c.action,
615
- scopeKind: c.scopeKind,
616
- scopeKey: c.scopeKey,
617
- attemptId: c.attemptId
618
- }).from(mapping.charge.table).where(and(eq(c.moduleId, input.moduleId), eq(c.action, candidate.action), eq(c.scopeKind, candidate.scopeKind), eq(c.scopeKey, candidate.scopeKey), eq(c.attemptId, candidate.attemptId), lte(c.retentionUntil, nativeNow))).limit(1), configuration.locking);
619
- if (locked[0] !== void 0) selectedCharges.push(locked[0]);
620
- }
452
+ const attempts = yield* selectRows(transaction.select({ attemptId: a.attemptId }).from(mapping.attempt.table).where(and(eq(a.moduleId, input.moduleId), lte(a.retentionUntil, nativeNow))).limit(input.limit + 1), false);
453
+ const hasMore = attempts.length > input.limit;
454
+ const attemptCandidates = attempts.slice(0, input.limit);
621
455
  const selectedAttempts = [];
622
456
  for (const candidate of attemptCandidates) {
623
457
  const locked = yield* selectRows(transaction.select({ attemptId: a.attemptId }).from(mapping.attempt.table).where(and(eq(a.moduleId, input.moduleId), eq(a.attemptId, candidate.attemptId), lte(a.retentionUntil, nativeNow))).limit(1), configuration.locking);
624
458
  if (locked[0] !== void 0) selectedAttempts.push(locked[0]);
625
459
  }
626
460
  const prepared = prepare({
627
- removed: selectedAttempts.length + selectedCharges.length + selectedScopes.length,
461
+ removed: selectedAttempts.length,
628
462
  hasMore
629
463
  }, journal);
630
- for (const row of selectedCharges) yield* transaction.delete(mapping.charge.table).where(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, nativeNow)));
631
464
  if (selectedAttempts.length > 0) yield* transaction.delete(mapping.attempt.table).where(and(eq(a.moduleId, input.moduleId), inArray(a.attemptId, selectedAttempts.map((row) => row.attemptId)), lte(a.retentionUntil, nativeNow)));
632
- for (const row of selectedScopes) yield* transaction.delete(mapping.rateScope.table).where(and(eq(sc.moduleId, input.moduleId), eq(sc.action, row.action), eq(sc.scopeKind, row.scopeKind), eq(sc.scopeKey, row.scopeKey), notExists(transaction.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))))));
633
465
  return prepared;
634
466
  })).pipe(Effect.provideService(CurrentPasswordSql, database), Effect.provideService(LifecycleHooks, hooks), translateFailure)
635
467
  });
@@ -646,7 +478,7 @@ const makePasswordKernel = (operations, proofs) => {
646
478
  const credential = (yield* database.select().from(mapping.credential.table).where(and(eq(c.moduleId, input.moduleId), eq(c.subjectId, nativeSubjectId), eq(c.credentialId, revisions.credentialId), eq(c.credentialRevision, revisions.credentialRevision), eq(c.verifierVersion, revisions.verifierVersion), eq(c.verifier, Redacted.value(input.replacement.verifier)), eq(c.normalization, input.replacement.normalization))).limit(1))[0];
647
479
  const subject = (yield* database.select().from(mapping.subject.table).where(and(eq(s.id, nativeSubjectId), eq(s.securityRevision, revisions.nextSecurityRevision))).limit(1))[0];
648
480
  const authorityCredential = (yield* database.select().from(mapping.authorityCredential.table).where(and(eq(authorityCredentialColumns(mapping).subjectId, nativeSubjectId), eq(authorityCredentialColumns(mapping).credentialId, revisions.credentialId), eq(authorityCredentialColumns(mapping).revision, revisions.credentialRevision))).limit(1))[0];
649
- return credential !== void 0 && (yield* mutationPostconditions(mapping, input, nativeSubjectId, subject, authorityCredential, now));
481
+ return credential !== void 0 && credential[mapping.credential.credentialId] === revisions.credentialId && credential[mapping.credential.credentialRevision] === revisions.credentialRevision && credential[mapping.credential.verifierVersion] === revisions.verifierVersion && credential[mapping.credential.verifier] === Redacted.value(input.replacement.verifier) && credential[mapping.credential.normalization] === input.replacement.normalization && (yield* mutationPostconditions(mapping, input, nativeSubjectId, subject, authorityCredential, now));
650
482
  });
651
483
  const writeReplacement = Effect.fn("DrizzlePassword.writeReplacement")(function* (mapping, input, nativeSubjectId, revisions, now) {
652
484
  const database = yield* CurrentPasswordSql;
@@ -665,6 +497,10 @@ const makePasswordKernel = (operations, proofs) => {
665
497
  credentialId: input.credential.credentialId
666
498
  }, now);
667
499
  });
500
+ const registerReplacement = (mapping, input, nativeSubjectId, revisions, now) => registerFinal(checkMutationApplied(mapping, input, nativeSubjectId, {
501
+ ...revisions,
502
+ credentialId: input.credential.credentialId
503
+ }, now));
668
504
  const replaceIn = Effect.fn("Drizzle.replaceIn")(function* (mapping, configuration, input, action, revisions, prepare) {
669
505
  const journal = yield* CurrentCommitJournal;
670
506
  const authority = yield* validateMutationAuthority(mapping, configuration, input, action);
@@ -673,6 +509,7 @@ const makePasswordKernel = (operations, proofs) => {
673
509
  if (!(yield* currentExpectedCredential(mapping, input, authority.nativeSubjectId))) return prepare("rejected", journal);
674
510
  const prepared = prepare("changed", journal);
675
511
  if (!(yield* writeReplacement(mapping, input, authority.nativeSubjectId, revisions, authority.now))) return yield* unavailable();
512
+ yield* registerReplacement(mapping, input, authority.nativeSubjectId, revisions, authority.now);
676
513
  return prepared;
677
514
  });
678
515
  return {
@@ -694,7 +531,7 @@ const makePasswordKernel = (operations, proofs) => {
694
531
  checkMutationApplied,
695
532
  writeReplacement,
696
533
  evidenceSatisfiedAt,
697
- sameCredentialSnapshot,
534
+ sameCredentialSnapshot: samePasswordCredentialSnapshot,
698
535
  subjectColumns,
699
536
  identifierColumns,
700
537
  credentialColumns,