@yielded/auth-persistence 0.1.0-beta.23 → 0.1.0-beta.25

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 +11 -0
  2. package/dist/Adapter.d.mts +12 -6
  3. package/dist/Adapter.mjs +8 -3
  4. package/dist/OAuthPersistence.d.mts +87 -0
  5. package/dist/OAuthPersistence.mjs +125 -0
  6. package/dist/OAuthPersistence.mjs.map +1 -0
  7. package/dist/_virtual/_rolldown/runtime.mjs +13 -0
  8. package/dist/index.d.mts +3 -1
  9. package/dist/index.mjs +3 -1
  10. package/dist/internal/configuration.d.mts +1 -1
  11. package/dist/internal/configuration.mjs.map +1 -1
  12. package/dist/internal/email-kernel.d.mts +1 -1
  13. package/dist/internal/email-kernel.mjs +1 -1
  14. package/dist/internal/email-kernel.mjs.map +1 -1
  15. package/dist/internal/models/oauth-connected-model.d.mts +294 -0
  16. package/dist/internal/models/oauth-connected-model.mjs +22 -0
  17. package/dist/internal/models/oauth-connected-model.mjs.map +1 -0
  18. package/dist/internal/models/oauth-model.d.mts +467 -0
  19. package/dist/internal/models/oauth-model.mjs +49 -0
  20. package/dist/internal/models/oauth-model.mjs.map +1 -0
  21. package/dist/internal/models/password-model.d.mts +6 -48
  22. package/dist/internal/models/password-model.mjs +0 -1
  23. package/dist/internal/models/password-model.mjs.map +1 -1
  24. package/dist/internal/native-sql-table.d.mts +4 -0
  25. package/dist/internal/native-sql-table.mjs +7 -0
  26. package/dist/internal/native-sql-table.mjs.map +1 -1
  27. package/dist/internal/oauth/accounts.d.mts +8 -0
  28. package/dist/internal/oauth/accounts.mjs +379 -0
  29. package/dist/internal/oauth/accounts.mjs.map +1 -0
  30. package/dist/internal/oauth/connected-access.d.mts +9 -0
  31. package/dist/internal/oauth/connected-access.mjs +229 -0
  32. package/dist/internal/oauth/connected-access.mjs.map +1 -0
  33. package/dist/internal/oauth/connected-collection.d.mts +7 -0
  34. package/dist/internal/oauth/connected-collection.mjs +214 -0
  35. package/dist/internal/oauth/connected-collection.mjs.map +1 -0
  36. package/dist/internal/oauth/connected-custody.d.mts +6 -0
  37. package/dist/internal/oauth/connected-custody.mjs +165 -0
  38. package/dist/internal/oauth/connected-custody.mjs.map +1 -0
  39. package/dist/internal/oauth/connected-flow.d.mts +7 -0
  40. package/dist/internal/oauth/connected-flow.mjs +324 -0
  41. package/dist/internal/oauth/connected-flow.mjs.map +1 -0
  42. package/dist/internal/oauth/connected-input.mjs +122 -0
  43. package/dist/internal/oauth/connected-input.mjs.map +1 -0
  44. package/dist/internal/oauth/connected-keys.d.mts +2 -0
  45. package/dist/internal/oauth/connected-keys.mjs +22 -0
  46. package/dist/internal/oauth/connected-keys.mjs.map +1 -0
  47. package/dist/internal/oauth/connected-maintenance.d.mts +9 -0
  48. package/dist/internal/oauth/connected-maintenance.mjs +226 -0
  49. package/dist/internal/oauth/connected-maintenance.mjs.map +1 -0
  50. package/dist/internal/oauth/connected-management.d.mts +9 -0
  51. package/dist/internal/oauth/connected-management.mjs +243 -0
  52. package/dist/internal/oauth/connected-management.mjs.map +1 -0
  53. package/dist/internal/oauth/connected-reference.mjs +63 -0
  54. package/dist/internal/oauth/connected-reference.mjs.map +1 -0
  55. package/dist/internal/oauth/connected-settlement.d.mts +8 -0
  56. package/dist/internal/oauth/connected-settlement.mjs +141 -0
  57. package/dist/internal/oauth/connected-settlement.mjs.map +1 -0
  58. package/dist/internal/oauth/connected-sign-in.d.mts +10 -0
  59. package/dist/internal/oauth/connected-sign-in.mjs +281 -0
  60. package/dist/internal/oauth/connected-sign-in.mjs.map +1 -0
  61. package/dist/internal/oauth/connected-state.d.mts +14 -0
  62. package/dist/internal/oauth/connected-state.mjs +469 -0
  63. package/dist/internal/oauth/connected-state.mjs.map +1 -0
  64. package/dist/internal/oauth/connected-target.d.mts +21 -0
  65. package/dist/internal/oauth/connected-target.mjs +117 -0
  66. package/dist/internal/oauth/connected-target.mjs.map +1 -0
  67. package/dist/internal/oauth/flow.d.mts +5 -0
  68. package/dist/internal/oauth/flow.mjs +279 -0
  69. package/dist/internal/oauth/flow.mjs.map +1 -0
  70. package/dist/internal/oauth/input.mjs +127 -0
  71. package/dist/internal/oauth/input.mjs.map +1 -0
  72. package/dist/internal/oauth/owner.d.mts +10 -0
  73. package/dist/internal/oauth/owner.mjs +10 -0
  74. package/dist/internal/oauth/owner.mjs.map +1 -0
  75. package/dist/internal/oauth/query-compiler.d.mts +7 -0
  76. package/dist/internal/oauth/query-compiler.mjs +9 -0
  77. package/dist/internal/oauth/query-compiler.mjs.map +1 -0
  78. package/dist/internal/oauth/registration.d.mts +7 -0
  79. package/dist/internal/oauth/registration.mjs +393 -0
  80. package/dist/internal/oauth/registration.mjs.map +1 -0
  81. package/dist/internal/oauth/state.mjs +107 -0
  82. package/dist/internal/oauth/state.mjs.map +1 -0
  83. package/dist/internal/oauth/target.d.mts +31 -0
  84. package/dist/internal/oauth/target.mjs +273 -0
  85. package/dist/internal/oauth/target.mjs.map +1 -0
  86. package/dist/internal/oauth-kernel.d.mts +1423 -0
  87. package/dist/internal/oauth-kernel.mjs +187 -0
  88. package/dist/internal/oauth-kernel.mjs.map +1 -0
  89. package/dist/internal/oauth-proxy.d.mts +31 -0
  90. package/dist/internal/oauth-proxy.mjs +129 -0
  91. package/dist/internal/oauth-proxy.mjs.map +1 -0
  92. package/dist/internal/passkey/write-state.mjs +9 -1
  93. package/dist/internal/passkey/write-state.mjs.map +1 -1
  94. package/dist/internal/passkey-kernel.d.mts +9 -9
  95. package/dist/internal/{password-attempts.d.mts → password-credentials.d.mts} +3 -4
  96. package/dist/internal/password-credentials.mjs +121 -0
  97. package/dist/internal/password-credentials.mjs.map +1 -0
  98. package/dist/internal/password-kernel.d.mts +8 -24
  99. package/dist/internal/password-kernel.mjs +5 -43
  100. package/dist/internal/password-kernel.mjs.map +1 -1
  101. package/dist/internal/persistence.d.mts +1 -1
  102. package/dist/internal/persistence.mjs +2 -2
  103. package/dist/internal/persistence.mjs.map +1 -1
  104. package/dist/internal/query-operations.d.mts +2 -0
  105. package/dist/internal/session-kernel.d.mts +2 -2
  106. package/dist/internal/session-kernel.mjs +74 -18
  107. package/dist/internal/session-kernel.mjs.map +1 -1
  108. package/dist/internal/sql-oauth-connected-model.d.mts +29 -0
  109. package/dist/internal/sql-oauth-connected-model.mjs +7 -0
  110. package/dist/internal/sql-oauth-connected-model.mjs.map +1 -0
  111. package/dist/internal/sql-oauth-model.d.mts +60 -0
  112. package/dist/internal/sql-oauth-model.mjs +10 -0
  113. package/dist/internal/sql-oauth-model.mjs.map +1 -0
  114. package/dist/internal/sql-query.d.mts +12 -0
  115. package/dist/internal/sql-query.mjs +15 -18
  116. package/dist/internal/sql-query.mjs.map +1 -1
  117. package/dist/internal/sql-storage-validation.mjs +54 -0
  118. package/dist/internal/sql-storage-validation.mjs.map +1 -0
  119. package/dist/internal/storage-mapping.mjs +1 -37
  120. package/dist/internal/storage-mapping.mjs.map +1 -1
  121. package/dist/internal/storage-plans.d.mts +7 -0
  122. package/dist/internal/storage-plans.mjs +189 -0
  123. package/dist/internal/storage-plans.mjs.map +1 -0
  124. package/dist/internal/storage-tables.d.mts +0 -56
  125. package/dist/internal/storage-tables.mjs +0 -17
  126. package/dist/internal/storage-tables.mjs.map +1 -1
  127. package/dist/internal/storage-validation.d.mts +6 -1
  128. package/dist/internal/storage-validation.mjs +42 -1
  129. package/dist/internal/storage-validation.mjs.map +1 -1
  130. package/dist/internal/transaction-kernel.mjs +3 -2
  131. package/dist/internal/transaction-kernel.mjs.map +1 -1
  132. package/package.json +1 -1
  133. package/src/Adapter.ts +89 -5
  134. package/src/OAuthPersistence.ts +427 -0
  135. package/src/index.ts +3 -0
  136. package/src/internal/configuration.ts +1 -3
  137. package/src/internal/email-kernel.ts +2 -1
  138. package/src/internal/models/oauth-connected-model.ts +396 -0
  139. package/src/internal/models/oauth-model.ts +708 -0
  140. package/src/internal/models/password-model.ts +2 -57
  141. package/src/internal/native-sql-table.ts +14 -0
  142. package/src/internal/oauth/accounts.ts +839 -0
  143. package/src/internal/oauth/connected-access.ts +388 -0
  144. package/src/internal/oauth/connected-collection.ts +345 -0
  145. package/src/internal/oauth/connected-custody.ts +308 -0
  146. package/src/internal/oauth/connected-flow.ts +562 -0
  147. package/src/internal/oauth/connected-input.ts +147 -0
  148. package/src/internal/oauth/connected-keys.ts +31 -0
  149. package/src/internal/oauth/connected-maintenance.ts +379 -0
  150. package/src/internal/oauth/connected-management.ts +391 -0
  151. package/src/internal/oauth/connected-reference.ts +135 -0
  152. package/src/internal/oauth/connected-settlement.ts +278 -0
  153. package/src/internal/oauth/connected-sign-in.ts +472 -0
  154. package/src/internal/oauth/connected-state.ts +924 -0
  155. package/src/internal/oauth/connected-target.ts +307 -0
  156. package/src/internal/oauth/flow.ts +505 -0
  157. package/src/internal/oauth/input.ts +201 -0
  158. package/src/internal/oauth/owner.ts +22 -0
  159. package/src/internal/oauth/query-compiler.ts +15 -0
  160. package/src/internal/oauth/registration.ts +676 -0
  161. package/src/internal/oauth/state.ts +218 -0
  162. package/src/internal/oauth/target.ts +803 -0
  163. package/src/internal/oauth-kernel.ts +294 -0
  164. package/src/internal/oauth-proxy.ts +176 -0
  165. package/src/internal/passkey/write-state.ts +18 -4
  166. package/src/internal/password-credentials.ts +245 -0
  167. package/src/internal/password-kernel.ts +7 -99
  168. package/src/internal/persistence.ts +1 -1
  169. package/src/internal/query-operations.ts +2 -0
  170. package/src/internal/session-kernel.ts +143 -55
  171. package/src/internal/sql-oauth-connected-model.ts +145 -0
  172. package/src/internal/sql-oauth-model.ts +263 -0
  173. package/src/internal/sql-query.ts +16 -13
  174. package/src/internal/sql-storage-validation.ts +64 -0
  175. package/src/internal/storage-mapping.ts +0 -37
  176. package/src/internal/storage-plans.ts +218 -0
  177. package/src/internal/storage-tables.ts +0 -20
  178. package/src/internal/storage-validation.ts +58 -0
  179. package/src/internal/transaction-kernel.ts +6 -2
  180. package/dist/internal/password-attempts.mjs +0 -197
  181. package/dist/internal/password-attempts.mjs.map +0 -1
  182. package/src/internal/password-attempts.ts +0 -466
