@yielded/auth-persistence-drizzle 0.1.0-beta.23 → 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 (185) hide show
  1. package/README.md +3 -0
  2. package/dist/D1.d.mts +9 -9
  3. package/dist/D1.mjs +2 -1
  4. package/dist/D1.mjs.map +1 -1
  5. package/dist/Libsql.d.mts +46 -62
  6. package/dist/Libsql.mjs +2 -1
  7. package/dist/Libsql.mjs.map +1 -1
  8. package/dist/Mysql2.d.mts +24 -25
  9. package/dist/Pglite.d.mts +26 -26
  10. package/dist/Pglite.mjs +2 -1
  11. package/dist/Pglite.mjs.map +1 -1
  12. package/dist/Postgres.d.mts +27 -27
  13. package/dist/Postgres.mjs +2 -1
  14. package/dist/Postgres.mjs.map +1 -1
  15. package/dist/SqliteBun.d.mts +47 -63
  16. package/dist/SqliteBun.mjs +2 -1
  17. package/dist/SqliteBun.mjs.map +1 -1
  18. package/dist/SqliteDo.d.mts +45 -61
  19. package/dist/SqliteDo.mjs +2 -1
  20. package/dist/SqliteDo.mjs.map +1 -1
  21. package/dist/SqliteNode.d.mts +49 -65
  22. package/dist/SqliteNode.mjs +2 -1
  23. package/dist/SqliteNode.mjs.map +1 -1
  24. package/dist/SqliteWasm.d.mts +45 -61
  25. package/dist/SqliteWasm.mjs +2 -1
  26. package/dist/SqliteWasm.mjs.map +1 -1
  27. package/dist/drizzle/crypto.mjs +1 -1
  28. package/dist/drizzle/d1-emails.d.mts +1 -1
  29. package/dist/drizzle/d1-oauth.d.mts +13 -13
  30. package/dist/drizzle/d1-passkey.d.mts +1 -1
  31. package/dist/drizzle/d1-password-prepared.d.mts +4 -4
  32. package/dist/drizzle/d1-password-prepared.mjs.map +1 -1
  33. package/dist/drizzle/d1-passwords.d.mts +17 -33
  34. package/dist/drizzle/d1-passwords.mjs +56 -198
  35. package/dist/drizzle/d1-passwords.mjs.map +1 -1
  36. package/dist/drizzle/d1-phone.d.mts +1 -1
  37. package/dist/drizzle/d1-proofs.d.mts +1 -1
  38. package/dist/drizzle/d1-sessions.d.mts +2 -2
  39. package/dist/drizzle/d1-sessions.mjs +11 -6
  40. package/dist/drizzle/d1-sessions.mjs.map +1 -1
  41. package/dist/drizzle/d1-totp.d.mts +1 -1
  42. package/dist/drizzle/mysql-password-prepared.d.mts +5 -5
  43. package/dist/drizzle/mysql-password-prepared.mjs.map +1 -1
  44. package/dist/drizzle/mysql-passwords.d.mts +16 -32
  45. package/dist/drizzle/mysql-passwords.mjs.map +1 -1
  46. package/dist/drizzle/mysql-sessions.d.mts +1 -1
  47. package/dist/drizzle/native-sql-table.mjs +26 -1
  48. package/dist/drizzle/native-sql-table.mjs.map +1 -1
  49. package/dist/drizzle/oauth-connected-model.d.mts +20 -281
  50. package/dist/drizzle/oauth-connected-model.mjs +3 -17
  51. package/dist/drizzle/oauth-connected-model.mjs.map +1 -1
  52. package/dist/drizzle/oauth-connected-reference.d.mts +1 -3
  53. package/dist/drizzle/oauth-connected-reference.mjs +4 -53
  54. package/dist/drizzle/oauth-connected-reference.mjs.map +1 -1
  55. package/dist/drizzle/oauth-connected-target.d.mts +3 -990
  56. package/dist/drizzle/oauth-connected-target.mjs +8 -92
  57. package/dist/drizzle/oauth-connected-target.mjs.map +1 -1
  58. package/dist/drizzle/oauth-kernel.mjs +11 -0
  59. package/dist/drizzle/oauth-kernel.mjs.map +1 -0
  60. package/dist/drizzle/oauth-model.d.mts +35 -438
  61. package/dist/drizzle/oauth-model.mjs +6 -44
  62. package/dist/drizzle/oauth-model.mjs.map +1 -1
  63. package/dist/drizzle/oauth-mysql.mjs +2 -2
  64. package/dist/drizzle/oauth-mysql.mjs.map +1 -1
  65. package/dist/drizzle/oauth-proxy.d.mts +10 -0
  66. package/dist/drizzle/oauth-proxy.mjs +54 -0
  67. package/dist/drizzle/oauth-proxy.mjs.map +1 -0
  68. package/dist/drizzle/oauth-target.d.mts +6 -107
  69. package/dist/drizzle/oauth-target.mjs +15 -245
  70. package/dist/drizzle/oauth-target.mjs.map +1 -1
  71. package/dist/drizzle/password-model.d.mts +5 -6
  72. package/dist/drizzle/password-prepared-model.d.mts +5 -5
  73. package/dist/drizzle/password-prepared-model.mjs.map +1 -1
  74. package/dist/drizzle/password-prepared-state.mjs +1 -1
  75. package/dist/drizzle/pg-password-prepared.d.mts +5 -5
  76. package/dist/drizzle/pg-password-prepared.mjs.map +1 -1
  77. package/dist/drizzle/pg-passwords.d.mts +16 -32
  78. package/dist/drizzle/pg-passwords.mjs.map +1 -1
  79. package/dist/drizzle/pg-sessions.d.mts +1 -1
  80. package/dist/drizzle/phone-target.d.mts +1 -1
  81. package/dist/drizzle/phone-target.mjs +1 -1
  82. package/dist/drizzle/query-operations.mjs +3 -1
  83. package/dist/drizzle/query-operations.mjs.map +1 -1
  84. package/dist/drizzle/sqlite-password-prepared.mjs.map +1 -1
  85. package/dist/drizzle/sqlite-passwords.mjs.map +1 -1
  86. package/dist/drizzle/storage-validation.mjs +10 -185
  87. package/dist/drizzle/storage-validation.mjs.map +1 -1
  88. package/dist/drizzle/totp-target.d.mts +2 -2
  89. package/dist/drizzle/totp-target.mjs +2 -2
  90. package/dist/drizzle/transaction-execution.d.mts +2 -2
  91. package/dist/drizzle/transaction-owner.d.mts +1 -2
  92. package/dist/index.d.mts +3 -2
  93. package/dist/internal/drizzle-postgres-oauth-proxy.d.mts +138 -0
  94. package/dist/internal/drizzle-postgres-oauth-proxy.mjs +29 -0
  95. package/dist/internal/drizzle-postgres-oauth-proxy.mjs.map +1 -0
  96. package/dist/internal/drizzle-sqlite-oauth-proxy.d.mts +146 -0
  97. package/dist/internal/drizzle-sqlite-oauth-proxy.mjs +29 -0
  98. package/dist/internal/drizzle-sqlite-oauth-proxy.mjs.map +1 -0
  99. package/package.json +1 -1
  100. package/src/D1.ts +2 -0
  101. package/src/Libsql.ts +2 -0
  102. package/src/Pglite.ts +2 -0
  103. package/src/Postgres.ts +2 -0
  104. package/src/SqliteBun.ts +2 -0
  105. package/src/SqliteDo.ts +2 -0
  106. package/src/SqliteNode.ts +2 -0
  107. package/src/SqliteWasm.ts +2 -0
  108. package/src/drizzle/d1-password-prepared.ts +2 -5
  109. package/src/drizzle/d1-passwords.ts +123 -458
  110. package/src/drizzle/d1-sessions.ts +21 -6
  111. package/src/drizzle/mysql-password-prepared.ts +4 -8
  112. package/src/drizzle/mysql-passwords.ts +5 -10
  113. package/src/drizzle/native-sql-table.ts +30 -0
  114. package/src/drizzle/oauth-connected-model.ts +97 -315
  115. package/src/drizzle/oauth-connected-reference.ts +13 -93
  116. package/src/drizzle/oauth-connected-target.ts +14 -271
  117. package/src/drizzle/oauth-kernel.ts +10 -0
  118. package/src/drizzle/oauth-model.ts +188 -541
  119. package/src/drizzle/oauth-mysql.ts +2 -2
  120. package/src/drizzle/oauth-proxy.ts +101 -0
  121. package/src/drizzle/oauth-target.ts +33 -737
  122. package/src/drizzle/password-model.ts +0 -13
  123. package/src/drizzle/password-prepared-model.ts +0 -6
  124. package/src/drizzle/pg-password-prepared.ts +4 -8
  125. package/src/drizzle/pg-passwords.ts +5 -10
  126. package/src/drizzle/query-operations.ts +4 -0
  127. package/src/drizzle/sqlite-password-prepared.ts +4 -8
  128. package/src/drizzle/sqlite-passwords.ts +7 -14
  129. package/src/drizzle/storage-validation.ts +13 -214
  130. package/src/index.ts +2 -4
  131. package/src/internal/drizzle-postgres-oauth-proxy.ts +37 -0
  132. package/src/internal/drizzle-sqlite-oauth-proxy.ts +44 -0
  133. package/dist/_virtual/_rolldown/runtime.mjs +0 -13
  134. package/dist/drizzle/oauth-accounts.mjs +0 -359
  135. package/dist/drizzle/oauth-accounts.mjs.map +0 -1
  136. package/dist/drizzle/oauth-connected-access.mjs +0 -212
  137. package/dist/drizzle/oauth-connected-access.mjs.map +0 -1
  138. package/dist/drizzle/oauth-connected-collection.mjs +0 -202
  139. package/dist/drizzle/oauth-connected-collection.mjs.map +0 -1
  140. package/dist/drizzle/oauth-connected-custody.mjs +0 -147
  141. package/dist/drizzle/oauth-connected-custody.mjs.map +0 -1
  142. package/dist/drizzle/oauth-connected-flow.mjs +0 -297
  143. package/dist/drizzle/oauth-connected-flow.mjs.map +0 -1
  144. package/dist/drizzle/oauth-connected-input.mjs +0 -122
  145. package/dist/drizzle/oauth-connected-input.mjs.map +0 -1
  146. package/dist/drizzle/oauth-connected-maintenance.mjs +0 -214
  147. package/dist/drizzle/oauth-connected-maintenance.mjs.map +0 -1
  148. package/dist/drizzle/oauth-connected-management.mjs +0 -229
  149. package/dist/drizzle/oauth-connected-management.mjs.map +0 -1
  150. package/dist/drizzle/oauth-connected-settlement.mjs +0 -128
  151. package/dist/drizzle/oauth-connected-settlement.mjs.map +0 -1
  152. package/dist/drizzle/oauth-connected-sign-in.mjs +0 -265
  153. package/dist/drizzle/oauth-connected-sign-in.mjs.map +0 -1
  154. package/dist/drizzle/oauth-connected-state.d.mts +0 -2062
  155. package/dist/drizzle/oauth-connected-state.mjs +0 -430
  156. package/dist/drizzle/oauth-connected-state.mjs.map +0 -1
  157. package/dist/drizzle/oauth-flow.d.mts +0 -22
  158. package/dist/drizzle/oauth-flow.mjs +0 -253
  159. package/dist/drizzle/oauth-flow.mjs.map +0 -1
  160. package/dist/drizzle/oauth-input.mjs +0 -127
  161. package/dist/drizzle/oauth-input.mjs.map +0 -1
  162. package/dist/drizzle/oauth-owner.d.mts +0 -10
  163. package/dist/drizzle/oauth-owner.mjs +0 -10
  164. package/dist/drizzle/oauth-owner.mjs.map +0 -1
  165. package/dist/drizzle/oauth-registration.d.mts +0 -5
  166. package/dist/drizzle/oauth-registration.mjs +0 -374
  167. package/dist/drizzle/oauth-registration.mjs.map +0 -1
  168. package/dist/drizzle/oauth-state.mjs +0 -107
  169. package/dist/drizzle/oauth-state.mjs.map +0 -1
  170. package/src/drizzle/oauth-accounts.ts +0 -805
  171. package/src/drizzle/oauth-connected-access.ts +0 -361
  172. package/src/drizzle/oauth-connected-collection.ts +0 -324
  173. package/src/drizzle/oauth-connected-custody.ts +0 -279
  174. package/src/drizzle/oauth-connected-flow.ts +0 -529
  175. package/src/drizzle/oauth-connected-input.ts +0 -147
  176. package/src/drizzle/oauth-connected-maintenance.ts +0 -346
  177. package/src/drizzle/oauth-connected-management.ts +0 -365
  178. package/src/drizzle/oauth-connected-settlement.ts +0 -251
  179. package/src/drizzle/oauth-connected-sign-in.ts +0 -443
  180. package/src/drizzle/oauth-connected-state.ts +0 -872
  181. package/src/drizzle/oauth-flow.ts +0 -462
  182. package/src/drizzle/oauth-input.ts +0 -201
  183. package/src/drizzle/oauth-owner.ts +0 -29
  184. package/src/drizzle/oauth-registration.ts +0 -640
  185. package/src/drizzle/oauth-state.ts +0 -218
