@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,251 +0,0 @@
1
- import * as M from "@yielded/auth/OAuth";
2
- import { snapshotOAuthSync } from "@yielded/auth/OAuth";
3
- import { ne, 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 * as S from "./oauth-connected-state";
9
- import { both, col, equal, CurrentOAuthTransaction } from "./oauth-owner";
10
- import { invariant, sameIdentity, oauthIdentityKey } from "./oauth-state";
11
-
12
- export const validMetadata = (context: M.OAuthConnectedTokenContext, now: number) => {
13
- const m = context.metadata,
14
- p = context.configuration.profile;
15
-
16
- return (
17
- p.provider === context.identity.provider &&
18
- context.configuration.provider === context.identity.provider &&
19
- context.configuration.issuer === context.identity.issuer &&
20
- m.obtainedAtMillis <= now &&
21
- m.useUntilMillis > m.obtainedAtMillis &&
22
- m.useUntilMillis <= m.obtainedAtMillis + p.maximumAccessLifetimeMillis &&
23
- (m.accessExpiresAtMillis === undefined || m.useUntilMillis <= m.accessExpiresAtMillis) &&
24
- m.scopes.every((v) => p.scopes.includes(v)) &&
25
- m.resources.every((v) => p.resources.includes(v)) &&
26
- new Set(m.scopes).size === m.scopes.length &&
27
- new Set(m.resources).size === m.resources.length &&
28
- (m.refreshUseUntilMillis === undefined ||
29
- (p.retention === "access-and-refresh" &&
30
- p.maximumRefreshLifetimeMillis !== undefined &&
31
- m.refreshUseUntilMillis > m.obtainedAtMillis &&
32
- m.refreshUseUntilMillis <= m.obtainedAtMillis + p.maximumRefreshLifetimeMillis &&
33
- (m.refreshExpiresAtMillis === undefined ||
34
- m.refreshUseUntilMillis <= m.refreshExpiresAtMillis)))
35
- );
36
- };
37
-
38
- /** The only writer that activates or replaces a confirmed connected grant. */
39
- export const activateGrant = Effect.fn("oauthConnected.activateGrant")(function* (
40
- mapping: S.Mapping,
41
- grant: M.OAuthConnectedStoredGrant,
42
- native: unknown,
43
- now: number,
44
- replacing: boolean,
45
- ) {
46
- const owner = yield* CurrentOAuthTransaction;
47
- const token = grant.context;
48
- const g = mapping.grant;
49
- const identityKey = yield* oauthIdentityKey(token.identity);
50
- const clientKey = yield* S.clientKey(token.configuration);
51
- const cohortKey = yield* S.cohortKey(clientKey, identityKey);
52
-
53
- const values = {
54
- ...g.encodeInsert({
55
- grant: snapshotOAuthSync(M.OAuthConnectedStoredGrant, grant),
56
- subjectId: S.nativeCopy(native),
57
- }),
58
- [g.moduleId]: token.moduleId,
59
- [g.grantId]: token.grantId,
60
- [g.subjectId]: native,
61
- [g.identityKey]: identityKey,
62
- [g.activeIdentityKey]: identityKey,
63
- [g.clientKey]: clientKey,
64
- [g.cohortKey]: cohortKey,
65
- [g.profileKey]: token.configuration.profile.key,
66
- [g.grantVersion]: token.grantVersion,
67
- [g.tokenVersion]: token.tokenVersion,
68
- [g.cohortGeneration]: token.cohortGeneration,
69
- [g.state]: "Active",
70
- [g.version]: owner.marker,
71
- [g.context]: S.tokenContextStorage.encode(token),
72
- [g.sealed]: S.sealedStorage.encode(grant.sealed),
73
- [g.summary]: S.summaryStorage.encode(C.summary(token)),
74
- [g.revocationJobId]: null,
75
- [g.refreshWork]: "None",
76
- [g.refreshClaim]: null,
77
- [g.refreshClaimExpiresAt]: null,
78
- [g.retentionUntil]: mapping.clock.encodeInstant(
79
- S.retainedUntil(
80
- mapping,
81
- Math.max(now, token.metadata.useUntilMillis, token.metadata.refreshUseUntilMillis ?? 0),
82
- ),
83
- ),
84
- };
85
-
86
- if (!replacing) {
87
- yield* owner.insert(g.table, values, {
88
- [g.moduleId]: token.moduleId,
89
- [g.grantId]: token.grantId,
90
- });
91
- } else
92
- yield* owner.update(
93
- g.table,
94
- { [g.moduleId]: token.moduleId, [g.grantId]: token.grantId },
95
- values,
96
- );
97
- });
98
-
99
- export const settle = Effect.fn("oauthConnected.settle")(function* (
100
- mapping: S.Mapping,
101
- input: F.Input<"settle">,
102
- ) {
103
- const owner = yield* CurrentOAuthTransaction;
104
-
105
- const c = input.claim.flow.context,
106
- out = input.outcome;
107
-
108
- const found = yield* S.current(mapping, c.revision.subjectId);
109
-
110
- const authorized =
111
- found !== undefined &&
112
- (yield* S.action(mapping, found, input.authorization, "settle", F.expected(input.claim.flow)));
113
-
114
- const cl = yield* S.client(mapping, S.configuration(input.claim.flow), false);
115
-
116
- if (cl === undefined) return { _tag: "Rejected" } as const;
117
- if (out._tag !== "Verified" && out._tag !== "Quarantined") {
118
- const claimed = yield* F.exact(mapping, input.claim);
119
-
120
- if (claimed === undefined) return { _tag: "Rejected" } as const;
121
- const state = out._tag === "Unissued" ? "Rejected" : out._tag;
122
-
123
- yield* F.terminal(
124
- mapping,
125
- input.claim,
126
- state,
127
- out._tag === "Cancelled" || out._tag === "Unissued" ? "Resolved" : "Unresolved",
128
- );
129
-
130
- return { _tag: state } as
131
- | { readonly _tag: "Cancelled" }
132
- | { readonly _tag: "Rejected" }
133
- | { readonly _tag: "Conflict" }
134
- | { readonly _tag: "Ambiguous" };
135
- }
136
-
137
- const grant = snapshotOAuthSync(M.OAuthConnectedStoredGrant, out.grant),
138
- token = grant.context;
139
-
140
- invariant(
141
- token.moduleId === c.moduleId &&
142
- token.subjectId === c.revision.subjectId &&
143
- token.grantId === c.grantId &&
144
- token.identity.provider === c.provider &&
145
- token.identity.issuer === c.issuer &&
146
- (yield* S.clientKey(token.configuration)) === cl.id &&
147
- S.tokenContextStorage.encode({
148
- ...token,
149
- configuration: S.configuration(input.claim.flow),
150
- }) === S.tokenContextStorage.encode(token),
151
- );
152
- const native = yield* mapping.subjectId.toNative(c.revision.subjectId);
153
-
154
- const tuple = yield* F.readTuple(mapping.ownership, token.identity),
155
- t = mapping.ownership.tuple;
156
-
157
- if (
158
- tuple.row[t.state] === "Reserved" ||
159
- (tuple.row[t.state] === "Owned" && !mapping.subjectId.equals(tuple.row[t.subjectId], native))
160
- ) {
161
- const claimed = yield* F.exact(mapping, input.claim);
162
-
163
- if (claimed !== undefined) yield* F.terminal(mapping, input.claim, "Conflict", "Unresolved");
164
-
165
- return { _tag: "Conflict" } as const;
166
- }
167
- const co = yield* S.cohort(mapping, cl.id, tuple.key, true);
168
- const original = yield* S.readGrant(mapping, c.moduleId, c.grantId);
169
- const g = mapping.grant;
170
-
171
- const other = yield* owner.read(
172
- g.table,
173
- both(
174
- equal(g.table, {
175
- [g.moduleId]: c.moduleId,
176
- [g.subjectId]: native,
177
- [g.profileKey]: c.profile.key,
178
- [g.activeIdentityKey]: tuple.key,
179
- }),
180
- ne(col(g.table, g.grantId), c.grantId),
181
- ),
182
- { limit: 1 },
183
- );
184
-
185
- const claimed = yield* F.exact(mapping, input.claim);
186
-
187
- if (claimed === undefined) return { _tag: "Rejected" } as const;
188
- invariant(
189
- token.exchangeOrder === input.claim.order &&
190
- validMetadata(token, claimed.now) &&
191
- token.metadata.obtainedAtMillis >= input.claim.claimedAtMillis,
192
- );
193
- const reconnect = c.reconnect;
194
-
195
- const targetOkay =
196
- reconnect === undefined
197
- ? original === undefined
198
- : original !== undefined &&
199
- F.sameTarget(reconnect, original.context) &&
200
- sameIdentity(reconnect.identity, token.identity) &&
201
- original.row[g.refreshWork] !== "Unresolved";
202
-
203
- const previousSafe = yield* C.noFormerOwner(mapping, tuple.key, cl.id, native);
204
-
205
- const active =
206
- out._tag === "Verified" &&
207
- authorized &&
208
- claimed.active &&
209
- c.profile.issuance === "active" &&
210
- token.metadata.useUntilMillis > claimed.now &&
211
- !co.blocked &&
212
- co.generation === token.cohortGeneration &&
213
- S.orderNumber(input.claim.order) > co.cutoff &&
214
- targetOkay &&
215
- other.rows.length === 0 &&
216
- previousSafe;
217
-
218
- if (!active) {
219
- // Foreign historical custody never authorizes a new remote operation.
220
- if (!previousSafe) {
221
- yield* F.terminal(mapping, input.claim, "Conflict", "Unresolved");
222
-
223
- return { _tag: "Conflict" } as const;
224
- }
225
- if (tuple.row[t.state] === "Unowned")
226
- yield* F.acquireTuple(mapping.ownership, token.identity, tuple.key, S.nativeCopy(native));
227
- yield* C.fence(mapping, cl, co);
228
- if (out.cleanup !== undefined) {
229
- yield* C.storeJob(mapping, out.cleanup, token, native, claimed.now);
230
- yield* F.terminal(mapping, input.claim, "Quarantined", "Resolved", { cohort: co.id });
231
- } else
232
- yield* F.terminal(mapping, input.claim, "Quarantined", "Unresolved", {
233
- cohort: co.id,
234
- sealed: grant,
235
- });
236
-
237
- return { _tag: targetOkay && other.rows.length === 0 ? "Rejected" : "Conflict" } as
238
- | { readonly _tag: "Rejected" }
239
- | { readonly _tag: "Conflict" };
240
- }
241
- if (tuple.row[t.state] === "Unowned")
242
- yield* F.acquireTuple(mapping.ownership, token.identity, tuple.key, S.nativeCopy(native));
243
-
244
- yield* activateGrant(mapping, grant, native, claimed.now, original !== undefined);
245
- yield* F.terminal(mapping, input.claim, "Connected", "Resolved", { cohort: co.id });
246
- owner.postconditions.push(
247
- sql`${mapping.clock.engineNowMillis} >= ${claimed.now} and ${mapping.clock.engineNowMillis} < ${Math.min(input.claim.claimExpiresAtMillis, token.metadata.useUntilMillis)}`,
248
- );
249
-
250
- return { _tag: "Connected", grant } as const;
251
- });
@@ -1,443 +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, Schema } from "effect";
5
-
6
- import * as C from "./oauth-connected-custody";
7
- import * as F from "./oauth-connected-flow";
8
- import { activateGrant, validMetadata } from "./oauth-connected-settlement";
9
- import * as S from "./oauth-connected-state";
10
- import { exactClaim } from "./oauth-flow";
11
- import { both, equal, CurrentOAuthTransaction } from "./oauth-owner";
12
- import {
13
- digest,
14
- invariant,
15
- oauthIdentityKey,
16
- sameIdentity,
17
- sameRevision,
18
- storage,
19
- unavailable,
20
- } from "./oauth-state";
21
-
22
- const reservationStorage = storage(M.OAuthSignInAccessClaim);
23
- const configurationStorage = storage(M.OAuthConnectedConfiguration);
24
-
25
- /** Reservation rows share the connected-work ledger, but have no browser
26
- * snapshot or subject. They can never enter the authenticated connect flow. */
27
- export const claimSignIn = Effect.fn("oauthConnected.claimSignIn")(function* (
28
- mapping: S.Mapping,
29
- input: F.Input<"claimSignIn">,
30
- ) {
31
- const owner = yield* CurrentOAuthTransaction;
32
- const signIn = mapping.signIn;
33
- const encode = mapping.flow.encodeSignIn;
34
-
35
- if (signIn === undefined || encode === undefined) return yield* unavailable();
36
- const { claim, configuration } = input;
37
- const c = claim.flow.context;
38
-
39
- invariant(
40
- c.access !== undefined &&
41
- configurationStorage.encode(configuration) ===
42
- configurationStorage.encode({ ...c, profile: c.access }),
43
- );
44
- if (configuration.profile.revocation === "cohort")
45
- invariant(mapping.revocation.mode === "cohort");
46
- const client = yield* S.client(mapping, configuration, true);
47
-
48
- invariant(client !== undefined);
49
- const exact = yield* exactClaim({ ...mapping, flow: signIn.flow }, claim, "sign-in");
50
-
51
- if (exact === undefined) return yield* unavailable();
52
- const f = mapping.flow;
53
- const key = { [f.moduleId]: c.moduleId, [f.flowId]: c.flowId };
54
- const prior = yield* owner.read(f.table, equal(f.table, key), { limit: 1 });
55
-
56
- const command = yield* owner.read(
57
- f.table,
58
- equal(f.table, { [f.moduleId]: c.moduleId, [f.commandId]: c.commandId }),
59
- { limit: 1 },
60
- );
61
-
62
- if (prior.rows.length || command.rows.length) return yield* unavailable();
63
- yield* S.touchScope(mapping, client.scope);
64
- const order = yield* S.nextOrder(mapping, client);
65
-
66
- const reservation = snapshotOAuthSync(M.OAuthSignInAccessClaim, {
67
- claim,
68
- configuration,
69
- order: String(order),
70
- });
71
-
72
- const inserted = yield* owner.insert(
73
- f.table,
74
- {
75
- ...encode(reservation),
76
- ...key,
77
- [f.commandId]: c.commandId,
78
- [f.subjectId]: null,
79
- [f.clientKey]: client.id,
80
- [f.cohortKey]: null,
81
- [f.state]: "Claimed",
82
- [f.version]: owner.marker,
83
- [f.stateDigest]: c.stateDigest,
84
- [f.snapshot]: null,
85
- [f.claimId]: claim.claimId,
86
- [f.claimDigest]: yield* digest(reservationStorage.encode(reservation)),
87
- [f.claimOrder]: mapping.order.encode(order),
88
- [f.claimedAt]: mapping.clock.encodeInstant(claim.claimedAtMillis),
89
- [f.claimExpiresAt]: mapping.clock.encodeInstant(claim.claimExpiresAtMillis),
90
- [f.expiresAt]: mapping.clock.encodeInstant(c.expiresAtMillis),
91
- [f.retentionUntil]: mapping.clock.encodeInstant(claim.flow.retentionUntilMillis),
92
- [f.work]: "Unresolved",
93
- [f.custody]: null,
94
- },
95
- key,
96
- );
97
-
98
- prior.rows = inserted.rows;
99
- command.rows = inserted.rows;
100
-
101
- return reservation;
102
- });
103
-
104
- const exact = Effect.fn("oauthConnected.exactSignIn")(function* (
105
- mapping: S.Mapping,
106
- reservation: M.OAuthSignInAccessClaim,
107
- ) {
108
- const owner = yield* CurrentOAuthTransaction;
109
- const f = mapping.flow;
110
- const c = reservation.claim.flow.context;
111
- const key = { [f.moduleId]: c.moduleId, [f.flowId]: c.flowId };
112
- const read = yield* owner.read(f.table, equal(f.table, key), { limit: 1 });
113
- const row = read.rows[0];
114
-
115
- if (
116
- row === undefined ||
117
- row[f.work] !== "Unresolved" ||
118
- row[f.claimId] !== reservation.claim.claimId ||
119
- row[f.claimDigest] !== (yield* digest(reservationStorage.encode(reservation)))
120
- )
121
- return undefined;
122
- invariant(
123
- row[f.subjectId] === null &&
124
- row[f.commandId] === c.commandId &&
125
- row[f.clientKey] === (yield* S.clientKey(reservation.configuration)) &&
126
- S.nativeOrder(mapping, row[f.claimOrder]) === S.orderNumber(reservation.order),
127
- );
128
- const now = yield* owner.now(mapping.clock);
129
-
130
- invariant(now >= reservation.claim.claimedAtMillis);
131
-
132
- return {
133
- key,
134
- row,
135
- now,
136
- active: row[f.state] === "Claimed" && now < reservation.claim.claimExpiresAtMillis,
137
- };
138
- });
139
-
140
- const credential = Effect.fn("oauthConnected.signInCredential")(function* (
141
- mapping: S.Mapping,
142
- reservation: M.OAuthSignInAccessClaim,
143
- input: M.OAuthCredentialSnapshot,
144
- ) {
145
- const owner = yield* CurrentOAuthTransaction;
146
- const signIn = mapping.signIn;
147
-
148
- if (signIn === undefined) return undefined;
149
- const context = reservation.claim.flow.context;
150
-
151
- if (
152
- input.moduleId !== context.moduleId ||
153
- input.identity.provider !== context.provider ||
154
- input.identity.issuer !== context.issuer
155
- )
156
- return undefined;
157
- const found = yield* S.current(mapping, input.revision.subjectId);
158
-
159
- if (found === undefined || !sameRevision(found.revision, input.revision)) return undefined;
160
- const c = signIn.credential;
161
-
162
- const loginWhere = both(
163
- equal(c.table, {
164
- [c.moduleId]: context.moduleId,
165
- [c.credentialId]: input.credentialId,
166
- [c.subjectId]: found.nativeId,
167
- [c.identityKey]: yield* oauthIdentityKey(input.identity),
168
- [c.credentialRevision]: input.credentialRevision,
169
- }),
170
- c.activeCondition,
171
- );
172
-
173
- const login = yield* owner.read(c.table, loginWhere, { limit: 1 });
174
-
175
- if (login.rows.length !== 1 || !c.isActiveStatus(login.rows[0]![c.status])) return undefined;
176
- const f = signIn.flow;
177
-
178
- const verifiedWhere = equal(f.table, {
179
- [f.moduleId]: context.moduleId,
180
- [f.flowId]: context.flowId,
181
- [f.state]: "Verified",
182
- [f.claimId]: reservation.claim.claimId,
183
- [f.stateDigest]: context.stateDigest,
184
- [f.claimedAt]: mapping.clock.encodeInstant(reservation.claim.claimedAtMillis),
185
- [f.claimExpiresAt]: mapping.clock.encodeInstant(reservation.claim.claimExpiresAtMillis),
186
- });
187
-
188
- const verified = yield* owner.read(f.table, verifiedWhere, { limit: 1 });
189
-
190
- if (verified.rows.length !== 1) return undefined;
191
- owner.postconditions.push(
192
- sql`exists(select 1 from ${c.table} where ${loginWhere})`,
193
- sql`exists(select 1 from ${f.table} where ${verifiedWhere})`,
194
- );
195
- if (
196
- !(yield* S.policy(mapping, {
197
- kind: "sign-in",
198
- subjectId: found.nativeId,
199
- revision: found.revision,
200
- credential: snapshotOAuthSync(M.OAuthCredentialSnapshot, input),
201
- configuration: reservation.configuration,
202
- }))
203
- )
204
- return undefined;
205
-
206
- return found;
207
- });
208
-
209
- const slot = Effect.fn("oauthConnected.signInSlot")(function* (
210
- mapping: S.Mapping,
211
- moduleId: string,
212
- native: unknown,
213
- identityKey: string,
214
- profileKey: string,
215
- ) {
216
- const owner = yield* CurrentOAuthTransaction;
217
- const g = mapping.grant;
218
-
219
- const rows = yield* owner.read(
220
- g.table,
221
- equal(g.table, {
222
- [g.moduleId]: moduleId,
223
- [g.subjectId]: native,
224
- [g.profileKey]: profileKey,
225
- [g.activeIdentityKey]: identityKey,
226
- }),
227
- { limit: 1, admissionOnly: true },
228
- );
229
-
230
- const row = rows.rows[0];
231
-
232
- if (row === undefined) return undefined;
233
-
234
- return yield* S.readGrant(
235
- mapping,
236
- moduleId,
237
- yield* Schema.decodeUnknownEffect(M.OAuthGrantId)(row[g.grantId]).pipe(
238
- Effect.mapError(unavailable),
239
- ),
240
- );
241
- });
242
-
243
- /** Orders belong to one client registration. Moving a profile to another client
244
- * requires a newer profile generation; retired clients cannot replace it later. */
245
- const canReplace = Effect.fnUntraced(function* (
246
- reservation: M.OAuthSignInAccessClaim,
247
- previous: M.OAuthConnectedTokenContext,
248
- ) {
249
- const generation = reservation.configuration.profile.generation;
250
- const oldGeneration = previous.configuration.profile.generation;
251
-
252
- return (yield* S.clientKey(reservation.configuration)) ===
253
- (yield* S.clientKey(previous.configuration))
254
- ? generation >= oldGeneration &&
255
- S.orderNumber(reservation.order) > S.orderNumber(previous.exchangeOrder)
256
- : generation > oldGeneration;
257
- });
258
-
259
- export const inspectSignIn = Effect.fn("oauthConnected.inspectSignIn")(function* (
260
- mapping: S.Mapping,
261
- input: F.Input<"inspectSignIn">,
262
- ) {
263
- const found = yield* credential(mapping, input.reservation, input.credential);
264
-
265
- if (found === undefined) return { _tag: "Rejected" } as const;
266
- const r = input.reservation;
267
- const cl = yield* S.client(mapping, r.configuration, false);
268
-
269
- if (cl === undefined) return { _tag: "Rejected" } as const;
270
- const tuple = yield* F.inspectTuple(mapping, input.credential.identity);
271
- const t = mapping.ownership.tuple;
272
-
273
- if (
274
- tuple.row?.[t.state] !== "Owned" ||
275
- !mapping.subjectId.equals(tuple.row[t.subjectId], found.nativeId)
276
- )
277
- return { _tag: "Rejected" } as const;
278
- const co = yield* S.cohort(mapping, cl.id, tuple.key, false);
279
-
280
- const previous = yield* slot(
281
- mapping,
282
- r.claim.flow.context.moduleId,
283
- found.nativeId,
284
- tuple.key,
285
- r.configuration.profile.key,
286
- );
287
-
288
- const held = yield* exact(mapping, r);
289
-
290
- if (held === undefined) return { _tag: "Rejected" } as const;
291
-
292
- const quarantine =
293
- !held.active ||
294
- co.blocked ||
295
- S.orderNumber(r.order) <= co.cutoff ||
296
- previous?.row[mapping.grant.refreshWork] === "Unresolved" ||
297
- (previous !== undefined && !(yield* canReplace(r, previous.context)));
298
-
299
- return snapshotOAuthSync(M.OAuthSignInAccessInspection, {
300
- _tag: quarantine ? "Quarantine" : "Target",
301
- grantId: previous?.context.grantId ?? input.grantId,
302
- cohortGeneration: co.generation,
303
- ...(previous === undefined
304
- ? {}
305
- : { previous: snapshotOAuthSync(M.OAuthConnectedTarget, previous.context) }),
306
- });
307
- });
308
-
309
- export const settleSignIn = Effect.fn("oauthConnected.settleSignIn")(function* (
310
- mapping: S.Mapping,
311
- input: F.Input<"settleSignIn">,
312
- ) {
313
- const owner = yield* CurrentOAuthTransaction;
314
- const r = input.reservation;
315
- const out = input.outcome;
316
- const found = out._tag === "Verified" ? yield* credential(mapping, r, out.credential) : undefined;
317
- const cl = yield* S.client(mapping, r.configuration, false);
318
-
319
- if (cl === undefined) return { _tag: "Rejected" } as const;
320
- const f = mapping.flow;
321
-
322
- if (out._tag !== "Verified") {
323
- const held = yield* exact(mapping, r);
324
-
325
- if (held === undefined) return { _tag: "Rejected" } as const;
326
- // Unissued means the provider definitely returned no token. Resolve that
327
- // reservation. A Rejected outcome can follow a real exchange, so it stays open.
328
- const state = out._tag === "Unissued" ? "Rejected" : out._tag;
329
-
330
- yield* owner.update(f.table, held.key, {
331
- [f.state]: state,
332
- [f.work]: out._tag === "Cancelled" || out._tag === "Unissued" ? "Resolved" : "Unresolved",
333
- [f.version]: owner.marker,
334
- });
335
-
336
- return { _tag: state };
337
- }
338
- const token = out.grant.context;
339
-
340
- invariant(
341
- token.moduleId === r.claim.flow.context.moduleId &&
342
- token.subjectId === out.credential.revision.subjectId &&
343
- sameIdentity(token.identity, out.credential.identity) &&
344
- configurationStorage.encode(token.configuration) ===
345
- configurationStorage.encode(r.configuration),
346
- );
347
- const native = yield* mapping.subjectId.toNative(token.subjectId);
348
- const tuple = yield* F.readTuple(mapping.ownership, token.identity);
349
- const t = mapping.ownership.tuple;
350
-
351
- const owned =
352
- tuple.row[t.state] === "Owned" && mapping.subjectId.equals(tuple.row[t.subjectId], native);
353
-
354
- const co = yield* S.cohort(mapping, cl.id, tuple.key, true);
355
- const previous = yield* S.readGrant(mapping, token.moduleId, token.grantId);
356
-
357
- const activeSlot = yield* slot(
358
- mapping,
359
- token.moduleId,
360
- native,
361
- tuple.key,
362
- token.configuration.profile.key,
363
- );
364
-
365
- const held = yield* exact(mapping, r);
366
-
367
- if (held === undefined) return { _tag: "Rejected" } as const;
368
- invariant(
369
- token.exchangeOrder === r.order &&
370
- validMetadata(token, held.now) &&
371
- token.metadata.obtainedAtMillis >= r.claim.claimedAtMillis,
372
- );
373
-
374
- const target =
375
- out.previous === undefined
376
- ? previous === undefined
377
- : previous !== undefined &&
378
- F.sameTarget(out.previous, previous.context) &&
379
- sameIdentity(previous.context.identity, token.identity) &&
380
- previous.row[mapping.grant.refreshWork] !== "Unresolved" &&
381
- (yield* canReplace(r, previous.context));
382
-
383
- const previousSafe = yield* C.noFormerOwner(mapping, tuple.key, cl.id, native);
384
-
385
- if (!owned || !previousSafe) {
386
- yield* owner.update(f.table, held.key, { [f.state]: "Conflict", [f.version]: owner.marker });
387
-
388
- return { _tag: "Conflict" } as const;
389
- }
390
- if (
391
- out.quarantine ||
392
- found === undefined ||
393
- !held.active ||
394
- !target ||
395
- (activeSlot !== undefined && activeSlot.context.grantId !== token.grantId) ||
396
- co.blocked ||
397
- S.orderNumber(r.order) <= co.cutoff ||
398
- co.generation !== token.cohortGeneration ||
399
- token.metadata.useUntilMillis <= held.now ||
400
- token.configuration.profile.issuance !== "active"
401
- ) {
402
- if (token.configuration.profile.revocation === "unsupported") {
403
- // No remote cleanup can run for this profile. Discard the losing material;
404
- // it must neither replace nor revoke the currently installed grant.
405
- yield* owner.update(f.table, held.key, {
406
- [f.state]: "Rejected",
407
- [f.work]: "Resolved",
408
- [f.subjectId]: native,
409
- [f.cohortKey]: co.id,
410
- [f.custody]: null,
411
- [f.version]: owner.marker,
412
- });
413
- } else {
414
- yield* C.fence(mapping, cl, co);
415
- if (out.cleanup === undefined) return yield* unavailable();
416
- yield* C.storeJob(mapping, out.cleanup, token, native, held.now);
417
- yield* owner.update(f.table, held.key, {
418
- [f.state]: "Quarantined",
419
- [f.work]: "Resolved",
420
- [f.subjectId]: native,
421
- [f.cohortKey]: co.id,
422
- [f.custody]: null,
423
- [f.version]: owner.marker,
424
- });
425
- }
426
-
427
- return { _tag: "Rejected" } as const;
428
- }
429
- yield* activateGrant(mapping, out.grant, native, held.now, previous !== undefined);
430
- yield* owner.update(f.table, held.key, {
431
- [f.state]: "Connected",
432
- [f.work]: "Resolved",
433
- [f.subjectId]: native,
434
- [f.cohortKey]: co.id,
435
- [f.custody]: null,
436
- [f.version]: owner.marker,
437
- });
438
- owner.postconditions.push(
439
- sql`${mapping.clock.engineNowMillis} >= ${held.now} and ${mapping.clock.engineNowMillis} < ${Math.min(r.claim.claimExpiresAtMillis, token.metadata.useUntilMillis)}`,
440
- );
441
-
442
- return { _tag: "Connected", grant: out.grant } as const;
443
- });