@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,1535 @@
1
+ import {
2
+ EmailAddressPersistence,
3
+ type EmailAddressMutation,
4
+ type EmailAction,
5
+ type EmailCredentialSnapshot,
6
+ EmailSignInTargets,
7
+ EmailUnavailable,
8
+ snapshotEmailCredential,
9
+ snapshotEmailRequirement,
10
+ snapshotEmailRevision,
11
+ } from "@yielded/auth/Email";
12
+ import {
13
+ coordinateCommit,
14
+ CurrentCommitJournal,
15
+ hasCommitScope,
16
+ type CommitJournal,
17
+ type PreparedCommit,
18
+ LifecycleHooks,
19
+ HookConfigurationError,
20
+ } from "@yielded/auth/Hooks";
21
+ import type { LoginIdentifier } from "@yielded/auth/Identity";
22
+ import { reportPersistenceFailure } from "@yielded/auth/Persistence";
23
+ import {
24
+ ProofUnavailable,
25
+ type ProofCompletionDecision,
26
+ type ProofCompletionInput,
27
+ } from "@yielded/auth/Proofs";
28
+ import {
29
+ assessAuthentication,
30
+ snapshotAuthenticationEvidence,
31
+ type AuthenticationRevision,
32
+ type SecurityRevision,
33
+ } from "@yielded/auth/Sessions";
34
+ import { Cause, Context, DateTime, Effect, Option, Schema } from "effect";
35
+ import type * as SqlError from "effect/unstable/sql/SqlError";
36
+
37
+ import { isMappedConstraintConflict, PersistenceMappingError } from "./mapping-error";
38
+ import {
39
+ type AnyEmailAddressMapping,
40
+ type AnyEmailSignInMapping,
41
+ requiredEmailAddressConstraints,
42
+ requiredEmailSignInConstraints,
43
+ } from "./models/email-model";
44
+ import {
45
+ CurrentProofSql,
46
+ type makeProofKernel,
47
+ type ProofSqlConfiguration,
48
+ type ProofSqlDatabase,
49
+ } from "./proof-kernel";
50
+ /* oxlint-disable no-explicit-any -- the shared dialect kernel erases consumer Drizzle table types internally. */
51
+ import type { QueryFailure } from "./query-operations";
52
+ import type { QueryOperations, SqlFragment, SqlColumn } from "./query-operations";
53
+
54
+ type AdapterFailure = QueryFailure | PersistenceMappingError | SqlError.SqlError;
55
+ type SignInMapping = AnyEmailSignInMapping;
56
+
57
+ export interface EmailSqlQuery<A = ReadonlyArray<any>> extends Effect.Effect<A, AdapterFailure> {
58
+ readonly getSQL: () => ReturnType<QueryOperations["sql"]>;
59
+ readonly from: (...args: ReadonlyArray<any>) => EmailSqlQuery<A>;
60
+ readonly where: (...args: ReadonlyArray<any>) => EmailSqlQuery<A>;
61
+ readonly limit: (...args: ReadonlyArray<any>) => EmailSqlQuery<A>;
62
+ readonly orderBy: (...args: ReadonlyArray<any>) => EmailSqlQuery<A>;
63
+ readonly for: (...args: ReadonlyArray<any>) => EmailSqlQuery<A>;
64
+ readonly set: (...args: ReadonlyArray<any>) => EmailSqlQuery<A>;
65
+ readonly values: (...args: ReadonlyArray<any>) => EmailSqlQuery<A>;
66
+ readonly returning: (...args: ReadonlyArray<any>) => EmailSqlQuery<A>;
67
+ readonly innerJoin: (...args: ReadonlyArray<any>) => EmailSqlQuery<A>;
68
+ readonly onConflictDoNothing: (...args: ReadonlyArray<any>) => EmailSqlQuery<A>;
69
+ readonly onDuplicateKeyUpdate: (...args: ReadonlyArray<any>) => EmailSqlQuery<A>;
70
+ readonly $returningId: (...args: ReadonlyArray<any>) => EmailSqlQuery<A>;
71
+ }
72
+
73
+ export interface EmailSqlDatabase {
74
+ readonly select: (...args: ReadonlyArray<any>) => EmailSqlQuery;
75
+ readonly insert: (...args: ReadonlyArray<any>) => EmailSqlQuery;
76
+ readonly update: (...args: ReadonlyArray<any>) => EmailSqlQuery;
77
+ readonly delete: (...args: ReadonlyArray<any>) => EmailSqlQuery;
78
+ readonly transaction: <A, E, R>(
79
+ body: (transaction: EmailSqlDatabase) => Effect.Effect<A, E, R>,
80
+ ) => Effect.Effect<A, E | SqlError.SqlError, R>;
81
+ }
82
+
83
+ export class CurrentEmailSql extends Context.Service<CurrentEmailSql, EmailSqlDatabase>()(
84
+ "effect-auth/CurrentEmailSql",
85
+ ) {}
86
+
87
+ export interface EmailSqlConfiguration {
88
+ readonly mode: "interactive" | "synchronous";
89
+ readonly locking: boolean;
90
+ readonly standaloneGuard: Effect.Effect<void, EmailUnavailable>;
91
+ readonly coordinated?: boolean;
92
+ readonly proof?: {
93
+ readonly mapping: any;
94
+ readonly configuration: ProofSqlConfiguration;
95
+ };
96
+ }
97
+
98
+ export interface CurrentAddress {
99
+ readonly nativeSubjectId: unknown;
100
+ readonly subject: any;
101
+ readonly revision: AuthenticationRevision;
102
+ readonly source?: {
103
+ readonly credential: any;
104
+ readonly identifier: any;
105
+ readonly snapshot: EmailCredentialSnapshot;
106
+ };
107
+ readonly targetIdentifier?: any;
108
+ readonly targetCredential?: any;
109
+ readonly targetIdentifierRevision?: SecurityRevision;
110
+ readonly eligible: boolean;
111
+ }
112
+
113
+ export const makeEmailKernel = <
114
+ Fragment extends SqlFragment = SqlFragment,
115
+ Column extends SqlColumn = SqlColumn,
116
+ >(
117
+ operations: QueryOperations<Fragment, Column>,
118
+ proofs: Pick<ReturnType<typeof makeProofKernel>, "completeProofPlanIn">,
119
+ ) => {
120
+ type AddressMapping = AnyEmailAddressMapping<Fragment>;
121
+
122
+ const { and, eq, inArray, lte, column, updateValues } = operations;
123
+ const { completeProofPlanIn } = proofs;
124
+ const unavailable = () => EmailUnavailable.make({});
125
+
126
+ const translateFailure = <A, E, R>(
127
+ effect: Effect.Effect<A, E, R>,
128
+ ): Effect.Effect<A, EmailUnavailable, R> =>
129
+ reportPersistenceFailure(
130
+ effect,
131
+ (error) =>
132
+ Schema.is(EmailUnavailable)(error) ||
133
+ Schema.is(ProofUnavailable)(error) ||
134
+ Schema.is(HookConfigurationError)(error),
135
+ ).pipe(Effect.catchCause((cause) => Effect.failCause(Cause.map(cause, () => unavailable()))));
136
+
137
+ const nowMillis = Effect.map(DateTime.now, DateTime.toEpochMillis);
138
+
139
+ const selectRows = (query: EmailSqlQuery, locking: boolean) =>
140
+ locking && typeof query.for === "function" ? query.for("update") : query;
141
+
142
+ const sameIdentifier = (left: LoginIdentifier, right: LoginIdentifier) =>
143
+ left.namespace === right.namespace && left.value === right.value;
144
+
145
+ const validEmailSignInConstraints = (mapping: SignInMapping) =>
146
+ Object.entries(requiredEmailSignInConstraints).every(
147
+ ([key, value]) =>
148
+ mapping.constraints[key as keyof typeof requiredEmailSignInConstraints] === value,
149
+ );
150
+
151
+ const validAddressConstraints = (mapping: AddressMapping) =>
152
+ Object.entries(requiredEmailAddressConstraints).every(
153
+ ([key, value]) =>
154
+ mapping.constraints[key as keyof typeof requiredEmailAddressConstraints] === value,
155
+ );
156
+
157
+ const subjectColumns = (mapping: SignInMapping) => ({
158
+ id: column(mapping.subject.table, mapping.subject.id),
159
+ status: column(mapping.subject.table, mapping.subject.status),
160
+ securityRevision: column(mapping.subject.table, mapping.subject.securityRevision),
161
+ });
162
+
163
+ const identifierColumns = (mapping: SignInMapping) => ({
164
+ namespace: column(mapping.identifier.table, mapping.identifier.namespace),
165
+ value: column(mapping.identifier.table, mapping.identifier.value),
166
+ subjectId: column(mapping.identifier.table, mapping.identifier.subjectId),
167
+ verifiedAt: column(mapping.identifier.table, mapping.identifier.verifiedAt),
168
+ bindingRevision: column(mapping.identifier.table, mapping.identifier.bindingRevision),
169
+ });
170
+
171
+ const credentialColumns = (mapping: SignInMapping) => ({
172
+ moduleId: column(mapping.credential.table, mapping.credential.moduleId),
173
+ subjectId: column(mapping.credential.table, mapping.credential.subjectId),
174
+ credentialId: column(mapping.credential.table, mapping.credential.credentialId),
175
+ identifierNamespace: column(mapping.credential.table, mapping.credential.identifierNamespace),
176
+ identifierValue: column(mapping.credential.table, mapping.credential.identifierValue),
177
+ credentialRevision: column(mapping.credential.table, mapping.credential.credentialRevision),
178
+ status: column(mapping.credential.table, mapping.credential.status),
179
+ });
180
+
181
+ const authorityColumns = (mapping: AddressMapping) => ({
182
+ subjectId: column(mapping.authorityCredential.table, mapping.authorityCredential.subjectId),
183
+ credentialId: column(
184
+ mapping.authorityCredential.table,
185
+ mapping.authorityCredential.credentialId,
186
+ ),
187
+ revision: column(mapping.authorityCredential.table, mapping.authorityCredential.revision),
188
+ status: column(mapping.authorityCredential.table, mapping.authorityCredential.status),
189
+ });
190
+
191
+ const commandColumns = (mapping: AddressMapping) => ({
192
+ moduleId: column(mapping.command.table, mapping.command.moduleId),
193
+ commandId: column(mapping.command.table, mapping.command.commandId),
194
+ action: column(mapping.command.table, mapping.command.action),
195
+ bindingDigest: column(mapping.command.table, mapping.command.bindingDigest),
196
+ retentionUntil: column(mapping.command.table, mapping.command.retentionUntil),
197
+ });
198
+
199
+ const allocate = <A>(
200
+ mode: EmailSqlConfiguration["mode"],
201
+ asynchronous: Effect.Effect<A, PersistenceMappingError> | undefined,
202
+ synchronous: (() => A) | undefined,
203
+ ) => {
204
+ if (mode === "synchronous")
205
+ return synchronous === undefined
206
+ ? Effect.fail(unavailable())
207
+ : Effect.try({
208
+ try: synchronous,
209
+ catch: (cause) => new PersistenceMappingError({ operation: "mapping", cause }),
210
+ });
211
+ if (asynchronous !== undefined) return asynchronous;
212
+
213
+ return synchronous === undefined
214
+ ? Effect.fail(unavailable())
215
+ : Effect.try({
216
+ try: synchronous,
217
+ catch: (cause) => new PersistenceMappingError({ operation: "mapping", cause }),
218
+ });
219
+ };
220
+
221
+ const nextSecurityRevision = (
222
+ mapping: AddressMapping,
223
+ mode: EmailSqlConfiguration["mode"],
224
+ current: SecurityRevision,
225
+ ) => {
226
+ if (mode === "synchronous")
227
+ return mapping.subject.nextSecurityRevisionSync === undefined
228
+ ? Effect.fail(unavailable())
229
+ : Effect.try({
230
+ try: () => mapping.subject.nextSecurityRevisionSync!(current),
231
+ catch: (cause) => new PersistenceMappingError({ operation: "mapping", cause }),
232
+ });
233
+ if (mapping.subject.nextSecurityRevision !== undefined)
234
+ return mapping.subject.nextSecurityRevision(current);
235
+ if (mapping.subject.nextSecurityRevisionSync !== undefined)
236
+ return Effect.try({
237
+ try: () => mapping.subject.nextSecurityRevisionSync!(current),
238
+ catch: (cause) => new PersistenceMappingError({ operation: "mapping", cause }),
239
+ });
240
+
241
+ return Effect.fail(unavailable());
242
+ };
243
+
244
+ const safeRead = <A, E, R>(
245
+ database: EmailSqlDatabase,
246
+ configuration: EmailSqlConfiguration,
247
+ body: Effect.Effect<A, E, R>,
248
+ ) =>
249
+ Effect.gen(function* () {
250
+ if (configuration.coordinated !== true) {
251
+ if (yield* hasCommitScope) return yield* unavailable();
252
+ yield* configuration.standaloneGuard;
253
+ }
254
+
255
+ return yield* database.transaction((transaction) =>
256
+ body.pipe(
257
+ Effect.provideService(CurrentEmailSql, transaction),
258
+ Effect.provideService(CurrentProofSql, transaction as unknown as ProofSqlDatabase),
259
+ ),
260
+ );
261
+ });
262
+
263
+ const owned = <A, E, R>(
264
+ database: EmailSqlDatabase,
265
+ configuration: EmailSqlConfiguration,
266
+ body: Effect.Effect<A, E, R>,
267
+ ) => {
268
+ const run = coordinateCommit(
269
+ () =>
270
+ database.transaction((transaction) =>
271
+ body.pipe(
272
+ Effect.provideService(CurrentEmailSql, transaction),
273
+ Effect.provideService(CurrentProofSql, transaction as unknown as ProofSqlDatabase),
274
+ ),
275
+ ),
276
+ { mode: configuration.mode },
277
+ ).pipe(Effect.map((result) => result.value));
278
+
279
+ return configuration.coordinated !== true
280
+ ? Effect.gen(function* () {
281
+ if (yield* hasCommitScope) return yield* unavailable();
282
+ yield* configuration.standaloneGuard;
283
+
284
+ return yield* run;
285
+ })
286
+ : run;
287
+ };
288
+
289
+ const emailLookupQuery = (
290
+ mapping: SignInMapping,
291
+ moduleId: string,
292
+ identifier: LoginIdentifier,
293
+ ) => {
294
+ const subject = subjectColumns(mapping),
295
+ binding = identifierColumns(mapping),
296
+ credential = credentialColumns(mapping);
297
+
298
+ return {
299
+ fields: {
300
+ subject: mapping.subject.table,
301
+ identifier: mapping.identifier.table,
302
+ credential: mapping.credential.table,
303
+ },
304
+ from: mapping.identifier.table,
305
+ subjectTable: mapping.subject.table,
306
+ subjectJoin: eq(subject.id, binding.subjectId),
307
+ credentialTable: mapping.credential.table,
308
+ credentialJoin: and(
309
+ eq(credential.moduleId, moduleId),
310
+ eq(credential.subjectId, binding.subjectId),
311
+ eq(credential.identifierNamespace, binding.namespace),
312
+ eq(credential.identifierValue, binding.value),
313
+ ),
314
+ predicate: and(
315
+ eq(binding.namespace, identifier.namespace),
316
+ eq(binding.value, identifier.value),
317
+ ),
318
+ limit: 1,
319
+ };
320
+ };
321
+
322
+ const emailLookupRows = Effect.fn("Drizzle.emailLookupRows")(function* (
323
+ mapping: SignInMapping,
324
+ moduleId: string,
325
+ identifier: LoginIdentifier,
326
+ ) {
327
+ const database = yield* CurrentEmailSql;
328
+ const query = emailLookupQuery(mapping, moduleId, identifier);
329
+
330
+ return yield* database
331
+ .select(query.fields)
332
+ .from(query.from)
333
+ .innerJoin(query.subjectTable, query.subjectJoin)
334
+ .innerJoin(query.credentialTable, query.credentialJoin)
335
+ .where(query.predicate)
336
+ .limit(query.limit);
337
+ });
338
+
339
+ const decodeEmailSnapshot = Effect.fn("DrizzleEmail.decodeSnapshot")(function* (
340
+ mapping: SignInMapping,
341
+ moduleId: string,
342
+ requested: LoginIdentifier,
343
+ row: any,
344
+ ) {
345
+ if (
346
+ !mapping.subject.isActiveStatus(row.subject[mapping.subject.status]) ||
347
+ !mapping.identifier.isCurrent(row.identifier) ||
348
+ !mapping.credential.isActiveStatus(row.credential[mapping.credential.status])
349
+ )
350
+ return undefined;
351
+
352
+ const snapshot = yield* mapping.credential
353
+ .decode({
354
+ moduleId,
355
+ subject: row.subject,
356
+ identifier: row.identifier,
357
+ credential: row.credential,
358
+ })
359
+ .pipe(Effect.flatMap(snapshotEmailCredential));
360
+
361
+ const subjectId = yield* mapping.subjectId.toSubject(row.subject[mapping.subject.id]);
362
+
363
+ const verifiedAtMillis = yield* mapping.decodeInstant(
364
+ row.identifier[mapping.identifier.verifiedAt],
365
+ );
366
+
367
+ if (
368
+ snapshot.moduleId !== moduleId ||
369
+ !sameIdentifier(snapshot.identifier, requested) ||
370
+ snapshot.revision.subjectId !== subjectId ||
371
+ snapshot.revision.securityRevision !== row.subject[mapping.subject.securityRevision] ||
372
+ snapshot.identifierRevision !== row.identifier[mapping.identifier.bindingRevision] ||
373
+ snapshot.verifiedAtMillis !== verifiedAtMillis ||
374
+ snapshot.credentialId !== row.credential[mapping.credential.credentialId] ||
375
+ snapshot.credentialRevision !== row.credential[mapping.credential.credentialRevision]
376
+ )
377
+ return undefined;
378
+
379
+ return snapshot;
380
+ });
381
+
382
+ const makeSqlEmailSignInTargets = Effect.fn("makeSqlEmailSignInTargets")(function (
383
+ database: EmailSqlDatabase,
384
+ mapping: SignInMapping,
385
+ configuration: EmailSqlConfiguration,
386
+ ): Effect.Effect<EmailSignInTargets["Service"]> {
387
+ return Effect.succeed(
388
+ EmailSignInTargets.of({
389
+ lookup: (input) =>
390
+ (validEmailSignInConstraints(mapping)
391
+ ? safeRead(
392
+ database,
393
+ configuration,
394
+ Effect.gen(function* () {
395
+ const row = (yield* emailLookupRows(
396
+ mapping,
397
+ input.moduleId,
398
+ input.identifier,
399
+ ))[0];
400
+
401
+ if (row === undefined) return Option.none();
402
+
403
+ const snapshot = yield* decodeEmailSnapshot(
404
+ mapping,
405
+ input.moduleId,
406
+ input.identifier,
407
+ row,
408
+ );
409
+
410
+ return snapshot === undefined ? Option.none() : Option.some(snapshot);
411
+ }),
412
+ )
413
+ : Effect.fail(unavailable())
414
+ ).pipe(Effect.provideService(CurrentEmailSql, database), translateFailure),
415
+ }),
416
+ );
417
+ });
418
+
419
+ const readSubject = Effect.fn("DrizzleEmail.readSubject")(function* (
420
+ mapping: AddressMapping,
421
+ subjectId: string,
422
+ locking: boolean,
423
+ ) {
424
+ const database = yield* CurrentEmailSql;
425
+
426
+ const nativeSubjectId = yield* mapping.subjectId.toNative(subjectId as any);
427
+ const s = subjectColumns(mapping);
428
+
429
+ const subject = (yield* selectRows(
430
+ database.select().from(mapping.subject.table).where(eq(s.id, nativeSubjectId)).limit(1),
431
+ locking,
432
+ ))[0];
433
+
434
+ return { nativeSubjectId, subject };
435
+ });
436
+
437
+ const identifierRow = Effect.fn("Drizzle.identifierRow")(function* (
438
+ mapping: SignInMapping,
439
+ identifier: LoginIdentifier,
440
+ locking: boolean,
441
+ ) {
442
+ const database = yield* CurrentEmailSql;
443
+
444
+ const i = identifierColumns(mapping);
445
+
446
+ return yield* selectRows(
447
+ database
448
+ .select()
449
+ .from(mapping.identifier.table)
450
+ .where(and(eq(i.namespace, identifier.namespace), eq(i.value, identifier.value)))
451
+ .limit(1),
452
+ locking,
453
+ );
454
+ });
455
+
456
+ const credentialById = Effect.fn("Drizzle.credentialById")(function* (
457
+ mapping: SignInMapping,
458
+ moduleId: string,
459
+ credentialId: string,
460
+ locking: boolean,
461
+ ) {
462
+ const database = yield* CurrentEmailSql;
463
+
464
+ const c = credentialColumns(mapping);
465
+
466
+ return yield* selectRows(
467
+ database
468
+ .select()
469
+ .from(mapping.credential.table)
470
+ .where(and(eq(c.moduleId, moduleId), eq(c.credentialId, credentialId)))
471
+ .limit(1),
472
+ locking,
473
+ );
474
+ });
475
+
476
+ const credentialByIdentifier = Effect.fn("Drizzle.credentialByIdentifier")(function* (
477
+ mapping: SignInMapping,
478
+ moduleId: string,
479
+ identifier: LoginIdentifier,
480
+ locking: boolean,
481
+ ) {
482
+ const database = yield* CurrentEmailSql;
483
+
484
+ const c = credentialColumns(mapping);
485
+
486
+ return yield* selectRows(
487
+ database
488
+ .select()
489
+ .from(mapping.credential.table)
490
+ .where(
491
+ and(
492
+ eq(c.moduleId, moduleId),
493
+ eq(c.identifierNamespace, identifier.namespace),
494
+ eq(c.identifierValue, identifier.value),
495
+ ),
496
+ )
497
+ .limit(1),
498
+ locking,
499
+ );
500
+ });
501
+
502
+ const currentAddress = Effect.fn("DrizzleEmail.currentAddress")(function* (
503
+ mapping: AddressMapping,
504
+ input: {
505
+ readonly moduleId: string;
506
+ readonly subjectId: string;
507
+ readonly target: LoginIdentifier;
508
+ readonly sourceCredentialId?: string;
509
+ },
510
+ locking: boolean,
511
+ ) {
512
+ const database = yield* CurrentEmailSql;
513
+
514
+ const locked = yield* readSubject(mapping, input.subjectId, locking);
515
+
516
+ if (
517
+ locked.subject === undefined ||
518
+ !mapping.subject.isActiveStatus(locked.subject[mapping.subject.status])
519
+ )
520
+ return undefined;
521
+
522
+ const discoveredSource =
523
+ input.sourceCredentialId === undefined
524
+ ? undefined
525
+ : (yield* credentialById(mapping, input.moduleId, input.sourceCredentialId, false))[0];
526
+
527
+ const sourceIdentifier: LoginIdentifier | undefined =
528
+ discoveredSource === undefined
529
+ ? undefined
530
+ : {
531
+ namespace: discoveredSource[mapping.credential.identifierNamespace],
532
+ value: discoveredSource[mapping.credential.identifierValue],
533
+ };
534
+
535
+ const identifiers = [
536
+ input.target,
537
+ ...(sourceIdentifier === undefined ? [] : [sourceIdentifier]),
538
+ ]
539
+ .filter(
540
+ (value, index, values) =>
541
+ values.findIndex((candidate) => sameIdentifier(candidate, value)) === index,
542
+ )
543
+ .sort((left, right) =>
544
+ `${left.namespace}\u0000${left.value}`.localeCompare(
545
+ `${right.namespace}\u0000${right.value}`,
546
+ ),
547
+ );
548
+
549
+ const lockedIdentifiers = new Map<string, any>();
550
+
551
+ for (const identifier of identifiers) {
552
+ const row = (yield* identifierRow(mapping, identifier, locking))[0];
553
+
554
+ if (row !== undefined)
555
+ lockedIdentifiers.set(`${identifier.namespace}\u0000${identifier.value}`, row);
556
+ }
557
+
558
+ const sourceCredential =
559
+ input.sourceCredentialId === undefined
560
+ ? undefined
561
+ : (yield* credentialById(mapping, input.moduleId, input.sourceCredentialId, locking))[0];
562
+
563
+ const targetCredential = (yield* credentialByIdentifier(
564
+ mapping,
565
+ input.moduleId,
566
+ input.target,
567
+ locking,
568
+ ))[0];
569
+
570
+ const a = authorityColumns(mapping);
571
+
572
+ const authority = yield* selectRows(
573
+ database
574
+ .select()
575
+ .from(mapping.authorityCredential.table)
576
+ .where(eq(a.subjectId, locked.nativeSubjectId))
577
+ .orderBy(a.credentialId),
578
+ locking,
579
+ );
580
+
581
+ const activeAuthority = authority.filter((row: any) =>
582
+ mapping.authorityCredential.isActiveStatus(row[mapping.authorityCredential.status]),
583
+ );
584
+
585
+ const subjectId = yield* mapping.subjectId.toSubject(locked.nativeSubjectId);
586
+
587
+ const revision: AuthenticationRevision = Object.freeze({
588
+ subjectId,
589
+ securityRevision: locked.subject[mapping.subject.securityRevision],
590
+ credentials: Object.freeze(
591
+ activeAuthority.map((row: any) =>
592
+ Object.freeze({
593
+ credentialId: row[mapping.authorityCredential.credentialId],
594
+ revision: row[mapping.authorityCredential.revision],
595
+ }),
596
+ ),
597
+ ),
598
+ });
599
+
600
+ const targetIdentifier = lockedIdentifiers.get(
601
+ `${input.target.namespace}\u0000${input.target.value}`,
602
+ );
603
+
604
+ const targetOwned =
605
+ targetIdentifier !== undefined &&
606
+ mapping.subjectId.equals(
607
+ targetIdentifier[mapping.identifier.subjectId],
608
+ locked.nativeSubjectId,
609
+ );
610
+
611
+ const targetMutable =
612
+ targetOwned &&
613
+ mapping.identifier.isMutableTarget(targetIdentifier) &&
614
+ (targetCredential === undefined ||
615
+ (!mapping.credential.isActiveStatus(targetCredential[mapping.credential.status]) &&
616
+ mapping.subjectId.equals(
617
+ targetCredential[mapping.credential.subjectId],
618
+ locked.nativeSubjectId,
619
+ )));
620
+
621
+ let source: CurrentAddress["source"];
622
+
623
+ if (input.sourceCredentialId !== undefined) {
624
+ if (
625
+ sourceCredential === undefined ||
626
+ sourceIdentifier === undefined ||
627
+ !mapping.subjectId.equals(
628
+ sourceCredential[mapping.credential.subjectId],
629
+ locked.nativeSubjectId,
630
+ ) ||
631
+ !mapping.credential.isActiveStatus(sourceCredential[mapping.credential.status])
632
+ )
633
+ return undefined;
634
+
635
+ const sourceIdentifierRow = lockedIdentifiers.get(
636
+ `${sourceIdentifier.namespace}\u0000${sourceIdentifier.value}`,
637
+ );
638
+
639
+ if (
640
+ sourceIdentifierRow === undefined ||
641
+ !mapping.identifier.isCurrent(sourceIdentifierRow) ||
642
+ !mapping.subjectId.equals(
643
+ sourceIdentifierRow[mapping.identifier.subjectId],
644
+ locked.nativeSubjectId,
645
+ )
646
+ )
647
+ return undefined;
648
+
649
+ const snapshot = yield* mapping.credential
650
+ .decode({
651
+ moduleId: input.moduleId,
652
+ subject: locked.subject,
653
+ identifier: sourceIdentifierRow,
654
+ credential: sourceCredential,
655
+ })
656
+ .pipe(Effect.flatMap(snapshotEmailCredential));
657
+
658
+ source = { credential: sourceCredential, identifier: sourceIdentifierRow, snapshot };
659
+ }
660
+ let eligible = targetIdentifier === undefined ? targetCredential === undefined : targetMutable;
661
+
662
+ if (input.sourceCredentialId !== undefined && source === undefined) eligible = false;
663
+ if (source !== undefined && sameIdentifier(source.snapshot.identifier, input.target))
664
+ eligible = false;
665
+ if (mapping.addressCardinality === "single" && input.sourceCredentialId === undefined) {
666
+ const c = credentialColumns(mapping);
667
+
668
+ const rows = yield* selectRows(
669
+ database
670
+ .select()
671
+ .from(mapping.credential.table)
672
+ .where(and(eq(c.moduleId, input.moduleId), eq(c.subjectId, locked.nativeSubjectId))),
673
+ locking,
674
+ );
675
+
676
+ if (
677
+ rows.some((row: any) => mapping.credential.isActiveStatus(row[mapping.credential.status]))
678
+ )
679
+ eligible = false;
680
+ }
681
+
682
+ return {
683
+ nativeSubjectId: locked.nativeSubjectId,
684
+ subject: locked.subject,
685
+ revision,
686
+ ...(source === undefined ? {} : { source }),
687
+ ...(targetIdentifier === undefined ? {} : { targetIdentifier }),
688
+ ...(targetCredential === undefined ? {} : { targetCredential }),
689
+ ...(targetMutable
690
+ ? { targetIdentifierRevision: targetIdentifier[mapping.identifier.bindingRevision] }
691
+ : {}),
692
+ eligible,
693
+ };
694
+ });
695
+
696
+ const sameEmailRevision = (left: AuthenticationRevision, right: AuthenticationRevision) => {
697
+ if (
698
+ left.subjectId !== right.subjectId ||
699
+ left.securityRevision !== right.securityRevision ||
700
+ left.credentials.length !== right.credentials.length
701
+ )
702
+ return false;
703
+
704
+ const expected = [...left.credentials].sort((a, b) =>
705
+ a.credentialId.localeCompare(b.credentialId),
706
+ );
707
+
708
+ const actual = [...right.credentials].sort((a, b) =>
709
+ a.credentialId.localeCompare(b.credentialId),
710
+ );
711
+
712
+ return expected.every(
713
+ (item, index) =>
714
+ item.credentialId === actual[index]!.credentialId &&
715
+ item.revision === actual[index]!.revision,
716
+ );
717
+ };
718
+
719
+ const actionModule = (moduleId: string, action: EmailAction) =>
720
+ `${moduleId}/${action === "verify-address" ? "verify-address" : "change-address"}`;
721
+
722
+ const actionPurpose = (action: EmailAction) =>
723
+ action === "verify-address" ? "email-address-verification" : "email-address-change";
724
+
725
+ const completionMatches = (input: EmailAddressMutation, action: EmailAction) => {
726
+ const completion = input.completion.input;
727
+ const binding = completion.binding;
728
+
729
+ return (
730
+ completion.moduleId === actionModule(input.moduleId, action) &&
731
+ completion.purpose === actionPurpose(action) &&
732
+ binding._tag === "IdentifierChange" &&
733
+ sameIdentifier(binding.identifier, input.target) &&
734
+ sameEmailRevision(binding.revision, input.captured.revision)
735
+ );
736
+ };
737
+
738
+ const snapshotEmailMutation = Effect.fn("DrizzleEmail.snapshotMutation")(function* (
739
+ input: EmailAddressMutation,
740
+ ) {
741
+ const evidence = yield* snapshotAuthenticationEvidence(input.authorization.evidence).pipe(
742
+ Effect.mapError(unavailable),
743
+ );
744
+
745
+ const requirement = yield* snapshotEmailRequirement(input.authorization.requirement);
746
+
747
+ const source =
748
+ input.captured.source === undefined
749
+ ? undefined
750
+ : yield* snapshotEmailCredential(input.captured.source);
751
+
752
+ return Object.freeze({
753
+ ...input,
754
+ target: Object.freeze({ ...input.target }),
755
+ ...(input.invalidation === undefined
756
+ ? {}
757
+ : { invalidation: Object.freeze({ ...input.invalidation }) }),
758
+ captured: Object.freeze({
759
+ eligible: input.captured.eligible,
760
+ revision: snapshotEmailRevision(input.captured.revision),
761
+ ...(input.captured.targetIdentifierRevision === undefined
762
+ ? {}
763
+ : { targetIdentifierRevision: input.captured.targetIdentifierRevision }),
764
+ ...(source === undefined ? {} : { source }),
765
+ }),
766
+ authorization: Object.freeze({
767
+ challenge: Object.freeze({
768
+ ...input.authorization.challenge,
769
+ target: Object.freeze({ ...input.authorization.challenge.target }),
770
+ revision: snapshotEmailRevision(input.authorization.challenge.revision),
771
+ }),
772
+ evidence,
773
+ requirement,
774
+ }),
775
+ });
776
+ });
777
+
778
+ const validateEmailAuthority = Effect.fn("DrizzleEmail.validateAuthority")(function* (
779
+ mapping: AddressMapping,
780
+ configuration: EmailSqlConfiguration,
781
+ input: EmailAddressMutation,
782
+ action: EmailAction,
783
+ ) {
784
+ const confirmsExisting =
785
+ action === "verify-address" && input.captured.targetIdentifierRevision !== undefined;
786
+
787
+ if (
788
+ !input.captured.eligible ||
789
+ !completionMatches(input, action) ||
790
+ input.authorization.challenge.moduleId !== input.moduleId ||
791
+ input.authorization.challenge.action !== action ||
792
+ input.authorization.challenge.commandId !== input.commandId ||
793
+ !sameIdentifier(input.authorization.challenge.target, input.target) ||
794
+ input.authorization.challenge.sourceCredentialId !== input.captured.source?.credentialId ||
795
+ !sameEmailRevision(input.authorization.challenge.revision, input.captured.revision) ||
796
+ input.authorization.challenge.targetIdentifierRevision !==
797
+ input.captured.targetIdentifierRevision ||
798
+ (input.authorization.evidence.flowId as string) !== (input.commandId as string) ||
799
+ input.authorization.evidence.bindingDigest !== input.authorization.challenge.bindingDigest ||
800
+ !sameEmailRevision(input.authorization.evidence.revision, input.captured.revision) ||
801
+ (action === "verify-address" && input.captured.source !== undefined) ||
802
+ (action === "change-address" && input.captured.source === undefined) ||
803
+ confirmsExisting !== (input.invalidation === undefined) ||
804
+ (input.invalidation?.existingSessions === "immediate" &&
805
+ mapping.sessionInvalidation !== "same-authority-immediate")
806
+ )
807
+ return undefined;
808
+
809
+ const current = yield* currentAddress(
810
+ mapping,
811
+ {
812
+ moduleId: input.moduleId,
813
+ subjectId: input.captured.revision.subjectId,
814
+ target: input.target,
815
+ ...(input.captured.source === undefined
816
+ ? {}
817
+ : { sourceCredentialId: input.captured.source.credentialId }),
818
+ },
819
+ configuration.locking,
820
+ );
821
+
822
+ if (
823
+ current === undefined ||
824
+ !current.eligible ||
825
+ !sameEmailRevision(current.revision, input.captured.revision) ||
826
+ current.targetIdentifierRevision !== input.captured.targetIdentifierRevision ||
827
+ (input.captured.source !== undefined &&
828
+ (current.source === undefined ||
829
+ current.source.snapshot.identifierRevision !== input.captured.source.identifierRevision ||
830
+ current.source.snapshot.credentialRevision !== input.captured.source.credentialRevision ||
831
+ !sameIdentifier(current.source.snapshot.identifier, input.captured.source.identifier)))
832
+ )
833
+ return undefined;
834
+
835
+ const currentRequirement = yield* mapping.subject.decodeActionRequirement(
836
+ current.subject,
837
+ action,
838
+ );
839
+
840
+ const original = yield* assessAuthentication(
841
+ input.authorization.evidence,
842
+ input.authorization.requirement,
843
+ ).pipe(Effect.mapError(unavailable));
844
+
845
+ const configured = yield* assessAuthentication(
846
+ input.authorization.evidence,
847
+ currentRequirement,
848
+ ).pipe(Effect.mapError(unavailable));
849
+
850
+ return original.satisfied && configured.satisfied ? current : undefined;
851
+ });
852
+
853
+ const commandExists = Effect.fn("Drizzle.commandExists")(function* (
854
+ mapping: AddressMapping,
855
+ moduleId: string,
856
+ commandId: string,
857
+ locking: boolean,
858
+ ) {
859
+ const database = yield* CurrentEmailSql;
860
+
861
+ const c = commandColumns(mapping);
862
+
863
+ return yield* selectRows(
864
+ database
865
+ .select()
866
+ .from(mapping.command.table)
867
+ .where(and(eq(c.moduleId, moduleId), eq(c.commandId, commandId)))
868
+ .limit(1),
869
+ locking,
870
+ );
871
+ });
872
+
873
+ const applyMutation = Effect.fn("DrizzleEmail.applyMutation")(function* (
874
+ mapping: AddressMapping,
875
+ input: EmailAddressMutation,
876
+ action: EmailAction,
877
+ current: CurrentAddress,
878
+ allocated: {
879
+ readonly targetCredentialId: string;
880
+ readonly targetIdentifierRevision: SecurityRevision;
881
+ readonly targetCredentialRevision: SecurityRevision;
882
+ readonly sourceIdentifierRevision: SecurityRevision;
883
+ readonly sourceCredentialRevision: SecurityRevision;
884
+ readonly nextSecurityRevision: SecurityRevision;
885
+ },
886
+ now: number,
887
+ ) {
888
+ const database = yield* CurrentEmailSql;
889
+
890
+ const i = identifierColumns(mapping);
891
+ const c = credentialColumns(mapping);
892
+ const a = authorityColumns(mapping);
893
+ const s = subjectColumns(mapping);
894
+
895
+ const targetCredentialId =
896
+ current.targetCredential?.[mapping.credential.credentialId] ?? allocated.targetCredentialId;
897
+
898
+ if (action === "change-address") {
899
+ const source = current.source!;
900
+
901
+ yield* database
902
+ .update(mapping.identifier.table)
903
+ .set(
904
+ mapping.identifier.encodeRetirement({
905
+ source: source.snapshot.identifier,
906
+ bindingRevision: allocated.sourceIdentifierRevision,
907
+ }),
908
+ )
909
+ .where(
910
+ and(
911
+ eq(i.namespace, source.snapshot.identifier.namespace),
912
+ eq(i.value, source.snapshot.identifier.value),
913
+ eq(i.subjectId, current.nativeSubjectId),
914
+ eq(i.bindingRevision, source.snapshot.identifierRevision),
915
+ ),
916
+ );
917
+ yield* database
918
+ .update(mapping.credential.table)
919
+ .set(
920
+ mapping.credential.encodeRetirement({
921
+ source: source.snapshot.identifier,
922
+ credentialRevision: allocated.sourceCredentialRevision,
923
+ }),
924
+ )
925
+ .where(
926
+ and(
927
+ eq(c.moduleId, input.moduleId),
928
+ eq(c.credentialId, source.snapshot.credentialId),
929
+ eq(c.subjectId, current.nativeSubjectId),
930
+ eq(c.credentialRevision, source.snapshot.credentialRevision),
931
+ ),
932
+ );
933
+ yield* database
934
+ .update(mapping.authorityCredential.table)
935
+ .set(mapping.authorityCredential.encodeRetirement(allocated.sourceCredentialRevision))
936
+ .where(
937
+ and(
938
+ eq(a.subjectId, current.nativeSubjectId),
939
+ eq(a.credentialId, source.snapshot.credentialId),
940
+ eq(a.revision, source.snapshot.credentialRevision),
941
+ ),
942
+ );
943
+ }
944
+ if (current.targetIdentifier === undefined) {
945
+ yield* database.insert(mapping.identifier.table).values(
946
+ mapping.identifier.encodeVerifiedInsert({
947
+ identifier: input.target,
948
+ subjectId: current.nativeSubjectId,
949
+ verifiedAtMillis: now,
950
+ bindingRevision: allocated.targetIdentifierRevision,
951
+ }),
952
+ );
953
+ } else {
954
+ yield* database
955
+ .update(mapping.identifier.table)
956
+ .set(
957
+ mapping.identifier.encodeVerification({
958
+ verifiedAtMillis: now,
959
+ bindingRevision: allocated.targetIdentifierRevision,
960
+ }),
961
+ )
962
+ .where(
963
+ and(
964
+ eq(i.namespace, input.target.namespace),
965
+ eq(i.value, input.target.value),
966
+ eq(i.subjectId, current.nativeSubjectId),
967
+ eq(i.bindingRevision, current.targetIdentifier![mapping.identifier.bindingRevision]),
968
+ ),
969
+ );
970
+ }
971
+ if (current.targetCredential === undefined) {
972
+ yield* database.insert(mapping.credential.table).values(
973
+ mapping.credential.encodeVerifiedInsert({
974
+ moduleId: input.moduleId,
975
+ subjectId: current.nativeSubjectId,
976
+ credentialId: targetCredentialId,
977
+ identifier: input.target,
978
+ credentialRevision: allocated.targetCredentialRevision,
979
+ }),
980
+ );
981
+ yield* database.insert(mapping.authorityCredential.table).values(
982
+ mapping.authorityCredential.encodeInsert({
983
+ subjectId: current.nativeSubjectId,
984
+ credentialId: targetCredentialId,
985
+ revision: allocated.targetCredentialRevision,
986
+ }),
987
+ );
988
+ } else {
989
+ const oldRevision = current.targetCredential[mapping.credential.credentialRevision];
990
+
991
+ yield* database
992
+ .update(mapping.credential.table)
993
+ .set(
994
+ mapping.credential.encodeActivation({
995
+ identifier: input.target,
996
+ credentialRevision: allocated.targetCredentialRevision,
997
+ }),
998
+ )
999
+ .where(
1000
+ and(
1001
+ eq(c.moduleId, input.moduleId),
1002
+ eq(c.credentialId, targetCredentialId),
1003
+ eq(c.subjectId, current.nativeSubjectId),
1004
+ eq(c.credentialRevision, oldRevision),
1005
+ ),
1006
+ );
1007
+ yield* database
1008
+ .update(mapping.authorityCredential.table)
1009
+ .set(mapping.authorityCredential.encodeActivation(allocated.targetCredentialRevision))
1010
+ .where(
1011
+ and(
1012
+ eq(a.subjectId, current.nativeSubjectId),
1013
+ eq(a.credentialId, targetCredentialId),
1014
+ eq(a.revision, oldRevision),
1015
+ ),
1016
+ );
1017
+ }
1018
+ yield* database
1019
+ .update(mapping.subject.table)
1020
+ .set(updateValues([[mapping.subject.securityRevision, allocated.nextSecurityRevision]]))
1021
+ .where(
1022
+ and(
1023
+ eq(s.id, current.nativeSubjectId),
1024
+ eq(s.securityRevision, input.captured.revision.securityRevision),
1025
+ ),
1026
+ );
1027
+ yield* database.insert(mapping.command.table).values(
1028
+ mapping.command.encodeInsert({
1029
+ moduleId: input.moduleId,
1030
+ commandId: input.commandId,
1031
+ action,
1032
+ bindingDigest: input.authorization.challenge.bindingDigest,
1033
+ retentionUntilMillis: now + mapping.commandRetentionMillis,
1034
+ }),
1035
+ );
1036
+
1037
+ const sourceStillActive =
1038
+ action === "verify-address"
1039
+ ? []
1040
+ : yield* selectRows(
1041
+ database
1042
+ .select()
1043
+ .from(mapping.credential.table)
1044
+ .where(
1045
+ and(
1046
+ eq(c.moduleId, input.moduleId),
1047
+ eq(c.credentialId, current.source!.snapshot.credentialId),
1048
+ eq(c.subjectId, current.nativeSubjectId),
1049
+ ),
1050
+ ),
1051
+ false,
1052
+ );
1053
+
1054
+ const target = (yield* emailLookupRows(mapping, input.moduleId, input.target))[0];
1055
+
1056
+ const targetAuthority = yield* selectRows(
1057
+ database
1058
+ .select()
1059
+ .from(mapping.authorityCredential.table)
1060
+ .where(
1061
+ and(eq(a.subjectId, current.nativeSubjectId), eq(a.credentialId, targetCredentialId)),
1062
+ )
1063
+ .limit(1),
1064
+ false,
1065
+ );
1066
+
1067
+ const command = yield* selectRows(
1068
+ database
1069
+ .select()
1070
+ .from(mapping.command.table)
1071
+ .where(
1072
+ and(
1073
+ eq(commandColumns(mapping).moduleId, input.moduleId),
1074
+ eq(commandColumns(mapping).commandId, input.commandId),
1075
+ eq(commandColumns(mapping).action, action),
1076
+ eq(commandColumns(mapping).bindingDigest, input.authorization.challenge.bindingDigest),
1077
+ eq(
1078
+ commandColumns(mapping).retentionUntil,
1079
+ mapping.encodeInstant(now + mapping.commandRetentionMillis),
1080
+ ),
1081
+ ),
1082
+ )
1083
+ .limit(1),
1084
+ false,
1085
+ );
1086
+
1087
+ const targetVerifiedAt =
1088
+ target === undefined
1089
+ ? undefined
1090
+ : yield* mapping.decodeInstant(target.identifier[mapping.identifier.verifiedAt]);
1091
+
1092
+ const sourceIdentifier =
1093
+ action === "verify-address"
1094
+ ? []
1095
+ : yield* identifierRow(mapping, current.source!.snapshot.identifier, false);
1096
+
1097
+ const sourceAuthority =
1098
+ action === "verify-address"
1099
+ ? []
1100
+ : yield* selectRows(
1101
+ database
1102
+ .select()
1103
+ .from(mapping.authorityCredential.table)
1104
+ .where(
1105
+ and(
1106
+ eq(a.subjectId, current.nativeSubjectId),
1107
+ eq(a.credentialId, current.source!.snapshot.credentialId),
1108
+ ),
1109
+ )
1110
+ .limit(1),
1111
+ false,
1112
+ );
1113
+
1114
+ return (
1115
+ target !== undefined &&
1116
+ mapping.subjectId.equals(target.subject[mapping.subject.id], current.nativeSubjectId) &&
1117
+ mapping.subject.isActiveStatus(target.subject[mapping.subject.status]) &&
1118
+ target.subject[mapping.subject.securityRevision] === allocated.nextSecurityRevision &&
1119
+ target.identifier[mapping.identifier.bindingRevision] ===
1120
+ allocated.targetIdentifierRevision &&
1121
+ targetVerifiedAt === now &&
1122
+ target.credential[mapping.credential.credentialRevision] ===
1123
+ allocated.targetCredentialRevision &&
1124
+ target.credential[mapping.credential.credentialId] === targetCredentialId &&
1125
+ mapping.subjectId.equals(
1126
+ target.credential[mapping.credential.subjectId],
1127
+ current.nativeSubjectId,
1128
+ ) &&
1129
+ mapping.identifier.isCurrent(target.identifier) &&
1130
+ mapping.credential.isActiveStatus(target.credential[mapping.credential.status]) &&
1131
+ targetAuthority.length === 1 &&
1132
+ targetAuthority[0]![mapping.authorityCredential.revision] ===
1133
+ allocated.targetCredentialRevision &&
1134
+ mapping.authorityCredential.isActiveStatus(
1135
+ targetAuthority[0]![mapping.authorityCredential.status],
1136
+ ) &&
1137
+ command.length === 1 &&
1138
+ (action === "verify-address" ||
1139
+ (sourceStillActive.length === 1 &&
1140
+ sourceStillActive[0]![mapping.credential.credentialRevision] ===
1141
+ allocated.sourceCredentialRevision &&
1142
+ !mapping.credential.isActiveStatus(sourceStillActive[0]![mapping.credential.status]) &&
1143
+ sourceIdentifier.length === 1 &&
1144
+ mapping.subjectId.equals(
1145
+ sourceIdentifier[0]![mapping.identifier.subjectId],
1146
+ current.nativeSubjectId,
1147
+ ) &&
1148
+ sourceIdentifier[0]![mapping.identifier.bindingRevision] ===
1149
+ allocated.sourceIdentifierRevision &&
1150
+ !mapping.identifier.isCurrent(sourceIdentifier[0]!) &&
1151
+ sourceAuthority.length === 1 &&
1152
+ sourceAuthority[0]![mapping.authorityCredential.revision] ===
1153
+ allocated.sourceCredentialRevision &&
1154
+ !mapping.authorityCredential.isActiveStatus(
1155
+ sourceAuthority[0]![mapping.authorityCredential.status],
1156
+ )))
1157
+ );
1158
+ });
1159
+
1160
+ const probeProofCompletion = Effect.fn("Drizzle.probeProofCompletion")(function* (
1161
+ configuration: EmailSqlConfiguration,
1162
+ input: ProofCompletionInput,
1163
+ ) {
1164
+ const database = yield* CurrentEmailSql;
1165
+
1166
+ if (configuration.proof === undefined) return yield* Effect.fail(unavailable());
1167
+ const validProbe = { _tag: "EmailProofCompletionValid" } as const;
1168
+
1169
+ const plan = {
1170
+ input,
1171
+ prepare: <A>(
1172
+ decision: ProofCompletionDecision,
1173
+ journal: CommitJournal,
1174
+ project: (decision: ProofCompletionDecision) => A,
1175
+ ) => journal.prepare(project(decision)),
1176
+ };
1177
+
1178
+ const probe = coordinateCommit(
1179
+ () =>
1180
+ database.transaction((transaction) =>
1181
+ completeProofPlanIn(
1182
+ configuration.proof!.mapping,
1183
+ configuration.proof!.configuration,
1184
+ plan,
1185
+ Effect.fail(validProbe),
1186
+ () => false,
1187
+ ).pipe(
1188
+ Effect.provideService(CurrentProofSql, transaction as unknown as ProofSqlDatabase),
1189
+ ),
1190
+ ),
1191
+ { mode: configuration.mode },
1192
+ );
1193
+
1194
+ return yield* probe.pipe(
1195
+ Effect.as(false),
1196
+ Effect.catchCause((cause) => {
1197
+ const reason = cause.reasons[0];
1198
+
1199
+ return cause.reasons.length === 1 &&
1200
+ reason !== undefined &&
1201
+ Cause.isFailReason(reason) &&
1202
+ reason.error === validProbe
1203
+ ? Effect.succeed(true)
1204
+ : Effect.failCause(cause);
1205
+ }),
1206
+ );
1207
+ });
1208
+
1209
+ const prepareRejected = Effect.fn("Drizzle.prepareRejected")(function* <A>(
1210
+ configuration: EmailSqlConfiguration,
1211
+ prepare: (value: "rejected", journal: CommitJournal) => PreparedCommit<A>,
1212
+ ) {
1213
+ const database = yield* CurrentEmailSql;
1214
+
1215
+ return yield* owned(
1216
+ database,
1217
+ configuration,
1218
+ Effect.gen(function* () {
1219
+ const journal = yield* CurrentCommitJournal;
1220
+
1221
+ return yield* Effect.succeed(prepare("rejected", journal));
1222
+ }),
1223
+ );
1224
+ });
1225
+
1226
+ const makeSqlEmailAddressPersistence = Effect.fn("makeSqlEmailAddressPersistence")(function* (
1227
+ database: EmailSqlDatabase,
1228
+ mapping: AddressMapping,
1229
+ configuration: EmailSqlConfiguration,
1230
+ ): Effect.fn.Return<EmailAddressPersistence["Service"], never, LifecycleHooks> {
1231
+ const hooks = yield* LifecycleHooks;
1232
+
1233
+ return EmailAddressPersistence.of({
1234
+ target: (input) =>
1235
+ (validAddressConstraints(mapping)
1236
+ ? safeRead(
1237
+ database,
1238
+ configuration,
1239
+ Effect.gen(function* () {
1240
+ const current = yield* currentAddress(mapping, input, configuration.locking);
1241
+
1242
+ if (current === undefined) return yield* unavailable();
1243
+
1244
+ return Object.freeze({
1245
+ revision: snapshotEmailRevision(current.revision),
1246
+ eligible: current.eligible,
1247
+ ...(current.targetIdentifierRevision === undefined
1248
+ ? {}
1249
+ : { targetIdentifierRevision: current.targetIdentifierRevision }),
1250
+ ...(current.source === undefined
1251
+ ? {}
1252
+ : { source: yield* snapshotEmailCredential(current.source.snapshot) }),
1253
+ });
1254
+ }),
1255
+ )
1256
+ : Effect.fail(unavailable())
1257
+ ).pipe(
1258
+ Effect.provideService(CurrentEmailSql, database),
1259
+ Effect.provideService(LifecycleHooks, hooks),
1260
+ translateFailure,
1261
+ ),
1262
+ checkCompletion: (input) => {
1263
+ if (configuration.proof === undefined || input.binding._tag !== "IdentifierChange")
1264
+ return Effect.succeed(false).pipe(
1265
+ Effect.provideService(CurrentEmailSql, database),
1266
+ Effect.provideService(LifecycleHooks, hooks),
1267
+ translateFailure,
1268
+ );
1269
+ const binding = input.binding;
1270
+
1271
+ return safeRead(
1272
+ database,
1273
+ configuration,
1274
+ Effect.gen(function* () {
1275
+ const action: EmailAction | undefined =
1276
+ input.moduleId.endsWith("/verify-address") &&
1277
+ input.purpose === "email-address-verification"
1278
+ ? "verify-address"
1279
+ : input.moduleId.endsWith("/change-address") &&
1280
+ input.purpose === "email-address-change"
1281
+ ? "change-address"
1282
+ : undefined;
1283
+
1284
+ if (action === undefined) return false;
1285
+ const moduleId = input.moduleId.slice(0, -`/${action}`.length);
1286
+
1287
+ const current = yield* currentAddress(
1288
+ mapping,
1289
+ {
1290
+ moduleId,
1291
+ subjectId: binding.revision.subjectId,
1292
+ target: binding.identifier,
1293
+ },
1294
+ false,
1295
+ );
1296
+
1297
+ return current !== undefined && sameEmailRevision(current.revision, binding.revision);
1298
+ }),
1299
+ )
1300
+ .pipe(
1301
+ Effect.flatMap((current) =>
1302
+ current ? probeProofCompletion(configuration, input) : Effect.succeed(false),
1303
+ ),
1304
+ )
1305
+ .pipe(
1306
+ Effect.provideService(CurrentEmailSql, database),
1307
+ Effect.provideService(LifecycleHooks, hooks),
1308
+ translateFailure,
1309
+ );
1310
+ },
1311
+ verifyWithProof: (uncaptured, prepare) =>
1312
+ mutateAddress(mapping, configuration, uncaptured, "verify-address", prepare).pipe(
1313
+ Effect.provideService(CurrentEmailSql, database),
1314
+ Effect.provideService(LifecycleHooks, hooks),
1315
+ translateFailure,
1316
+ ),
1317
+ changeWithProof: (uncaptured, prepare) =>
1318
+ mutateAddress(mapping, configuration, uncaptured, "change-address", prepare).pipe(
1319
+ Effect.provideService(CurrentEmailSql, database),
1320
+ Effect.provideService(LifecycleHooks, hooks),
1321
+ translateFailure,
1322
+ ),
1323
+ cleanup: (input, prepare) =>
1324
+ owned(
1325
+ database,
1326
+ configuration,
1327
+ Effect.gen(function* () {
1328
+ const transaction = yield* CurrentEmailSql;
1329
+ const journal = yield* CurrentCommitJournal;
1330
+
1331
+ const c = commandColumns(mapping);
1332
+ const now = yield* nowMillis;
1333
+
1334
+ const rows = yield* selectRows(
1335
+ transaction
1336
+ .select({ commandId: c.commandId })
1337
+ .from(mapping.command.table)
1338
+ .where(
1339
+ and(
1340
+ eq(c.moduleId, input.moduleId),
1341
+ lte(c.retentionUntil, mapping.encodeInstant(now)),
1342
+ ),
1343
+ )
1344
+ .limit(input.limit + 1),
1345
+ configuration.locking,
1346
+ );
1347
+
1348
+ const selected = rows.slice(0, input.limit);
1349
+
1350
+ const receipt = prepare(
1351
+ { removed: selected.length, hasMore: rows.length > input.limit },
1352
+ journal,
1353
+ );
1354
+
1355
+ if (selected.length > 0)
1356
+ yield* transaction.delete(mapping.command.table).where(
1357
+ and(
1358
+ eq(c.moduleId, input.moduleId),
1359
+ inArray(
1360
+ c.commandId,
1361
+ selected.map((row: any) => row.commandId),
1362
+ ),
1363
+ lte(c.retentionUntil, mapping.encodeInstant(now)),
1364
+ ),
1365
+ );
1366
+
1367
+ return receipt;
1368
+ }),
1369
+ ).pipe(
1370
+ Effect.provideService(CurrentEmailSql, database),
1371
+ Effect.provideService(LifecycleHooks, hooks),
1372
+ translateFailure,
1373
+ ),
1374
+ });
1375
+ });
1376
+
1377
+ const mutateAddress = Effect.fn("Drizzle.mutateAddress")(function* <A>(
1378
+ mapping: AddressMapping,
1379
+ configuration: EmailSqlConfiguration,
1380
+ uncaptured: EmailAddressMutation,
1381
+ action: EmailAction,
1382
+ prepare: (value: "changed" | "rejected", journal: CommitJournal) => PreparedCommit<A>,
1383
+ ) {
1384
+ const database = yield* CurrentEmailSql;
1385
+
1386
+ if (!validAddressConstraints(mapping) || configuration.proof === undefined)
1387
+ return yield* unavailable();
1388
+ const input = yield* snapshotEmailMutation(uncaptured);
1389
+
1390
+ const targetCredentialId = yield* allocate(
1391
+ configuration.mode,
1392
+ mapping.allocateCredentialId,
1393
+ mapping.allocateCredentialIdSync,
1394
+ );
1395
+
1396
+ const targetIdentifierRevision = yield* allocate(
1397
+ configuration.mode,
1398
+ mapping.allocateRevision,
1399
+ mapping.allocateRevisionSync,
1400
+ );
1401
+
1402
+ const targetCredentialRevision = yield* allocate(
1403
+ configuration.mode,
1404
+ mapping.allocateRevision,
1405
+ mapping.allocateRevisionSync,
1406
+ );
1407
+
1408
+ const sourceIdentifierRevision = yield* allocate(
1409
+ configuration.mode,
1410
+ mapping.allocateRevision,
1411
+ mapping.allocateRevisionSync,
1412
+ );
1413
+
1414
+ const sourceCredentialRevision = yield* allocate(
1415
+ configuration.mode,
1416
+ mapping.allocateRevision,
1417
+ mapping.allocateRevisionSync,
1418
+ );
1419
+
1420
+ const confirmsExisting =
1421
+ action === "verify-address" && input.captured.targetIdentifierRevision !== undefined;
1422
+
1423
+ const nextRevision = confirmsExisting
1424
+ ? input.captured.revision.securityRevision
1425
+ : yield* nextSecurityRevision(
1426
+ mapping,
1427
+ configuration.mode,
1428
+ input.captured.revision.securityRevision,
1429
+ );
1430
+
1431
+ if (
1432
+ (!confirmsExisting && nextRevision === input.captured.revision.securityRevision) ||
1433
+ [
1434
+ targetIdentifierRevision,
1435
+ targetCredentialRevision,
1436
+ sourceIdentifierRevision,
1437
+ sourceCredentialRevision,
1438
+ ].some((value) => input.captured.revision.credentials.some((item) => item.revision === value))
1439
+ )
1440
+ return yield* unavailable();
1441
+
1442
+ const run = owned(
1443
+ database,
1444
+ configuration,
1445
+ Effect.gen(function* () {
1446
+ const journal = yield* CurrentCommitJournal;
1447
+
1448
+ const current = yield* validateEmailAuthority(mapping, configuration, input, action);
1449
+
1450
+ if (current === undefined) return prepare("rejected", journal);
1451
+ if (
1452
+ (yield* commandExists(mapping, input.moduleId, input.commandId, configuration.locking))
1453
+ .length > 0
1454
+ )
1455
+ return prepare("rejected", journal);
1456
+ let receipt: PreparedCommit<A> | undefined;
1457
+
1458
+ yield* completeProofPlanIn(
1459
+ configuration.proof!.mapping,
1460
+ configuration.proof!.configuration,
1461
+ input.completion,
1462
+ Effect.gen(function* () {
1463
+ // The proof helper may wait on continuation/series locks. Read the
1464
+ // commit clock and reassess both policies only after those locks.
1465
+ const currentRequirement = yield* mapping.subject.decodeActionRequirement(
1466
+ current.subject,
1467
+ action,
1468
+ );
1469
+
1470
+ const original = yield* assessAuthentication(
1471
+ input.authorization.evidence,
1472
+ input.authorization.requirement,
1473
+ ).pipe(Effect.mapError(unavailable));
1474
+
1475
+ const configured = yield* assessAuthentication(
1476
+ input.authorization.evidence,
1477
+ currentRequirement,
1478
+ ).pipe(Effect.mapError(unavailable));
1479
+
1480
+ if (!original.satisfied || !configured.satisfied) return false;
1481
+ const now = yield* nowMillis;
1482
+
1483
+ return yield* applyMutation(
1484
+ mapping,
1485
+ input,
1486
+ action,
1487
+ current,
1488
+ {
1489
+ targetCredentialId,
1490
+ targetIdentifierRevision,
1491
+ targetCredentialRevision,
1492
+ sourceIdentifierRevision,
1493
+ sourceCredentialRevision,
1494
+ nextSecurityRevision: nextRevision,
1495
+ },
1496
+ now,
1497
+ );
1498
+ }),
1499
+ (decision) => {
1500
+ receipt = prepare(decision === "completed" ? "changed" : "rejected", journal);
1501
+
1502
+ return decision;
1503
+ },
1504
+ );
1505
+
1506
+ return receipt ?? prepare("rejected", journal);
1507
+ }),
1508
+ );
1509
+
1510
+ return yield* run.pipe(
1511
+ Effect.catchCause((cause) =>
1512
+ (cause.reasons.every(Cause.isFailReason) &&
1513
+ isMappedConstraintConflict(mapping.isCommandConflict, cause)) ||
1514
+ (cause.reasons.every(Cause.isFailReason) &&
1515
+ isMappedConstraintConflict(mapping.isIdentifierConflict, cause)) ||
1516
+ (cause.reasons.every(Cause.isFailReason) &&
1517
+ isMappedConstraintConflict(mapping.isCredentialConflict, cause))
1518
+ ? prepareRejected(configuration, prepare)
1519
+ : Effect.failCause(cause),
1520
+ ),
1521
+ );
1522
+ });
1523
+
1524
+ return {
1525
+ validEmailSignInConstraints,
1526
+ emailLookupQuery,
1527
+ decodeEmailSnapshot,
1528
+ makeSqlEmailSignInTargets,
1529
+ currentAddress,
1530
+ sameEmailRevision,
1531
+ snapshotEmailMutation,
1532
+ validateEmailAuthority,
1533
+ makeSqlEmailAddressPersistence,
1534
+ };
1535
+ };