@yielded/auth-persistence 0.1.0-beta.10

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 (182) hide show
  1. package/README.md +27 -0
  2. package/dist/Adapter.d.mts +30 -0
  3. package/dist/Adapter.mjs +25 -0
  4. package/dist/index.d.mts +7 -0
  5. package/dist/index.mjs +7 -0
  6. package/dist/internal/PasswordPreparedPostconditions.d.mts +22 -0
  7. package/dist/internal/PasswordPreparedPostconditions.mjs +12 -0
  8. package/dist/internal/PasswordPreparedPostconditions.mjs.map +1 -0
  9. package/dist/internal/configuration.d.mts +174 -0
  10. package/dist/internal/configuration.mjs +7 -0
  11. package/dist/internal/configuration.mjs.map +1 -0
  12. package/dist/internal/email-kernel.d.mts +275 -0
  13. package/dist/internal/email-kernel.mjs +487 -0
  14. package/dist/internal/email-kernel.mjs.map +1 -0
  15. package/dist/internal/mapping-error.d.mts +10 -0
  16. package/dist/internal/mapping-error.mjs +30 -0
  17. package/dist/internal/mapping-error.mjs.map +1 -0
  18. package/dist/internal/models/common.d.mts +12 -0
  19. package/dist/internal/models/email-model.d.mts +353 -0
  20. package/dist/internal/models/email-model.mjs +21 -0
  21. package/dist/internal/models/email-model.mjs.map +1 -0
  22. package/dist/internal/models/passkey-model.d.mts +266 -0
  23. package/dist/internal/models/passkey-model.mjs +34 -0
  24. package/dist/internal/models/passkey-model.mjs.map +1 -0
  25. package/dist/internal/models/passkey-registration-ceremony-model.d.mts +49 -0
  26. package/dist/internal/models/passkey-registration-ceremony-model.mjs +18 -0
  27. package/dist/internal/models/passkey-registration-ceremony-model.mjs.map +1 -0
  28. package/dist/internal/models/passkey-write-model.d.mts +189 -0
  29. package/dist/internal/models/passkey-write-model.mjs +13 -0
  30. package/dist/internal/models/passkey-write-model.mjs.map +1 -0
  31. package/dist/internal/models/password-model.d.mts +328 -0
  32. package/dist/internal/models/password-model.mjs +23 -0
  33. package/dist/internal/models/password-model.mjs.map +1 -0
  34. package/dist/internal/models/proof-model.d.mts +285 -0
  35. package/dist/internal/models/proof-model.mjs +17 -0
  36. package/dist/internal/models/proof-model.mjs.map +1 -0
  37. package/dist/internal/models/session-model.d.mts +199 -0
  38. package/dist/internal/models/session-model.mjs +20 -0
  39. package/dist/internal/models/session-model.mjs.map +1 -0
  40. package/dist/internal/models/step-up-model.d.mts +66 -0
  41. package/dist/internal/models/step-up-model.mjs +9 -0
  42. package/dist/internal/models/step-up-model.mjs.map +1 -0
  43. package/dist/internal/oauth-app.d.mts +11 -0
  44. package/dist/internal/oauth-app.mjs +57 -0
  45. package/dist/internal/oauth-app.mjs.map +1 -0
  46. package/dist/internal/oauth-server.d.mts +10 -0
  47. package/dist/internal/oauth-server.mjs +66 -0
  48. package/dist/internal/oauth-server.mjs.map +1 -0
  49. package/dist/internal/passkey/admission.d.mts +6 -0
  50. package/dist/internal/passkey/admission.mjs +216 -0
  51. package/dist/internal/passkey/admission.mjs.map +1 -0
  52. package/dist/internal/passkey/credentials.d.mts +14 -0
  53. package/dist/internal/passkey/credentials.mjs +249 -0
  54. package/dist/internal/passkey/credentials.mjs.map +1 -0
  55. package/dist/internal/passkey/enrollment.mjs +138 -0
  56. package/dist/internal/passkey/enrollment.mjs.map +1 -0
  57. package/dist/internal/passkey/flow.d.mts +21 -0
  58. package/dist/internal/passkey/flow.mjs +452 -0
  59. package/dist/internal/passkey/flow.mjs.map +1 -0
  60. package/dist/internal/passkey/management.mjs +231 -0
  61. package/dist/internal/passkey/management.mjs.map +1 -0
  62. package/dist/internal/passkey/registration-ceremony.d.mts +9 -0
  63. package/dist/internal/passkey/registration-ceremony.mjs +129 -0
  64. package/dist/internal/passkey/registration-ceremony.mjs.map +1 -0
  65. package/dist/internal/passkey/registration-custody.d.mts +2 -0
  66. package/dist/internal/passkey/registration-custody.mjs +52 -0
  67. package/dist/internal/passkey/registration-custody.mjs.map +1 -0
  68. package/dist/internal/passkey/registration-write.mjs +211 -0
  69. package/dist/internal/passkey/registration-write.mjs.map +1 -0
  70. package/dist/internal/passkey/state.d.mts +10 -0
  71. package/dist/internal/passkey/state.mjs +234 -0
  72. package/dist/internal/passkey/state.mjs.map +1 -0
  73. package/dist/internal/passkey/target.d.mts +19 -0
  74. package/dist/internal/passkey/target.mjs +190 -0
  75. package/dist/internal/passkey/target.mjs.map +1 -0
  76. package/dist/internal/passkey/write-state.d.mts +17 -0
  77. package/dist/internal/passkey/write-state.mjs +345 -0
  78. package/dist/internal/passkey/write-state.mjs.map +1 -0
  79. package/dist/internal/passkey/write-target.mjs +192 -0
  80. package/dist/internal/passkey/write-target.mjs.map +1 -0
  81. package/dist/internal/passkey-kernel.d.mts +5592 -0
  82. package/dist/internal/passkey-kernel.mjs +49 -0
  83. package/dist/internal/passkey-kernel.mjs.map +1 -0
  84. package/dist/internal/passkeys.d.mts +7 -0
  85. package/dist/internal/passkeys.mjs +368 -0
  86. package/dist/internal/passkeys.mjs.map +1 -0
  87. package/dist/internal/password-kernel.d.mts +290 -0
  88. package/dist/internal/password-kernel.mjs +704 -0
  89. package/dist/internal/password-kernel.mjs.map +1 -0
  90. package/dist/internal/persistence.d.mts +25 -0
  91. package/dist/internal/persistence.mjs +291 -0
  92. package/dist/internal/persistence.mjs.map +1 -0
  93. package/dist/internal/phone-kernel.d.mts +162 -0
  94. package/dist/internal/phone-kernel.mjs +434 -0
  95. package/dist/internal/phone-kernel.mjs.map +1 -0
  96. package/dist/internal/proof-kernel.d.mts +93 -0
  97. package/dist/internal/proof-kernel.mjs +619 -0
  98. package/dist/internal/proof-kernel.mjs.map +1 -0
  99. package/dist/internal/query-operations.d.mts +61 -0
  100. package/dist/internal/registration-contract.d.mts +16 -0
  101. package/dist/internal/registration.mjs +66 -0
  102. package/dist/internal/registration.mjs.map +1 -0
  103. package/dist/internal/session-kernel.d.mts +48 -0
  104. package/dist/internal/session-kernel.mjs +737 -0
  105. package/dist/internal/session-kernel.mjs.map +1 -0
  106. package/dist/internal/sql-persistence.d.mts +10 -0
  107. package/dist/internal/sql-persistence.mjs +37 -0
  108. package/dist/internal/sql-persistence.mjs.map +1 -0
  109. package/dist/internal/sql-query.mjs +207 -0
  110. package/dist/internal/sql-query.mjs.map +1 -0
  111. package/dist/internal/sql-table.d.mts +36 -0
  112. package/dist/internal/sql-table.mjs +81 -0
  113. package/dist/internal/sql-table.mjs.map +1 -0
  114. package/dist/internal/standalone.d.mts +9 -0
  115. package/dist/internal/standalone.mjs +13 -0
  116. package/dist/internal/standalone.mjs.map +1 -0
  117. package/dist/internal/step-up-state.d.mts +49 -0
  118. package/dist/internal/step-up-state.mjs +32 -0
  119. package/dist/internal/step-up-state.mjs.map +1 -0
  120. package/dist/internal/storage-mapping.mjs +686 -0
  121. package/dist/internal/storage-mapping.mjs.map +1 -0
  122. package/dist/internal/storage-tables.d.mts +913 -0
  123. package/dist/internal/storage-tables.mjs +387 -0
  124. package/dist/internal/storage-tables.mjs.map +1 -0
  125. package/dist/internal/storage-validation.mjs +58 -0
  126. package/dist/internal/storage-validation.mjs.map +1 -0
  127. package/dist/internal/transaction-execution-kernel.d.mts +38 -0
  128. package/dist/internal/transaction-execution-kernel.mjs +116 -0
  129. package/dist/internal/transaction-execution-kernel.mjs.map +1 -0
  130. package/dist/internal/transaction-kernel.d.mts +77 -0
  131. package/dist/internal/transaction-kernel.mjs +260 -0
  132. package/dist/internal/transaction-kernel.mjs.map +1 -0
  133. package/package.json +1 -0
  134. package/src/Adapter.ts +291 -0
  135. package/src/index.ts +6 -0
  136. package/src/internal/PasswordPreparedPostconditions.ts +30 -0
  137. package/src/internal/configuration.ts +282 -0
  138. package/src/internal/email-kernel.ts +1535 -0
  139. package/src/internal/mapping-error.ts +36 -0
  140. package/src/internal/models/common.ts +11 -0
  141. package/src/internal/models/email-model.ts +559 -0
  142. package/src/internal/models/passkey-model.ts +390 -0
  143. package/src/internal/models/passkey-registration-ceremony-model.ts +72 -0
  144. package/src/internal/models/passkey-write-model.ts +297 -0
  145. package/src/internal/models/password-model.ts +513 -0
  146. package/src/internal/models/proof-model.ts +424 -0
  147. package/src/internal/models/session-model.ts +375 -0
  148. package/src/internal/models/step-up-model.ts +135 -0
  149. package/src/internal/oauth-app.ts +82 -0
  150. package/src/internal/oauth-server.ts +89 -0
  151. package/src/internal/passkey/admission.ts +361 -0
  152. package/src/internal/passkey/credentials.ts +511 -0
  153. package/src/internal/passkey/enrollment.ts +326 -0
  154. package/src/internal/passkey/flow.ts +1062 -0
  155. package/src/internal/passkey/management.ts +421 -0
  156. package/src/internal/passkey/registration-ceremony.ts +283 -0
  157. package/src/internal/passkey/registration-custody.ts +91 -0
  158. package/src/internal/passkey/registration-write.ts +426 -0
  159. package/src/internal/passkey/state.ts +353 -0
  160. package/src/internal/passkey/target.ts +586 -0
  161. package/src/internal/passkey/write-state.ts +681 -0
  162. package/src/internal/passkey/write-target.ts +452 -0
  163. package/src/internal/passkey-kernel.ts +104 -0
  164. package/src/internal/passkeys.ts +606 -0
  165. package/src/internal/password-kernel.ts +2124 -0
  166. package/src/internal/persistence.ts +588 -0
  167. package/src/internal/phone-kernel.ts +846 -0
  168. package/src/internal/proof-kernel.ts +1954 -0
  169. package/src/internal/query-operations.ts +81 -0
  170. package/src/internal/registration-contract.ts +22 -0
  171. package/src/internal/registration.ts +161 -0
  172. package/src/internal/session-kernel.ts +2217 -0
  173. package/src/internal/sql-persistence.ts +45 -0
  174. package/src/internal/sql-query.ts +298 -0
  175. package/src/internal/sql-table.ts +104 -0
  176. package/src/internal/standalone.ts +30 -0
  177. package/src/internal/step-up-state.ts +139 -0
  178. package/src/internal/storage-mapping.ts +708 -0
  179. package/src/internal/storage-tables.ts +408 -0
  180. package/src/internal/storage-validation.ts +117 -0
  181. package/src/internal/transaction-execution-kernel.ts +313 -0
  182. package/src/internal/transaction-kernel.ts +593 -0
