@yielded/auth-persistence-drizzle 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 (200) hide show
  1. package/README.md +3 -0
  2. package/dist/D1.d.mts +11 -11
  3. package/dist/D1.mjs +2 -1
  4. package/dist/D1.mjs.map +1 -1
  5. package/dist/Libsql.d.mts +40 -56
  6. package/dist/Libsql.mjs +2 -1
  7. package/dist/Libsql.mjs.map +1 -1
  8. package/dist/Mysql2.d.mts +16 -17
  9. package/dist/Pglite.d.mts +18 -18
  10. package/dist/Pglite.mjs +2 -1
  11. package/dist/Pglite.mjs.map +1 -1
  12. package/dist/Postgres.d.mts +24 -24
  13. package/dist/Postgres.mjs +2 -1
  14. package/dist/Postgres.mjs.map +1 -1
  15. package/dist/SqliteBun.d.mts +39 -55
  16. package/dist/SqliteBun.mjs +2 -1
  17. package/dist/SqliteBun.mjs.map +1 -1
  18. package/dist/SqliteDo.d.mts +48 -64
  19. package/dist/SqliteDo.mjs +2 -1
  20. package/dist/SqliteDo.mjs.map +1 -1
  21. package/dist/SqliteNode.d.mts +45 -61
  22. package/dist/SqliteNode.mjs +2 -1
  23. package/dist/SqliteNode.mjs.map +1 -1
  24. package/dist/SqliteWasm.d.mts +48 -64
  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 +14 -14
  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 +18 -35
  34. package/dist/drizzle/d1-passwords.mjs +78 -290
  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 +2 -2
  38. package/dist/drizzle/d1-sessions.d.mts +3 -3
  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 +135 -0
  48. package/dist/drizzle/native-sql-table.mjs.map +1 -0
  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 -8
  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/password-sql.mjs +2 -1
  76. package/dist/drizzle/password-sql.mjs.map +1 -1
  77. package/dist/drizzle/password-target.mjs +13 -2
  78. package/dist/drizzle/password-target.mjs.map +1 -1
  79. package/dist/drizzle/pg-password-prepared.d.mts +5 -5
  80. package/dist/drizzle/pg-password-prepared.mjs.map +1 -1
  81. package/dist/drizzle/pg-passwords.d.mts +16 -32
  82. package/dist/drizzle/pg-passwords.mjs.map +1 -1
  83. package/dist/drizzle/pg-sessions.d.mts +1 -1
  84. package/dist/drizzle/phone-target.d.mts +2 -2
  85. package/dist/drizzle/phone-target.mjs +1 -1
  86. package/dist/drizzle/query-operations.mjs +3 -1
  87. package/dist/drizzle/query-operations.mjs.map +1 -1
  88. package/dist/drizzle/session-sql.mjs +2 -1
  89. package/dist/drizzle/session-sql.mjs.map +1 -1
  90. package/dist/drizzle/sqlite-password-prepared.mjs.map +1 -1
  91. package/dist/drizzle/sqlite-passwords.mjs.map +1 -1
  92. package/dist/drizzle/storage-validation.mjs +10 -198
  93. package/dist/drizzle/storage-validation.mjs.map +1 -1
  94. package/dist/drizzle/totp-target.d.mts +3 -3
  95. package/dist/drizzle/totp-target.mjs +2 -2
  96. package/dist/drizzle/transaction-execution.d.mts +2 -2
  97. package/dist/drizzle/transaction-owner.d.mts +1 -2
  98. package/dist/index.d.mts +3 -2
  99. package/dist/internal/drizzle-postgres-oauth-proxy.d.mts +138 -0
  100. package/dist/internal/drizzle-postgres-oauth-proxy.mjs +29 -0
  101. package/dist/internal/drizzle-postgres-oauth-proxy.mjs.map +1 -0
  102. package/dist/internal/drizzle-postgres.mjs +2 -0
  103. package/dist/internal/drizzle-postgres.mjs.map +1 -1
  104. package/dist/internal/drizzle-sqlite-oauth-proxy.d.mts +146 -0
  105. package/dist/internal/drizzle-sqlite-oauth-proxy.mjs +29 -0
  106. package/dist/internal/drizzle-sqlite-oauth-proxy.mjs.map +1 -0
  107. package/dist/internal/drizzle-sqlite.mjs +2 -0
  108. package/dist/internal/drizzle-sqlite.mjs.map +1 -1
  109. package/package.json +1 -1
  110. package/src/D1.ts +2 -0
  111. package/src/Libsql.ts +2 -0
  112. package/src/Pglite.ts +2 -0
  113. package/src/Postgres.ts +2 -0
  114. package/src/SqliteBun.ts +2 -0
  115. package/src/SqliteDo.ts +2 -0
  116. package/src/SqliteNode.ts +2 -0
  117. package/src/SqliteWasm.ts +2 -0
  118. package/src/drizzle/d1-password-prepared.ts +2 -23
  119. package/src/drizzle/d1-passwords.ts +158 -780
  120. package/src/drizzle/d1-sessions.ts +21 -6
  121. package/src/drizzle/mysql-password-prepared.ts +4 -16
  122. package/src/drizzle/mysql-passwords.ts +5 -20
  123. package/src/drizzle/native-sql-table.ts +259 -0
  124. package/src/drizzle/oauth-connected-model.ts +97 -315
  125. package/src/drizzle/oauth-connected-reference.ts +13 -93
  126. package/src/drizzle/oauth-connected-target.ts +14 -271
  127. package/src/drizzle/oauth-kernel.ts +10 -0
  128. package/src/drizzle/oauth-model.ts +188 -541
  129. package/src/drizzle/oauth-mysql.ts +2 -2
  130. package/src/drizzle/oauth-proxy.ts +101 -0
  131. package/src/drizzle/oauth-target.ts +33 -737
  132. package/src/drizzle/password-model.ts +0 -33
  133. package/src/drizzle/password-prepared-model.ts +0 -18
  134. package/src/drizzle/password-sql.ts +3 -0
  135. package/src/drizzle/password-target.ts +41 -4
  136. package/src/drizzle/pg-password-prepared.ts +4 -16
  137. package/src/drizzle/pg-passwords.ts +5 -20
  138. package/src/drizzle/query-operations.ts +4 -0
  139. package/src/drizzle/session-sql.ts +2 -1
  140. package/src/drizzle/sqlite-password-prepared.ts +4 -52
  141. package/src/drizzle/sqlite-passwords.ts +7 -28
  142. package/src/drizzle/storage-validation.ts +13 -222
  143. package/src/index.ts +2 -8
  144. package/src/internal/drizzle-postgres-oauth-proxy.ts +37 -0
  145. package/src/internal/drizzle-postgres.ts +2 -0
  146. package/src/internal/drizzle-sqlite-oauth-proxy.ts +44 -0
  147. package/src/internal/drizzle-sqlite.ts +2 -0
  148. package/dist/_virtual/_rolldown/runtime.mjs +0 -13
  149. package/dist/drizzle/oauth-accounts.mjs +0 -359
  150. package/dist/drizzle/oauth-accounts.mjs.map +0 -1
  151. package/dist/drizzle/oauth-connected-access.mjs +0 -212
  152. package/dist/drizzle/oauth-connected-access.mjs.map +0 -1
  153. package/dist/drizzle/oauth-connected-collection.mjs +0 -202
  154. package/dist/drizzle/oauth-connected-collection.mjs.map +0 -1
  155. package/dist/drizzle/oauth-connected-custody.mjs +0 -147
  156. package/dist/drizzle/oauth-connected-custody.mjs.map +0 -1
  157. package/dist/drizzle/oauth-connected-flow.mjs +0 -297
  158. package/dist/drizzle/oauth-connected-flow.mjs.map +0 -1
  159. package/dist/drizzle/oauth-connected-input.mjs +0 -122
  160. package/dist/drizzle/oauth-connected-input.mjs.map +0 -1
  161. package/dist/drizzle/oauth-connected-maintenance.mjs +0 -214
  162. package/dist/drizzle/oauth-connected-maintenance.mjs.map +0 -1
  163. package/dist/drizzle/oauth-connected-management.mjs +0 -229
  164. package/dist/drizzle/oauth-connected-management.mjs.map +0 -1
  165. package/dist/drizzle/oauth-connected-settlement.mjs +0 -128
  166. package/dist/drizzle/oauth-connected-settlement.mjs.map +0 -1
  167. package/dist/drizzle/oauth-connected-sign-in.mjs +0 -265
  168. package/dist/drizzle/oauth-connected-sign-in.mjs.map +0 -1
  169. package/dist/drizzle/oauth-connected-state.d.mts +0 -2062
  170. package/dist/drizzle/oauth-connected-state.mjs +0 -430
  171. package/dist/drizzle/oauth-connected-state.mjs.map +0 -1
  172. package/dist/drizzle/oauth-flow.d.mts +0 -22
  173. package/dist/drizzle/oauth-flow.mjs +0 -253
  174. package/dist/drizzle/oauth-flow.mjs.map +0 -1
  175. package/dist/drizzle/oauth-input.mjs +0 -127
  176. package/dist/drizzle/oauth-input.mjs.map +0 -1
  177. package/dist/drizzle/oauth-owner.d.mts +0 -10
  178. package/dist/drizzle/oauth-owner.mjs +0 -10
  179. package/dist/drizzle/oauth-owner.mjs.map +0 -1
  180. package/dist/drizzle/oauth-registration.d.mts +0 -5
  181. package/dist/drizzle/oauth-registration.mjs +0 -374
  182. package/dist/drizzle/oauth-registration.mjs.map +0 -1
  183. package/dist/drizzle/oauth-state.mjs +0 -107
  184. package/dist/drizzle/oauth-state.mjs.map +0 -1
  185. package/src/drizzle/oauth-accounts.ts +0 -805
  186. package/src/drizzle/oauth-connected-access.ts +0 -361
  187. package/src/drizzle/oauth-connected-collection.ts +0 -324
  188. package/src/drizzle/oauth-connected-custody.ts +0 -279
  189. package/src/drizzle/oauth-connected-flow.ts +0 -529
  190. package/src/drizzle/oauth-connected-input.ts +0 -147
  191. package/src/drizzle/oauth-connected-maintenance.ts +0 -346
  192. package/src/drizzle/oauth-connected-management.ts +0 -365
  193. package/src/drizzle/oauth-connected-settlement.ts +0 -251
  194. package/src/drizzle/oauth-connected-sign-in.ts +0 -443
  195. package/src/drizzle/oauth-connected-state.ts +0 -872
  196. package/src/drizzle/oauth-flow.ts +0 -462
  197. package/src/drizzle/oauth-input.ts +0 -201
  198. package/src/drizzle/oauth-owner.ts +0 -29
  199. package/src/drizzle/oauth-registration.ts +0 -640
  200. package/src/drizzle/oauth-state.ts +0 -218