@@ -0,0 +1,676 @@
1
+ /* oxlint-disable no-explicit-any -- compiler table shapes are erased; domain codecs and error channels remain typed. */
2
+ import {
3
+ OAuthRegistrationAccess,
4
+ OAuthRegistrationDecision,
5
+ OAuthRegistrationFingerprint,
6
+ OAuthRegistrationInspection,
7
+ OAuthRegistrationIntent,
8
+ OAuthCredentialSnapshot,
9
+ OAuthDisplayProfile,
10
+ OAuthSignInTransactionContext,
11
+ type OAuthExternalIdentity,
12
+ snapshotOAuthSync,
13
+ } from "@yielded/auth/OAuth";
14
+ import type { SecurityRevision } from "@yielded/auth/Sessions";
15
+ import { Effect, Schema, Context, Layer } from "effect";
16
+
17
+ import type { PersistenceMappingError, SubjectIdCodec } from "../models/common";
18
+ import type { OAuthRegistrationBase } from "../models/oauth-model";
19
+ import { OAuthFlow } from "./flow";
20
+ import { CurrentOAuthTransaction } from "./owner";
21
+ import { OAuthQueryCompiler } from "./query-compiler";
22
+ import { invariant, oauthIdentityKey, sameIdentity, storage } from "./state";
23
+
24
+ export interface RegistrationResources {
25
+ readonly provisioningIdentity: string;
26
+ readonly pendingReference?: string;
27
+ readonly subjectId?: unknown;
28
+ readonly credentialId?: string;
29
+ readonly revision?: string;
30
+ }
31
+
32
+ /** Public mapping callbacks are closed Effects; erasing native row shapes must
33
+ * not erase their error or requirements channels. */
34
+ export type RegistrationCallbacks<Registration> = Pick<
35
+ OAuthRegistrationBase<Registration, any, any, any, any, unknown>,
36
+ "snapshot" | "snapshotSync" | "inspect" | "inspectSync"
37
+ >;
38
+
39
+ const make = Effect.gen(function* () {
40
+ const operations = yield* OAuthQueryCompiler;
41
+ const flow = yield* OAuthFlow;
42
+
43
+ const { eq, sql } = operations;
44
+ const { discoverOwned, exactClaim, resolveOwned, terminalFlow } = flow;
45
+ const { both, col, equal, matchesNativeRow } = operations;
46
+
47
+ const intentStorage = storage(OAuthRegistrationIntent);
48
+
49
+ const decisionStorage = storage(OAuthRegistrationDecision);
50
+
51
+ const contextStorage = storage(OAuthSignInTransactionContext);
52
+
53
+ const profileStorage = storage(Schema.NullOr(OAuthDisplayProfile));
54
+
55
+ const reservationStorage = storage(
56
+ Schema.Struct({
57
+ moduleId: Schema.String,
58
+ reference: Schema.String,
59
+ commandId: Schema.String,
60
+ fingerprint: Schema.String,
61
+ provisioningIdentity: Schema.String,
62
+ }),
63
+ );
64
+
65
+ const readTuple = (ownership: any, identity: typeof OAuthExternalIdentity.Type) =>
66
+ Effect.flatMap(CurrentOAuthTransaction, (owner) =>
67
+ Effect.gen(function* () {
68
+ const t = ownership.tuple,
69
+ key = yield* oauthIdentityKey(identity);
70
+
71
+ const where = { [t.identityKey]: key };
72
+ const observed = yield* owner.read(t.table, equal(t.table, where), { limit: 1 });
73
+
74
+ if (observed.rows.length === 0) {
75
+ const values = {
76
+ ...t.encodeInsert({ identityKey: key, identity }),
77
+ ...where,
78
+ [t.provider]: identity.provider,
79
+ [t.issuer]: identity.issuer,
80
+ [t.externalSubject]: identity.subject,
81
+ [t.state]: "Unowned",
82
+ [t.version]: owner.marker,
83
+ [t.subjectId]: null,
84
+ [t.reservation]: null,
85
+ };
86
+
87
+ const inserted = yield* owner.insert(t.table, values, where, true);
88
+
89
+ observed.rows = inserted.rows;
90
+ }
91
+ const row = observed.rows[0]!;
92
+
93
+ invariant(
94
+ row[t.provider] === identity.provider &&
95
+ row[t.issuer] === identity.issuer &&
96
+ row[t.externalSubject] === identity.subject &&
97
+ ["Unowned", "Reserved", "Owned"].includes(row[t.state]),
98
+ );
99
+ invariant(
100
+ row[t.state] === "Owned"
101
+ ? row[t.subjectId] !== null
102
+ : row[t.subjectId] === null &&
103
+ (row[t.state] === "Reserved"
104
+ ? typeof row[t.reservation] === "string"
105
+ : row[t.reservation] === null),
106
+ );
107
+ if (ownership.mode === "separate") {
108
+ const o = ownership.external;
109
+
110
+ const external = yield* owner.read(o.table, equal(o.table, { [o.identityKey]: key }), {
111
+ limit: 1,
112
+ });
113
+
114
+ if (row[t.state] === "Owned") {
115
+ const linked = external.rows[0];
116
+
117
+ invariant(
118
+ linked !== undefined &&
119
+ linked[o.provider] === identity.provider &&
120
+ linked[o.issuer] === identity.issuer &&
121
+ linked[o.externalSubject] === identity.subject &&
122
+ matchesNativeRow(o.table, linked, { [o.subjectId]: row[t.subjectId] }),
123
+ );
124
+ const owned = sql`exists(select 1 from ${o.table} where ${both(equal(o.table, { [o.identityKey]: key }), o.ownedCondition)})`;
125
+
126
+ invariant(yield* owner.check(owned));
127
+ owner.postconditions.push(owned);
128
+ } else invariant(external.rows.length === 0);
129
+ }
130
+
131
+ return { observed, row, key };
132
+ }),
133
+ );
134
+
135
+ const acquireTuple = (
136
+ ownership: any,
137
+ identity: typeof OAuthExternalIdentity.Type,
138
+ key: string,
139
+ subjectId: unknown,
140
+ ) =>
141
+ Effect.flatMap(CurrentOAuthTransaction, (owner) =>
142
+ Effect.gen(function* () {
143
+ const t = ownership.tuple;
144
+
145
+ yield* owner.update(
146
+ t.table,
147
+ { [t.identityKey]: key },
148
+ {
149
+ [t.state]: "Owned",
150
+ [t.subjectId]: subjectId,
151
+ [t.reservation]: null,
152
+ [t.version]: owner.marker,
153
+ },
154
+ );
155
+ if (ownership.mode === "separate") {
156
+ const o = ownership.external;
157
+
158
+ const values = {
159
+ ...o.encodeInsert({ identity, identityKey: key, subjectId }),
160
+ [o.identityKey]: key,
161
+ [o.provider]: identity.provider,
162
+ [o.issuer]: identity.issuer,
163
+ [o.externalSubject]: identity.subject,
164
+ [o.subjectId]: subjectId,
165
+ };
166
+
167
+ yield* owner.insert(o.table, values, { [o.identityKey]: key });
168
+ }
169
+ }),
170
+ );
171
+
172
+ const settleRegistrationIntent = (mapping: any, input: any) =>
173
+ Effect.flatMap(CurrentOAuthTransaction, (owner) =>
174
+ Effect.gen(function* () {
175
+ const identity = input.identity.identity,
176
+ context = input.claim.flow.context;
177
+
178
+ invariant(identity.provider === context.provider && identity.issuer === context.issuer);
179
+ const found = yield* discoverOwned(mapping.signIn, identity);
180
+ const tuple = found.owned ? undefined : yield* readTuple(mapping.ownership, identity);
181
+ const exact = yield* exactClaim(mapping.signIn, input.claim, "sign-in");
182
+
183
+ if (exact === undefined) return { _tag: "Rejected" } as const;
184
+ const credential = resolveOwned(mapping.signIn, context.moduleId, identity, found);
185
+
186
+ if (credential !== undefined) {
187
+ yield* terminalFlow(mapping.signIn, input.claim.flow, "Verified");
188
+
189
+ return { _tag: "Verified", credential } as const;
190
+ }
191
+ let decision: any = { _tag: "Rejected" };
192
+
193
+ if (
194
+ !found.owned &&
195
+ tuple?.row[mapping.ownership.tuple.state] === "Unowned" &&
196
+ input.intent !== undefined
197
+ ) {
198
+ const intent = snapshotOAuthSync(OAuthRegistrationIntent, input.intent);
199
+ const c = intent.context;
200
+
201
+ invariant(
202
+ sameIdentity(intent.identity, identity) &&
203
+ profileStorage.encode(intent.profile ?? null) ===
204
+ profileStorage.encode(input.identity.profile ?? null) &&
205
+ intent.claimId === input.claim.claimId &&
206
+ intent.claimedAtMillis === input.claim.claimedAtMillis &&
207
+ contextStorage.encode(c) === contextStorage.encode(context),
208
+ );
209
+
210
+ const allowed =
211
+ intent.issuedAtMillis >= intent.claimedAtMillis &&
212
+ intent.verifiedAtMillis <= intent.issuedAtMillis &&
213
+ intent.issuedAtMillis <= exact.now &&
214
+ exact.now < intent.expiresAtMillis &&
215
+ intent.expiresAtMillis <= c.requestBindingExpiresAtMillis &&
216
+ intent.retentionUntilMillis >= intent.expiresAtMillis &&
217
+ (yield* owner.check(mapping.eligibility.condition({ intent })));
218
+
219
+ if (allowed) {
220
+ const i = mapping.intent,
221
+ encode = mapping.signIn.clock.encodeInstant;
222
+
223
+ const key = { [i.moduleId]: c.moduleId, [i.reference]: intent.reference };
224
+
225
+ const values = {
226
+ ...i.encodeInsert(snapshotOAuthSync(OAuthRegistrationIntent, intent)),
227
+ ...key,
228
+ [i.flowId]: c.flowId,
229
+ [i.claimId]: intent.claimId,
230
+ [i.identityKey]: tuple.key,
231
+ [i.version]: owner.marker,
232
+ [i.state]: "Unbound",
233
+ [i.snapshot]: intentStorage.encode(intent),
234
+ [i.commandId]: null,
235
+ [i.fingerprint]: null,
236
+ [i.pendingReference]: null,
237
+ [i.expiresAt]: encode(intent.expiresAtMillis),
238
+ [i.retentionUntil]: encode(intent.retentionUntilMillis),
239
+ };
240
+
241
+ yield* owner.insert(i.table, values, key);
242
+ owner.postconditions.push(
243
+ mapping.eligibility.condition({ intent }),
244
+ sql`${mapping.signIn.clock.engineNowMillis} >= ${intent.issuedAtMillis} and
245
+ ${mapping.signIn.clock.engineNowMillis} < ${intent.expiresAtMillis}`,
246
+ );
247
+ decision = { _tag: "RegistrationIssued", intent };
248
+ }
249
+ }
250
+ yield* terminalFlow(mapping.signIn, input.claim.flow, decision._tag);
251
+
252
+ return decision;
253
+ }),
254
+ );
255
+
256
+ const inspectIntent = (
257
+ mapping: any,
258
+ original: typeof OAuthRegistrationAccess.Type,
259
+ discovery = false,
260
+ ) =>
261
+ Effect.flatMap(CurrentOAuthTransaction, (owner) =>
262
+ Effect.gen(function* () {
263
+ const access = snapshotOAuthSync(OAuthRegistrationAccess, original),
264
+ i = mapping.intent;
265
+
266
+ const observed = yield* owner.read(
267
+ i.table,
268
+ equal(i.table, {
269
+ [i.moduleId]: access.moduleId,
270
+ [i.reference]: access.reference,
271
+ }),
272
+ { limit: 1, ...(discovery ? { lock: false, observe: false } : {}) },
273
+ );
274
+
275
+ const row = observed.rows[0];
276
+
277
+ if (row === undefined) return undefined;
278
+
279
+ const intent = intentStorage.decode(row[i.snapshot]),
280
+ c = intent.context,
281
+ now = yield* owner.now(mapping.clock);
282
+
283
+ invariant(
284
+ row[i.moduleId] === c.moduleId &&
285
+ row[i.reference] === intent.reference &&
286
+ row[i.flowId] === c.flowId &&
287
+ row[i.claimId] === intent.claimId &&
288
+ row[i.identityKey] === (yield* oauthIdentityKey(intent.identity)) &&
289
+ mapping.clock.decodeInstant(row[i.expiresAt]) === intent.expiresAtMillis &&
290
+ mapping.clock.decodeInstant(row[i.retentionUntil]) === intent.retentionUntilMillis,
291
+ );
292
+ if (
293
+ access.moduleId !== c.moduleId ||
294
+ access.reference !== intent.reference ||
295
+ access.flowId !== c.flowId ||
296
+ access.requestBindingVerifier !== c.requestBindingVerifier ||
297
+ access.requestBindingExpiresAtMillis !== c.requestBindingExpiresAtMillis ||
298
+ access.credentialDigest !== intent.credentialDigest ||
299
+ now < intent.issuedAtMillis ||
300
+ now >= intent.expiresAtMillis
301
+ )
302
+ return undefined;
303
+ let application: any;
304
+
305
+ if (row[i.state] === "Unbound") {
306
+ invariant(
307
+ row[i.commandId] === null &&
308
+ row[i.fingerprint] === null &&
309
+ row[i.pendingReference] === null,
310
+ );
311
+ application = { _tag: "Unbound" };
312
+ } else {
313
+ application = {
314
+ _tag: row[i.state],
315
+ commandId: row[i.commandId],
316
+ fingerprint: row[i.fingerprint],
317
+ ...(row[i.state] === "ProvisioningPending"
318
+ ? { reference: row[i.pendingReference] }
319
+ : {}),
320
+ };
321
+ }
322
+ const inspection = snapshotOAuthSync(OAuthRegistrationInspection, { intent, application });
323
+
324
+ owner.postconditions
325
+ .push(sql`${mapping.clock.engineNowMillis} >= ${intent.issuedAtMillis} and
326
+ ${mapping.clock.engineNowMillis} < ${intent.expiresAtMillis}`);
327
+
328
+ return { observed, row, inspection };
329
+ }),
330
+ );
331
+
332
+ const registrationResources = (mapping: any) =>
333
+ Effect.gen(function* () {
334
+ const provisioningIdentity = yield* mapping.allocateProvisioningIdentity as Effect.Effect<
335
+ string,
336
+ PersistenceMappingError
337
+ >;
338
+
339
+ invariant(
340
+ typeof provisioningIdentity === "string" &&
341
+ provisioningIdentity.length > 0 &&
342
+ provisioningIdentity.length <= 256,
343
+ );
344
+ if (mapping.mode === "pending") {
345
+ const pendingReference = yield* mapping.allocatePendingReference as Effect.Effect<
346
+ string,
347
+ PersistenceMappingError
348
+ >;
349
+
350
+ invariant(
351
+ typeof pendingReference === "string" &&
352
+ pendingReference.length > 0 &&
353
+ pendingReference.length <= 256,
354
+ );
355
+
356
+ return { provisioningIdentity, pendingReference } as RegistrationResources;
357
+ }
358
+
359
+ const subjectId = yield* mapping.allocateSubjectId as Effect.Effect<
360
+ unknown,
361
+ PersistenceMappingError
362
+ >;
363
+
364
+ const publicId = yield* (mapping.subjectId as SubjectIdCodec<unknown>).toSubject(subjectId);
365
+
366
+ invariant(
367
+ mapping.subjectId.equals(
368
+ subjectId,
369
+ yield* (mapping.subjectId as SubjectIdCodec<unknown>).toNative(publicId),
370
+ ),
371
+ );
372
+
373
+ const credentialId = snapshotOAuthSync(
374
+ OAuthCredentialSnapshot.fields.credentialId,
375
+ yield* mapping.allocateCredentialId as Effect.Effect<string, PersistenceMappingError>,
376
+ );
377
+
378
+ const revision = snapshotOAuthSync(
379
+ OAuthCredentialSnapshot.fields.credentialRevision,
380
+ yield* mapping.allocateRevision as Effect.Effect<SecurityRevision, PersistenceMappingError>,
381
+ );
382
+
383
+ return { provisioningIdentity, subjectId, credentialId, revision } as RegistrationResources;
384
+ });
385
+
386
+ const registrationData = <Registration>(
387
+ mapping: any,
388
+ input: {
389
+ readonly intent: OAuthRegistrationIntent;
390
+ readonly registration: Registration;
391
+ readonly fingerprint: string;
392
+ },
393
+ bound: boolean,
394
+ ) =>
395
+ Effect.gen(function* () {
396
+ const snapshot = bound
397
+ ? (invariant(mapping.snapshotSync !== undefined), mapping.snapshotSync(input.registration))
398
+ : yield* (mapping as RegistrationCallbacks<Registration>).snapshot(input.registration);
399
+
400
+ const encoded = mapping.application.encode(snapshot);
401
+
402
+ invariant(typeof encoded === "string" && new TextEncoder().encode(encoded).length <= 1048576);
403
+ const detached = mapping.application.decode(encoded);
404
+
405
+ invariant(mapping.application.encode(detached) === encoded);
406
+
407
+ const checked = bound
408
+ ? (invariant(mapping.inspectSync !== undefined),
409
+ mapping.inspectSync({
410
+ intent: snapshotOAuthSync(OAuthRegistrationIntent, input.intent),
411
+ registration: mapping.application.decode(encoded),
412
+ }))
413
+ : yield* (mapping as RegistrationCallbacks<Registration>).inspect({
414
+ intent: snapshotOAuthSync(OAuthRegistrationIntent, input.intent),
415
+ registration: mapping.application.decode(encoded),
416
+ });
417
+
418
+ const fingerprint = snapshotOAuthSync(OAuthRegistrationFingerprint, checked.fingerprint);
419
+
420
+ invariant(typeof checked.eligible === "boolean");
421
+
422
+ return {
423
+ encoded,
424
+ registration: detached as Registration,
425
+ fingerprint,
426
+ eligible: checked.eligible && fingerprint === input.fingerprint,
427
+ };
428
+ });
429
+
430
+ const register = (
431
+ mapping: any,
432
+ input: any,
433
+ data: { readonly encoded: string; readonly fingerprint: string; readonly eligible: boolean },
434
+ resources: RegistrationResources,
435
+ ) =>
436
+ Effect.flatMap(CurrentOAuthTransaction, (owner) =>
437
+ Effect.gen(function* () {
438
+ const intent = snapshotOAuthSync(OAuthRegistrationIntent, input.intent),
439
+ c = intent.context;
440
+
441
+ const discovered = yield* inspectIntent(mapping, input.access, true);
442
+
443
+ if (
444
+ discovered === undefined ||
445
+ intentStorage.encode(discovered.inspection.intent) !== intentStorage.encode(intent)
446
+ )
447
+ return { _tag: "Rejected" } as const;
448
+ const nativeSubjectId = resources.subjectId;
449
+
450
+ const policyInput = () => ({
451
+ intent: snapshotOAuthSync(OAuthRegistrationIntent, intent),
452
+ registration: mapping.application.decode(data.encoded),
453
+ ...(mapping.mode === "atomic" ? { nativeSubjectId } : {}),
454
+ });
455
+
456
+ invariant((mapping.eligibility.guards?.length ?? 0) <= 32);
457
+ for (const guard of mapping.eligibility.guards ?? []) {
458
+ const locked = yield* owner.read(guard.table, guard.condition(policyInput()), {
459
+ orderBy: col(guard.table, guard.orderBy),
460
+ admissionOnly: true,
461
+ });
462
+
463
+ invariant(locked.rows.length > 0);
464
+ }
465
+ const tuple = yield* readTuple(mapping.ownership, intent.identity);
466
+ const inspected = yield* inspectIntent(mapping, input.access);
467
+
468
+ if (
469
+ inspected === undefined ||
470
+ intentStorage.encode(inspected.inspection.intent) !== intentStorage.encode(intent)
471
+ )
472
+ return { _tag: "Rejected" } as const;
473
+
474
+ const r = mapping.command,
475
+ i = mapping.intent,
476
+ t = mapping.ownership.tuple;
477
+
478
+ const commandKey = { [r.moduleId]: c.moduleId, [r.commandId]: input.commandId };
479
+ const command = yield* owner.read(r.table, equal(r.table, commandKey), { limit: 1 });
480
+ const previous = command.rows[0];
481
+
482
+ if (previous !== undefined) {
483
+ if (
484
+ previous[r.reference] !== intent.reference ||
485
+ previous[r.identityKey] !== tuple.key ||
486
+ previous[r.fingerprint] !== data.fingerprint ||
487
+ previous[r.intentSnapshot] !== intentStorage.encode(intent) ||
488
+ previous[r.applicationSnapshot] !== data.encoded
489
+ )
490
+ return { _tag: "Conflict" } as const;
491
+ invariant(
492
+ mapping.application.encode(
493
+ mapping.application.decode(previous[r.applicationSnapshot]),
494
+ ) === data.encoded,
495
+ );
496
+ const old = decisionStorage.decode(previous[r.decision]);
497
+
498
+ invariant(inspected.inspection.application._tag === old._tag);
499
+
500
+ return old._tag === "Registered" || old._tag === "ProvisioningPending"
501
+ ? { ...old, replayed: true }
502
+ : old;
503
+ }
504
+ if (inspected.inspection.application._tag !== "Unbound" || tuple.row[t.state] !== "Unowned")
505
+ return { _tag: "Conflict" } as const;
506
+ if (data.fingerprint !== input.fingerprint) return { _tag: "Rejected" } as const;
507
+
508
+ const admitted =
509
+ data.eligible && (yield* owner.check(mapping.eligibility.admission(policyInput())));
510
+
511
+ let decision: typeof OAuthRegistrationDecision.Type;
512
+
513
+ if (!admitted) decision = { _tag: "Rejected" };
514
+ else if (mapping.mode === "pending") {
515
+ invariant(resources.pendingReference !== undefined);
516
+
517
+ const reservation = reservationStorage.encode({
518
+ moduleId: c.moduleId,
519
+ reference: intent.reference,
520
+ commandId: input.commandId,
521
+ fingerprint: data.fingerprint,
522
+ provisioningIdentity: resources.provisioningIdentity,
523
+ });
524
+
525
+ yield* owner.update(
526
+ t.table,
527
+ { [t.identityKey]: tuple.key },
528
+ {
529
+ [t.state]: "Reserved",
530
+ [t.version]: owner.marker,
531
+ [t.reservation]: reservation,
532
+ },
533
+ );
534
+ decision = {
535
+ _tag: "ProvisioningPending",
536
+ reference: resources.pendingReference,
537
+ replayed: false,
538
+ };
539
+ } else {
540
+ invariant(resources.credentialId !== undefined && resources.revision !== undefined);
541
+
542
+ const s = mapping.subject,
543
+ cr = mapping.credential,
544
+ a = mapping.authority;
545
+
546
+ const subjectValues = {
547
+ ...mapping.encodeSubjectInsert(policyInput(), {
548
+ subjectId: nativeSubjectId,
549
+ securityRevision: resources.revision,
550
+ }),
551
+ [s.id]: nativeSubjectId,
552
+ [s.securityRevision]: resources.revision,
553
+ };
554
+
555
+ yield* owner.insert(s.table, subjectValues, { [s.id]: nativeSubjectId });
556
+
557
+ const credentialValues = {
558
+ ...cr.encodeInsert({
559
+ moduleId: c.moduleId,
560
+ subjectId: nativeSubjectId,
561
+ identityKey: tuple.key,
562
+ credentialId: resources.credentialId,
563
+ credentialRevision: resources.revision,
564
+ }),
565
+ [cr.moduleId]: c.moduleId,
566
+ [cr.subjectId]: nativeSubjectId,
567
+ [cr.identityKey]: tuple.key,
568
+ [cr.credentialId]: resources.credentialId,
569
+ [cr.credentialRevision]: resources.revision,
570
+ };
571
+
572
+ invariant(
573
+ cr.isActiveStatus(credentialValues[cr.status]) &&
574
+ s.isActiveStatus(subjectValues[s.status]),
575
+ );
576
+ yield* owner.insert(cr.table, credentialValues, {
577
+ [cr.credentialId]: resources.credentialId,
578
+ });
579
+
580
+ const authorityValues = {
581
+ ...a.encodeInsert({
582
+ subjectId: nativeSubjectId,
583
+ credentialId: resources.credentialId,
584
+ revision: resources.revision,
585
+ }),
586
+ [a.subjectId]: nativeSubjectId,
587
+ [a.credentialId]: resources.credentialId,
588
+ [a.revision]: resources.revision,
589
+ };
590
+
591
+ invariant(a.isActiveStatus(authorityValues[a.status]));
592
+ yield* owner.insert(a.table, authorityValues, {
593
+ [a.subjectId]: nativeSubjectId,
594
+ [a.credentialId]: resources.credentialId,
595
+ });
596
+ yield* acquireTuple(mapping.ownership, intent.identity, tuple.key, nativeSubjectId);
597
+ owner.postconditions.push(
598
+ sql`exists(select 1 from ${s.table} where ${both(
599
+ eq(col(s.table, s.id), nativeSubjectId),
600
+ s.activeCondition,
601
+ )})`,
602
+ sql`exists(select 1 from ${cr.table} where ${both(eq(col(cr.table, cr.credentialId), resources.credentialId), cr.activeCondition)})`,
603
+ sql`exists(select 1 from ${a.table} where ${both(
604
+ eq(col(a.table, a.credentialId), resources.credentialId),
605
+ eq(col(a.table, a.subjectId), nativeSubjectId),
606
+ a.activeCondition,
607
+ )})`,
608
+ );
609
+ decision = { _tag: "Registered", replayed: false };
610
+ }
611
+ const now = yield* owner.now(mapping.clock);
612
+
613
+ invariant(
614
+ Number.isSafeInteger(mapping.retentionMillis) && mapping.retentionMillis >= 120000,
615
+ );
616
+
617
+ const values = {
618
+ ...r.encodeInsert({
619
+ intent: snapshotOAuthSync(OAuthRegistrationIntent, intent),
620
+ commandId: input.commandId,
621
+ fingerprint: data.fingerprint,
622
+ registration: mapping.application.decode(data.encoded),
623
+ provisioningIdentity: resources.provisioningIdentity,
624
+ }),
625
+ ...commandKey,
626
+ [r.reference]: intent.reference,
627
+ [r.identityKey]: tuple.key,
628
+ [r.fingerprint]: data.fingerprint,
629
+ [r.intentSnapshot]: intentStorage.encode(intent),
630
+ [r.applicationSnapshot]: data.encoded,
631
+ [r.provisioningIdentity]: resources.provisioningIdentity,
632
+ [r.decision]: decisionStorage.encode(decision),
633
+ [r.retentionUntil]: mapping.clock.encodeInstant(
634
+ Math.max(intent.retentionUntilMillis, now + mapping.retentionMillis),
635
+ ),
636
+ };
637
+
638
+ const inserted = yield* owner.insert(r.table, values, commandKey);
639
+
640
+ command.rows = inserted.rows;
641
+ yield* owner.update(
642
+ i.table,
643
+ { [i.moduleId]: c.moduleId, [i.reference]: intent.reference },
644
+ {
645
+ [i.state]: decision._tag,
646
+ [i.commandId]: input.commandId,
647
+ [i.fingerprint]: data.fingerprint,
648
+ [i.version]: owner.marker,
649
+ [i.pendingReference]:
650
+ decision._tag === "ProvisioningPending" ? decision.reference : null,
651
+ },
652
+ );
653
+ if (admitted) owner.postconditions.push(mapping.eligibility.postcondition(policyInput()));
654
+
655
+ return decision;
656
+ }),
657
+ );
658
+
659
+ return {
660
+ intentStorage,
661
+ readTuple,
662
+ acquireTuple,
663
+ settleRegistrationIntent,
664
+ inspectIntent,
665
+ registrationResources,
666
+ registrationData,
667
+ register,
668
+ };
669
+ });
670
+
671
+ export class OAuthRegistration extends Context.Service<
672
+ OAuthRegistration,
673
+ Effect.Success<typeof make>
674
+ >()("effect-auth/persistence/OAuthRegistration") {
675
+ static readonly layer = Layer.effect(OAuthRegistration, make);
676
+ }