@@ -0,0 +1,1954 @@
1
+ import {
2
+ coordinateCommit,
3
+ CurrentCommitJournal,
4
+ hasCommitScope,
5
+ LifecycleHooks,
6
+ HookConfigurationError,
7
+ } from "@yielded/auth/Hooks";
8
+ import { reportPersistenceFailure } from "@yielded/auth/Persistence";
9
+ import {
10
+ type ProofCompletionPlan,
11
+ ProofBinding,
12
+ type ProofAttemptDecision,
13
+ type ProofCleanupResult,
14
+ type ProofCompletionDecision,
15
+ type ProofDeliveryOutcome,
16
+ type ProofVersion,
17
+ type ProofBudget,
18
+ type ProofPolicy,
19
+ ProofPersistence,
20
+ type ProofDeliveryClaim,
21
+ ProofRequestConflict,
22
+ ProofUnavailable,
23
+ } from "@yielded/auth/Proofs";
24
+ import { Cause, Context, DateTime, Effect, Option, Schema } from "effect";
25
+ import type * as SqlError from "effect/unstable/sql/SqlError";
26
+
27
+ import { PersistenceMappingError } from "./mapping-error";
28
+ import {
29
+ type AnyProofPersistenceMapping,
30
+ requiredProofConstraints,
31
+ type ProofPersistenceMapping,
32
+ type ProofAction,
33
+ type ProofCommandDecision,
34
+ type ProofContinuationRecord,
35
+ type ProofScopeKeys,
36
+ type ProofScopeKind,
37
+ } from "./models/proof-model";
38
+ import {
39
+ CurrentPasswordPreparedTransaction,
40
+ PasswordPreparedPostconditions,
41
+ } from "./PasswordPreparedPostconditions";
42
+ /* oxlint-disable no-explicit-any -- existing storage kernels erase foreign table shapes; domain errors remain typed. */
43
+ import type { TableModel as Table } from "./query-operations";
44
+ import type { QueryFailure } from "./query-operations";
45
+ import type { QueryOperations, SqlFragment, SqlColumn } from "./query-operations";
46
+
47
+ type AdapterFailure = QueryFailure | PersistenceMappingError | SqlError.SqlError;
48
+
49
+ export interface ProofSqlQuery<A = ReadonlyArray<any>> extends Effect.Effect<A, AdapterFailure> {
50
+ readonly getSQL: () => ReturnType<QueryOperations["sql"]>;
51
+ readonly from: (...args: ReadonlyArray<any>) => ProofSqlQuery<A>;
52
+ readonly where: (...args: ReadonlyArray<any>) => ProofSqlQuery<A>;
53
+ readonly limit: (...args: ReadonlyArray<any>) => ProofSqlQuery<A>;
54
+ readonly orderBy: (...args: ReadonlyArray<any>) => ProofSqlQuery<A>;
55
+ readonly for: (...args: ReadonlyArray<any>) => ProofSqlQuery<A>;
56
+ readonly set: (...args: ReadonlyArray<any>) => ProofSqlQuery<A>;
57
+ readonly values: (...args: ReadonlyArray<any>) => ProofSqlQuery<A>;
58
+ readonly onConflictDoNothing: (...args: ReadonlyArray<any>) => ProofSqlQuery<A>;
59
+ readonly onDuplicateKeyUpdate: (...args: ReadonlyArray<any>) => ProofSqlQuery<A>;
60
+ }
61
+
62
+ export interface ProofSqlDatabase {
63
+ readonly select: (...args: ReadonlyArray<any>) => ProofSqlQuery;
64
+ readonly insert: (...args: ReadonlyArray<any>) => ProofSqlQuery;
65
+ readonly update: (...args: ReadonlyArray<any>) => ProofSqlQuery;
66
+ readonly delete: (...args: ReadonlyArray<any>) => ProofSqlQuery;
67
+ readonly transaction: <A, E, R>(
68
+ body: (transaction: ProofSqlDatabase) => Effect.Effect<A, E, R>,
69
+ ) => Effect.Effect<A, E | SqlError.SqlError, R>;
70
+ }
71
+
72
+ export class CurrentProofSql extends Context.Service<CurrentProofSql, ProofSqlDatabase>()(
73
+ "effect-auth/CurrentProofSql",
74
+ ) {}
75
+
76
+ export interface ProofSqlConfiguration {
77
+ readonly mode: "interactive" | "synchronous";
78
+ readonly locking: boolean;
79
+ readonly standaloneGuard: Effect.Effect<void, ProofUnavailable>;
80
+ readonly coordinated?: boolean;
81
+ /** Dialect-native insert-if-absent used to create serialization anchors. */
82
+ readonly insertIfAbsent: (
83
+ query: ProofSqlQuery,
84
+ selfKey: string,
85
+ selfValue: unknown,
86
+ ) => ProofSqlQuery;
87
+ }
88
+
89
+ type Database = ProofSqlDatabase;
90
+
91
+ interface ScopeEntry {
92
+ readonly kind: ProofScopeKind;
93
+ readonly key: string;
94
+ readonly budget: ProofBudget;
95
+ }
96
+
97
+ export const makeProofKernel = <
98
+ Fragment extends SqlFragment = SqlFragment,
99
+ Column extends SqlColumn = SqlColumn,
100
+ >(
101
+ operations: QueryOperations<Fragment, Column>,
102
+ ) => {
103
+ type Mapping = AnyProofPersistenceMapping<Fragment>;
104
+
105
+ const { and, eq, gte, inArray, isNull, lte, notExists, sql, column, updateValues } = operations;
106
+ const unavailable = () => ProofUnavailable.make({});
107
+
108
+ const translateFailure = <A, E, R>(
109
+ effect: Effect.Effect<A, E, R>,
110
+ ): Effect.Effect<A, ProofUnavailable, R> =>
111
+ reportPersistenceFailure(
112
+ effect,
113
+ (error) => Schema.is(ProofUnavailable)(error) || Schema.is(HookConfigurationError)(error),
114
+ ).pipe(Effect.catchCause((cause) => Effect.failCause(Cause.map(cause, () => unavailable()))));
115
+
116
+ const translateIssueFailure = <A, E, R>(
117
+ effect: Effect.Effect<A, E, R>,
118
+ ): Effect.Effect<A, ProofUnavailable | ProofRequestConflict, R> =>
119
+ reportPersistenceFailure(
120
+ effect,
121
+ (error) =>
122
+ Schema.is(ProofUnavailable)(error) ||
123
+ Schema.is(ProofRequestConflict)(error) ||
124
+ Schema.is(HookConfigurationError)(error),
125
+ ).pipe(
126
+ Effect.catchCause((cause) =>
127
+ Effect.failCause(
128
+ Cause.map(cause, (error) =>
129
+ Schema.is(ProofRequestConflict)(error) ? error : unavailable(),
130
+ ),
131
+ ),
132
+ ),
133
+ );
134
+
135
+ const conflict = () => ProofRequestConflict.make({});
136
+
137
+ const validConstraints = (mapping: Mapping) =>
138
+ Object.entries(requiredProofConstraints).every(
139
+ ([key, value]) => mapping.constraints[key as keyof typeof requiredProofConstraints] === value,
140
+ );
141
+
142
+ const selectRows = (query: ProofSqlQuery, locking: boolean) =>
143
+ locking && typeof query.for === "function" ? query.for("update") : query;
144
+
145
+ const allocateVersion = (
146
+ mapping: Mapping,
147
+ mode: ProofSqlConfiguration["mode"],
148
+ ): Effect.Effect<ProofVersion, ProofUnavailable | PersistenceMappingError> => {
149
+ if (mode === "synchronous")
150
+ return mapping.allocateVersionSync === undefined
151
+ ? Effect.fail(unavailable())
152
+ : Effect.try({
153
+ try: mapping.allocateVersionSync,
154
+ catch: (cause) => new PersistenceMappingError({ operation: "mapping", cause }),
155
+ });
156
+ if (mapping.allocateVersion !== undefined) return mapping.allocateVersion;
157
+
158
+ return mapping.allocateVersionSync === undefined
159
+ ? Effect.fail(unavailable())
160
+ : Effect.try({
161
+ try: mapping.allocateVersionSync,
162
+ catch: (cause) => new PersistenceMappingError({ operation: "mapping", cause }),
163
+ });
164
+ };
165
+
166
+ const canonicalBinding = Schema.encodeSync(Schema.fromJsonString(ProofBinding));
167
+
168
+ const sameBinding = (left: ProofBinding, right: ProofBinding): boolean =>
169
+ canonicalBinding(left) === canonicalBinding(right);
170
+
171
+ const requestColumns = (mapping: Mapping) => ({
172
+ moduleId: column(mapping.request.table, mapping.request.moduleId),
173
+ requestId: column(mapping.request.table, mapping.request.requestId),
174
+ fingerprint: column(mapping.request.table, mapping.request.fingerprint),
175
+ proofId: column(mapping.request.table, mapping.request.proofId),
176
+ retentionUntil: column(mapping.request.table, mapping.request.retentionUntil),
177
+ });
178
+
179
+ const seriesColumns = (mapping: Mapping) => ({
180
+ moduleId: column(mapping.series.table, mapping.series.moduleId),
181
+ purpose: column(mapping.series.table, mapping.series.purpose),
182
+ scopeKey: column(mapping.series.table, mapping.series.scopeKey),
183
+ activeProofId: column(mapping.series.table, mapping.series.activeProofId),
184
+ lastIssueAt: column(mapping.series.table, mapping.series.lastIssueAt),
185
+ version: column(mapping.series.table, mapping.series.version),
186
+ });
187
+
188
+ const generationColumns = (mapping: Mapping) => ({
189
+ moduleId: column(mapping.generation.table, mapping.generation.moduleId),
190
+ purpose: column(mapping.generation.table, mapping.generation.purpose),
191
+ proofId: column(mapping.generation.table, mapping.generation.proofId),
192
+ requestId: column(mapping.generation.table, mapping.generation.requestId),
193
+ seriesKey: column(mapping.generation.table, mapping.generation.seriesKey),
194
+ deliveryId: column(mapping.generation.table, mapping.generation.deliveryId),
195
+ verifierKeyId: column(mapping.generation.table, mapping.generation.verifierKeyId),
196
+ verifierDigest: column(mapping.generation.table, mapping.generation.verifierDigest),
197
+ issuedAt: column(mapping.generation.table, mapping.generation.issuedAt),
198
+ expiresAt: column(mapping.generation.table, mapping.generation.expiresAt),
199
+ version: column(mapping.generation.table, mapping.generation.version),
200
+ state: column(mapping.generation.table, mapping.generation.state),
201
+ sendCount: column(mapping.generation.table, mapping.generation.sendCount),
202
+ deliveryState: column(mapping.generation.table, mapping.generation.deliveryState),
203
+ claimVersion: column(mapping.generation.table, mapping.generation.claimVersion),
204
+ claimDeadline: column(mapping.generation.table, mapping.generation.claimDeadline),
205
+ retryAt: column(mapping.generation.table, mapping.generation.retryAt),
206
+ deliveryRetryMillis: column(mapping.generation.table, mapping.generation.deliveryRetryMillis),
207
+ retentionUntil: column(mapping.generation.table, mapping.generation.retentionUntil),
208
+ });
209
+
210
+ const continuationColumns = (mapping: Mapping) => ({
211
+ moduleId: column(mapping.continuation.table, mapping.continuation.moduleId),
212
+ purpose: column(mapping.continuation.table, mapping.continuation.purpose),
213
+ continuationId: column(mapping.continuation.table, mapping.continuation.continuationId),
214
+ digest: column(mapping.continuation.table, mapping.continuation.digest),
215
+ proofId: column(mapping.continuation.table, mapping.continuation.proofId),
216
+ seriesKey: column(mapping.continuation.table, mapping.continuation.seriesKey),
217
+ expiresAt: column(mapping.continuation.table, mapping.continuation.expiresAt),
218
+ consumed: column(mapping.continuation.table, mapping.continuation.consumed),
219
+ version: column(mapping.continuation.table, mapping.continuation.version),
220
+ retentionUntil: column(mapping.continuation.table, mapping.continuation.retentionUntil),
221
+ });
222
+
223
+ const scopeColumns = (mapping: Mapping) => ({
224
+ moduleId: column(mapping.rateScope.table, mapping.rateScope.moduleId),
225
+ purpose: column(mapping.rateScope.table, mapping.rateScope.purpose),
226
+ action: column(mapping.rateScope.table, mapping.rateScope.action),
227
+ scopeKind: column(mapping.rateScope.table, mapping.rateScope.scopeKind),
228
+ scopeKey: column(mapping.rateScope.table, mapping.rateScope.scopeKey),
229
+ });
230
+
231
+ const abuseColumns = (mapping: Mapping) => ({
232
+ moduleId: column(mapping.abuseEvent.table, mapping.abuseEvent.moduleId),
233
+ purpose: column(mapping.abuseEvent.table, mapping.abuseEvent.purpose),
234
+ action: column(mapping.abuseEvent.table, mapping.abuseEvent.action),
235
+ scopeKind: column(mapping.abuseEvent.table, mapping.abuseEvent.scopeKind),
236
+ scopeKey: column(mapping.abuseEvent.table, mapping.abuseEvent.scopeKey),
237
+ commandId: column(mapping.abuseEvent.table, mapping.abuseEvent.commandId),
238
+ occurredAt: column(mapping.abuseEvent.table, mapping.abuseEvent.occurredAt),
239
+ retentionUntil: column(mapping.abuseEvent.table, mapping.abuseEvent.retentionUntil),
240
+ });
241
+
242
+ const failureColumns = (mapping: Mapping) => ({
243
+ moduleId: column(mapping.failureEvent.table, mapping.failureEvent.moduleId),
244
+ purpose: column(mapping.failureEvent.table, mapping.failureEvent.purpose),
245
+ seriesKey: column(mapping.failureEvent.table, mapping.failureEvent.seriesKey),
246
+ commandId: column(mapping.failureEvent.table, mapping.failureEvent.commandId),
247
+ occurredAt: column(mapping.failureEvent.table, mapping.failureEvent.occurredAt),
248
+ retentionUntil: column(mapping.failureEvent.table, mapping.failureEvent.retentionUntil),
249
+ });
250
+
251
+ const commandColumns = (mapping: Mapping) => ({
252
+ moduleId: column(mapping.command.table, mapping.command.moduleId),
253
+ commandId: column(mapping.command.table, mapping.command.commandId),
254
+ kind: column(mapping.command.table, mapping.command.kind),
255
+ decision: column(mapping.command.table, mapping.command.decision),
256
+ retentionUntil: column(mapping.command.table, mapping.command.retentionUntil),
257
+ });
258
+
259
+ const nowMillis = DateTime.now.pipe(Effect.map(DateTime.toEpochMillis));
260
+
261
+ const lockAuthority = Effect.fn("DrizzleProof.lockAuthority")(function* (
262
+ mapping: Mapping,
263
+ moduleId: string,
264
+ purpose: any,
265
+ binding: ProofBinding,
266
+ locking: boolean,
267
+ ) {
268
+ const database = yield* CurrentProofSql;
269
+
270
+ const authority = mapping.authority;
271
+ let nativeSubjectId: unknown | undefined;
272
+
273
+ // Canonical order: subject, identifier rows, credentials sorted by ID.
274
+ if (binding._tag !== "Identifier") {
275
+ if (
276
+ authority.subject === undefined ||
277
+ authority.credential === undefined ||
278
+ authority.subjectId === undefined
279
+ )
280
+ return yield* unavailable();
281
+ nativeSubjectId = yield* authority.subjectId.toNative(binding.revision.subjectId);
282
+ const subject = authority.subject;
283
+
284
+ const rows = yield* selectRows(
285
+ database
286
+ .select()
287
+ .from(subject.table)
288
+ .where(eq(column(subject.table, subject.id), nativeSubjectId))
289
+ .limit(1),
290
+ locking,
291
+ );
292
+
293
+ const row = rows[0];
294
+
295
+ if (
296
+ row === undefined ||
297
+ !subject.isActiveStatus(row[subject.status]) ||
298
+ row[subject.securityRevision] !== binding.revision.securityRevision
299
+ )
300
+ return false;
301
+ }
302
+ const identifier = authority.identifier;
303
+
304
+ const identifierRows = yield* selectRows(
305
+ database
306
+ .select()
307
+ .from(identifier.table)
308
+ .where(
309
+ and(
310
+ eq(column(identifier.table, identifier.namespace), binding.identifier.namespace),
311
+ eq(column(identifier.table, identifier.value), binding.identifier.value),
312
+ ),
313
+ ),
314
+ locking,
315
+ );
316
+
317
+ if (
318
+ !identifier.isCurrent(
319
+ {
320
+ moduleId,
321
+ purpose,
322
+ binding,
323
+ ...(nativeSubjectId === undefined ? {} : { nativeSubjectId }),
324
+ },
325
+ identifierRows,
326
+ )
327
+ )
328
+ return false;
329
+ if (binding._tag === "Identifier") return true;
330
+ const credential = authority.credential!;
331
+
332
+ const expected = [...binding.revision.credentials].sort((a, b) =>
333
+ a.credentialId.localeCompare(b.credentialId),
334
+ );
335
+
336
+ const rows =
337
+ expected.length === 0
338
+ ? []
339
+ : yield* selectRows(
340
+ database
341
+ .select()
342
+ .from(credential.table)
343
+ .where(
344
+ and(
345
+ eq(column(credential.table, credential.subjectId), nativeSubjectId),
346
+ inArray(
347
+ column(credential.table, credential.credentialId),
348
+ expected.map((item) => item.credentialId),
349
+ ),
350
+ ),
351
+ ),
352
+ locking,
353
+ );
354
+
355
+ if (rows.length !== expected.length) return false;
356
+
357
+ const actual = rows
358
+ .map((row: any) => ({
359
+ credentialId: row[credential.credentialId] as string,
360
+ revision: row[credential.revision],
361
+ active:
362
+ credential.status === undefined ||
363
+ credential.isActiveStatus?.(row[credential.status]) === true,
364
+ }))
365
+ .sort((a: any, b: any) => a.credentialId.localeCompare(b.credentialId));
366
+
367
+ return actual.every(
368
+ (item: any, index: number) =>
369
+ item.active &&
370
+ item.credentialId === expected[index]?.credentialId &&
371
+ item.revision === expected[index]?.revision,
372
+ );
373
+ });
374
+
375
+ const scopeEntries = (
376
+ keys: ProofScopeKeys,
377
+ binding: ProofBinding,
378
+ action: ProofAction,
379
+ policy: ProofPolicy,
380
+ ): ReadonlyArray<ScopeEntry> => {
381
+ const suffix = action === "issue" ? "Issues" : "Attempts";
382
+ const simple = action === "issue" ? "issues" : "attempts";
383
+
384
+ const entries: ScopeEntry[] = [
385
+ { kind: "action", key: "*", budget: policy.abuse[`action${suffix}`] },
386
+ { kind: "identifier", key: keys.identifier, budget: policy.abuse[simple] },
387
+ ];
388
+
389
+ if (binding._tag !== "Identifier")
390
+ entries.push({
391
+ kind: "subject",
392
+ key: keys.subject,
393
+ budget: policy.abuse[`subject${suffix}`],
394
+ });
395
+
396
+ return entries;
397
+ };
398
+
399
+ const lockScopes = Effect.fn("DrizzleProof.lockScopes")(function* (
400
+ mapping: Mapping,
401
+ configuration: ProofSqlConfiguration,
402
+ moduleId: string,
403
+ purpose: any,
404
+ action: ProofAction,
405
+ entries: ReadonlyArray<ScopeEntry>,
406
+ ) {
407
+ const database = yield* CurrentProofSql;
408
+
409
+ const c = scopeColumns(mapping);
410
+
411
+ for (const entry of entries) {
412
+ const values = mapping.rateScope.encodeInsert({
413
+ moduleId,
414
+ purpose,
415
+ action,
416
+ scopeKind: entry.kind,
417
+ scopeKey: entry.key,
418
+ });
419
+
420
+ const query = database.insert(mapping.rateScope.table).values(values);
421
+
422
+ yield* configuration.insertIfAbsent(query, mapping.rateScope.scopeKey, entry.key);
423
+ yield* selectRows(
424
+ database
425
+ .select()
426
+ .from(mapping.rateScope.table)
427
+ .where(
428
+ and(
429
+ eq(c.moduleId, moduleId),
430
+ eq(c.purpose, purpose),
431
+ eq(c.action, action),
432
+ eq(c.scopeKind, entry.kind),
433
+ eq(c.scopeKey, entry.key),
434
+ ),
435
+ )
436
+ .limit(1),
437
+ configuration.locking,
438
+ );
439
+ }
440
+ });
441
+
442
+ const admittedScopes = Effect.fn("DrizzleProof.admittedScopes")(function* (
443
+ mapping: Mapping,
444
+ moduleId: string,
445
+ purpose: any,
446
+ action: ProofAction,
447
+ entries: ReadonlyArray<ScopeEntry>,
448
+ now: number,
449
+ ) {
450
+ const database = yield* CurrentProofSql;
451
+
452
+ const c = abuseColumns(mapping);
453
+ const admitted: ScopeEntry[] = [];
454
+
455
+ for (const entry of entries) {
456
+ const rows = yield* selectRows(
457
+ database
458
+ .select({ commandId: c.commandId })
459
+ .from(mapping.abuseEvent.table)
460
+ .where(
461
+ and(
462
+ eq(c.moduleId, moduleId),
463
+ eq(c.purpose, purpose),
464
+ eq(c.action, action),
465
+ eq(c.scopeKind, entry.kind),
466
+ eq(c.scopeKey, entry.key),
467
+ gte(c.occurredAt, mapping.encodeInstant(now - entry.budget.windowMillis)),
468
+ ),
469
+ )
470
+ .limit(entry.budget.limit),
471
+ true,
472
+ );
473
+
474
+ if (rows.length < entry.budget.limit) admitted.push(entry);
475
+ }
476
+
477
+ return admitted;
478
+ });
479
+
480
+ const insertScopeEvents = Effect.fn("Drizzle.insertScopeEvents")(function* (
481
+ mapping: Mapping,
482
+ moduleId: string,
483
+ purpose: any,
484
+ action: ProofAction,
485
+ commandId: string,
486
+ entries: ReadonlyArray<ScopeEntry>,
487
+ now: number,
488
+ retentionUntil: number,
489
+ ) {
490
+ const database = yield* CurrentProofSql;
491
+
492
+ return yield* Effect.forEach(
493
+ entries,
494
+ (entry) =>
495
+ database.insert(mapping.abuseEvent.table).values(
496
+ mapping.abuseEvent.encodeInsert({
497
+ moduleId,
498
+ purpose,
499
+ action,
500
+ scopeKind: entry.kind,
501
+ scopeKey: entry.key,
502
+ commandId,
503
+ occurredAtMillis: now,
504
+ retentionUntilMillis: retentionUntil,
505
+ }),
506
+ ),
507
+ { discard: true },
508
+ );
509
+ });
510
+
511
+ const ensureSeries = Effect.fn("DrizzleProof.ensureSeries")(function* (
512
+ mapping: Mapping,
513
+ configuration: ProofSqlConfiguration,
514
+ moduleId: string,
515
+ purpose: any,
516
+ scopeKey: string,
517
+ ) {
518
+ const database = yield* CurrentProofSql;
519
+
520
+ const c = seriesColumns(mapping);
521
+ const initialVersion = yield* allocateVersion(mapping, configuration.mode);
522
+
523
+ const query = database
524
+ .insert(mapping.series.table)
525
+ .values(
526
+ mapping.series.encodeInsert({ moduleId, purpose, scopeKey, version: initialVersion }),
527
+ );
528
+
529
+ yield* configuration.insertIfAbsent(query, mapping.series.scopeKey, scopeKey);
530
+
531
+ const rows = yield* selectRows(
532
+ database
533
+ .select()
534
+ .from(mapping.series.table)
535
+ .where(and(eq(c.moduleId, moduleId), eq(c.purpose, purpose), eq(c.scopeKey, scopeKey)))
536
+ .limit(1),
537
+ configuration.locking,
538
+ );
539
+
540
+ return rows[0] ?? (yield* unavailable());
541
+ });
542
+
543
+ const readGeneration = Effect.fn("Drizzle.readGeneration")(function* (
544
+ mapping: Mapping,
545
+ moduleId: string,
546
+ proofId: string,
547
+ locking: boolean,
548
+ ) {
549
+ const database = yield* CurrentProofSql;
550
+
551
+ const c = generationColumns(mapping);
552
+
553
+ return yield* selectRows(
554
+ database
555
+ .select()
556
+ .from(mapping.generation.table)
557
+ .where(and(eq(c.moduleId, moduleId), eq(c.proofId, proofId)))
558
+ .limit(1),
559
+ locking,
560
+ );
561
+ });
562
+
563
+ const failureCount = Effect.fn("DrizzleProof.failureCount")(function* (
564
+ mapping: Mapping,
565
+ moduleId: string,
566
+ purpose: any,
567
+ seriesKey: string,
568
+ now: number,
569
+ policy: ProofPolicy,
570
+ ) {
571
+ const database = yield* CurrentProofSql;
572
+
573
+ const c = failureColumns(mapping);
574
+
575
+ const rows = yield* selectRows(
576
+ database
577
+ .select({ commandId: c.commandId })
578
+ .from(mapping.failureEvent.table)
579
+ .where(
580
+ and(
581
+ eq(c.moduleId, moduleId),
582
+ eq(c.purpose, purpose),
583
+ eq(c.seriesKey, seriesKey),
584
+ gte(c.occurredAt, mapping.encodeInstant(now - policy.abuse.attempts.windowMillis)),
585
+ ),
586
+ )
587
+ .limit(policy.maximumFailedAttempts),
588
+ true,
589
+ );
590
+
591
+ return rows.length;
592
+ });
593
+
594
+ const commandRow = Effect.fn("Drizzle.commandRow")(function* (
595
+ mapping: Mapping,
596
+ moduleId: string,
597
+ commandId: string,
598
+ locking: boolean,
599
+ ) {
600
+ const database = yield* CurrentProofSql;
601
+
602
+ const c = commandColumns(mapping);
603
+
604
+ return yield* selectRows(
605
+ database
606
+ .select()
607
+ .from(mapping.command.table)
608
+ .where(and(eq(c.moduleId, moduleId), eq(c.commandId, commandId)))
609
+ .limit(1),
610
+ locking,
611
+ );
612
+ });
613
+
614
+ const insertCommand = Effect.fn("Drizzle.insertCommand")(function* (
615
+ mapping: Mapping,
616
+ moduleId: string,
617
+ commandId: string,
618
+ kind: "attempt" | "complete",
619
+ decision: ProofCommandDecision,
620
+ retentionUntil: number,
621
+ ) {
622
+ const database = yield* CurrentProofSql;
623
+
624
+ return yield* database.insert(mapping.command.table).values(
625
+ mapping.command.encodeInsert({
626
+ moduleId,
627
+ commandId,
628
+ kind,
629
+ decision,
630
+ retentionUntilMillis: retentionUntil,
631
+ }),
632
+ );
633
+ });
634
+
635
+ const owned = <A, E, R>(
636
+ database: Database,
637
+ mapping: Mapping,
638
+ configuration: ProofSqlConfiguration,
639
+ body: Effect.Effect<A, E, R>,
640
+ ) => {
641
+ if (!validConstraints(mapping)) return Effect.fail(unavailable());
642
+
643
+ const run = coordinateCommit(
644
+ () =>
645
+ database.transaction((transaction) =>
646
+ body.pipe(Effect.provideService(CurrentProofSql, transaction)),
647
+ ),
648
+ { mode: configuration.mode },
649
+ ).pipe(Effect.map((result) => result.value));
650
+
651
+ return configuration.coordinated !== true
652
+ ? Effect.gen(function* () {
653
+ if (yield* hasCommitScope) return yield* unavailable();
654
+ yield* configuration.standaloneGuard;
655
+
656
+ return yield* run;
657
+ })
658
+ : run;
659
+ };
660
+
661
+ const makeSqlProofPersistence = Effect.fn("makeSqlProofPersistence")(function* (
662
+ database: Database,
663
+ mapping: Mapping,
664
+ configuration: ProofSqlConfiguration,
665
+ ): Effect.fn.Return<ProofPersistence["Service"], never, LifecycleHooks> {
666
+ const hooks = yield* LifecycleHooks;
667
+
668
+ return ProofPersistence.of({
669
+ issue: (input, prepare) =>
670
+ owned(
671
+ database,
672
+ mapping,
673
+ configuration,
674
+ Effect.gen(function* () {
675
+ const transaction = yield* CurrentProofSql;
676
+ const journal = yield* CurrentCommitJournal;
677
+
678
+ const keys = mapping.scopeKeys({
679
+ moduleId: input.record.moduleId,
680
+ purpose: input.record.purpose,
681
+ binding: input.record.binding,
682
+ });
683
+
684
+ const authorityCurrent = yield* lockAuthority(
685
+ mapping,
686
+ input.record.moduleId,
687
+ input.record.purpose,
688
+ input.record.binding,
689
+ configuration.locking,
690
+ );
691
+
692
+ const scopes = scopeEntries(keys, input.record.binding, "issue", input.policy);
693
+
694
+ yield* lockScopes(
695
+ mapping,
696
+ configuration,
697
+ input.record.moduleId,
698
+ input.record.purpose,
699
+ "issue",
700
+ scopes.slice(0, 1),
701
+ );
702
+ const actionNow = yield* nowMillis;
703
+
704
+ const actionOpen =
705
+ (yield* admittedScopes(
706
+ mapping,
707
+ input.record.moduleId,
708
+ input.record.purpose,
709
+ "issue",
710
+ scopes.slice(0, 1),
711
+ actionNow,
712
+ )).length === 1;
713
+
714
+ if (actionOpen)
715
+ yield* lockScopes(
716
+ mapping,
717
+ configuration,
718
+ input.record.moduleId,
719
+ input.record.purpose,
720
+ "issue",
721
+ scopes.slice(1),
722
+ );
723
+
724
+ const series = actionOpen
725
+ ? yield* ensureSeries(
726
+ mapping,
727
+ configuration,
728
+ input.record.moduleId,
729
+ input.record.purpose,
730
+ keys.series,
731
+ )
732
+ : undefined;
733
+
734
+ const now = yield* nowMillis;
735
+ const retentionUntil = now + input.policy.requestRetentionMillis;
736
+ const rc = requestColumns(mapping);
737
+
738
+ let requestRows = yield* selectRows(
739
+ transaction
740
+ .select()
741
+ .from(mapping.request.table)
742
+ .where(
743
+ and(
744
+ eq(rc.moduleId, input.record.moduleId),
745
+ eq(rc.requestId, input.record.requestId),
746
+ ),
747
+ )
748
+ .limit(1),
749
+ configuration.locking,
750
+ );
751
+
752
+ let request = requestRows[0];
753
+
754
+ if (request !== undefined) {
755
+ const retainedUntil = yield* mapping.decodeInstant(
756
+ request[mapping.request.retentionUntil],
757
+ );
758
+
759
+ if (retainedUntil > now) {
760
+ if (request[mapping.request.fingerprint] !== input.record.fingerprint)
761
+ return yield* conflict();
762
+ const receipt = yield* mapping.request.decodeReceipt(request);
763
+
764
+ return prepare({ _tag: "Existing", receipt }, journal);
765
+ }
766
+ yield* transaction
767
+ .delete(mapping.request.table)
768
+ .where(
769
+ and(
770
+ eq(rc.moduleId, input.record.moduleId),
771
+ eq(rc.requestId, input.record.requestId),
772
+ lte(rc.retentionUntil, mapping.encodeInstant(now)),
773
+ ),
774
+ );
775
+ }
776
+
777
+ const requestInsert = mapping.request.encodeInsert({
778
+ record: input.record,
779
+ createdAtMillis: now,
780
+ retentionUntilMillis: retentionUntil,
781
+ });
782
+
783
+ const requestQuery = transaction.insert(mapping.request.table).values(requestInsert);
784
+
785
+ yield* configuration.insertIfAbsent(
786
+ requestQuery,
787
+ mapping.request.requestId,
788
+ input.record.requestId,
789
+ );
790
+ requestRows = yield* selectRows(
791
+ transaction
792
+ .select()
793
+ .from(mapping.request.table)
794
+ .where(
795
+ and(
796
+ eq(rc.moduleId, input.record.moduleId),
797
+ eq(rc.requestId, input.record.requestId),
798
+ ),
799
+ )
800
+ .limit(1),
801
+ configuration.locking,
802
+ );
803
+ request = requestRows[0];
804
+
805
+ if (request === undefined) return yield* unavailable();
806
+ if (request[mapping.request.fingerprint] !== input.record.fingerprint)
807
+ return yield* conflict();
808
+ if (request[mapping.request.proofId] !== input.record.proofId) {
809
+ const receipt = yield* mapping.request.decodeReceipt(request);
810
+
811
+ return prepare({ _tag: "Existing", receipt }, journal);
812
+ }
813
+
814
+ const admitted = actionOpen
815
+ ? yield* admittedScopes(
816
+ mapping,
817
+ input.record.moduleId,
818
+ input.record.purpose,
819
+ "issue",
820
+ scopes,
821
+ now,
822
+ )
823
+ : [];
824
+
825
+ const sc = seriesColumns(mapping);
826
+
827
+ const lastIssueAt =
828
+ series?.[mapping.series.lastIssueAt] === null ||
829
+ series?.[mapping.series.lastIssueAt] === undefined
830
+ ? undefined
831
+ : yield* mapping.decodeInstant(series[mapping.series.lastIssueAt]);
832
+
833
+ const activeProofId = series?.[mapping.series.activeProofId] as
834
+ | string
835
+ | null
836
+ | undefined;
837
+
838
+ const supersedesCurrent =
839
+ input.supersedes === undefined || input.supersedes === activeProofId;
840
+
841
+ const allowed =
842
+ authorityCurrent &&
843
+ input.eligible &&
844
+ admitted.length === scopes.length &&
845
+ input.record.issuedAtMillis <= now &&
846
+ input.record.expiresAtMillis > now &&
847
+ supersedesCurrent &&
848
+ (lastIssueAt === undefined ||
849
+ now - lastIssueAt >= input.policy.abuse.resendCooldownMillis);
850
+
851
+ const receipt = {
852
+ requestId: input.record.requestId,
853
+ reference: {
854
+ proofId: input.record.proofId,
855
+ purpose: input.record.purpose,
856
+ keyId: input.record.verifier.keyId,
857
+ },
858
+ };
859
+
860
+ const prepared = prepare(
861
+ allowed
862
+ ? ({ _tag: "Issued", record: input.record } as const)
863
+ : ({ _tag: "Suppressed", receipt } as const),
864
+ journal,
865
+ );
866
+
867
+ if (admitted.length > 0)
868
+ yield* insertScopeEvents(
869
+ mapping,
870
+ input.record.moduleId,
871
+ input.record.purpose,
872
+ "issue",
873
+ input.record.requestId,
874
+ admitted,
875
+ now,
876
+ retentionUntil,
877
+ );
878
+ if (!allowed) return prepared;
879
+ if (activeProofId !== null && activeProofId !== undefined)
880
+ yield* transaction
881
+ .update(mapping.generation.table)
882
+ .set(updateValues([[mapping.generation.state, "superseded"]]))
883
+ .where(
884
+ and(
885
+ eq(generationColumns(mapping).moduleId, input.record.moduleId),
886
+ eq(generationColumns(mapping).proofId, activeProofId),
887
+ eq(generationColumns(mapping).state, "active"),
888
+ ),
889
+ );
890
+ yield* transaction.insert(mapping.generation.table).values(
891
+ mapping.generation.encodeInsert({
892
+ record: input.record,
893
+ seriesKey: keys.series,
894
+ retentionUntilMillis: retentionUntil,
895
+ state: "active",
896
+ deliveryState: "new",
897
+ policy: input.policy,
898
+ }),
899
+ );
900
+ const nextVersion = yield* allocateVersion(mapping, configuration.mode);
901
+
902
+ yield* transaction
903
+ .update(mapping.series.table)
904
+ .set(
905
+ updateValues([
906
+ [mapping.series.activeProofId, input.record.proofId],
907
+ [mapping.series.lastIssueAt, mapping.encodeInstant(now)],
908
+ [mapping.series.version, nextVersion],
909
+ ]),
910
+ )
911
+ .where(
912
+ and(
913
+ eq(sc.moduleId, input.record.moduleId),
914
+ eq(sc.purpose, input.record.purpose),
915
+ eq(sc.scopeKey, keys.series),
916
+ ),
917
+ );
918
+
919
+ return prepared;
920
+ }),
921
+ ).pipe(
922
+ Effect.provideService(CurrentProofSql, database),
923
+ Effect.provideService(LifecycleHooks, hooks),
924
+ translateIssueFailure,
925
+ ),
926
+ attempt: (input, prepare) =>
927
+ owned(
928
+ database,
929
+ mapping,
930
+ configuration,
931
+ Effect.gen(function* () {
932
+ const transaction = yield* CurrentProofSql;
933
+ const journal = yield* CurrentCommitJournal;
934
+
935
+ const keys = mapping.scopeKeys({
936
+ moduleId: input.moduleId,
937
+ purpose: input.purpose,
938
+ binding: input.binding,
939
+ });
940
+
941
+ const authorityCurrent = yield* lockAuthority(
942
+ mapping,
943
+ input.moduleId,
944
+ input.purpose,
945
+ input.binding,
946
+ configuration.locking,
947
+ );
948
+
949
+ const scopes = scopeEntries(keys, input.binding, "attempt", input.policy);
950
+
951
+ yield* lockScopes(
952
+ mapping,
953
+ configuration,
954
+ input.moduleId,
955
+ input.purpose,
956
+ "attempt",
957
+ scopes.slice(0, 1),
958
+ );
959
+ const actionNow = yield* nowMillis;
960
+
961
+ const actionOpen =
962
+ (yield* admittedScopes(
963
+ mapping,
964
+ input.moduleId,
965
+ input.purpose,
966
+ "attempt",
967
+ scopes.slice(0, 1),
968
+ actionNow,
969
+ )).length === 1;
970
+
971
+ if (actionOpen)
972
+ yield* lockScopes(
973
+ mapping,
974
+ configuration,
975
+ input.moduleId,
976
+ input.purpose,
977
+ "attempt",
978
+ scopes.slice(1),
979
+ );
980
+
981
+ const series = actionOpen
982
+ ? yield* ensureSeries(
983
+ mapping,
984
+ configuration,
985
+ input.moduleId,
986
+ input.purpose,
987
+ keys.series,
988
+ )
989
+ : undefined;
990
+
991
+ const generation = (yield* readGeneration(
992
+ mapping,
993
+ input.moduleId,
994
+ input.proofId,
995
+ configuration.locking,
996
+ ))[0];
997
+
998
+ const existingCommand = (yield* commandRow(
999
+ mapping,
1000
+ input.moduleId,
1001
+ input.continuationId,
1002
+ configuration.locking,
1003
+ ))[0];
1004
+
1005
+ if (existingCommand !== undefined) {
1006
+ if (existingCommand[mapping.command.decision] === "rejected")
1007
+ return prepare({ _tag: "Rejected" }, journal);
1008
+
1009
+ return yield* unavailable();
1010
+ }
1011
+ const now = yield* nowMillis;
1012
+
1013
+ const admitted = actionOpen
1014
+ ? yield* admittedScopes(
1015
+ mapping,
1016
+ input.moduleId,
1017
+ input.purpose,
1018
+ "attempt",
1019
+ scopes,
1020
+ now,
1021
+ )
1022
+ : [];
1023
+
1024
+ const failures = yield* failureCount(
1025
+ mapping,
1026
+ input.moduleId,
1027
+ input.purpose,
1028
+ keys.series,
1029
+ now,
1030
+ input.policy,
1031
+ );
1032
+
1033
+ const gc = generationColumns(mapping);
1034
+ const activeProofId = series?.[mapping.series.activeProofId];
1035
+
1036
+ const storedBinding =
1037
+ generation === undefined
1038
+ ? undefined
1039
+ : yield* mapping.generation.decodeBinding(generation);
1040
+
1041
+ const candidateMatches =
1042
+ generation !== undefined &&
1043
+ input.candidate !== undefined &&
1044
+ generation[mapping.generation.verifierKeyId] === input.candidate.keyId &&
1045
+ generation[mapping.generation.verifierDigest] === input.candidate.digest;
1046
+
1047
+ const currentGeneration =
1048
+ generation !== undefined &&
1049
+ generation[mapping.generation.purpose] === input.purpose &&
1050
+ generation[mapping.generation.state] === "active" &&
1051
+ activeProofId === input.proofId &&
1052
+ storedBinding !== undefined &&
1053
+ sameBinding(storedBinding, input.binding) &&
1054
+ Number(yield* mapping.decodeInstant(generation[mapping.generation.expiresAt])) > now;
1055
+
1056
+ const accepted =
1057
+ authorityCurrent &&
1058
+ currentGeneration &&
1059
+ candidateMatches &&
1060
+ admitted.length === scopes.length &&
1061
+ failures < input.policy.maximumFailedAttempts;
1062
+
1063
+ const retentionUntil = now + input.policy.requestRetentionMillis;
1064
+
1065
+ if (!accepted) {
1066
+ const prepared = prepare({ _tag: "Rejected" }, journal);
1067
+
1068
+ yield* insertCommand(
1069
+ mapping,
1070
+ input.moduleId,
1071
+ input.continuationId,
1072
+ "attempt",
1073
+ "rejected",
1074
+ retentionUntil,
1075
+ );
1076
+ yield* insertScopeEvents(
1077
+ mapping,
1078
+ input.moduleId,
1079
+ input.purpose,
1080
+ "attempt",
1081
+ input.continuationId,
1082
+ admitted,
1083
+ now,
1084
+ retentionUntil,
1085
+ );
1086
+ if (
1087
+ admitted.length === scopes.length &&
1088
+ currentGeneration &&
1089
+ !candidateMatches &&
1090
+ failures < input.policy.maximumFailedAttempts
1091
+ )
1092
+ yield* transaction.insert(mapping.failureEvent.table).values(
1093
+ mapping.failureEvent.encodeInsert({
1094
+ moduleId: input.moduleId,
1095
+ purpose: input.purpose,
1096
+ seriesKey: keys.series,
1097
+ commandId: input.continuationId,
1098
+ occurredAtMillis: now,
1099
+ retentionUntilMillis: retentionUntil,
1100
+ }),
1101
+ );
1102
+
1103
+ return prepared;
1104
+ }
1105
+ const version = yield* allocateVersion(mapping, configuration.mode);
1106
+
1107
+ const expiresAt = Math.min(
1108
+ Number(yield* mapping.decodeInstant(generation![mapping.generation.expiresAt])),
1109
+ now + input.policy.continuationLifetimeMillis,
1110
+ );
1111
+
1112
+ const continuation: ProofContinuationRecord = {
1113
+ moduleId: input.moduleId,
1114
+ purpose: input.purpose,
1115
+ continuationId: input.continuationId,
1116
+ digest: input.continuationDigest,
1117
+ proofId: input.proofId,
1118
+ seriesKey: keys.series,
1119
+ binding: input.binding,
1120
+ expiresAtMillis: expiresAt,
1121
+ version,
1122
+ };
1123
+
1124
+ const decision: ProofAttemptDecision = {
1125
+ _tag: "Accepted",
1126
+ continuation: {
1127
+ continuationId: input.continuationId,
1128
+ purpose: input.purpose,
1129
+ expiresAtMillis: expiresAt,
1130
+ },
1131
+ };
1132
+
1133
+ const prepared = prepare(decision, journal);
1134
+
1135
+ yield* insertCommand(
1136
+ mapping,
1137
+ input.moduleId,
1138
+ input.continuationId,
1139
+ "attempt",
1140
+ "accepted",
1141
+ retentionUntil,
1142
+ );
1143
+ yield* insertScopeEvents(
1144
+ mapping,
1145
+ input.moduleId,
1146
+ input.purpose,
1147
+ "attempt",
1148
+ input.continuationId,
1149
+ scopes,
1150
+ now,
1151
+ retentionUntil,
1152
+ );
1153
+ yield* transaction
1154
+ .update(mapping.generation.table)
1155
+ .set(updateValues([[mapping.generation.state, "consumed"]]))
1156
+ .where(
1157
+ and(
1158
+ eq(gc.moduleId, input.moduleId),
1159
+ eq(gc.proofId, input.proofId),
1160
+ eq(gc.state, "active"),
1161
+ ),
1162
+ );
1163
+ yield* transaction.insert(mapping.continuation.table).values(
1164
+ mapping.continuation.encodeInsert({
1165
+ ...continuation,
1166
+ retentionUntilMillis: retentionUntil,
1167
+ }),
1168
+ );
1169
+
1170
+ return prepared;
1171
+ }),
1172
+ ).pipe(
1173
+ Effect.provideService(CurrentProofSql, database),
1174
+ Effect.provideService(LifecycleHooks, hooks),
1175
+ translateFailure,
1176
+ ),
1177
+ complete: (input, prepare) =>
1178
+ owned(
1179
+ database,
1180
+ mapping,
1181
+ configuration,
1182
+ Effect.gen(function* () {
1183
+ const journal = yield* CurrentCommitJournal;
1184
+
1185
+ return yield* completeIn(mapping, configuration, input, (decision) =>
1186
+ prepare(decision, journal),
1187
+ );
1188
+ }),
1189
+ ).pipe(
1190
+ Effect.provideService(CurrentProofSql, database),
1191
+ Effect.provideService(LifecycleHooks, hooks),
1192
+ translateFailure,
1193
+ ),
1194
+ claimDelivery: (input, prepare) =>
1195
+ owned(
1196
+ database,
1197
+ mapping,
1198
+ configuration,
1199
+ Effect.gen(function* () {
1200
+ const transaction = yield* CurrentProofSql;
1201
+ const journal = yield* CurrentCommitJournal;
1202
+
1203
+ const rows = yield* readGeneration(
1204
+ mapping,
1205
+ input.moduleId,
1206
+ input.proofId,
1207
+ configuration.locking,
1208
+ );
1209
+
1210
+ const row = rows[0];
1211
+ const now = yield* nowMillis;
1212
+
1213
+ if (row === undefined) return prepare({ _tag: "Declined" }, journal);
1214
+ const c = generationColumns(mapping);
1215
+ const expiresAt = yield* mapping.decodeInstant(row[mapping.generation.expiresAt]);
1216
+
1217
+ const claimDeadline =
1218
+ row[mapping.generation.claimDeadline] === null ||
1219
+ row[mapping.generation.claimDeadline] === undefined
1220
+ ? undefined
1221
+ : yield* mapping.decodeInstant(row[mapping.generation.claimDeadline]);
1222
+
1223
+ const retryAt =
1224
+ row[mapping.generation.retryAt] === null ||
1225
+ row[mapping.generation.retryAt] === undefined
1226
+ ? undefined
1227
+ : yield* mapping.decodeInstant(row[mapping.generation.retryAt]);
1228
+
1229
+ let state = row[mapping.generation.deliveryState] as string;
1230
+
1231
+ if (state === "claimed" && claimDeadline !== undefined && claimDeadline <= now) {
1232
+ state = "ambiguous";
1233
+ yield* transaction
1234
+ .update(mapping.generation.table)
1235
+ .set(updateValues([[mapping.generation.deliveryState, "ambiguous"]]))
1236
+ .where(and(eq(c.moduleId, input.moduleId), eq(c.proofId, input.proofId)));
1237
+ }
1238
+
1239
+ const canClaim =
1240
+ row[mapping.generation.version] === input.version &&
1241
+ row[mapping.generation.deliveryId] === input.deliveryId &&
1242
+ row[mapping.generation.state] === "active" &&
1243
+ expiresAt > now &&
1244
+ Number(row[mapping.generation.sendCount]) < input.policy.maximumDeliveryAttempts &&
1245
+ (state === "new" ||
1246
+ (state === "ambiguous" &&
1247
+ input.allowAmbiguousRetry &&
1248
+ (retryAt === undefined || retryAt <= now)));
1249
+
1250
+ if (!canClaim) return prepare({ _tag: "Declined" }, journal);
1251
+ const claimVersion = yield* allocateVersion(mapping, configuration.mode);
1252
+ const decision: ProofDeliveryClaim = { _tag: "Claimed", claimVersion };
1253
+ const prepared = prepare(decision, journal);
1254
+
1255
+ yield* transaction
1256
+ .update(mapping.generation.table)
1257
+ .set(
1258
+ updateValues([
1259
+ [mapping.generation.sendCount, Number(row[mapping.generation.sendCount]) + 1],
1260
+ [mapping.generation.deliveryState, "claimed"],
1261
+ [mapping.generation.claimVersion, claimVersion],
1262
+ [
1263
+ mapping.generation.claimDeadline,
1264
+ mapping.encodeInstant(now + input.policy.deliveryClaimMillis),
1265
+ ],
1266
+ [
1267
+ mapping.generation.retryAt,
1268
+ mapping.encodeInstant(now + input.policy.deliveryRetryMillis),
1269
+ ],
1270
+ ]),
1271
+ )
1272
+ .where(and(eq(c.moduleId, input.moduleId), eq(c.proofId, input.proofId)));
1273
+
1274
+ return prepared;
1275
+ }),
1276
+ ).pipe(
1277
+ Effect.provideService(CurrentProofSql, database),
1278
+ Effect.provideService(LifecycleHooks, hooks),
1279
+ translateFailure,
1280
+ ),
1281
+ settleDelivery: (input, prepare) =>
1282
+ owned(
1283
+ database,
1284
+ mapping,
1285
+ configuration,
1286
+ Effect.gen(function* () {
1287
+ const transaction = yield* CurrentProofSql;
1288
+ const journal = yield* CurrentCommitJournal;
1289
+
1290
+ const initial = (yield* readGeneration(
1291
+ mapping,
1292
+ input.moduleId,
1293
+ input.proofId,
1294
+ false,
1295
+ ))[0];
1296
+
1297
+ if (initial === undefined) return prepare(undefined, journal);
1298
+ const keys = initial[mapping.generation.seriesKey] as string;
1299
+
1300
+ yield* ensureSeries(
1301
+ mapping,
1302
+ configuration,
1303
+ input.moduleId,
1304
+ initial[mapping.generation.purpose],
1305
+ keys,
1306
+ );
1307
+
1308
+ const row = (yield* readGeneration(
1309
+ mapping,
1310
+ input.moduleId,
1311
+ input.proofId,
1312
+ configuration.locking,
1313
+ ))[0];
1314
+
1315
+ const prepared = prepare(undefined, journal);
1316
+
1317
+ if (
1318
+ row === undefined ||
1319
+ row[mapping.generation.version] !== input.version ||
1320
+ row[mapping.generation.deliveryId] !== input.deliveryId ||
1321
+ row[mapping.generation.claimVersion] !== input.claimVersion ||
1322
+ row[mapping.generation.deliveryState] !== "claimed"
1323
+ )
1324
+ return prepared;
1325
+ const c = generationColumns(mapping);
1326
+ const outcome: ProofDeliveryOutcome = input.outcome;
1327
+
1328
+ const state =
1329
+ outcome._tag === "Accepted"
1330
+ ? "accepted"
1331
+ : outcome._tag === "DefiniteFailure"
1332
+ ? "failed"
1333
+ : "ambiguous";
1334
+
1335
+ const values: Array<readonly [string, unknown]> = [
1336
+ [mapping.generation.deliveryState, state],
1337
+ [mapping.generation.claimDeadline, null],
1338
+ ];
1339
+
1340
+ if (outcome._tag === "Ambiguous")
1341
+ values.push([
1342
+ mapping.generation.retryAt,
1343
+ mapping.encodeInstant(
1344
+ (yield* nowMillis) + Number(row[mapping.generation.deliveryRetryMillis]),
1345
+ ),
1346
+ ]);
1347
+ if (outcome._tag === "DefiniteFailure")
1348
+ values.push([mapping.generation.state, "cancelled"]);
1349
+ yield* transaction
1350
+ .update(mapping.generation.table)
1351
+ .set(updateValues(values))
1352
+ .where(and(eq(c.moduleId, input.moduleId), eq(c.proofId, input.proofId)));
1353
+ if (outcome._tag === "DefiniteFailure") {
1354
+ const sc = seriesColumns(mapping);
1355
+
1356
+ yield* transaction
1357
+ .update(mapping.series.table)
1358
+ .set(updateValues([[mapping.series.activeProofId, null]]))
1359
+ .where(
1360
+ and(
1361
+ eq(sc.moduleId, input.moduleId),
1362
+ eq(sc.purpose, row[mapping.generation.purpose]),
1363
+ eq(sc.scopeKey, keys),
1364
+ eq(sc.activeProofId, input.proofId),
1365
+ ),
1366
+ );
1367
+ }
1368
+
1369
+ return prepared;
1370
+ }),
1371
+ ).pipe(
1372
+ Effect.provideService(CurrentProofSql, database),
1373
+ Effect.provideService(LifecycleHooks, hooks),
1374
+ translateFailure,
1375
+ ),
1376
+ cancel: (input, prepare) =>
1377
+ owned(
1378
+ database,
1379
+ mapping,
1380
+ configuration,
1381
+ Effect.gen(function* () {
1382
+ const transaction = yield* CurrentProofSql;
1383
+ const journal = yield* CurrentCommitJournal;
1384
+
1385
+ const authorityCurrent = yield* lockAuthority(
1386
+ mapping,
1387
+ input.moduleId,
1388
+ input.purpose,
1389
+ input.binding,
1390
+ configuration.locking,
1391
+ );
1392
+
1393
+ const keys = mapping.scopeKeys({
1394
+ moduleId: input.moduleId,
1395
+ purpose: input.purpose,
1396
+ binding: input.binding,
1397
+ });
1398
+
1399
+ const series = yield* ensureSeries(
1400
+ mapping,
1401
+ configuration,
1402
+ input.moduleId,
1403
+ input.purpose,
1404
+ keys.series,
1405
+ );
1406
+
1407
+ const active = series[mapping.series.activeProofId] as string | undefined | null;
1408
+ const prepared = prepare(undefined, journal);
1409
+
1410
+ if (authorityCurrent && active !== undefined && active !== null) {
1411
+ const gc = generationColumns(mapping);
1412
+ const sc = seriesColumns(mapping);
1413
+
1414
+ const row = (yield* readGeneration(
1415
+ mapping,
1416
+ input.moduleId,
1417
+ active,
1418
+ configuration.locking,
1419
+ ))[0];
1420
+
1421
+ const storedBinding =
1422
+ row === undefined ? undefined : yield* mapping.generation.decodeBinding(row);
1423
+
1424
+ if (storedBinding === undefined || !sameBinding(storedBinding, input.binding))
1425
+ return prepared;
1426
+
1427
+ yield* transaction
1428
+ .update(mapping.generation.table)
1429
+ .set(updateValues([[mapping.generation.state, "cancelled"]]))
1430
+ .where(and(eq(gc.moduleId, input.moduleId), eq(gc.proofId, active)));
1431
+ yield* transaction
1432
+ .update(mapping.series.table)
1433
+ .set(updateValues([[mapping.series.activeProofId, null]]))
1434
+ .where(
1435
+ and(
1436
+ eq(sc.moduleId, input.moduleId),
1437
+ eq(sc.purpose, input.purpose),
1438
+ eq(sc.scopeKey, keys.series),
1439
+ eq(sc.activeProofId, active),
1440
+ ),
1441
+ );
1442
+ }
1443
+
1444
+ return prepared;
1445
+ }),
1446
+ ).pipe(
1447
+ Effect.provideService(CurrentProofSql, database),
1448
+ Effect.provideService(LifecycleHooks, hooks),
1449
+ translateFailure,
1450
+ ),
1451
+ cleanup: (input, prepare) =>
1452
+ owned(
1453
+ database,
1454
+ mapping,
1455
+ configuration,
1456
+ Effect.gen(function* () {
1457
+ const transaction = yield* CurrentProofSql;
1458
+ const journal = yield* CurrentCommitJournal;
1459
+
1460
+ const now = yield* nowMillis;
1461
+ const nativeNow = mapping.encodeInstant(now);
1462
+ const gc = generationColumns(mapping);
1463
+ const cc = continuationColumns(mapping);
1464
+ const rc = requestColumns(mapping);
1465
+ const ac = abuseColumns(mapping);
1466
+ const fc = failureColumns(mapping);
1467
+ const mc = commandColumns(mapping);
1468
+ const sc = seriesColumns(mapping);
1469
+ const rsc = scopeColumns(mapping);
1470
+ let remaining = input.limit;
1471
+ let hasMore = false;
1472
+
1473
+ const take = <A>(query: ProofSqlQuery<ReadonlyArray<A>>) =>
1474
+ selectRows(query.limit(remaining + 1), configuration.locking).pipe(
1475
+ Effect.map((rows) => {
1476
+ if (rows.length > remaining) hasMore = true;
1477
+ const selected = rows.slice(0, remaining);
1478
+
1479
+ remaining -= selected.length;
1480
+
1481
+ return selected;
1482
+ }),
1483
+ );
1484
+
1485
+ // Child rows and secret-bearing generations are removed before their
1486
+ // retained request/decision records. Every selected row counts toward
1487
+ // one caller-wide limit and is locked until this owner commits.
1488
+ const continuations = yield* take(
1489
+ transaction
1490
+ .select({ continuationId: cc.continuationId })
1491
+ .from(mapping.continuation.table)
1492
+ .where(and(eq(cc.moduleId, input.moduleId), lte(cc.retentionUntil, nativeNow)))
1493
+ .orderBy(cc.retentionUntil),
1494
+ );
1495
+
1496
+ const generations = yield* take(
1497
+ transaction
1498
+ .select({ proofId: gc.proofId })
1499
+ .from(mapping.generation.table)
1500
+ .where(and(eq(gc.moduleId, input.moduleId), lte(gc.retentionUntil, nativeNow)))
1501
+ .orderBy(gc.retentionUntil),
1502
+ );
1503
+
1504
+ const requests = yield* take(
1505
+ transaction
1506
+ .select({ requestId: rc.requestId })
1507
+ .from(mapping.request.table)
1508
+ .where(and(eq(rc.moduleId, input.moduleId), lte(rc.retentionUntil, nativeNow)))
1509
+ .orderBy(rc.retentionUntil),
1510
+ );
1511
+
1512
+ const abuse = yield* take(
1513
+ transaction
1514
+ .select({
1515
+ action: ac.action,
1516
+ scopeKind: ac.scopeKind,
1517
+ scopeKey: ac.scopeKey,
1518
+ commandId: ac.commandId,
1519
+ })
1520
+ .from(mapping.abuseEvent.table)
1521
+ .where(and(eq(ac.moduleId, input.moduleId), lte(ac.retentionUntil, nativeNow)))
1522
+ .orderBy(ac.retentionUntil),
1523
+ );
1524
+
1525
+ const failures = yield* take(
1526
+ transaction
1527
+ .select({ seriesKey: fc.seriesKey, commandId: fc.commandId })
1528
+ .from(mapping.failureEvent.table)
1529
+ .where(and(eq(fc.moduleId, input.moduleId), lte(fc.retentionUntil, nativeNow)))
1530
+ .orderBy(fc.retentionUntil),
1531
+ );
1532
+
1533
+ const commands = yield* take(
1534
+ transaction
1535
+ .select({ commandId: mc.commandId })
1536
+ .from(mapping.command.table)
1537
+ .where(and(eq(mc.moduleId, input.moduleId), lte(mc.retentionUntil, nativeNow)))
1538
+ .orderBy(mc.retentionUntil),
1539
+ );
1540
+
1541
+ const series = yield* take(
1542
+ transaction
1543
+ .select({ purpose: sc.purpose, scopeKey: sc.scopeKey })
1544
+ .from(mapping.series.table)
1545
+ .where(
1546
+ and(
1547
+ eq(sc.moduleId, input.moduleId),
1548
+ isNull(sc.activeProofId),
1549
+ notExists(
1550
+ transaction
1551
+ .select({ one: sql`1` })
1552
+ .from(mapping.generation.table)
1553
+ .where(
1554
+ and(
1555
+ eq(gc.moduleId, sc.moduleId),
1556
+ eq(gc.purpose, sc.purpose),
1557
+ eq(gc.seriesKey, sc.scopeKey),
1558
+ ),
1559
+ ),
1560
+ ),
1561
+ notExists(
1562
+ transaction
1563
+ .select({ one: sql`1` })
1564
+ .from(mapping.failureEvent.table)
1565
+ .where(
1566
+ and(
1567
+ eq(fc.moduleId, sc.moduleId),
1568
+ eq(fc.purpose, sc.purpose),
1569
+ eq(fc.seriesKey, sc.scopeKey),
1570
+ ),
1571
+ ),
1572
+ ),
1573
+ ),
1574
+ ),
1575
+ );
1576
+
1577
+ const rateScopes = yield* take(
1578
+ transaction
1579
+ .select({
1580
+ purpose: rsc.purpose,
1581
+ action: rsc.action,
1582
+ scopeKind: rsc.scopeKind,
1583
+ scopeKey: rsc.scopeKey,
1584
+ })
1585
+ .from(mapping.rateScope.table)
1586
+ .where(
1587
+ and(
1588
+ eq(rsc.moduleId, input.moduleId),
1589
+ notExists(
1590
+ transaction
1591
+ .select({ one: sql`1` })
1592
+ .from(mapping.abuseEvent.table)
1593
+ .where(
1594
+ and(
1595
+ eq(ac.moduleId, rsc.moduleId),
1596
+ eq(ac.purpose, rsc.purpose),
1597
+ eq(ac.action, rsc.action),
1598
+ eq(ac.scopeKind, rsc.scopeKind),
1599
+ eq(ac.scopeKey, rsc.scopeKey),
1600
+ ),
1601
+ ),
1602
+ ),
1603
+ ),
1604
+ ),
1605
+ );
1606
+
1607
+ // Removing the final history row can make an anchor eligible only after this
1608
+ // transaction/batch commits. One extra page lets the caller reclaim it.
1609
+ hasMore ||= generations.length > 0 || failures.length > 0 || abuse.length > 0;
1610
+ const removed = input.limit - remaining;
1611
+ const prepared = prepare({ removed, hasMore } satisfies ProofCleanupResult, journal);
1612
+
1613
+ if (continuations.length > 0)
1614
+ yield* transaction.delete(mapping.continuation.table).where(
1615
+ and(
1616
+ eq(cc.moduleId, input.moduleId),
1617
+ inArray(
1618
+ cc.continuationId,
1619
+ continuations.map((row: any) => row.continuationId),
1620
+ ),
1621
+ lte(cc.retentionUntil, nativeNow),
1622
+ ),
1623
+ );
1624
+ if (generations.length > 0)
1625
+ yield* transaction
1626
+ .update(mapping.series.table)
1627
+ .set(updateValues([[mapping.series.activeProofId, null]]))
1628
+ .where(
1629
+ and(
1630
+ eq(sc.moduleId, input.moduleId),
1631
+ inArray(
1632
+ sc.activeProofId,
1633
+ generations.map((row: any) => row.proofId),
1634
+ ),
1635
+ ),
1636
+ );
1637
+ if (generations.length > 0)
1638
+ yield* transaction.delete(mapping.generation.table).where(
1639
+ and(
1640
+ eq(gc.moduleId, input.moduleId),
1641
+ inArray(
1642
+ gc.proofId,
1643
+ generations.map((row: any) => row.proofId),
1644
+ ),
1645
+ lte(gc.retentionUntil, nativeNow),
1646
+ ),
1647
+ );
1648
+ if (requests.length > 0)
1649
+ yield* transaction.delete(mapping.request.table).where(
1650
+ and(
1651
+ eq(rc.moduleId, input.moduleId),
1652
+ inArray(
1653
+ rc.requestId,
1654
+ requests.map((row: any) => row.requestId),
1655
+ ),
1656
+ lte(rc.retentionUntil, nativeNow),
1657
+ ),
1658
+ );
1659
+ for (const row of abuse)
1660
+ yield* transaction
1661
+ .delete(mapping.abuseEvent.table)
1662
+ .where(
1663
+ and(
1664
+ eq(ac.moduleId, input.moduleId),
1665
+ eq(ac.action, row.action),
1666
+ eq(ac.scopeKind, row.scopeKind),
1667
+ eq(ac.scopeKey, row.scopeKey),
1668
+ eq(ac.commandId, row.commandId),
1669
+ lte(ac.retentionUntil, nativeNow),
1670
+ ),
1671
+ );
1672
+ for (const row of failures)
1673
+ yield* transaction
1674
+ .delete(mapping.failureEvent.table)
1675
+ .where(
1676
+ and(
1677
+ eq(fc.moduleId, input.moduleId),
1678
+ eq(fc.seriesKey, row.seriesKey),
1679
+ eq(fc.commandId, row.commandId),
1680
+ lte(fc.retentionUntil, nativeNow),
1681
+ ),
1682
+ );
1683
+ if (commands.length > 0)
1684
+ yield* transaction.delete(mapping.command.table).where(
1685
+ and(
1686
+ eq(mc.moduleId, input.moduleId),
1687
+ inArray(
1688
+ mc.commandId,
1689
+ commands.map((row: any) => row.commandId),
1690
+ ),
1691
+ lte(mc.retentionUntil, nativeNow),
1692
+ ),
1693
+ );
1694
+ for (const row of series)
1695
+ yield* transaction.delete(mapping.series.table).where(
1696
+ and(
1697
+ eq(sc.moduleId, input.moduleId),
1698
+ eq(sc.purpose, row.purpose),
1699
+ eq(sc.scopeKey, row.scopeKey),
1700
+ isNull(sc.activeProofId),
1701
+ notExists(
1702
+ transaction
1703
+ .select({ one: sql`1` })
1704
+ .from(mapping.generation.table)
1705
+ .where(
1706
+ and(
1707
+ eq(gc.moduleId, sc.moduleId),
1708
+ eq(gc.purpose, sc.purpose),
1709
+ eq(gc.seriesKey, sc.scopeKey),
1710
+ ),
1711
+ ),
1712
+ ),
1713
+ ),
1714
+ );
1715
+ for (const row of rateScopes)
1716
+ yield* transaction.delete(mapping.rateScope.table).where(
1717
+ and(
1718
+ eq(rsc.moduleId, input.moduleId),
1719
+ eq(rsc.purpose, row.purpose),
1720
+ eq(rsc.action, row.action),
1721
+ eq(rsc.scopeKind, row.scopeKind),
1722
+ eq(rsc.scopeKey, row.scopeKey),
1723
+ notExists(
1724
+ transaction
1725
+ .select({ one: sql`1` })
1726
+ .from(mapping.abuseEvent.table)
1727
+ .where(
1728
+ and(
1729
+ eq(ac.moduleId, rsc.moduleId),
1730
+ eq(ac.purpose, rsc.purpose),
1731
+ eq(ac.action, rsc.action),
1732
+ eq(ac.scopeKind, rsc.scopeKind),
1733
+ eq(ac.scopeKey, rsc.scopeKey),
1734
+ ),
1735
+ ),
1736
+ ),
1737
+ ),
1738
+ );
1739
+
1740
+ return prepared;
1741
+ }),
1742
+ ).pipe(
1743
+ Effect.provideService(CurrentProofSql, database),
1744
+ Effect.provideService(LifecycleHooks, hooks),
1745
+ translateFailure,
1746
+ ),
1747
+ });
1748
+ });
1749
+
1750
+ const inspectCompletionIn = Effect.fn("DrizzleProof.inspectCompletionIn")(function* (
1751
+ mapping: Mapping,
1752
+ configuration: ProofSqlConfiguration,
1753
+ input: ProofCompletionPlan["input"],
1754
+ ensure: boolean,
1755
+ ) {
1756
+ const database = yield* CurrentProofSql;
1757
+
1758
+ const authorityCurrent = yield* lockAuthority(
1759
+ mapping,
1760
+ input.moduleId,
1761
+ input.purpose,
1762
+ input.binding,
1763
+ configuration.locking,
1764
+ );
1765
+
1766
+ const keys = mapping.scopeKeys({
1767
+ moduleId: input.moduleId,
1768
+ purpose: input.purpose,
1769
+ binding: input.binding,
1770
+ });
1771
+
1772
+ if (ensure)
1773
+ yield* ensureSeries(mapping, configuration, input.moduleId, input.purpose, keys.series);
1774
+ const c = continuationColumns(mapping);
1775
+
1776
+ const rows = yield* selectRows(
1777
+ database
1778
+ .select()
1779
+ .from(mapping.continuation.table)
1780
+ .where(and(eq(c.moduleId, input.moduleId), eq(c.continuationId, input.continuationId)))
1781
+ .limit(1),
1782
+ configuration.locking,
1783
+ );
1784
+
1785
+ const row = rows[0];
1786
+ const now = yield* nowMillis;
1787
+ const record = row === undefined ? undefined : yield* mapping.continuation.decode(row);
1788
+
1789
+ const completed =
1790
+ authorityCurrent &&
1791
+ row !== undefined &&
1792
+ record !== undefined &&
1793
+ record.moduleId === input.moduleId &&
1794
+ record.continuationId === input.continuationId &&
1795
+ record.purpose === input.purpose &&
1796
+ record.digest === input.continuationDigest &&
1797
+ record.seriesKey === keys.series &&
1798
+ sameBinding(record.binding, input.binding) &&
1799
+ row[mapping.continuation.consumed] === false &&
1800
+ record.expiresAtMillis > now;
1801
+
1802
+ return completed ? { record: record!, row, columns: c } : undefined;
1803
+ });
1804
+
1805
+ /** Nonconsuming same-owner preflight; it never inserts a series or changes expiry. */
1806
+ const checkProofCompletionIn = Effect.fn("Drizzle.checkProofCompletionIn")(function* (
1807
+ mapping: Mapping,
1808
+ configuration: ProofSqlConfiguration,
1809
+ input: ProofCompletionPlan["input"],
1810
+ ) {
1811
+ return yield* inspectCompletionIn(mapping, configuration, input, false).pipe(
1812
+ Effect.map((value) => value !== undefined),
1813
+ );
1814
+ });
1815
+
1816
+ const completeIn = Effect.fn("DrizzleProof.completeIn")(function* <A, E = never, R = never>(
1817
+ mapping: Mapping,
1818
+ configuration: ProofSqlConfiguration,
1819
+ input: ProofCompletionPlan["input"],
1820
+ prepare: (decision: ProofCompletionDecision) => A,
1821
+ protectedMutation?: Effect.Effect<boolean, E, R>,
1822
+ ) {
1823
+ const database = yield* CurrentProofSql;
1824
+
1825
+ const accepted = yield* inspectCompletionIn(mapping, configuration, input, true);
1826
+
1827
+ if (accepted === undefined) return prepare("rejected");
1828
+ const { record, row: original, columns: c } = accepted;
1829
+
1830
+ if (protectedMutation !== undefined && !(yield* protectedMutation)) return yield* unavailable();
1831
+ const value = prepare("completed");
1832
+
1833
+ yield* database
1834
+ .update(mapping.continuation.table)
1835
+ .set(updateValues([[mapping.continuation.consumed, true]]))
1836
+ .where(
1837
+ and(
1838
+ eq(c.moduleId, input.moduleId),
1839
+ eq(c.continuationId, input.continuationId),
1840
+ eq(c.digest, input.continuationDigest),
1841
+ eq(c.consumed, false),
1842
+ ),
1843
+ );
1844
+ yield* insertCommand(
1845
+ mapping,
1846
+ input.moduleId,
1847
+ input.continuationDigest,
1848
+ "complete",
1849
+ "completed",
1850
+ record.expiresAtMillis,
1851
+ );
1852
+
1853
+ const assertApplied = Effect.flatMap(CurrentPasswordPreparedTransaction, (currentTransaction) =>
1854
+ Effect.gen(function* () {
1855
+ const persisted = (yield* selectRows(
1856
+ currentTransaction
1857
+ .select()
1858
+ .from(mapping.continuation.table)
1859
+ .where(and(eq(c.moduleId, input.moduleId), eq(c.continuationId, input.continuationId)))
1860
+ .limit(1),
1861
+ false,
1862
+ ))[0];
1863
+
1864
+ const command = (yield* commandRow(
1865
+ mapping,
1866
+ input.moduleId,
1867
+ input.continuationDigest,
1868
+ false,
1869
+ ))[0];
1870
+
1871
+ const decoded =
1872
+ persisted === undefined ? undefined : yield* mapping.continuation.decode(persisted);
1873
+
1874
+ const immutable = [
1875
+ mapping.continuation.moduleId,
1876
+ mapping.continuation.purpose,
1877
+ mapping.continuation.continuationId,
1878
+ mapping.continuation.digest,
1879
+ mapping.continuation.proofId,
1880
+ mapping.continuation.seriesKey,
1881
+ mapping.continuation.version,
1882
+ ];
1883
+
1884
+ if (
1885
+ persisted === undefined ||
1886
+ decoded === undefined ||
1887
+ !sameBinding(decoded.binding, record.binding) ||
1888
+ persisted[mapping.continuation.consumed] !== true ||
1889
+ immutable.some((key) => persisted[key] !== original[key]) ||
1890
+ (yield* mapping.decodeInstant(persisted[mapping.continuation.expiresAt])) !==
1891
+ record.expiresAtMillis ||
1892
+ (yield* mapping.decodeInstant(persisted[mapping.continuation.retentionUntil])) !==
1893
+ (yield* mapping.decodeInstant(original[mapping.continuation.retentionUntil])) ||
1894
+ command === undefined ||
1895
+ command[mapping.command.kind] !== "complete" ||
1896
+ command[mapping.command.decision] !== "completed" ||
1897
+ (yield* mapping.decodeInstant(command[mapping.command.retentionUntil])) !==
1898
+ record.expiresAtMillis
1899
+ )
1900
+ return yield* unavailable();
1901
+
1902
+ if (record.expiresAtMillis <= (yield* nowMillis)) return yield* unavailable();
1903
+ }).pipe(Effect.provideService(CurrentProofSql, currentTransaction)),
1904
+ );
1905
+
1906
+ yield* assertApplied.pipe(Effect.provideService(CurrentPasswordPreparedTransaction, database));
1907
+ const postconditions = yield* Effect.serviceOption(PasswordPreparedPostconditions);
1908
+
1909
+ if (
1910
+ Option.isSome(postconditions) &&
1911
+ !postconditions.value.register(assertApplied.pipe(translateFailure))
1912
+ )
1913
+ return yield* unavailable();
1914
+
1915
+ return value;
1916
+ });
1917
+
1918
+ /** Private same-owner seam for password and identifier mutations. */
1919
+ const completeProofPlanIn = Effect.fn("Drizzle.completeProofPlanIn")(function* <
1920
+ Rq extends Table,
1921
+ S extends Table,
1922
+ G extends Table,
1923
+ Cn extends Table,
1924
+ Rs extends Table,
1925
+ Aev extends Table,
1926
+ F extends Table,
1927
+ C extends Table,
1928
+ Sub extends Table,
1929
+ I extends Table,
1930
+ Cr extends Table,
1931
+ NativeId,
1932
+ A,
1933
+ E,
1934
+ R,
1935
+ >(
1936
+ mapping: ProofPersistenceMapping<Rq, S, G, Cn, Rs, Aev, F, C, Sub, I, Cr, NativeId>,
1937
+ configuration: ProofSqlConfiguration,
1938
+ plan: ProofCompletionPlan,
1939
+ protectedMutation: Effect.Effect<boolean, E, R>,
1940
+ project: (decision: ProofCompletionDecision) => A,
1941
+ ) {
1942
+ const journal = yield* CurrentCommitJournal;
1943
+
1944
+ return yield* completeIn(
1945
+ mapping as unknown as Mapping,
1946
+ configuration,
1947
+ plan.input,
1948
+ (decision) => plan.prepare(decision, journal, project),
1949
+ protectedMutation,
1950
+ );
1951
+ });
1952
+
1953
+ return { makeSqlProofPersistence, checkProofCompletionIn, completeProofPlanIn };
1954
+ };