@@ -1,346 +0,0 @@
1
- import type * as M from "@yielded/auth/OAuth";
2
- import {
3
- OAuthConnectedRevocationClaim,
4
- type OAuthConnectedRevocations,
5
- snapshotOAuthSync,
6
- } from "@yielded/auth/OAuth";
7
- import { asc, eq, lte, sql } from "drizzle-orm";
8
- import { Effect } from "effect";
9
-
10
- import { collect, discover } from "./oauth-connected-collection";
11
- import * as C from "./oauth-connected-custody";
12
- import * as F from "./oauth-connected-flow";
13
- import * as S from "./oauth-connected-state";
14
- import { both, col, equal, CurrentOAuthTransaction } from "./oauth-owner";
15
- import { invariant, storage } from "./oauth-state";
16
-
17
- type WorkerInput<K extends keyof OAuthConnectedRevocations["Service"]> = Parameters<
18
- OAuthConnectedRevocations["Service"][K]
19
- >[0];
20
- const revocationClaimStorage = storage(OAuthConnectedRevocationClaim);
21
-
22
- const jobLocks = Effect.fn("oauthConnected.jobLocks")(function* (
23
- mapping: S.Revocations,
24
- job: M.OAuthConnectedRevocationJob,
25
- ) {
26
- const owner = yield* CurrentOAuthTransaction;
27
-
28
- const token = job.context.token,
29
- client = yield* S.client(mapping, token.configuration, false);
30
-
31
- if (client === undefined) return undefined;
32
-
33
- const tuple = yield* F.inspectTuple(mapping, token.identity),
34
- t = mapping.ownership.tuple;
35
-
36
- const native = yield* mapping.subjectId.toNative(token.subjectId);
37
-
38
- if (tuple.row?.[t.state] !== "Owned" || !mapping.subjectId.equals(tuple.row[t.subjectId], native))
39
- return undefined;
40
- const cohort = yield* S.cohort(mapping, client.id, tuple.key, false);
41
-
42
- invariant(cohort.row !== undefined);
43
-
44
- const j = mapping.job,
45
- read = yield* owner.read(j.table, equal(j.table, { [j.jobId]: job.context.jobId }), {
46
- limit: 1,
47
- }),
48
- row = read.rows[0];
49
-
50
- if (row === undefined) return undefined;
51
- invariant(row[j.state] === "Confirmed" || cohort.blocked);
52
- invariant(
53
- row[j.snapshot] === S.jobStorage.encode(job) &&
54
- row[j.moduleId] === token.moduleId &&
55
- mapping.subjectId.equals(native, row[j.subjectId]) &&
56
- row[j.identityKey] === tuple.key &&
57
- row[j.clientKey] === client.id &&
58
- row[j.cohortKey] === cohort.id &&
59
- row[j.grantId] === token.grantId &&
60
- ["Pending", "Claimed", "Unknown", "Confirmed"].includes(row[j.state]),
61
- );
62
-
63
- return { client, cohort, row };
64
- });
65
-
66
- export const claimRevocation = Effect.fn("oauthConnected.claimRevocation")(function* (
67
- mapping: S.Revocations,
68
- input: WorkerInput<"claim">,
69
- ) {
70
- const owner = yield* CurrentOAuthTransaction;
71
-
72
- invariant(
73
- Number.isSafeInteger(input.lifetimeMillis) &&
74
- input.lifetimeMillis >= 1000 &&
75
- input.lifetimeMillis <= 120000,
76
- );
77
- const j = mapping.job;
78
-
79
- const candidate = yield* owner.read(
80
- j.table,
81
- equal(j.table, { [j.moduleId]: input.moduleId, [j.state]: "Pending" }),
82
- { limit: 1, takeOnly: true, lock: false, observe: false, orderBy: asc(col(j.table, j.jobId)) },
83
- );
84
-
85
- const row = candidate.rows[0];
86
-
87
- if (row === undefined) return { _tag: "Empty" } as const;
88
- const job = S.jobStorage.decode(row[j.snapshot]);
89
- const locks = yield* jobLocks(mapping, job);
90
-
91
- if (locks === undefined || locks.row[j.state] !== "Pending") return { _tag: "Empty" } as const;
92
- invariant(
93
- locks.row[j.claimId] === null &&
94
- locks.row[j.claimedAt] === null &&
95
- locks.row[j.claimExpiresAt] === null,
96
- );
97
-
98
- const now = yield* owner.now(mapping.clock),
99
- expires = now + input.lifetimeMillis;
100
-
101
- invariant(Number.isSafeInteger(expires));
102
-
103
- const claim = snapshotOAuthSync(OAuthConnectedRevocationClaim, {
104
- job,
105
- claimId: input.claimId,
106
- claimedAtMillis: now,
107
- claimExpiresAtMillis: expires,
108
- });
109
-
110
- yield* owner.update(
111
- j.table,
112
- { [j.jobId]: job.context.jobId, [j.state]: "Pending" },
113
- {
114
- [j.state]: "Claimed",
115
- [j.claimId]: input.claimId,
116
- [j.claimedAt]: mapping.clock.encodeInstant(now),
117
- [j.claimExpiresAt]: mapping.clock.encodeInstant(expires),
118
- [j.retentionUntil]: mapping.clock.encodeInstant(S.retainedUntil(mapping, expires)),
119
- [j.version]: owner.marker,
120
- },
121
- );
122
- owner.postconditions.push(
123
- sql`${mapping.clock.engineNowMillis} >= ${now} and ${mapping.clock.engineNowMillis} < ${expires}`,
124
- );
125
-
126
- return { _tag: "Claimed", claim } as const;
127
- });
128
-
129
- export const settleRevocation = Effect.fn("oauthConnected.settleRevocation")(function* (
130
- mapping: S.Revocations,
131
- input: WorkerInput<"settle">,
132
- ) {
133
- const owner = yield* CurrentOAuthTransaction;
134
- const locks = yield* jobLocks(mapping, input.claim.job);
135
-
136
- if (locks === undefined) return { settled: false };
137
-
138
- const j = mapping.job,
139
- row = locks.row;
140
-
141
- if (row[j.state] !== "Claimed" || row[j.claimId] !== input.claim.claimId)
142
- return { settled: false };
143
- invariant(
144
- revocationClaimStorage.encode({
145
- job: input.claim.job,
146
- claimId: row[j.claimId],
147
- claimedAtMillis: mapping.clock.decodeInstant(row[j.claimedAt]),
148
- claimExpiresAtMillis: mapping.clock.decodeInstant(row[j.claimExpiresAt]),
149
- }) === revocationClaimStorage.encode(input.claim),
150
- );
151
- const now = yield* owner.now(mapping.clock);
152
-
153
- invariant(now >= input.claim.claimedAtMillis);
154
- const confirmed = input.outcome === "Confirmed" && now < input.claim.claimExpiresAtMillis;
155
-
156
- yield* owner.update(
157
- j.table,
158
- {
159
- [j.jobId]: input.claim.job.context.jobId,
160
- [j.state]: "Claimed",
161
- [j.claimId]: input.claim.claimId,
162
- },
163
- { [j.state]: confirmed ? "Confirmed" : "Unknown", [j.version]: owner.marker },
164
- );
165
- if (confirmed) {
166
- owner.postconditions.push(
167
- sql`${mapping.clock.engineNowMillis} >= ${now} and ${mapping.clock.engineNowMillis} < ${input.claim.claimExpiresAtMillis}`,
168
- );
169
- yield* C.clear(mapping, locks.client, locks.cohort, input.claim.job.context.jobId);
170
- }
171
-
172
- return { settled: true };
173
- });
174
-
175
- /** Candidate discovery is not authority. Each conditional write re-reads its row
176
- * and engine time under the client/tuple/cohort order and retains a final guard. */
177
- export const cleanup = Effect.fn("oauthConnected.cleanup")(function* (
178
- mapping: S.Mapping,
179
- input: F.Input<"cleanup">,
180
- ) {
181
- const owner = yield* CurrentOAuthTransaction;
182
-
183
- invariant(Number.isSafeInteger(input.limit) && input.limit >= 1 && input.limit <= 1000);
184
-
185
- const f = mapping.flow,
186
- a = mapping.admission,
187
- d = mapping.command;
188
-
189
- let terminalized = 0,
190
- removed = 0,
191
- visited = 0,
192
- hasMore = false;
193
-
194
- const at = yield* owner.now(mapping.clock);
195
-
196
- const candidates = yield* owner.read(
197
- f.table,
198
- both(
199
- eq(col(f.table, f.moduleId), input.moduleId),
200
- sql`((${col(f.table, f.state)} in ('Prepared','Pending') and ${col(f.table, f.expiresAt)} <= ${mapping.clock.encodeInstant(at)}) or (${col(f.table, f.state)} = 'Claimed' and ${col(f.table, f.claimExpiresAt)} <= ${mapping.clock.encodeInstant(at)}) or (${col(f.table, f.state)} not in ('Prepared','Pending','Claimed') and ${col(f.table, f.work)} <> 'Unresolved' and ${col(f.table, f.retentionUntil)} <= ${mapping.clock.encodeInstant(at)}))`,
201
- ),
202
- {
203
- limit: input.limit + 1,
204
- takeOnly: true,
205
- observe: false,
206
- lock: false,
207
- orderBy: asc(col(f.table, f.flowId)),
208
- },
209
- );
210
-
211
- const collectionCandidates = yield* discover(mapping, input.moduleId, input.limit);
212
-
213
- const g = mapping.grant,
214
- j = S.jobTable(mapping),
215
- h = mapping.cohort;
216
-
217
- yield* S.prelockClients(
218
- mapping,
219
- [
220
- ...candidates.rows.slice(0, input.limit).map((row) => row[f.clientKey]),
221
- ...collectionCandidates.jobs.slice(0, input.limit).map((row) => row[j!.clientKey]),
222
- ...collectionCandidates.grants.slice(0, input.limit).map((row) => row[g.clientKey]),
223
- ...collectionCandidates.cohorts.slice(0, input.limit).map((row) => row[h.clientKey]),
224
- ],
225
- collectionCandidates.tuples.slice(0, input.limit).map((row) => ({
226
- provider: row[mapping.ownership.tuple.provider],
227
- issuer: row[mapping.ownership.tuple.issuer],
228
- })),
229
- );
230
- hasMore = candidates.rows.length > input.limit;
231
- for (const candidate of candidates.rows.slice(0, input.limit)) {
232
- visited++;
233
- // A terminal ledger retains client key but may erase the decryptable browser snapshot.
234
- const heldClient = yield* S.lockClientById(mapping, candidate[f.clientKey]);
235
-
236
- const read = yield* owner.read(
237
- f.table,
238
- equal(f.table, { [f.moduleId]: input.moduleId, [f.flowId]: candidate[f.flowId] }),
239
- { limit: 1 },
240
- ),
241
- row = read.rows[0];
242
-
243
- if (row === undefined) continue;
244
- invariant(row[f.clientKey] === heldClient.id);
245
-
246
- const now = yield* owner.now(mapping.clock),
247
- state = row[f.state];
248
-
249
- const expires = mapping.clock.decodeInstant(
250
- state === "Claimed" ? row[f.claimExpiresAt] : row[f.expiresAt],
251
- );
252
-
253
- if ((state === "Prepared" || state === "Pending" || state === "Claimed") && now >= expires) {
254
- yield* owner.update(
255
- f.table,
256
- { [f.moduleId]: input.moduleId, [f.flowId]: row[f.flowId], [f.version]: row[f.version] },
257
- {
258
- [f.state]: state === "Claimed" ? "Ambiguous" : "Expired",
259
- [f.snapshot]: null,
260
- [f.version]: owner.marker,
261
- },
262
- );
263
- owner.postconditions.push(sql`${mapping.clock.engineNowMillis} >= ${expires}`);
264
- terminalized++;
265
- } else if (
266
- state !== "Prepared" &&
267
- state !== "Pending" &&
268
- state !== "Claimed" &&
269
- row[f.work] !== "Unresolved" &&
270
- now >= mapping.clock.decodeInstant(row[f.retentionUntil])
271
- ) {
272
- yield* owner.remove(f.table, {
273
- [f.moduleId]: input.moduleId,
274
- [f.flowId]: row[f.flowId],
275
- [f.version]: row[f.version],
276
- });
277
- owner.postconditions.push(
278
- sql`${mapping.clock.engineNowMillis} >= ${mapping.clock.decodeInstant(row[f.retentionUntil])}`,
279
- );
280
- removed++;
281
- }
282
- }
283
- // Expired admission and command metadata have no external-operation obligation.
284
- for (const table of [a, d]) {
285
- if (visited >= input.limit) {
286
- hasMore = true;
287
- break;
288
- }
289
-
290
- const deadline = "expiresAt" in table ? table.expiresAt : table.retentionUntil,
291
- key = "admissionId" in table ? table.admissionId : table.commandId;
292
-
293
- const rows = yield* owner.read(
294
- table.table,
295
- both(
296
- eq(col(table.table, table.moduleId), input.moduleId),
297
- lte(col(table.table, deadline), mapping.clock.encodeInstant(at)),
298
- ),
299
- {
300
- limit: input.limit - visited + 1,
301
- takeOnly: true,
302
- observe: false,
303
- lock: false,
304
- orderBy: asc(col(table.table, key)),
305
- },
306
- );
307
-
308
- if (rows.rows.length > input.limit - visited) hasMore = true;
309
- for (const candidate of rows.rows.slice(0, input.limit - visited)) {
310
- visited++;
311
-
312
- const read = yield* owner.read(
313
- table.table,
314
- equal(table.table, { [table.moduleId]: input.moduleId, [key]: candidate[key] }),
315
- { limit: 1 },
316
- ),
317
- row = read.rows[0];
318
-
319
- const now = yield* owner.now(mapping.clock);
320
-
321
- if (row !== undefined && now >= mapping.clock.decodeInstant(row[deadline])) {
322
- yield* owner.remove(table.table, {
323
- [table.moduleId]: input.moduleId,
324
- [key]: row[key],
325
- [table.version]: row[table.version],
326
- });
327
- owner.postconditions.push(
328
- sql`${mapping.clock.engineNowMillis} >= ${mapping.clock.decodeInstant(row[deadline])}`,
329
- );
330
- removed++;
331
- }
332
- }
333
- }
334
-
335
- const collection = yield* collect(
336
- mapping,
337
- input.moduleId,
338
- input.limit - visited,
339
- collectionCandidates,
340
- );
341
-
342
- removed += collection.removed;
343
- hasMore ||= collection.hasMore;
344
-
345
- return { terminalized, removed, hasMore };
346
- });
@@ -1,365 +0,0 @@
1
- import * as M from "@yielded/auth/OAuth";
2
- import { snapshotOAuthSync } from "@yielded/auth/OAuth";
3
- import { asc, eq, gt, lte } from "drizzle-orm";
4
- import { Effect } from "effect";
5
-
6
- import * as A from "./oauth-connected-access";
7
- import * as C from "./oauth-connected-custody";
8
- import * as F from "./oauth-connected-flow";
9
- import * as S from "./oauth-connected-state";
10
- import { both, col, equal, CurrentOAuthTransaction } from "./oauth-owner";
11
- import { digest, invariant } from "./oauth-state";
12
-
13
- export const list = Effect.fn("oauthConnected.list")(function* (
14
- mapping: S.Mapping,
15
- input: F.Input<"list">,
16
- ) {
17
- const owner = yield* CurrentOAuthTransaction;
18
- const found = yield* S.useAuthority(mapping, input.authorization, "metadata");
19
-
20
- if (found === undefined) return { items: [] };
21
- invariant(Number.isSafeInteger(input.limit) && input.limit >= 1 && input.limit <= 100);
22
-
23
- const g = mapping.grant,
24
- h = mapping.cohort,
25
- j = S.jobTable(mapping);
26
-
27
- const cursor = input.cursor === undefined ? undefined : M.OAuthGrantId.make(input.cursor);
28
-
29
- const where = both(
30
- equal(g.table, { [g.moduleId]: input.authorization.moduleId, [g.subjectId]: found.nativeId }),
31
- cursor === undefined ? undefined : gt(col(g.table, g.grantId), cursor),
32
- input.authorization.grantId === undefined
33
- ? undefined
34
- : eq(col(g.table, g.grantId), input.authorization.grantId),
35
- input.authorization.profileKey === undefined
36
- ? undefined
37
- : eq(col(g.table, g.profileKey), input.authorization.profileKey),
38
- );
39
-
40
- const columns = [
41
- g.moduleId,
42
- g.grantId,
43
- g.subjectId,
44
- g.identityKey,
45
- g.clientKey,
46
- g.cohortKey,
47
- g.cohortGeneration,
48
- g.profileKey,
49
- g.state,
50
- g.summary,
51
- g.revocationJobId,
52
- g.version,
53
- ];
54
-
55
- const read = yield* owner.read(g.table, where, {
56
- columns,
57
- limit: input.limit + 1,
58
- takeOnly: true,
59
- observe: false,
60
- lock: false,
61
- orderBy: asc(col(g.table, g.grantId)),
62
- });
63
-
64
- const more = read.rows.length > input.limit,
65
- rows = read.rows.slice(0, input.limit),
66
- last = rows.at(-1)?.[g.grantId];
67
-
68
- owner.observations.push({
69
- table: g.table,
70
- where: more ? both(where, lte(col(g.table, g.grantId), last)) : where,
71
- rows,
72
- });
73
- const items: Array<typeof M.OAuthConnectedSummary.Type> = [];
74
-
75
- for (const row of rows) {
76
- const summary = S.summaryStorage.decode(row[g.summary]);
77
-
78
- invariant(
79
- summary.grantId === row[g.grantId] &&
80
- summary.profileKey === row[g.profileKey] &&
81
- mapping.subjectId.equals(found.nativeId, row[g.subjectId]),
82
- );
83
-
84
- const co = yield* owner.read(h.table, equal(h.table, { [h.cohortKey]: row[g.cohortKey] }), {
85
- limit: 1,
86
- lock: false,
87
- });
88
-
89
- const cr = co.rows[0];
90
-
91
- invariant(
92
- cr !== undefined &&
93
- cr[h.clientKey] === row[g.clientKey] &&
94
- cr[h.identityKey] === row[g.identityKey],
95
- );
96
- let status = row[g.state] as (typeof M.OAuthConnectedSummary.Type)["status"];
97
-
98
- if (
99
- status !== "Disconnected" &&
100
- (cr[h.state] !== "Open" || cr[h.generation] !== row[g.cohortGeneration])
101
- )
102
- status = "ReauthorizationRequired";
103
- let remote = summary.remoteRevocation;
104
-
105
- if (row[g.revocationJobId] !== null) {
106
- if (j === undefined) remote = "Unknown";
107
- else {
108
- const job = yield* owner.read(
109
- j.table,
110
- equal(j.table, { [j.jobId]: row[g.revocationJobId] }),
111
- {
112
- limit: 1,
113
- lock: false,
114
- columns: [j.jobId, j.moduleId, j.grantId, j.cohortKey, j.state],
115
- },
116
- );
117
-
118
- const jr = job.rows[0];
119
-
120
- invariant(
121
- jr !== undefined &&
122
- jr[j.moduleId] === row[g.moduleId] &&
123
- jr[j.grantId] === row[g.grantId] &&
124
- jr[j.cohortKey] === row[g.cohortKey],
125
- );
126
- remote =
127
- jr[j.state] === "Confirmed"
128
- ? "Confirmed"
129
- : jr[j.state] === "Unknown"
130
- ? "Unknown"
131
- : "Pending";
132
- }
133
- }
134
- items.push(
135
- snapshotOAuthSync(M.OAuthConnectedSummary, { ...summary, status, remoteRevocation: remote }),
136
- );
137
- }
138
-
139
- return { items, ...(more ? { cursor: last as string } : {}) };
140
- });
141
-
142
- const command = Effect.fn("oauthConnected.disconnectCommand")(function* (
143
- mapping: S.Mapping,
144
- moduleId: string,
145
- commandId: string,
146
- lock = true,
147
- ) {
148
- const owner = yield* CurrentOAuthTransaction;
149
-
150
- const d = mapping.command,
151
- read = yield* owner.read(
152
- d.table,
153
- equal(d.table, { [d.moduleId]: moduleId, [d.commandId]: commandId }),
154
- { limit: 1, lock, observe: lock },
155
- );
156
-
157
- const row = read.rows[0];
158
-
159
- if (row === undefined) return { read };
160
-
161
- const grant = S.commandStorage.decode(row[d.intent]),
162
- result = S.disconnectedStorage.decode(row[d.decision]);
163
-
164
- const native = yield* mapping.subjectId.toNative(grant.context.subjectId);
165
-
166
- invariant(
167
- grant.context.moduleId === moduleId &&
168
- grant.context.grantId === row[d.grantId] &&
169
- result.grantId === grant.context.grantId &&
170
- mapping.subjectId.equals(native, row[d.subjectId]) &&
171
- !result.replayed,
172
- );
173
-
174
- return { read, row, grant, result };
175
- });
176
-
177
- export const inspectDisconnect = Effect.fn("oauthConnected.inspectDisconnect")(function* (
178
- mapping: S.Mapping,
179
- input: F.Input<"inspectDisconnect">,
180
- ) {
181
- const found = yield* S.useAuthority(mapping, input.authorization, "metadata");
182
-
183
- if (
184
- found === undefined ||
185
- input.subjectId !== found.revision.subjectId ||
186
- input.moduleId !== input.authorization.moduleId
187
- )
188
- return { _tag: "Rejected" } as const;
189
- const discovered = yield* command(mapping, input.moduleId, input.commandId, false);
190
-
191
- if (discovered.row !== undefined) {
192
- if (
193
- discovered.grant.context.subjectId !== input.subjectId ||
194
- discovered.grant.context.grantId !== input.grantId
195
- )
196
- return { _tag: "Conflict" } as const;
197
- if (
198
- (yield* S.useAuthority(
199
- mapping,
200
- input.authorization,
201
- "metadata",
202
- discovered.grant.context,
203
- )) === undefined
204
- )
205
- return { _tag: "Rejected" } as const;
206
- const prior = yield* command(mapping, input.moduleId, input.commandId);
207
- const d = mapping.command;
208
-
209
- if (
210
- prior.row === undefined ||
211
- prior.row[d.intent] !== discovered.row[d.intent] ||
212
- prior.row[d.decision] !== discovered.row[d.decision] ||
213
- prior.row[d.version] !== discovered.row[d.version]
214
- )
215
- return { _tag: "Rejected" } as const;
216
-
217
- return { _tag: "Replay", result: { ...prior.result, replayed: true } } as const;
218
- }
219
- const read = yield* A.locked(mapping, input.authorization, input.grantId, "metadata");
220
-
221
- if (read === undefined) return { _tag: "Rejected" } as const;
222
- const finalCommand = yield* command(mapping, input.moduleId, input.commandId);
223
-
224
- if (finalCommand.row !== undefined) return { _tag: "Rejected" } as const;
225
-
226
- return {
227
- _tag: "Target",
228
- grant: {
229
- context: read.grant.context,
230
- ...(read.grant.sealed === undefined ? {} : { sealed: read.grant.sealed }),
231
- },
232
- revision: read.found.revision,
233
- } as const;
234
- });
235
-
236
- export const disconnect = Effect.fn("oauthConnected.disconnect")(function* (
237
- mapping: S.Mapping,
238
- input: F.Input<"disconnect">,
239
- ) {
240
- const owner = yield* CurrentOAuthTransaction;
241
-
242
- const token = input.grant.context,
243
- found = yield* S.current(mapping, token.subjectId);
244
-
245
- if (
246
- found === undefined ||
247
- input.moduleId !== token.moduleId ||
248
- !(yield* S.action(mapping, found, input.authorization, "disconnect", {
249
- moduleId: input.moduleId,
250
- flowId: input.commandId,
251
- commandId: input.commandId,
252
- revision: input.authorization.challenge.revision,
253
- intent: S.disconnectStorage.encode(input.grant),
254
- maximumAgeMillis: 300000,
255
- configuration: token.configuration,
256
- grant: token,
257
- }))
258
- )
259
- return { _tag: "Rejected" } as const;
260
- const client = yield* S.client(mapping, token.configuration, false);
261
-
262
- if (client === undefined) return { _tag: "Rejected" } as const;
263
-
264
- const tuple = yield* F.inspectTuple(mapping, token.identity),
265
- t = mapping.ownership.tuple;
266
-
267
- if (
268
- tuple.row?.[t.state] !== "Owned" ||
269
- !mapping.subjectId.equals(tuple.row[t.subjectId], found.nativeId)
270
- )
271
- return { _tag: "Conflict" } as const;
272
- const cohort = yield* S.cohort(mapping, client.id, tuple.key, false);
273
-
274
- invariant(cohort.row !== undefined);
275
-
276
- const read = yield* S.readGrant(mapping, input.moduleId, token.grantId),
277
- g = mapping.grant;
278
-
279
- if (
280
- read === undefined ||
281
- !C.sameContext(read.context, token) ||
282
- S.disconnectStorage.encode({
283
- context: read.context,
284
- ...(read.sealed === undefined ? {} : { sealed: read.sealed }),
285
- }) !== S.disconnectStorage.encode(input.grant)
286
- )
287
- return { _tag: "Conflict" } as const;
288
- const prior = yield* command(mapping, input.moduleId, input.commandId);
289
-
290
- // Only inspectDisconnect exposes replay. Mutation replay cannot repeat effects.
291
- if (prior.row !== undefined) return { _tag: "Conflict" } as const;
292
-
293
- const now = yield* owner.now(mapping.clock),
294
- wasOpen = !cohort.blocked;
295
-
296
- invariant(input.retentionUntilMillis >= now && input.retentionUntilMillis <= now + 2592000000);
297
- const affected = yield* C.fence(mapping, client, cohort);
298
- let remote: (typeof M.OAuthConnectedSummary.Type)["remoteRevocation"] = "Unsupported";
299
-
300
- if (input.revocation !== undefined) {
301
- yield* C.storeJob(mapping, input.revocation, token, found.nativeId, now);
302
- remote = "Pending";
303
- } else if (token.configuration.profile.revocation === "cohort") {
304
- invariant(read.sealed === undefined);
305
- remote = "Unknown";
306
- }
307
-
308
- const result = snapshotOAuthSync(M.OAuthConnectedDisconnected, {
309
- _tag: "Disconnected",
310
- grantId: token.grantId,
311
- remoteRevocation: remote,
312
- affectedGrantCount: affected,
313
- replayed: false,
314
- });
315
-
316
- yield* owner.update(
317
- g.table,
318
- { [g.moduleId]: input.moduleId, [g.grantId]: token.grantId },
319
- {
320
- [g.state]: "Disconnected",
321
- [g.activeIdentityKey]: null,
322
- [g.sealed]: null,
323
- [g.revocationJobId]: input.revocation?.context.jobId ?? read.row[g.revocationJobId],
324
- [g.version]: owner.marker,
325
- },
326
- );
327
-
328
- const d = mapping.command,
329
- key = { [d.moduleId]: input.moduleId, [d.commandId]: input.commandId };
330
-
331
- const inserted = yield* owner.insert(
332
- d.table,
333
- {
334
- ...d.encodeInsert({
335
- commandId: input.commandId,
336
- grant: snapshotOAuthSync(M.OAuthConnectedDisconnectGrant, input.grant),
337
- subjectId: S.nativeCopy(found.nativeId),
338
- }),
339
- ...key,
340
- [d.subjectId]: found.nativeId,
341
- [d.grantId]: token.grantId,
342
- [d.intent]: S.commandStorage.encode({
343
- context: token,
344
- originalDigest: yield* digest(S.disconnectStorage.encode(input.grant)),
345
- ...(input.revocation === undefined
346
- ? {}
347
- : { revocationJobId: input.revocation.context.jobId }),
348
- }),
349
- [d.decision]: S.disconnectedStorage.encode(result),
350
- [d.retentionUntil]: mapping.clock.encodeInstant(input.retentionUntilMillis),
351
- [d.version]: owner.marker,
352
- },
353
- key,
354
- );
355
-
356
- prior.read.rows = inserted.rows;
357
- if (
358
- wasOpen &&
359
- input.revocation === undefined &&
360
- token.configuration.profile.revocation === "unsupported"
361
- )
362
- yield* C.clear(mapping, client, cohort, undefined, true);
363
-
364
- return result;
365
- });