@@ -1,361 +0,0 @@
1
- import * as M from "@yielded/auth/OAuth";
2
- import { snapshotOAuthSync } from "@yielded/auth/OAuth";
3
- import { sql } from "drizzle-orm";
4
- import { Effect } from "effect";
5
-
6
- import * as C from "./oauth-connected-custody";
7
- import * as F from "./oauth-connected-flow";
8
- import { validMetadata } from "./oauth-connected-settlement";
9
- import * as S from "./oauth-connected-state";
10
- import { equal, CurrentOAuthTransaction } from "./oauth-owner";
11
- import { invariant } from "./oauth-state";
12
-
13
- export const locked = Effect.fn("oauthConnected.lockedGrant")(function* (
14
- mapping: S.Mapping,
15
- authorization: M.OAuthConnectedUseAuthorization,
16
- grantId: M.OAuthConnectedTokenContext["grantId"],
17
- purpose: "use" | "metadata",
18
- ) {
19
- const owner = yield* CurrentOAuthTransaction;
20
- const initial = yield* S.readGrant(mapping, authorization.moduleId, grantId, false);
21
-
22
- if (initial === undefined) return undefined;
23
- const found = yield* S.useAuthority(mapping, authorization, purpose, initial.context);
24
-
25
- if (found === undefined) return undefined;
26
- const client = yield* S.client(mapping, initial.context.configuration, false);
27
-
28
- if (client === undefined) return undefined;
29
-
30
- const tuple = yield* F.inspectTuple(mapping, initial.context.identity),
31
- t = mapping.ownership.tuple;
32
-
33
- if (
34
- tuple.row?.[t.state] !== "Owned" ||
35
- !mapping.subjectId.equals(tuple.row[t.subjectId], found.nativeId)
36
- )
37
- return undefined;
38
- const cohort = yield* S.cohort(mapping, client.id, tuple.key, false);
39
-
40
- invariant(cohort.row !== undefined);
41
- const grant = yield* S.readGrant(mapping, authorization.moduleId, grantId);
42
-
43
- if (grant === undefined || !C.sameContext(grant.context, initial.context)) return undefined;
44
-
45
- return { found, client, tuple, cohort, grant, now: yield* owner.now(mapping.clock) };
46
- });
47
-
48
- export const state = (
49
- mapping: S.Authority,
50
- read: NonNullable<Effect.Success<ReturnType<typeof locked>>>,
51
- ) => {
52
- const g = mapping.grant;
53
-
54
- if (
55
- read.cohort.blocked ||
56
- read.cohort.generation !== read.grant.context.cohortGeneration ||
57
- read.grant.row[g.activeIdentityKey] !== read.tuple.key
58
- )
59
- return "ReauthorizationRequired" as const;
60
-
61
- return read.grant.row[g.state] === "Refreshing"
62
- ? ("Busy" as const)
63
- : read.grant.row[g.state] === "Active"
64
- ? ("Active" as const)
65
- : ("ReauthorizationRequired" as const);
66
- };
67
-
68
- export const inspectAccess = Effect.fn("oauthConnected.inspectAccess")(function* (
69
- mapping: S.Mapping,
70
- input: F.Input<"inspectAccess">,
71
- ) {
72
- const read = yield* locked(mapping, input.authorization, input.grantId, "use");
73
-
74
- if (read === undefined || read.grant.context.configuration.profile.key !== input.profileKey)
75
- return { _tag: "Rejected" } as const;
76
- const status = state(mapping, read);
77
-
78
- if (status !== "Active") return { _tag: status };
79
- if (read.grant.sealed === undefined) return { _tag: "ReauthorizationRequired" } as const;
80
-
81
- return {
82
- _tag: "Target",
83
- grant: { context: read.grant.context, sealed: read.grant.sealed },
84
- } as const;
85
- });
86
-
87
- export const claimRefresh = Effect.fn("oauthConnected.claimRefresh")(function* (
88
- mapping: S.Mapping,
89
- input: F.Input<"claimRefresh">,
90
- ) {
91
- const owner = yield* CurrentOAuthTransaction;
92
- const read = yield* locked(mapping, input.authorization, input.grant.context.grantId, "use");
93
-
94
- if (read === undefined) return { _tag: "Rejected" } as const;
95
-
96
- const status = state(mapping, read),
97
- g = mapping.grant;
98
-
99
- if (status !== "Active") return { _tag: status };
100
-
101
- const token = read.grant.context,
102
- p = token.configuration.profile;
103
-
104
- if (
105
- read.grant.sealed === undefined ||
106
- !C.sameGrant(input.grant, { context: token, sealed: read.grant.sealed })
107
- )
108
- return { _tag: "Rejected" } as const;
109
- if (read.grant.row[g.refreshWork] === "Unresolved") return { _tag: "Busy" } as const;
110
- if (
111
- p.refresh === "unsupported" ||
112
- token.metadata.refreshUseUntilMillis === undefined ||
113
- read.now >= token.metadata.refreshUseUntilMillis
114
- )
115
- return { _tag: "ReauthorizationRequired" } as const;
116
- if (read.now < token.metadata.useUntilMillis - p.refreshAheadMillis)
117
- return { _tag: "Busy" } as const;
118
- invariant(
119
- Number.isSafeInteger(input.lifetimeMillis) &&
120
- input.lifetimeMillis >= 1000 &&
121
- input.lifetimeMillis <= 120000 &&
122
- input.nextTokenVersion !== token.tokenVersion,
123
- );
124
-
125
- const expires = Math.min(
126
- read.now + input.lifetimeMillis,
127
- token.metadata.refreshUseUntilMillis,
128
- input.authorization.expiresAtMillis,
129
- );
130
-
131
- if (expires <= read.now) return { _tag: "Rejected" } as const;
132
-
133
- const claim = snapshotOAuthSync(M.OAuthConnectedRefreshClaim, {
134
- grant: input.grant,
135
- order: String(yield* S.nextOrder(mapping, read.client)),
136
- claimId: input.claimId,
137
- claimedAtMillis: read.now,
138
- claimExpiresAtMillis: expires,
139
- nextTokenVersion: input.nextTokenVersion,
140
- });
141
-
142
- yield* owner.update(
143
- g.table,
144
- { [g.moduleId]: token.moduleId, [g.grantId]: token.grantId },
145
- {
146
- [g.state]: "Refreshing",
147
- [g.refreshWork]: "Unresolved",
148
- [g.refreshClaim]: S.refreshStorage.encode(claim),
149
- [g.refreshClaimExpiresAt]: mapping.clock.encodeInstant(expires),
150
- [g.version]: owner.marker,
151
- },
152
- );
153
- owner.postconditions.push(
154
- sql`${mapping.clock.engineNowMillis} >= ${read.now} and ${mapping.clock.engineNowMillis} < ${expires}`,
155
- );
156
-
157
- return { _tag: "Claimed", claim } as const;
158
- });
159
-
160
- export const settleRefresh = Effect.fn("oauthConnected.settleRefresh")(function* (
161
- mapping: S.Mapping,
162
- input: F.Input<"settleRefresh">,
163
- ) {
164
- const owner = yield* CurrentOAuthTransaction;
165
-
166
- const old = input.claim.grant.context,
167
- g = mapping.grant;
168
-
169
- // Capture policy before suffix locks. Loss of authorization still permits only
170
- // cleanup of exact original work under its unchanged global owner.
171
- const authorized =
172
- (yield* S.useAuthority(mapping, input.authorization, "use", old)) !== undefined;
173
-
174
- const native = yield* mapping.subjectId.toNative(old.subjectId);
175
- const client = yield* S.client(mapping, old.configuration, false);
176
-
177
- if (client === undefined) return { _tag: "Rejected" } as const;
178
-
179
- const tuple = yield* F.inspectTuple(mapping, old.identity),
180
- t = mapping.ownership.tuple;
181
-
182
- if (tuple.row?.[t.state] !== "Owned" || !mapping.subjectId.equals(tuple.row[t.subjectId], native))
183
- return { _tag: "Rejected" } as const;
184
- const cohort = yield* S.cohort(mapping, client.id, tuple.key, false);
185
-
186
- invariant(cohort.row !== undefined);
187
- const read = yield* S.readGrant(mapping, old.moduleId, old.grantId);
188
-
189
- if (
190
- read === undefined ||
191
- read.row[g.refreshWork] !== "Unresolved" ||
192
- read.row[g.refreshClaim] !== S.refreshStorage.encode(input.claim)
193
- )
194
- return { _tag: "Rejected" } as const;
195
- invariant(
196
- mapping.clock.decodeInstant(read.row[g.refreshClaimExpiresAt]) ===
197
- input.claim.claimExpiresAtMillis,
198
- );
199
- const now = yield* owner.now(mapping.clock);
200
-
201
- invariant(now >= input.claim.claimedAtMillis);
202
- const outcome = input.outcome;
203
- const incoming = outcome._tag === "Refreshed" ? outcome.grant : undefined;
204
-
205
- if (incoming !== undefined)
206
- invariant(
207
- C.sameContext(
208
- {
209
- ...incoming.context,
210
- tokenVersion: old.tokenVersion,
211
- metadata: old.metadata,
212
- exchangeOrder: old.exchangeOrder,
213
- },
214
- old,
215
- ) &&
216
- incoming.context.tokenVersion === input.claim.nextTokenVersion &&
217
- incoming.context.exchangeOrder === input.claim.order &&
218
- validMetadata(incoming.context, now) &&
219
- incoming.context.metadata.obtainedAtMillis >= input.claim.claimedAtMillis &&
220
- (incoming.context.metadata.refreshUseUntilMillis === undefined ||
221
- (old.metadata.refreshUseUntilMillis !== undefined &&
222
- incoming.context.metadata.refreshUseUntilMillis <= old.metadata.refreshUseUntilMillis)),
223
- );
224
-
225
- const active =
226
- incoming !== undefined &&
227
- authorized &&
228
- read.row[g.state] === "Refreshing" &&
229
- C.sameContext(read.context, old) &&
230
- !cohort.blocked &&
231
- cohort.generation === old.cohortGeneration &&
232
- now < input.claim.claimExpiresAtMillis &&
233
- now < incoming.context.metadata.useUntilMillis;
234
-
235
- if (active) {
236
- yield* owner.update(
237
- g.table,
238
- { [g.moduleId]: old.moduleId, [g.grantId]: old.grantId },
239
- {
240
- [g.state]: "Active",
241
- [g.context]: S.tokenContextStorage.encode(incoming.context),
242
- [g.sealed]: S.sealedStorage.encode(incoming.sealed),
243
- [g.summary]: S.summaryStorage.encode(C.summary(incoming.context)),
244
- [g.tokenVersion]: incoming.context.tokenVersion,
245
- [g.refreshWork]: "Resolved",
246
- [g.version]: owner.marker,
247
- },
248
- );
249
- owner.postconditions.push(
250
- sql`${mapping.clock.engineNowMillis} >= ${now} and ${mapping.clock.engineNowMillis} < ${Math.min(input.claim.claimExpiresAtMillis, incoming.context.metadata.useUntilMillis)}`,
251
- );
252
-
253
- return { _tag: "Refreshed", grant: incoming } as const;
254
- }
255
- yield* C.fence(mapping, client, cohort);
256
- if (incoming !== undefined && outcome._tag === "Refreshed" && outcome.cleanup !== undefined) {
257
- yield* C.storeJob(mapping, outcome.cleanup, incoming.context, native, now);
258
- yield* owner.update(
259
- g.table,
260
- { [g.moduleId]: old.moduleId, [g.grantId]: old.grantId },
261
- {
262
- [g.refreshWork]: "Resolved",
263
- [g.revocationJobId]: outcome.cleanup.context.jobId,
264
- [g.sealed]: null,
265
- [g.version]: owner.marker,
266
- },
267
- );
268
- } else if (incoming !== undefined)
269
- yield* owner.update(
270
- g.table,
271
- { [g.moduleId]: old.moduleId, [g.grantId]: old.grantId },
272
- {
273
- [g.context]: S.tokenContextStorage.encode(incoming.context),
274
- [g.sealed]: S.sealedStorage.encode(incoming.sealed),
275
- [g.tokenVersion]: incoming.context.tokenVersion,
276
- [g.summary]: S.summaryStorage.encode(C.summary(incoming.context)),
277
- [g.version]: owner.marker,
278
- },
279
- );
280
-
281
- return { _tag: "ReauthorizationRequired" } as const;
282
- });
283
-
284
- export const admitUse = Effect.fn("oauthConnected.admitUse")(function* (
285
- mapping: S.Mapping,
286
- input: F.Input<"admitUse">,
287
- ) {
288
- const owner = yield* CurrentOAuthTransaction;
289
- const read = yield* locked(mapping, input.authorization, input.grant.context.grantId, "use");
290
-
291
- if (read === undefined) return { _tag: "Rejected" } as const;
292
- const status = state(mapping, read);
293
-
294
- if (status !== "Active") return { _tag: status };
295
- const token = read.grant.context;
296
-
297
- if (
298
- read.grant.sealed === undefined ||
299
- !C.sameGrant(input.grant, { context: token, sealed: read.grant.sealed }) ||
300
- read.now >= token.metadata.useUntilMillis
301
- )
302
- return { _tag: "ReauthorizationRequired" } as const;
303
- invariant(
304
- Number.isSafeInteger(input.lifetimeMillis) &&
305
- input.lifetimeMillis >= 1 &&
306
- input.lifetimeMillis <= 30000,
307
- );
308
-
309
- const expires = Math.min(
310
- read.now + input.lifetimeMillis,
311
- input.authorization.expiresAtMillis,
312
- token.metadata.useUntilMillis,
313
- ),
314
- a = mapping.admission;
315
-
316
- const key = { [a.admissionId]: input.admissionId },
317
- prior = yield* owner.read(a.table, equal(a.table, key), { limit: 1 });
318
-
319
- if (prior.rows.length) return { _tag: "Rejected" } as const;
320
-
321
- const inserted = yield* owner.insert(
322
- a.table,
323
- {
324
- ...a.encodeInsert({
325
- admissionId: input.admissionId,
326
- grant: snapshotOAuthSync(M.OAuthConnectedStoredGrant, input.grant),
327
- authorization: snapshotOAuthSync(M.OAuthConnectedUseAuthorization, input.authorization),
328
- subjectId: S.nativeCopy(read.found.nativeId),
329
- }),
330
- ...key,
331
- [a.moduleId]: token.moduleId,
332
- [a.grantId]: token.grantId,
333
- [a.subjectId]: read.found.nativeId,
334
- [a.identityKey]: read.tuple.key,
335
- [a.clientKey]: read.client.id,
336
- [a.cohortKey]: read.cohort.id,
337
- [a.snapshot]: S.admissionStorage.encode({
338
- grant: input.grant,
339
- authorization: input.authorization,
340
- }),
341
- [a.admittedAt]: mapping.clock.encodeInstant(read.now),
342
- [a.expiresAt]: mapping.clock.encodeInstant(expires),
343
- [a.version]: owner.marker,
344
- },
345
- key,
346
- );
347
-
348
- prior.rows = inserted.rows;
349
- owner.postconditions.push(
350
- sql`${mapping.clock.engineNowMillis} >= ${read.now} and ${mapping.clock.engineNowMillis} < ${expires}`,
351
- );
352
-
353
- return {
354
- _tag: "Admitted",
355
- admissionId: input.admissionId,
356
- grantId: token.grantId,
357
- tokenVersion: token.tokenVersion,
358
- admittedAtMillis: read.now,
359
- expiresAtMillis: expires,
360
- } as const;
361
- });
@@ -1,324 +0,0 @@
1
- import { asc, eq, lte, sql } from "drizzle-orm";
2
- import { Effect } from "effect";
3
-
4
- import * as C from "./oauth-connected-custody";
5
- import * as F from "./oauth-connected-flow";
6
- import { connectedReferenceCondition } from "./oauth-connected-reference";
7
- import * as S from "./oauth-connected-state";
8
- import { both, col, equal, CurrentOAuthTransaction, type Row } from "./oauth-owner";
9
- import { invariant, oauthIdentityKey } from "./oauth-state";
10
-
11
- export interface Candidates {
12
- readonly jobs: ReadonlyArray<Row>;
13
- readonly grants: ReadonlyArray<Row>;
14
- readonly cohorts: ReadonlyArray<Row>;
15
- readonly tuples: ReadonlyArray<Row>;
16
- }
17
-
18
- export const discover = Effect.fn("oauthConnected.collectionCandidates")(function* (
19
- mapping: S.Mapping,
20
- moduleId: string,
21
- limit: number,
22
- ) {
23
- const owner = yield* CurrentOAuthTransaction;
24
-
25
- const g = mapping.grant,
26
- j = S.jobTable(mapping),
27
- h = mapping.cohort,
28
- at = yield* owner.now(mapping.clock);
29
-
30
- const jobs =
31
- j === undefined
32
- ? { rows: [] }
33
- : yield* owner.read(
34
- j.table,
35
- both(
36
- eq(col(j.table, j.moduleId), moduleId),
37
- eq(col(j.table, j.state), "Confirmed"),
38
- lte(col(j.table, j.retentionUntil), mapping.clock.encodeInstant(at)),
39
- ),
40
- {
41
- limit: limit + 1,
42
- takeOnly: true,
43
- observe: false,
44
- lock: false,
45
- orderBy: asc(col(j.table, j.jobId)),
46
- },
47
- );
48
-
49
- const grants = yield* owner.read(
50
- g.table,
51
- both(
52
- eq(col(g.table, g.moduleId), moduleId),
53
- sql`${col(g.table, g.state)} in ('Disconnected','ReauthorizationRequired')`,
54
- sql`${col(g.table, g.sealed)} is null`,
55
- sql`${col(g.table, g.refreshWork)} <> 'Unresolved'`,
56
- lte(col(g.table, g.retentionUntil), mapping.clock.encodeInstant(at)),
57
- sql`${col(g.table, g.revocationJobId)} is null`,
58
- sql`not exists(select 1 from ${mapping.admission.table} where ${col(mapping.admission.table, mapping.admission.moduleId)} = ${col(g.table, g.moduleId)} and ${col(mapping.admission.table, mapping.admission.grantId)} = ${col(g.table, g.grantId)})`,
59
- sql`not exists(select 1 from ${mapping.command.table} where ${col(mapping.command.table, mapping.command.moduleId)} = ${col(g.table, g.moduleId)} and ${col(mapping.command.table, mapping.command.grantId)} = ${col(g.table, g.grantId)})`,
60
- j === undefined
61
- ? undefined
62
- : sql`not exists(select 1 from ${j.table} where ${col(j.table, j.moduleId)} = ${col(g.table, g.moduleId)} and ${col(j.table, j.grantId)} = ${col(g.table, g.grantId)})`,
63
- ),
64
- {
65
- limit: limit + 1,
66
- takeOnly: true,
67
- observe: false,
68
- lock: false,
69
- orderBy: asc(col(g.table, g.grantId)),
70
- },
71
- );
72
-
73
- const cohorts =
74
- j === undefined
75
- ? { rows: [] }
76
- : yield* owner.read(
77
- h.table,
78
- both(
79
- equal(h.table, { [h.state]: "Blocked" }),
80
- C.clearableCondition(
81
- mapping,
82
- sql`${col(h.table, h.cohortKey)}`,
83
- sql`${col(h.table, h.clientKey)}`,
84
- ),
85
- ),
86
- {
87
- limit: limit + 1,
88
- takeOnly: true,
89
- observe: false,
90
- lock: false,
91
- orderBy: asc(col(h.table, h.cohortKey)),
92
- },
93
- );
94
-
95
- const t = mapping.ownership.tuple;
96
-
97
- const tuples =
98
- mapping.externalReference === undefined
99
- ? { rows: [] }
100
- : yield* owner.read(
101
- t.table,
102
- both(
103
- eq(col(t.table, t.state), "Owned"),
104
- sql`not (${connectedReferenceCondition(mapping, sql`${col(t.table, t.identityKey)}`, { provider: sql`${col(t.table, t.provider)}`, issuer: sql`${col(t.table, t.issuer)}` })})`,
105
- sql`not (${mapping.externalReference({ identityKey: sql`${col(t.table, t.identityKey)}`, subjectId: sql`${col(t.table, t.subjectId)}` })})`,
106
- ),
107
- {
108
- limit: limit + 1,
109
- takeOnly: true,
110
- observe: false,
111
- lock: false,
112
- orderBy: asc(col(t.table, t.identityKey)),
113
- },
114
- );
115
-
116
- return { jobs: jobs.rows, grants: grants.rows, cohorts: cohorts.rows, tuples: tuples.rows };
117
- });
118
-
119
- export const collect = Effect.fn("oauthConnected.collect")(function* (
120
- mapping: S.Mapping,
121
- moduleId: string,
122
- limit: number,
123
- discovered: Candidates,
124
- ) {
125
- const owner = yield* CurrentOAuthTransaction;
126
-
127
- let visited = 0,
128
- removed = 0,
129
- hasMore = false;
130
-
131
- if (limit === 0) return { visited, removed, hasMore: true };
132
-
133
- const g = mapping.grant,
134
- j = S.jobTable(mapping),
135
- h = mapping.cohort;
136
-
137
- if (j !== undefined) {
138
- const candidates = { rows: discovered.jobs };
139
-
140
- hasMore = candidates.rows.length > limit;
141
- for (const candidate of candidates.rows.slice(0, limit)) {
142
- visited++;
143
-
144
- const job = S.jobStorage.decode(candidate[j.snapshot]),
145
- token = job.context.token;
146
-
147
- const client = yield* S.client(mapping, token.configuration, false);
148
-
149
- invariant(client !== undefined);
150
- const tuple = yield* F.inspectTuple(mapping, token.identity);
151
- const cohort = yield* S.cohort(mapping, client.id, tuple.key, false);
152
-
153
- invariant(cohort.row !== undefined);
154
- const grant = yield* S.readGrant(mapping, token.moduleId, token.grantId);
155
-
156
- const read = yield* owner.read(j.table, equal(j.table, { [j.jobId]: job.context.jobId }), {
157
- limit: 1,
158
- }),
159
- row = read.rows[0];
160
-
161
- if (row === undefined) continue;
162
- const native = yield* mapping.subjectId.toNative(token.subjectId);
163
-
164
- invariant(
165
- row[j.snapshot] === S.jobStorage.encode(job) &&
166
- row[j.moduleId] === moduleId &&
167
- row[j.identityKey] === tuple.key &&
168
- row[j.clientKey] === client.id &&
169
- row[j.cohortKey] === cohort.id &&
170
- row[j.grantId] === token.grantId &&
171
- mapping.subjectId.equals(row[j.subjectId], native),
172
- );
173
-
174
- const now = yield* owner.now(mapping.clock),
175
- until = mapping.clock.decodeInstant(row[j.retentionUntil]);
176
-
177
- if (row[j.state] !== "Confirmed" || now < until) continue;
178
- if (grant?.row[g.revocationJobId] === job.context.jobId) {
179
- const summary = S.summaryStorage.decode(grant.row[g.summary]);
180
-
181
- yield* owner.update(
182
- g.table,
183
- { [g.moduleId]: token.moduleId, [g.grantId]: token.grantId },
184
- {
185
- [g.revocationJobId]: null,
186
- [g.summary]: S.summaryStorage.encode({ ...summary, remoteRevocation: "Confirmed" }),
187
- [g.version]: owner.marker,
188
- },
189
- );
190
- }
191
- yield* owner.remove(j.table, {
192
- [j.jobId]: job.context.jobId,
193
- [j.state]: "Confirmed",
194
- [j.version]: row[j.version],
195
- });
196
- owner.postconditions.push(sql`${mapping.clock.engineNowMillis} >= ${until}`);
197
- removed++;
198
- }
199
- }
200
- if (visited < limit) {
201
- const candidates = { rows: discovered.grants };
202
- const remaining = limit - visited;
203
-
204
- if (candidates.rows.length > remaining) hasMore = true;
205
- for (const candidate of candidates.rows.slice(0, remaining)) {
206
- visited++;
207
- const context = S.tokenContextStorage.decode(candidate[g.context]);
208
- const client = yield* S.client(mapping, context.configuration, false);
209
-
210
- invariant(client !== undefined);
211
- const tuple = yield* F.inspectTuple(mapping, context.identity);
212
- const cohort = yield* S.cohort(mapping, client.id, tuple.key, false);
213
-
214
- invariant(cohort.row !== undefined);
215
- const grant = yield* S.readGrant(mapping, moduleId, context.grantId);
216
-
217
- if (grant === undefined) continue;
218
-
219
- const now = yield* owner.now(mapping.clock),
220
- until = mapping.clock.decodeInstant(grant.row[g.retentionUntil]);
221
-
222
- if (
223
- !["Disconnected", "ReauthorizationRequired"].includes(grant.row[g.state]) ||
224
- grant.row[g.sealed] !== null ||
225
- grant.row[g.refreshWork] === "Unresolved" ||
226
- grant.row[g.revocationJobId] !== null ||
227
- now < until
228
- )
229
- continue;
230
-
231
- const a = mapping.admission,
232
- d = mapping.command;
233
-
234
- const references = both(
235
- sql`not exists(select 1 from ${a.table} where ${equal(a.table, { [a.moduleId]: moduleId, [a.grantId]: context.grantId })})`,
236
- sql`not exists(select 1 from ${d.table} where ${equal(d.table, { [d.moduleId]: moduleId, [d.grantId]: context.grantId })})`,
237
- ...(j === undefined
238
- ? []
239
- : [
240
- sql`not exists(select 1 from ${j.table} where ${equal(j.table, { [j.moduleId]: moduleId, [j.grantId]: context.grantId })})`,
241
- ]),
242
- );
243
-
244
- if (!(yield* owner.check(references))) continue;
245
- yield* owner.remove(g.table, {
246
- [g.moduleId]: moduleId,
247
- [g.grantId]: context.grantId,
248
- [g.version]: grant.row[g.version],
249
- });
250
- owner.postconditions.push(references, sql`${mapping.clock.engineNowMillis} >= ${until}`);
251
- removed++;
252
- }
253
- }
254
- // Clearing is global remote authority, independent of the module which happens
255
- // to run cleanup. No subject filter may hide a former owner's unresolved work.
256
- if (visited < limit && j !== undefined) {
257
- const candidates = { rows: discovered.cohorts };
258
- const remaining = limit - visited;
259
-
260
- if (candidates.rows.length > remaining) hasMore = true;
261
- for (const candidate of candidates.rows.slice(0, remaining)) {
262
- visited++;
263
- const id = candidate[h.clientKey];
264
- const client = yield* S.lockClientById(mapping, id);
265
- const cohort = yield* S.cohort(mapping, id, candidate[h.identityKey], false);
266
-
267
- invariant(cohort.id === candidate[h.cohortKey] && cohort.row !== undefined);
268
- yield* C.clear(mapping, client, cohort);
269
- }
270
- }
271
- if (visited < limit && mapping.externalReference !== undefined) {
272
- const t = mapping.ownership.tuple,
273
- remaining = limit - visited;
274
-
275
- if (discovered.tuples.length > remaining) hasMore = true;
276
- for (const candidate of discovered.tuples.slice(0, remaining)) {
277
- visited++;
278
- const scope = yield* S.heldScope(mapping, candidate[t.provider], candidate[t.issuer]);
279
-
280
- if (scope === undefined) continue;
281
-
282
- const identity = {
283
- provider: candidate[t.provider],
284
- issuer: candidate[t.issuer],
285
- subject: candidate[t.externalSubject],
286
- };
287
-
288
- invariant((yield* oauthIdentityKey(identity)) === candidate[t.identityKey]);
289
-
290
- const tuple = yield* F.inspectTuple(mapping, identity),
291
- row = tuple.row;
292
-
293
- if (row === undefined || row[t.state] !== "Owned") continue;
294
- const native = row[t.subjectId];
295
-
296
- invariant(native !== null);
297
-
298
- const noRefs = both(
299
- sql`not (${connectedReferenceCondition(mapping, tuple.key)})`,
300
- sql`not (${mapping.externalReference({ identityKey: tuple.key, subjectId: S.nativeCopy(native) })})`,
301
- );
302
-
303
- if (!(yield* owner.check(noRefs))) continue;
304
- yield* S.touchScope(mapping, scope);
305
- yield* owner.update(
306
- t.table,
307
- { [t.identityKey]: tuple.key, [t.state]: "Owned", [t.subjectId]: native },
308
- {
309
- [t.state]: "Unowned",
310
- [t.subjectId]: null,
311
- [t.reservation]: null,
312
- [t.version]: owner.marker,
313
- },
314
- );
315
- if (mapping.ownership.mode === "separate")
316
- yield* owner.remove(mapping.ownership.external.table, {
317
- [mapping.ownership.external.identityKey]: tuple.key,
318
- });
319
- owner.postconditions.push(noRefs);
320
- }
321
- }
322
-
323
- return { visited, removed, hasMore };
324
- });