@yielded/auth-persistence 0.1.0-beta.22 → 0.1.0-beta.24

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 +13 -5
  3. package/dist/Adapter.mjs +9 -2
  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/authority-snapshot.mjs +53 -0
  11. package/dist/internal/authority-snapshot.mjs.map +1 -0
  12. package/dist/internal/configuration.d.mts +1 -1
  13. package/dist/internal/configuration.mjs.map +1 -1
  14. package/dist/internal/models/oauth-connected-model.d.mts +294 -0
  15. package/dist/internal/models/oauth-connected-model.mjs +22 -0
  16. package/dist/internal/models/oauth-connected-model.mjs.map +1 -0
  17. package/dist/internal/models/oauth-model.d.mts +467 -0
  18. package/dist/internal/models/oauth-model.mjs +49 -0
  19. package/dist/internal/models/oauth-model.mjs.map +1 -0
  20. package/dist/internal/models/password-model.d.mts +4 -97
  21. package/dist/internal/models/password-model.mjs +0 -3
  22. package/dist/internal/models/password-model.mjs.map +1 -1
  23. package/dist/internal/native-sql-table.d.mts +34 -0
  24. package/dist/internal/native-sql-table.mjs +111 -0
  25. package/dist/internal/native-sql-table.mjs.map +1 -0
  26. package/dist/internal/oauth/accounts.d.mts +8 -0
  27. package/dist/internal/oauth/accounts.mjs +379 -0
  28. package/dist/internal/oauth/accounts.mjs.map +1 -0
  29. package/dist/internal/oauth/connected-access.d.mts +9 -0
  30. package/dist/internal/oauth/connected-access.mjs +229 -0
  31. package/dist/internal/oauth/connected-access.mjs.map +1 -0
  32. package/dist/internal/oauth/connected-collection.d.mts +7 -0
  33. package/dist/internal/oauth/connected-collection.mjs +214 -0
  34. package/dist/internal/oauth/connected-collection.mjs.map +1 -0
  35. package/dist/internal/oauth/connected-custody.d.mts +6 -0
  36. package/dist/internal/oauth/connected-custody.mjs +165 -0
  37. package/dist/internal/oauth/connected-custody.mjs.map +1 -0
  38. package/dist/internal/oauth/connected-flow.d.mts +7 -0
  39. package/dist/internal/oauth/connected-flow.mjs +324 -0
  40. package/dist/internal/oauth/connected-flow.mjs.map +1 -0
  41. package/dist/internal/oauth/connected-input.mjs +122 -0
  42. package/dist/internal/oauth/connected-input.mjs.map +1 -0
  43. package/dist/internal/oauth/connected-keys.d.mts +2 -0
  44. package/dist/internal/oauth/connected-keys.mjs +22 -0
  45. package/dist/internal/oauth/connected-keys.mjs.map +1 -0
  46. package/dist/internal/oauth/connected-maintenance.d.mts +9 -0
  47. package/dist/internal/oauth/connected-maintenance.mjs +226 -0
  48. package/dist/internal/oauth/connected-maintenance.mjs.map +1 -0
  49. package/dist/internal/oauth/connected-management.d.mts +9 -0
  50. package/dist/internal/oauth/connected-management.mjs +243 -0
  51. package/dist/internal/oauth/connected-management.mjs.map +1 -0
  52. package/dist/internal/oauth/connected-reference.mjs +63 -0
  53. package/dist/internal/oauth/connected-reference.mjs.map +1 -0
  54. package/dist/internal/oauth/connected-settlement.d.mts +8 -0
  55. package/dist/internal/oauth/connected-settlement.mjs +141 -0
  56. package/dist/internal/oauth/connected-settlement.mjs.map +1 -0
  57. package/dist/internal/oauth/connected-sign-in.d.mts +10 -0
  58. package/dist/internal/oauth/connected-sign-in.mjs +281 -0
  59. package/dist/internal/oauth/connected-sign-in.mjs.map +1 -0
  60. package/dist/internal/oauth/connected-state.d.mts +14 -0
  61. package/dist/internal/oauth/connected-state.mjs +469 -0
  62. package/dist/internal/oauth/connected-state.mjs.map +1 -0
  63. package/dist/internal/oauth/connected-target.d.mts +21 -0
  64. package/dist/internal/oauth/connected-target.mjs +117 -0
  65. package/dist/internal/oauth/connected-target.mjs.map +1 -0
  66. package/dist/internal/oauth/flow.d.mts +5 -0
  67. package/dist/internal/oauth/flow.mjs +279 -0
  68. package/dist/internal/oauth/flow.mjs.map +1 -0
  69. package/dist/internal/oauth/input.mjs +127 -0
  70. package/dist/internal/oauth/input.mjs.map +1 -0
  71. package/dist/internal/oauth/owner.d.mts +10 -0
  72. package/dist/internal/oauth/owner.mjs +10 -0
  73. package/dist/internal/oauth/owner.mjs.map +1 -0
  74. package/dist/internal/oauth/query-compiler.d.mts +7 -0
  75. package/dist/internal/oauth/query-compiler.mjs +9 -0
  76. package/dist/internal/oauth/query-compiler.mjs.map +1 -0
  77. package/dist/internal/oauth/registration.d.mts +7 -0
  78. package/dist/internal/oauth/registration.mjs +393 -0
  79. package/dist/internal/oauth/registration.mjs.map +1 -0
  80. package/dist/internal/oauth/state.mjs +107 -0
  81. package/dist/internal/oauth/state.mjs.map +1 -0
  82. package/dist/internal/oauth/target.d.mts +31 -0
  83. package/dist/internal/oauth/target.mjs +273 -0
  84. package/dist/internal/oauth/target.mjs.map +1 -0
  85. package/dist/internal/oauth-kernel.d.mts +1423 -0
  86. package/dist/internal/oauth-kernel.mjs +187 -0
  87. package/dist/internal/oauth-kernel.mjs.map +1 -0
  88. package/dist/internal/oauth-proxy.d.mts +31 -0
  89. package/dist/internal/oauth-proxy.mjs +129 -0
  90. package/dist/internal/oauth-proxy.mjs.map +1 -0
  91. package/dist/internal/passkey/target.d.mts +1 -1
  92. package/dist/internal/passkey-kernel.d.mts +5 -5
  93. package/dist/internal/passkeys.mjs +1 -1
  94. package/dist/internal/password-credentials.d.mts +11 -0
  95. package/dist/internal/password-credentials.mjs +121 -0
  96. package/dist/internal/password-credentials.mjs.map +1 -0
  97. package/dist/internal/password-kernel.d.mts +15 -28
  98. package/dist/internal/password-kernel.mjs +35 -236
  99. package/dist/internal/password-kernel.mjs.map +1 -1
  100. package/dist/internal/persistence.d.mts +2 -0
  101. package/dist/internal/persistence.mjs +5 -5
  102. package/dist/internal/persistence.mjs.map +1 -1
  103. package/dist/internal/query-operations.d.mts +2 -0
  104. package/dist/internal/session-kernel.d.mts +5 -3
  105. package/dist/internal/session-kernel.mjs +95 -29
  106. package/dist/internal/session-kernel.mjs.map +1 -1
  107. package/dist/internal/sql-oauth-connected-model.d.mts +29 -0
  108. package/dist/internal/sql-oauth-connected-model.mjs +7 -0
  109. package/dist/internal/sql-oauth-connected-model.mjs.map +1 -0
  110. package/dist/internal/sql-oauth-model.d.mts +60 -0
  111. package/dist/internal/sql-oauth-model.mjs +10 -0
  112. package/dist/internal/sql-oauth-model.mjs.map +1 -0
  113. package/dist/internal/sql-persistence.mjs +3 -1
  114. package/dist/internal/sql-persistence.mjs.map +1 -1
  115. package/dist/internal/sql-query.d.mts +12 -0
  116. package/dist/internal/sql-query.mjs +15 -18
  117. package/dist/internal/sql-query.mjs.map +1 -1
  118. package/dist/internal/sql-storage-validation.mjs +54 -0
  119. package/dist/internal/sql-storage-validation.mjs.map +1 -0
  120. package/dist/internal/storage-mapping.mjs +2 -67
  121. package/dist/internal/storage-mapping.mjs.map +1 -1
  122. package/dist/internal/storage-plans.d.mts +7 -0
  123. package/dist/internal/storage-plans.mjs +189 -0
  124. package/dist/internal/storage-plans.mjs.map +1 -0
  125. package/dist/internal/storage-tables.d.mts +0 -99
  126. package/dist/internal/storage-tables.mjs +0 -43
  127. package/dist/internal/storage-tables.mjs.map +1 -1
  128. package/dist/internal/storage-validation.d.mts +6 -1
  129. package/dist/internal/storage-validation.mjs +42 -1
  130. package/dist/internal/storage-validation.mjs.map +1 -1
  131. package/dist/internal/transaction-execution-kernel.d.mts +1 -1
  132. package/dist/internal/transaction-kernel.d.mts +1 -1
  133. package/dist/internal/transaction-kernel.mjs +3 -2
  134. package/dist/internal/transaction-kernel.mjs.map +1 -1
  135. package/package.json +1 -1
  136. package/src/Adapter.ts +97 -8
  137. package/src/OAuthPersistence.ts +427 -0
  138. package/src/index.ts +3 -0
  139. package/src/internal/authority-snapshot.ts +91 -0
  140. package/src/internal/configuration.ts +1 -8
  141. package/src/internal/models/oauth-connected-model.ts +396 -0
  142. package/src/internal/models/oauth-model.ts +708 -0
  143. package/src/internal/models/password-model.ts +0 -120
  144. package/src/internal/native-sql-table.ts +178 -0
  145. package/src/internal/oauth/accounts.ts +838 -0
  146. package/src/internal/oauth/connected-access.ts +388 -0
  147. package/src/internal/oauth/connected-collection.ts +345 -0
  148. package/src/internal/oauth/connected-custody.ts +308 -0
  149. package/src/internal/oauth/connected-flow.ts +562 -0
  150. package/src/internal/oauth/connected-input.ts +147 -0
  151. package/src/internal/oauth/connected-keys.ts +31 -0
  152. package/src/internal/oauth/connected-maintenance.ts +379 -0
  153. package/src/internal/oauth/connected-management.ts +391 -0
  154. package/src/internal/oauth/connected-reference.ts +135 -0
  155. package/src/internal/oauth/connected-settlement.ts +278 -0
  156. package/src/internal/oauth/connected-sign-in.ts +472 -0
  157. package/src/internal/oauth/connected-state.ts +924 -0
  158. package/src/internal/oauth/connected-target.ts +307 -0
  159. package/src/internal/oauth/flow.ts +505 -0
  160. package/src/internal/oauth/input.ts +201 -0
  161. package/src/internal/oauth/owner.ts +22 -0
  162. package/src/internal/oauth/query-compiler.ts +15 -0
  163. package/src/internal/oauth/registration.ts +676 -0
  164. package/src/internal/oauth/state.ts +218 -0
  165. package/src/internal/oauth/target.ts +803 -0
  166. package/src/internal/oauth-kernel.ts +294 -0
  167. package/src/internal/oauth-proxy.ts +176 -0
  168. package/src/internal/password-credentials.ts +245 -0
  169. package/src/internal/password-kernel.ts +98 -712
  170. package/src/internal/persistence.ts +5 -10
  171. package/src/internal/query-operations.ts +2 -0
  172. package/src/internal/session-kernel.ts +231 -81
  173. package/src/internal/sql-oauth-connected-model.ts +145 -0
  174. package/src/internal/sql-oauth-model.ts +263 -0
  175. package/src/internal/sql-persistence.ts +2 -0
  176. package/src/internal/sql-query.ts +16 -13
  177. package/src/internal/sql-storage-validation.ts +64 -0
  178. package/src/internal/storage-mapping.ts +0 -66
  179. package/src/internal/storage-plans.ts +218 -0
  180. package/src/internal/storage-tables.ts +0 -35
  181. package/src/internal/storage-validation.ts +58 -0
  182. package/src/internal/transaction-kernel.ts +6 -2
