@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,391 @@
1
+ import * as M from "@yielded/auth/OAuth";
2
+ import { snapshotOAuthSync } from "@yielded/auth/OAuth";
3
+ import { Effect, Context, Layer } from "effect";
4
+
5
+ import { OAuthConnectedAccess } from "./connected-access";
6
+ import { OAuthConnectedCustody } from "./connected-custody";
7
+ import { OAuthConnectedFlow } from "./connected-flow";
8
+ import type * as FTypes from "./connected-flow";
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 { digest, invariant } from "./state";
14
+
15
+ const make = Effect.gen(function* () {
16
+ const operations = yield* OAuthQueryCompiler;
17
+ const A = yield* OAuthConnectedAccess;
18
+ const C = yield* OAuthConnectedCustody;
19
+ const F = yield* OAuthConnectedFlow;
20
+ const S = yield* OAuthConnectedState;
21
+
22
+ const { asc, eq, gt, lte } = operations;
23
+ const { both, col, equal } = operations;
24
+
25
+ const list = Effect.fn("oauthConnected.list")(function* (
26
+ mapping: STypes.Mapping,
27
+ input: FTypes.Input<"list">,
28
+ ) {
29
+ const owner = yield* CurrentOAuthTransaction;
30
+ const found = yield* S.useAuthority(mapping, input.authorization, "metadata");
31
+
32
+ if (found === undefined) return { items: [] };
33
+ invariant(Number.isSafeInteger(input.limit) && input.limit >= 1 && input.limit <= 100);
34
+
35
+ const g = mapping.grant,
36
+ h = mapping.cohort,
37
+ j = S.jobTable(mapping);
38
+
39
+ const cursor = input.cursor === undefined ? undefined : M.OAuthGrantId.make(input.cursor);
40
+
41
+ const where = both(
42
+ equal(g.table, { [g.moduleId]: input.authorization.moduleId, [g.subjectId]: found.nativeId }),
43
+ cursor === undefined ? undefined : gt(col(g.table, g.grantId), cursor),
44
+ input.authorization.grantId === undefined
45
+ ? undefined
46
+ : eq(col(g.table, g.grantId), input.authorization.grantId),
47
+ input.authorization.profileKey === undefined
48
+ ? undefined
49
+ : eq(col(g.table, g.profileKey), input.authorization.profileKey),
50
+ );
51
+
52
+ const columns = [
53
+ g.moduleId,
54
+ g.grantId,
55
+ g.subjectId,
56
+ g.identityKey,
57
+ g.clientKey,
58
+ g.cohortKey,
59
+ g.cohortGeneration,
60
+ g.profileKey,
61
+ g.state,
62
+ g.summary,
63
+ g.revocationJobId,
64
+ g.version,
65
+ ];
66
+
67
+ const read = yield* owner.read(g.table, where, {
68
+ columns,
69
+ limit: input.limit + 1,
70
+ takeOnly: true,
71
+ observe: false,
72
+ lock: false,
73
+ orderBy: asc(col(g.table, g.grantId)),
74
+ });
75
+
76
+ const more = read.rows.length > input.limit,
77
+ rows = read.rows.slice(0, input.limit),
78
+ last = rows.at(-1)?.[g.grantId];
79
+
80
+ owner.observations.push({
81
+ table: g.table,
82
+ where: more ? both(where, lte(col(g.table, g.grantId), last)) : where,
83
+ rows,
84
+ });
85
+ const items: Array<typeof M.OAuthConnectedSummary.Type> = [];
86
+
87
+ for (const row of rows) {
88
+ const summary = S.summaryStorage.decode(row[g.summary]);
89
+
90
+ invariant(
91
+ summary.grantId === row[g.grantId] &&
92
+ summary.profileKey === row[g.profileKey] &&
93
+ mapping.subjectId.equals(found.nativeId, row[g.subjectId]),
94
+ );
95
+
96
+ const co = yield* owner.read(h.table, equal(h.table, { [h.cohortKey]: row[g.cohortKey] }), {
97
+ limit: 1,
98
+ lock: false,
99
+ });
100
+
101
+ const cr = co.rows[0];
102
+
103
+ invariant(
104
+ cr !== undefined &&
105
+ cr[h.clientKey] === row[g.clientKey] &&
106
+ cr[h.identityKey] === row[g.identityKey],
107
+ );
108
+ let status = row[g.state] as (typeof M.OAuthConnectedSummary.Type)["status"];
109
+
110
+ if (
111
+ status !== "Disconnected" &&
112
+ (cr[h.state] !== "Open" || cr[h.generation] !== row[g.cohortGeneration])
113
+ )
114
+ status = "ReauthorizationRequired";
115
+ let remote = summary.remoteRevocation;
116
+
117
+ if (row[g.revocationJobId] !== null) {
118
+ if (j === undefined) remote = "Unknown";
119
+ else {
120
+ const job = yield* owner.read(
121
+ j.table,
122
+ equal(j.table, { [j.jobId]: row[g.revocationJobId] }),
123
+ {
124
+ limit: 1,
125
+ lock: false,
126
+ columns: [j.jobId, j.moduleId, j.grantId, j.cohortKey, j.state],
127
+ },
128
+ );
129
+
130
+ const jr = job.rows[0];
131
+
132
+ invariant(
133
+ jr !== undefined &&
134
+ jr[j.moduleId] === row[g.moduleId] &&
135
+ jr[j.grantId] === row[g.grantId] &&
136
+ jr[j.cohortKey] === row[g.cohortKey],
137
+ );
138
+ remote =
139
+ jr[j.state] === "Confirmed"
140
+ ? "Confirmed"
141
+ : jr[j.state] === "Unknown"
142
+ ? "Unknown"
143
+ : "Pending";
144
+ }
145
+ }
146
+ items.push(
147
+ snapshotOAuthSync(M.OAuthConnectedSummary, {
148
+ ...summary,
149
+ status,
150
+ remoteRevocation: remote,
151
+ }),
152
+ );
153
+ }
154
+
155
+ return { items, ...(more ? { cursor: last as string } : {}) };
156
+ });
157
+
158
+ const command = Effect.fn("oauthConnected.disconnectCommand")(function* (
159
+ mapping: STypes.Mapping,
160
+ moduleId: string,
161
+ commandId: string,
162
+ lock = true,
163
+ ) {
164
+ const owner = yield* CurrentOAuthTransaction;
165
+
166
+ const d = mapping.command,
167
+ read = yield* owner.read(
168
+ d.table,
169
+ equal(d.table, { [d.moduleId]: moduleId, [d.commandId]: commandId }),
170
+ { limit: 1, lock, observe: lock },
171
+ );
172
+
173
+ const row = read.rows[0];
174
+
175
+ if (row === undefined) return { read };
176
+
177
+ const grant = S.commandStorage.decode(row[d.intent]),
178
+ result = S.disconnectedStorage.decode(row[d.decision]);
179
+
180
+ const native = yield* mapping.subjectId.toNative(grant.context.subjectId);
181
+
182
+ invariant(
183
+ grant.context.moduleId === moduleId &&
184
+ grant.context.grantId === row[d.grantId] &&
185
+ result.grantId === grant.context.grantId &&
186
+ mapping.subjectId.equals(native, row[d.subjectId]) &&
187
+ !result.replayed,
188
+ );
189
+
190
+ return { read, row, grant, result };
191
+ });
192
+
193
+ const inspectDisconnect = Effect.fn("oauthConnected.inspectDisconnect")(function* (
194
+ mapping: STypes.Mapping,
195
+ input: FTypes.Input<"inspectDisconnect">,
196
+ ) {
197
+ const found = yield* S.useAuthority(mapping, input.authorization, "metadata");
198
+
199
+ if (
200
+ found === undefined ||
201
+ input.subjectId !== found.revision.subjectId ||
202
+ input.moduleId !== input.authorization.moduleId
203
+ )
204
+ return { _tag: "Rejected" } as const;
205
+ const discovered = yield* command(mapping, input.moduleId, input.commandId, false);
206
+
207
+ if (discovered.row !== undefined) {
208
+ if (
209
+ discovered.grant.context.subjectId !== input.subjectId ||
210
+ discovered.grant.context.grantId !== input.grantId
211
+ )
212
+ return { _tag: "Conflict" } as const;
213
+ if (
214
+ (yield* S.useAuthority(
215
+ mapping,
216
+ input.authorization,
217
+ "metadata",
218
+ discovered.grant.context,
219
+ )) === undefined
220
+ )
221
+ return { _tag: "Rejected" } as const;
222
+ const prior = yield* command(mapping, input.moduleId, input.commandId);
223
+ const d = mapping.command;
224
+
225
+ if (
226
+ prior.row === undefined ||
227
+ prior.row[d.intent] !== discovered.row[d.intent] ||
228
+ prior.row[d.decision] !== discovered.row[d.decision] ||
229
+ prior.row[d.version] !== discovered.row[d.version]
230
+ )
231
+ return { _tag: "Rejected" } as const;
232
+
233
+ return { _tag: "Replay", result: { ...prior.result, replayed: true } } as const;
234
+ }
235
+ const read = yield* A.locked(mapping, input.authorization, input.grantId, "metadata");
236
+
237
+ if (read === undefined) return { _tag: "Rejected" } as const;
238
+ const finalCommand = yield* command(mapping, input.moduleId, input.commandId);
239
+
240
+ if (finalCommand.row !== undefined) return { _tag: "Rejected" } as const;
241
+
242
+ return {
243
+ _tag: "Target",
244
+ grant: {
245
+ context: read.grant.context,
246
+ ...(read.grant.sealed === undefined ? {} : { sealed: read.grant.sealed }),
247
+ },
248
+ revision: read.found.revision,
249
+ } as const;
250
+ });
251
+
252
+ const disconnect = Effect.fn("oauthConnected.disconnect")(function* (
253
+ mapping: STypes.Mapping,
254
+ input: FTypes.Input<"disconnect">,
255
+ ) {
256
+ const owner = yield* CurrentOAuthTransaction;
257
+
258
+ const token = input.grant.context,
259
+ found = yield* S.current(mapping, token.subjectId);
260
+
261
+ if (
262
+ found === undefined ||
263
+ input.moduleId !== token.moduleId ||
264
+ !(yield* S.action(mapping, found, input.authorization, "disconnect", {
265
+ moduleId: input.moduleId,
266
+ flowId: input.commandId,
267
+ commandId: input.commandId,
268
+ revision: input.authorization.challenge.revision,
269
+ intent: S.disconnectStorage.encode(input.grant),
270
+ maximumAgeMillis: 300000,
271
+ configuration: token.configuration,
272
+ grant: token,
273
+ }))
274
+ )
275
+ return { _tag: "Rejected" } as const;
276
+ const client = yield* S.client(mapping, token.configuration, false);
277
+
278
+ if (client === undefined) return { _tag: "Rejected" } as const;
279
+
280
+ const tuple = yield* F.inspectTuple(mapping, token.identity),
281
+ t = mapping.ownership.tuple;
282
+
283
+ if (
284
+ tuple.row?.[t.state] !== "Owned" ||
285
+ !mapping.subjectId.equals(tuple.row[t.subjectId], found.nativeId)
286
+ )
287
+ return { _tag: "Conflict" } as const;
288
+ const cohort = yield* S.cohort(mapping, client.id, tuple.key, false);
289
+
290
+ invariant(cohort.row !== undefined);
291
+
292
+ const read = yield* S.readGrant(mapping, input.moduleId, token.grantId),
293
+ g = mapping.grant;
294
+
295
+ if (
296
+ read === undefined ||
297
+ !C.sameContext(read.context, token) ||
298
+ S.disconnectStorage.encode({
299
+ context: read.context,
300
+ ...(read.sealed === undefined ? {} : { sealed: read.sealed }),
301
+ }) !== S.disconnectStorage.encode(input.grant)
302
+ )
303
+ return { _tag: "Conflict" } as const;
304
+ const prior = yield* command(mapping, input.moduleId, input.commandId);
305
+
306
+ // Only inspectDisconnect exposes replay. Mutation replay cannot repeat effects.
307
+ if (prior.row !== undefined) return { _tag: "Conflict" } as const;
308
+
309
+ const now = yield* owner.now(mapping.clock),
310
+ wasOpen = !cohort.blocked;
311
+
312
+ invariant(input.retentionUntilMillis >= now && input.retentionUntilMillis <= now + 2592000000);
313
+ const affected = yield* C.fence(mapping, client, cohort);
314
+ let remote: (typeof M.OAuthConnectedSummary.Type)["remoteRevocation"] = "Unsupported";
315
+
316
+ if (input.revocation !== undefined) {
317
+ yield* C.storeJob(mapping, input.revocation, token, found.nativeId, now);
318
+ remote = "Pending";
319
+ } else if (token.configuration.profile.revocation === "cohort") {
320
+ invariant(read.sealed === undefined);
321
+ remote = "Unknown";
322
+ }
323
+
324
+ const result = snapshotOAuthSync(M.OAuthConnectedDisconnected, {
325
+ _tag: "Disconnected",
326
+ grantId: token.grantId,
327
+ remoteRevocation: remote,
328
+ affectedGrantCount: affected,
329
+ replayed: false,
330
+ });
331
+
332
+ yield* owner.update(
333
+ g.table,
334
+ { [g.moduleId]: input.moduleId, [g.grantId]: token.grantId },
335
+ {
336
+ [g.state]: "Disconnected",
337
+ [g.activeIdentityKey]: null,
338
+ [g.sealed]: null,
339
+ [g.revocationJobId]: input.revocation?.context.jobId ?? read.row[g.revocationJobId],
340
+ [g.version]: owner.marker,
341
+ },
342
+ );
343
+
344
+ const d = mapping.command,
345
+ key = { [d.moduleId]: input.moduleId, [d.commandId]: input.commandId };
346
+
347
+ const inserted = yield* owner.insert(
348
+ d.table,
349
+ {
350
+ ...d.encodeInsert({
351
+ commandId: input.commandId,
352
+ grant: snapshotOAuthSync(M.OAuthConnectedDisconnectGrant, input.grant),
353
+ subjectId: S.nativeCopy(found.nativeId),
354
+ }),
355
+ ...key,
356
+ [d.subjectId]: found.nativeId,
357
+ [d.grantId]: token.grantId,
358
+ [d.intent]: S.commandStorage.encode({
359
+ context: token,
360
+ originalDigest: yield* digest(S.disconnectStorage.encode(input.grant)),
361
+ ...(input.revocation === undefined
362
+ ? {}
363
+ : { revocationJobId: input.revocation.context.jobId }),
364
+ }),
365
+ [d.decision]: S.disconnectedStorage.encode(result),
366
+ [d.retentionUntil]: mapping.clock.encodeInstant(input.retentionUntilMillis),
367
+ [d.version]: owner.marker,
368
+ },
369
+ key,
370
+ );
371
+
372
+ prior.read.rows = inserted.rows;
373
+ if (
374
+ wasOpen &&
375
+ input.revocation === undefined &&
376
+ token.configuration.profile.revocation === "unsupported"
377
+ )
378
+ yield* C.clear(mapping, client, cohort, undefined, true);
379
+
380
+ return result;
381
+ });
382
+
383
+ return { list, inspectDisconnect, disconnect };
384
+ });
385
+
386
+ export class OAuthConnectedManagement extends Context.Service<
387
+ OAuthConnectedManagement,
388
+ Effect.Success<typeof make>
389
+ >()("effect-auth/persistence/OAuthConnectedManagement") {
390
+ static readonly layer = Layer.effect(OAuthConnectedManagement, make);
391
+ }
@@ -0,0 +1,135 @@
1
+ import { Effect } from "effect";
2
+
3
+ import type { OAuthConnectedMapping } from "../models/oauth-connected-model";
4
+ import {
5
+ oauthReferenceGuardTable,
6
+ type OAuthReferenceGuardDescriptor,
7
+ } from "../models/oauth-model";
8
+ /* oxlint-disable no-explicit-any -- only adapter SQL expression handles are erased. */
9
+ import type { SqlExpression as SQL } from "../query-operations";
10
+ import type { QueryOperations } from "../query-operations";
11
+ import type { TableModel } from "../query-operations";
12
+ import { makeTransactionKernel } from "../transaction-kernel";
13
+ import { scopeKey } from "./connected-keys";
14
+ import type { Mapping } from "./connected-state";
15
+ import { unavailable, captureOAuthMapping } from "./state";
16
+ type Table = object;
17
+
18
+ export const makeOAuthConnectedReferenceKernel = (
19
+ operations: QueryOperations,
20
+ dialect: (table: object) => "pg" | "sqlite" | "mysql",
21
+ ) => {
22
+ const { eq, sql } = operations;
23
+ const transactions = makeTransactionKernel(operations);
24
+ const { both } = transactions;
25
+ const { col, equal } = transactions.makeTransactionRows(unavailable);
26
+
27
+ const exactText = (table: Table, key: string, value: string) => {
28
+ const column = col(table, key),
29
+ bound = sql.param(value, column);
30
+
31
+ return dialect(table) === "mysql"
32
+ ? sql`binary ${column} = binary ${bound}`
33
+ : dialect(table) === "pg"
34
+ ? sql`convert_to(cast(${column} as text),'UTF8') = convert_to(cast(${bound} as text),'UTF8')`
35
+ : sql`cast(${column} as blob) = cast(${bound} as blob)`;
36
+ };
37
+
38
+ /** Compose Accounts.Unlink with this connected authority. The stable scope lock
39
+ * serializes unknown claims across every client registration for an issuer. */
40
+ const oauthConnectedOwnershipReferences = <
41
+ S extends TableModel,
42
+ AC extends TableModel,
43
+ T extends TableModel,
44
+ O extends TableModel,
45
+ F extends TableModel,
46
+ G extends TableModel,
47
+ C extends TableModel,
48
+ H extends TableModel,
49
+ A extends TableModel,
50
+ D extends TableModel,
51
+ N,
52
+ J extends TableModel,
53
+ >(
54
+ original: OAuthConnectedMapping<S, AC, T, O, F, G, C, H, A, D, N, J, any>,
55
+ ): {
56
+ readonly connectedReferenceGuards: ReadonlyArray<OAuthReferenceGuardDescriptor<N>>;
57
+ readonly connectedReference: (input: {
58
+ readonly identityKey: string;
59
+ readonly subjectId: N;
60
+ }) => SQL;
61
+ } => {
62
+ const mapping = captureOAuthMapping(original),
63
+ c = mapping.client;
64
+
65
+ return Object.freeze({
66
+ connectedReferenceGuards: [
67
+ oauthReferenceGuardTable<C, N>({
68
+ table: c.table,
69
+ orderBy: c.clientKey,
70
+ condition: (input) =>
71
+ Effect.gen(function* () {
72
+ const key = yield* scopeKey(input.identity.provider, input.identity.issuer);
73
+
74
+ return both(
75
+ equal(c.table, {
76
+ [c.clientKey]: key,
77
+ [c.counter]: mapping.order.encode(0),
78
+ }),
79
+ exactText(c.table, c.provider, input.identity.provider),
80
+ exactText(c.table, c.issuer, input.identity.issuer),
81
+ exactText(c.table, c.clientRegistrationId, ""),
82
+ );
83
+ }),
84
+ }),
85
+ ],
86
+ connectedReference: (input) => connectedReferenceCondition(mapping, input.identityKey),
87
+ });
88
+ };
89
+
90
+ const connectedReferenceCondition = (
91
+ mapping: Mapping,
92
+ identityKey: string | SQL,
93
+ knownIdentity?: { readonly provider: string | SQL; readonly issuer: string | SQL },
94
+ ) => {
95
+ const c = mapping.client,
96
+ t = mapping.ownership.tuple,
97
+ f = mapping.flow,
98
+ g = mapping.grant,
99
+ h = mapping.cohort,
100
+ a = mapping.admission,
101
+ j = mapping.revocation.mode === "cohort" ? mapping.revocation.job : undefined;
102
+
103
+ const tuple = equal(t.table, { [t.identityKey]: identityKey });
104
+ const knownFlow = sql`exists(select 1 from ${h.table} where ${col(h.table, h.cohortKey)} = ${col(f.table, f.cohortKey)} and ${col(h.table, h.identityKey)} = ${identityKey})`;
105
+
106
+ const unknownFlow =
107
+ knownIdentity === undefined
108
+ ? sql`${col(f.table, f.cohortKey)} is null and exists(select 1 from ${c.table} where ${col(c.table, c.clientKey)} = ${col(f.table, f.clientKey)} and exists(select 1 from ${t.table} where ${tuple} and ${col(c.table, c.provider)} = ${col(t.table, t.provider)} and ${col(c.table, c.issuer)} = ${col(t.table, t.issuer)}))`
109
+ : sql`${col(f.table, f.cohortKey)} is null and exists(select 1 from ${c.table} where ${col(c.table, c.clientKey)} = ${col(f.table, f.clientKey)} and ${col(c.table, c.provider)} = ${knownIdentity.provider} and ${col(c.table, c.issuer)} = ${knownIdentity.issuer})`;
110
+
111
+ const related = [f, g, h, ...(j === undefined ? [] : [j])];
112
+
113
+ return sql`(${sql.join(
114
+ [
115
+ sql`exists(select 1 from ${g.table} where ${eq(col(g.table, g.identityKey), identityKey)})`,
116
+ sql`exists(select 1 from ${a.table} where ${eq(col(a.table, a.identityKey), identityKey)})`,
117
+ sql`exists(select 1 from ${h.table} where ${both(eq(col(h.table, h.identityKey), identityKey), eq(col(h.table, h.state), "Blocked"))})`,
118
+ sql`exists(select 1 from ${f.table} where ${col(f.table, f.work)} = 'Unresolved' and (${knownFlow} or (${unknownFlow})))`,
119
+ ...(j === undefined
120
+ ? []
121
+ : [
122
+ sql`exists(select 1 from ${j.table} where ${eq(col(j.table, j.identityKey), identityKey)})`,
123
+ ]),
124
+ // A missing real client cannot turn erased unknown work into absence.
125
+ ...related.map(
126
+ (v) =>
127
+ sql`exists(select 1 from ${v.table} where not exists(select 1 from ${c.table} where ${col(c.table, c.clientKey)} = ${col(v.table, v.clientKey)}))`,
128
+ ),
129
+ ],
130
+ sql` or `,
131
+ )})`;
132
+ };
133
+
134
+ return { oauthConnectedOwnershipReferences, connectedReferenceCondition };
135
+ };