@@ -0,0 +1,562 @@
1
+ import {
2
+ OAuthAccountRevision,
3
+ snapshotOAuthSync,
4
+ type OAuthConnectedPersistence,
5
+ } from "@yielded/auth/OAuth";
6
+ import * as M from "@yielded/auth/OAuth";
7
+ import { Effect, Context, Layer } from "effect";
8
+
9
+ import { OAuthConnectedState } from "./connected-state";
10
+ import type * as STypes from "./connected-state";
11
+ import { CurrentOAuthTransaction } from "./owner";
12
+ import { OAuthQueryCompiler } from "./query-compiler";
13
+ import { OAuthRegistration } from "./registration";
14
+ import { digest, invariant, oauthIdentityKey, sameIdentity, sameRevision } from "./state";
15
+
16
+ export type Input<K extends keyof OAuthConnectedPersistence["Service"]> = Parameters<
17
+ OAuthConnectedPersistence["Service"][K]
18
+ >[0];
19
+
20
+ const make = Effect.gen(function* () {
21
+ const operations = yield* OAuthQueryCompiler;
22
+ const S = yield* OAuthConnectedState;
23
+ const registration = yield* OAuthRegistration;
24
+
25
+ const { sql } = operations;
26
+ const { both, copiedRow, equal } = operations;
27
+ const { acquireTuple, readTuple } = registration;
28
+
29
+ const pending = Effect.fn("oauthConnected.pending")(function* (
30
+ mapping: STypes.Authority,
31
+ access: M.OAuthConnectedAccess,
32
+ lock = true,
33
+ ) {
34
+ const owner = yield* CurrentOAuthTransaction;
35
+ const f = mapping.flow;
36
+
37
+ const read = yield* owner.read(
38
+ f.table,
39
+ equal(f.table, { [f.moduleId]: access.moduleId, [f.flowId]: access.flowId }),
40
+ { limit: 1, lock, observe: lock },
41
+ );
42
+
43
+ const row = read.rows[0];
44
+
45
+ if (row === undefined || row[f.snapshot] === null) return undefined;
46
+
47
+ const flow = S.flowStorage.decode(row[f.snapshot]),
48
+ c = flow.context;
49
+
50
+ const native = yield* mapping.subjectId.toNative(c.revision.subjectId);
51
+
52
+ invariant(
53
+ row[f.moduleId] === c.moduleId &&
54
+ row[f.flowId] === c.flowId &&
55
+ row[f.commandId] === c.commandId &&
56
+ mapping.subjectId.equals(native, row[f.subjectId]) &&
57
+ row[f.clientKey] === (yield* S.clientKey(S.configuration(flow))) &&
58
+ row[f.stateDigest] === c.stateDigest &&
59
+ mapping.clock.decodeInstant(row[f.expiresAt]) === c.expiresAtMillis &&
60
+ mapping.clock.decodeInstant(row[f.retentionUntil]) === flow.retentionUntilMillis,
61
+ );
62
+
63
+ const matches =
64
+ c.moduleId === access.moduleId &&
65
+ c.generation === access.generation &&
66
+ c.flowId === access.flowId &&
67
+ c.revision.subjectId === access.subjectId &&
68
+ c.provider === access.provider &&
69
+ c.callbackId === access.callbackId &&
70
+ c.stateDigest === access.stateDigest &&
71
+ c.requestBindingVerifier === access.requestBindingVerifier &&
72
+ c.requestBindingExpiresAtMillis === access.requestBindingExpiresAtMillis &&
73
+ (c.responseIssuerMode === "required"
74
+ ? access.responseIssuer === c.issuer
75
+ : access.responseIssuer === undefined);
76
+
77
+ return matches ? { row, flow, native } : undefined;
78
+ });
79
+
80
+ const live = (flow: M.OAuthConnectedPendingFlow, now: number) =>
81
+ flow.context.issuedAtMillis <= now &&
82
+ now < flow.context.expiresAtMillis &&
83
+ flow.context.expiresAtMillis <= flow.context.requestBindingExpiresAtMillis &&
84
+ flow.retentionUntilMillis >= flow.context.expiresAtMillis + flow.context.claimLifetimeMillis;
85
+
86
+ const expected = (flow: M.OAuthConnectedPendingFlow) => ({
87
+ moduleId: flow.context.moduleId,
88
+ flowId: flow.context.flowId,
89
+ commandId: flow.context.commandId,
90
+ revision: flow.context.revision,
91
+ intent: S.contextStorage.encode(flow.context),
92
+ maximumAgeMillis: flow.context.maximumEvidenceAgeMillis,
93
+ configuration: S.configuration(flow),
94
+ });
95
+
96
+ const retain = Effect.fn("oauthConnected.retain")(function* (
97
+ mapping: STypes.Mapping,
98
+ input:
99
+ | ({ readonly _tag: "Prepare" } & Input<"prepare">)
100
+ | ({ readonly _tag: "Issue" } & Input<"issue">),
101
+ ) {
102
+ const owner = yield* CurrentOAuthTransaction;
103
+
104
+ const flow = snapshotOAuthSync(M.OAuthConnectedPendingFlow, input.flow),
105
+ c = flow.context;
106
+
107
+ const found = yield* S.current(mapping, c.revision.subjectId);
108
+
109
+ if (found === undefined) return { _tag: "Rejected" } as const;
110
+ if (input._tag === "Prepare") {
111
+ if (
112
+ input.authorization.moduleId !== c.moduleId ||
113
+ !sameRevision(input.authorization.revision, c.revision) ||
114
+ !sameRevision(found.revision, c.revision) ||
115
+ (yield* S.useAuthority(mapping, input.authorization, "metadata")) === undefined
116
+ )
117
+ return { _tag: "Rejected" } as const;
118
+ } else if (!(yield* S.action(mapping, found, input.authorization, "issue", expected(flow))))
119
+ return { _tag: "Rejected" } as const;
120
+ if (c.profile.revocation === "cohort") invariant(mapping.revocation.mode === "cohort");
121
+ const client = yield* S.client(mapping, S.configuration(flow), true);
122
+
123
+ invariant(client !== undefined);
124
+
125
+ const now = yield* owner.now(mapping.clock),
126
+ f = mapping.flow;
127
+
128
+ if (!live(flow, now)) return { _tag: "Rejected" } as const;
129
+ if (c.reconnect !== undefined) {
130
+ const original = yield* S.readGrant(mapping, c.moduleId, c.reconnect.grantId, false);
131
+
132
+ if (
133
+ original === undefined ||
134
+ original.context.subjectId !== c.revision.subjectId ||
135
+ !sameTarget(c.reconnect, original.context)
136
+ )
137
+ return { _tag: "Rejected" } as const;
138
+ owner.postconditions.push(
139
+ sql`exists(select 1 from ${mapping.grant.table} where ${owner.exact(mapping.grant.table, original.row)})`,
140
+ );
141
+ }
142
+ const key = { [f.moduleId]: c.moduleId, [f.flowId]: c.flowId };
143
+ const prior = yield* owner.read(f.table, equal(f.table, key), { limit: 1 });
144
+
145
+ const command = yield* owner.read(
146
+ f.table,
147
+ equal(f.table, { [f.moduleId]: c.moduleId, [f.commandId]: c.commandId }),
148
+ { limit: 1 },
149
+ );
150
+
151
+ owner.postconditions.push(
152
+ sql`${mapping.clock.engineNowMillis} >= ${c.issuedAtMillis} and ${mapping.clock.engineNowMillis} < ${c.expiresAtMillis}`,
153
+ );
154
+ if (input._tag === "Issue") {
155
+ const row = prior.rows[0];
156
+
157
+ if (
158
+ row === undefined ||
159
+ command.rows[0] === undefined ||
160
+ row[f.state] !== "Prepared" ||
161
+ row[f.snapshot] !== S.flowStorage.encode(flow) ||
162
+ command.rows[0][f.flowId] !== c.flowId
163
+ )
164
+ return { _tag: "Rejected" } as const;
165
+ yield* owner.update(
166
+ f.table,
167
+ { ...key, [f.state]: "Prepared", [f.version]: row[f.version] },
168
+ { [f.state]: "Pending", [f.version]: owner.marker },
169
+ );
170
+ owner.postconditions.push(
171
+ sql`exists(select 1 from ${f.table} where ${equal(f.table, { ...key, [f.state]: "Pending", [f.version]: owner.marker })})`,
172
+ );
173
+
174
+ return { _tag: "Issued", flow } as const;
175
+ }
176
+ if (prior.rows.length || command.rows.length) return { _tag: "Rejected" } as const;
177
+
178
+ const values = {
179
+ ...f.encodeInsert({
180
+ flow: snapshotOAuthSync(M.OAuthConnectedPendingFlow, flow),
181
+ subjectId: S.nativeCopy(found.nativeId),
182
+ }),
183
+ ...key,
184
+ [f.commandId]: c.commandId,
185
+ [f.subjectId]: found.nativeId,
186
+ [f.clientKey]: client.id,
187
+ [f.cohortKey]: null,
188
+ [f.state]: "Prepared",
189
+ [f.version]: owner.marker,
190
+ [f.stateDigest]: c.stateDigest,
191
+ [f.snapshot]: S.flowStorage.encode(flow),
192
+ [f.claimId]: null,
193
+ [f.claimDigest]: null,
194
+ [f.claimOrder]: null,
195
+ [f.claimedAt]: null,
196
+ [f.claimExpiresAt]: null,
197
+ [f.expiresAt]: mapping.clock.encodeInstant(c.expiresAtMillis),
198
+ [f.retentionUntil]: mapping.clock.encodeInstant(flow.retentionUntilMillis),
199
+ [f.work]: "None",
200
+ [f.custody]: null,
201
+ };
202
+
203
+ const inserted = yield* owner.insert(f.table, values, key, true);
204
+
205
+ prior.rows = inserted.rows;
206
+ command.rows = inserted.rows;
207
+ if (inserted.rows[0]?.[f.version] !== owner.marker) return { _tag: "Rejected" } as const;
208
+
209
+ return { _tag: "Issued", flow } as const;
210
+ });
211
+
212
+ const prepare = (mapping: STypes.Mapping, input: Input<"prepare">) =>
213
+ retain(mapping, { _tag: "Prepare", ...input });
214
+
215
+ const issue = (mapping: STypes.Mapping, input: Input<"issue">) =>
216
+ retain(mapping, { _tag: "Issue", ...input });
217
+
218
+ const inspectPrepared = Effect.fn("oauthConnected.inspectPrepared")(function* (
219
+ mapping: STypes.Mapping,
220
+ access: M.OAuthConnectedPreparedAccess,
221
+ ) {
222
+ const owner = yield* CurrentOAuthTransaction;
223
+ const found = yield* S.current(mapping, access.subjectId);
224
+
225
+ if (found === undefined) return undefined;
226
+ const f = mapping.flow;
227
+
228
+ const read = yield* owner.read(
229
+ f.table,
230
+ equal(f.table, {
231
+ [f.moduleId]: access.moduleId,
232
+ [f.flowId]: access.flowId,
233
+ [f.subjectId]: found.nativeId,
234
+ }),
235
+ { limit: 1 },
236
+ );
237
+
238
+ const row = read.rows[0];
239
+
240
+ if (row === undefined || row[f.state] !== "Prepared" || row[f.snapshot] === null)
241
+ return undefined;
242
+ const flow = S.flowStorage.decode(row[f.snapshot]);
243
+ const c = flow.context;
244
+
245
+ invariant(
246
+ row[f.moduleId] === c.moduleId &&
247
+ row[f.flowId] === c.flowId &&
248
+ row[f.commandId] === c.commandId &&
249
+ mapping.subjectId.equals(found.nativeId, row[f.subjectId]) &&
250
+ row[f.clientKey] === (yield* S.clientKey(S.configuration(flow))) &&
251
+ row[f.stateDigest] === c.stateDigest &&
252
+ mapping.clock.decodeInstant(row[f.expiresAt]) === c.expiresAtMillis &&
253
+ mapping.clock.decodeInstant(row[f.retentionUntil]) === flow.retentionUntilMillis,
254
+ );
255
+ const now = yield* owner.now(mapping.clock);
256
+
257
+ return c.moduleId === access.moduleId &&
258
+ c.generation === access.generation &&
259
+ c.flowId === access.flowId &&
260
+ c.commandId === access.commandId &&
261
+ c.revision.subjectId === access.subjectId &&
262
+ sameRevision(found.revision, c.revision) &&
263
+ c.requestBindingVerifier === access.requestBindingVerifier &&
264
+ c.requestBindingExpiresAtMillis === access.requestBindingExpiresAtMillis &&
265
+ live(flow, now)
266
+ ? flow
267
+ : undefined;
268
+ });
269
+
270
+ const sameTarget = (target: M.OAuthConnectedTarget, context: M.OAuthConnectedTokenContext) =>
271
+ target.grantId === context.grantId &&
272
+ target.grantVersion === context.grantVersion &&
273
+ target.tokenVersion === context.tokenVersion &&
274
+ target.cohortGeneration === context.cohortGeneration &&
275
+ sameIdentity(target.identity, context.identity) &&
276
+ S.tokenContextStorage.encode({ ...context, configuration: target.configuration }) ===
277
+ S.tokenContextStorage.encode(context);
278
+
279
+ const capture = Effect.fn("oauthConnected.capture")(function* (
280
+ mapping: STypes.Mapping,
281
+ input: Input<"capture">,
282
+ ) {
283
+ const found = yield* S.current(mapping, input.subjectId);
284
+
285
+ if (found === undefined) return undefined;
286
+ if (input.grantId === undefined)
287
+ return { revision: snapshotOAuthSync(OAuthAccountRevision, found.revision) };
288
+ const grant = yield* S.readGrant(mapping, input.moduleId, input.grantId, false);
289
+
290
+ if (grant === undefined || grant.context.subjectId !== input.subjectId) return undefined;
291
+
292
+ return {
293
+ revision: snapshotOAuthSync(OAuthAccountRevision, found.revision),
294
+ target: snapshotOAuthSync(M.OAuthConnectedTarget, grant.context),
295
+ };
296
+ });
297
+
298
+ const preflight = Effect.fn("oauthConnected.preflight")(function* (
299
+ mapping: STypes.Mapping,
300
+ input: Input<"preflight">,
301
+ ) {
302
+ const owner = yield* CurrentOAuthTransaction;
303
+ const read = yield* pending(mapping, input);
304
+ const now = yield* owner.now(mapping.clock);
305
+
306
+ if (read === undefined || read.row[mapping.flow.state] !== "Pending" || !live(read.flow, now))
307
+ return undefined;
308
+
309
+ return read.flow;
310
+ });
311
+
312
+ const claim = Effect.fn("oauthConnected.claim")(function* (
313
+ mapping: STypes.Mapping,
314
+ input: Input<"claim">,
315
+ ) {
316
+ const owner = yield* CurrentOAuthTransaction;
317
+
318
+ const flow = snapshotOAuthSync(M.OAuthConnectedPendingFlow, input.flow),
319
+ c = flow.context;
320
+
321
+ const found = yield* S.current(mapping, c.revision.subjectId);
322
+
323
+ if (
324
+ found === undefined ||
325
+ !(yield* S.action(mapping, found, input.authorization, "claim", expected(flow)))
326
+ )
327
+ return { _tag: "Rejected" } as const;
328
+ const client = yield* S.client(mapping, S.configuration(flow), false);
329
+
330
+ if (client === undefined) return { _tag: "Rejected" } as const;
331
+
332
+ const read = yield* pending(mapping, input.access),
333
+ now = yield* owner.now(mapping.clock),
334
+ f = mapping.flow;
335
+
336
+ if (
337
+ read === undefined ||
338
+ read.row[f.state] !== "Pending" ||
339
+ S.flowStorage.encode(read.flow) !== S.flowStorage.encode(flow) ||
340
+ !live(flow, now)
341
+ )
342
+ return { _tag: "Rejected" } as const;
343
+ const expires = now + c.claimLifetimeMillis;
344
+
345
+ invariant(Number.isSafeInteger(expires) && expires <= flow.retentionUntilMillis);
346
+ yield* S.touchScope(mapping, client.scope);
347
+ const order = yield* S.nextOrder(mapping, client);
348
+
349
+ const claimed = snapshotOAuthSync(M.OAuthConnectedClaim, {
350
+ flow,
351
+ claimId: input.claimId,
352
+ claimedAtMillis: now,
353
+ claimExpiresAtMillis: expires,
354
+ order: String(order),
355
+ });
356
+
357
+ yield* owner.update(
358
+ f.table,
359
+ {
360
+ [f.moduleId]: c.moduleId,
361
+ [f.flowId]: c.flowId,
362
+ [f.state]: "Pending",
363
+ [f.version]: read.row[f.version],
364
+ },
365
+ {
366
+ [f.state]: "Claimed",
367
+ [f.claimId]: claimed.claimId,
368
+ [f.claimDigest]: yield* digest(S.claimStorage.encode(claimed)),
369
+ [f.claimOrder]: mapping.order.encode(order),
370
+ [f.claimedAt]: mapping.clock.encodeInstant(now),
371
+ [f.claimExpiresAt]: mapping.clock.encodeInstant(expires),
372
+ [f.work]: "Unresolved",
373
+ [f.version]: owner.marker,
374
+ },
375
+ );
376
+ owner.postconditions.push(
377
+ sql`${mapping.clock.engineNowMillis} >= ${now} and ${mapping.clock.engineNowMillis} < ${Math.min(c.expiresAtMillis, expires)}`,
378
+ );
379
+
380
+ return { _tag: "Claimed", claim: claimed } as const;
381
+ });
382
+
383
+ const exact = Effect.fn("oauthConnected.exactClaim")(function* (
384
+ mapping: STypes.Authority,
385
+ claim: M.OAuthConnectedClaim,
386
+ ) {
387
+ const owner = yield* CurrentOAuthTransaction;
388
+
389
+ const f = mapping.flow,
390
+ c = claim.flow.context;
391
+
392
+ const read = yield* owner.read(
393
+ f.table,
394
+ equal(f.table, { [f.moduleId]: c.moduleId, [f.flowId]: c.flowId }),
395
+ { limit: 1 },
396
+ );
397
+
398
+ const row = read.rows[0];
399
+
400
+ if (
401
+ row === undefined ||
402
+ row[f.work] !== "Unresolved" ||
403
+ row[f.claimId] !== claim.claimId ||
404
+ row[f.claimDigest] !== (yield* digest(S.claimStorage.encode(claim)))
405
+ )
406
+ return undefined;
407
+ const native = yield* mapping.subjectId.toNative(c.revision.subjectId);
408
+
409
+ invariant(
410
+ row[f.commandId] === c.commandId &&
411
+ mapping.subjectId.equals(native, row[f.subjectId]) &&
412
+ row[f.stateDigest] === c.stateDigest &&
413
+ mapping.clock.decodeInstant(row[f.expiresAt]) === c.expiresAtMillis &&
414
+ mapping.clock.decodeInstant(row[f.retentionUntil]) === claim.flow.retentionUntilMillis &&
415
+ row[f.clientKey] === (yield* S.clientKey(S.configuration(claim.flow))) &&
416
+ S.nativeOrder(mapping, row[f.claimOrder]) === S.orderNumber(claim.order) &&
417
+ mapping.clock.decodeInstant(row[f.claimedAt]) === claim.claimedAtMillis &&
418
+ mapping.clock.decodeInstant(row[f.claimExpiresAt]) === claim.claimExpiresAtMillis,
419
+ );
420
+ const now = yield* owner.now(mapping.clock);
421
+
422
+ invariant(now >= claim.claimedAtMillis);
423
+
424
+ return { row, now, active: row[f.state] === "Claimed" && now < claim.claimExpiresAtMillis };
425
+ });
426
+
427
+ const terminal = Effect.fn("oauthConnected.terminal")(function* (
428
+ mapping: STypes.Authority,
429
+ claim: M.OAuthConnectedClaim,
430
+ state: string,
431
+ work: "Resolved" | "Unresolved",
432
+ custody?: { cohort: string; sealed?: M.OAuthConnectedStoredGrant },
433
+ ) {
434
+ const owner = yield* CurrentOAuthTransaction;
435
+
436
+ const f = mapping.flow,
437
+ c = claim.flow.context;
438
+
439
+ yield* owner.update(
440
+ f.table,
441
+ { [f.moduleId]: c.moduleId, [f.flowId]: c.flowId, [f.claimId]: claim.claimId },
442
+ {
443
+ [f.state]: state,
444
+ [f.snapshot]: null,
445
+ [f.work]: work,
446
+ [f.cohortKey]: custody?.cohort ?? null,
447
+ [f.custody]: custody?.sealed === undefined ? null : S.grantStorage.encode(custody.sealed),
448
+ [f.version]: owner.marker,
449
+ },
450
+ );
451
+ });
452
+
453
+ const inspectTuple = Effect.fn("oauthConnected.inspectTuple")(function* (
454
+ mapping: STypes.Authority,
455
+ identity: M.OAuthConnectedTokenContext["identity"],
456
+ ) {
457
+ const owner = yield* CurrentOAuthTransaction;
458
+
459
+ const t = mapping.ownership.tuple,
460
+ key = yield* oauthIdentityKey(identity);
461
+
462
+ const read = yield* owner.read(t.table, equal(t.table, { [t.identityKey]: key }), { limit: 1 });
463
+ const row = read.rows[0];
464
+
465
+ if (row !== undefined) {
466
+ invariant(
467
+ row[t.provider] === identity.provider &&
468
+ row[t.issuer] === identity.issuer &&
469
+ row[t.externalSubject] === identity.subject &&
470
+ ["Owned", "Unowned", "Reserved"].includes(row[t.state]),
471
+ );
472
+ }
473
+ if (mapping.ownership.mode === "separate") {
474
+ const o = mapping.ownership.external;
475
+
476
+ const external = yield* owner.read(o.table, equal(o.table, { [o.identityKey]: key }), {
477
+ limit: 1,
478
+ });
479
+
480
+ if (row?.[t.state] === "Owned") {
481
+ const e = external.rows[0];
482
+
483
+ invariant(
484
+ e !== undefined &&
485
+ e[o.provider] === identity.provider &&
486
+ e[o.issuer] === identity.issuer &&
487
+ e[o.externalSubject] === identity.subject &&
488
+ mapping.subjectId.equals(o.decodeSubjectId(copiedRow(e)), row[t.subjectId]),
489
+ );
490
+ const condition = sql`exists(select 1 from ${o.table} where ${both(equal(o.table, { [o.identityKey]: key }), o.ownedCondition)})`;
491
+
492
+ invariant(yield* owner.check(condition));
493
+ owner.postconditions.push(condition);
494
+ } else invariant(external.rows.length === 0);
495
+ }
496
+
497
+ return { key, row };
498
+ });
499
+
500
+ const inspectGrant = Effect.fn("oauthConnected.inspectGrant")(function* (
501
+ mapping: STypes.Mapping,
502
+ input: Input<"inspectGrant">,
503
+ ) {
504
+ const c = input.claim.flow.context;
505
+
506
+ if (input.identity.provider !== c.provider || input.identity.issuer !== c.issuer)
507
+ return { _tag: "Rejected" } as const;
508
+ const found = yield* S.current(mapping, c.revision.subjectId);
509
+
510
+ if (found === undefined || !sameRevision(found.revision, c.revision))
511
+ return { _tag: "Rejected" } as const;
512
+ const cl = yield* S.client(mapping, S.configuration(input.claim.flow), false);
513
+
514
+ if (cl === undefined) return { _tag: "Rejected" } as const;
515
+
516
+ const tuple = yield* inspectTuple(mapping, input.identity),
517
+ t = mapping.ownership.tuple;
518
+
519
+ if (
520
+ tuple.row?.[t.state] === "Reserved" ||
521
+ (tuple.row?.[t.state] === "Owned" &&
522
+ !mapping.subjectId.equals(tuple.row[t.subjectId], found.nativeId))
523
+ )
524
+ return { _tag: "Conflict" } as const;
525
+ const co = yield* S.cohort(mapping, cl.id, tuple.key, false);
526
+ const claimed = yield* exact(mapping, input.claim);
527
+
528
+ if (claimed === undefined) return { _tag: "Rejected" } as const;
529
+ if (c.reconnect !== undefined && !sameIdentity(c.reconnect.identity, input.identity))
530
+ return { _tag: "Conflict" } as const;
531
+
532
+ return co.blocked || S.orderNumber(input.claim.order) <= co.cutoff || !claimed.active
533
+ ? ({ _tag: "Quarantine", cohortGeneration: co.generation } as const)
534
+ : ({ _tag: "Target", cohortGeneration: co.generation } as const);
535
+ });
536
+
537
+ return {
538
+ pending,
539
+ live,
540
+ expected,
541
+ prepare,
542
+ issue,
543
+ inspectPrepared,
544
+ sameTarget,
545
+ capture,
546
+ preflight,
547
+ claim,
548
+ exact,
549
+ terminal,
550
+ inspectTuple,
551
+ inspectGrant,
552
+ acquireTuple,
553
+ readTuple,
554
+ };
555
+ });
556
+
557
+ export class OAuthConnectedFlow extends Context.Service<
558
+ OAuthConnectedFlow,
559
+ Effect.Success<typeof make>
560
+ >()("effect-auth/persistence/OAuthConnectedFlow") {
561
+ static readonly layer = Layer.effect(OAuthConnectedFlow, make);
562
+ }
@@ -0,0 +1,147 @@
1
+ import * as M from "@yielded/auth/OAuth";
2
+ import {
3
+ OAuthConnectedRevocationClaim,
4
+ OAuthClaimId,
5
+ OAuthCleanupInput,
6
+ OAuthCommandId,
7
+ OAuthExternalIdentity,
8
+ OAuthInstant,
9
+ OAuthModuleId,
10
+ snapshotOAuthSync,
11
+ } from "@yielded/auth/OAuth";
12
+ import { Schema } from "effect";
13
+
14
+ const capture = Schema.Struct({
15
+ moduleId: OAuthModuleId,
16
+ subjectId: M.OAuthConnectedUseAuthorization.fields.revision.fields.subjectId,
17
+ grantId: Schema.optionalKey(M.OAuthGrantId),
18
+ });
19
+
20
+ const issue = Schema.Struct({
21
+ flow: M.OAuthConnectedPendingFlow,
22
+ authorization: M.OAuthConnectedActionAuthorization,
23
+ });
24
+
25
+ const preparation = Schema.Struct({
26
+ flow: M.OAuthConnectedPendingFlow,
27
+ authorization: M.OAuthConnectedUseAuthorization,
28
+ });
29
+
30
+ const claim = Schema.Struct({
31
+ ...issue.fields,
32
+ access: M.OAuthConnectedAccess,
33
+ claimId: OAuthClaimId,
34
+ });
35
+
36
+ const inspectGrant = Schema.Struct({
37
+ claim: M.OAuthConnectedClaim,
38
+ identity: OAuthExternalIdentity,
39
+ });
40
+
41
+ const settle = Schema.Struct({
42
+ claim: M.OAuthConnectedClaim,
43
+ outcome: M.OAuthConnectedOutcome,
44
+ authorization: M.OAuthConnectedActionAuthorization,
45
+ nowMillis: OAuthInstant,
46
+ });
47
+
48
+ const list = Schema.Struct({
49
+ ...M.OAuthConnectedList.fields,
50
+ authorization: M.OAuthConnectedUseAuthorization,
51
+ });
52
+
53
+ const inspectDisconnect = Schema.Struct({
54
+ moduleId: OAuthModuleId,
55
+ subjectId: capture.fields.subjectId,
56
+ authorization: M.OAuthConnectedUseAuthorization,
57
+ commandId: OAuthCommandId,
58
+ grantId: M.OAuthGrantId,
59
+ });
60
+
61
+ const disconnect = Schema.Struct({
62
+ moduleId: OAuthModuleId,
63
+ commandId: OAuthCommandId,
64
+ grant: M.OAuthConnectedDisconnectGrant,
65
+ authorization: M.OAuthConnectedActionAuthorization,
66
+ revocation: Schema.optionalKey(M.OAuthConnectedRevocationJob),
67
+ retentionUntilMillis: OAuthInstant,
68
+ });
69
+
70
+ const inspectAccess = Schema.Struct({
71
+ authorization: M.OAuthConnectedUseAuthorization,
72
+ ...M.OAuthConnectedUse.fields,
73
+ });
74
+
75
+ const claimRefresh = Schema.Struct({
76
+ grant: M.OAuthConnectedStoredGrant,
77
+ authorization: M.OAuthConnectedUseAuthorization,
78
+ claimId: OAuthClaimId,
79
+ nextTokenVersion: M.OAuthConnectedRefreshClaim.fields.nextTokenVersion,
80
+ lifetimeMillis: M.OAuthConnectedPolicy.fields.refreshClaimLifetimeMillis,
81
+ });
82
+
83
+ const settleRefresh = Schema.Struct({
84
+ claim: M.OAuthConnectedRefreshClaim,
85
+ authorization: M.OAuthConnectedUseAuthorization,
86
+ outcome: M.OAuthConnectedRefreshOutcome,
87
+ });
88
+
89
+ const admitUse = Schema.Struct({
90
+ grant: M.OAuthConnectedStoredGrant,
91
+ authorization: M.OAuthConnectedUseAuthorization,
92
+ admissionId: OAuthClaimId,
93
+ lifetimeMillis: M.OAuthConnectedPolicy.fields.useAdmissionLifetimeMillis,
94
+ });
95
+
96
+ const revocationClaim = Schema.Struct({
97
+ moduleId: OAuthModuleId,
98
+ claimId: OAuthClaimId,
99
+ lifetimeMillis: M.OAuthConnectedPolicy.fields.refreshClaimLifetimeMillis,
100
+ });
101
+
102
+ const revocationSettle = Schema.Struct({
103
+ claim: OAuthConnectedRevocationClaim,
104
+ outcome: Schema.Literals(["Confirmed", "Unknown"]),
105
+ });
106
+
107
+ const snapshot =
108
+ <A, I>(schema: Schema.Codec<A, I, never, never>) =>
109
+ (input: A) =>
110
+ snapshotOAuthSync(schema, input);
111
+
112
+ export const connectedInputs = {
113
+ claimSignIn: snapshot(
114
+ Schema.Struct({ claim: M.OAuthClaim, configuration: M.OAuthConnectedConfiguration }),
115
+ ),
116
+ inspectSignIn: snapshot(
117
+ Schema.Struct({
118
+ reservation: M.OAuthSignInAccessClaim,
119
+ credential: M.OAuthCredentialSnapshot,
120
+ grantId: M.OAuthGrantId,
121
+ }),
122
+ ),
123
+ settleSignIn: snapshot(
124
+ Schema.Struct({ reservation: M.OAuthSignInAccessClaim, outcome: M.OAuthSignInAccessOutcome }),
125
+ ),
126
+ capture: snapshot(capture),
127
+ issue: snapshot(issue),
128
+ prepare: snapshot(preparation),
129
+ inspectPrepared: snapshot(M.OAuthConnectedPreparedAccess),
130
+ preflight: snapshot(M.OAuthConnectedAccess),
131
+ claim: snapshot(claim),
132
+ inspectGrant: snapshot(inspectGrant),
133
+ settle: snapshot(settle),
134
+ list: snapshot(list),
135
+ inspectDisconnect: snapshot(inspectDisconnect),
136
+ disconnect: snapshot(disconnect),
137
+ inspectAccess: snapshot(inspectAccess),
138
+ claimRefresh: snapshot(claimRefresh),
139
+ settleRefresh: snapshot(settleRefresh),
140
+ admitUse: snapshot(admitUse),
141
+ cleanup: snapshot(OAuthCleanupInput),
142
+ };
143
+
144
+ export const connectedRevocationInputs = {
145
+ claim: snapshot(revocationClaim),
146
+ settle: snapshot(revocationSettle),
147
+ };