@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,805 +0,0 @@
1
- import {
2
- OAuthAccountRevision,
3
- OAuthActionAuthorization,
4
- OAuthLinkAccess,
5
- OAuthLinkClaim,
6
- OAuthLinkPendingFlow,
7
- OAuthLinkTransactionContext,
8
- OAuthUnlinked,
9
- OAuthCredentialSnapshot,
10
- snapshotOAuthSync,
11
- } from "@yielded/auth/OAuth";
12
- import {
13
- SessionInvalidationWindow,
14
- AuthenticationRequirement,
15
- SecurityRevision,
16
- } from "@yielded/auth/Sessions";
17
- /* oxlint-disable no-explicit-any -- private owner implementation; public mappings retain native IDs. */
18
- import { eq, sql } from "drizzle-orm";
19
- import { DateTime, Effect } from "effect";
20
-
21
- import type { SubjectIdCodec } from "./model";
22
- import {
23
- claimFlow,
24
- credentialStorage,
25
- currentSubject,
26
- exactClaim,
27
- issueFlow,
28
- linkStorage,
29
- matchesAccess,
30
- readFlow,
31
- terminalFlow,
32
- } from "./oauth-flow";
33
- import type { OAuthReferenceGuardDescriptor, OAuthEligibilityFact } from "./oauth-model";
34
- import { both, col, copiedRow, equal, CurrentOAuthTransaction } from "./oauth-owner";
35
- import { acquireTuple, readTuple } from "./oauth-registration";
36
- import {
37
- invariant,
38
- oauthIdentityKey,
39
- sameRevision,
40
- satisfies,
41
- storage,
42
- validAction,
43
- } from "./oauth-state";
44
-
45
- const contextStorage = storage(OAuthLinkTransactionContext);
46
- const unlinkedStorage = storage(OAuthUnlinked);
47
-
48
- export const accountCurrent = (mapping: any, subjectId: OAuthAccountRevision["subjectId"]) =>
49
- Effect.gen(function* () {
50
- const native = yield* (mapping.subjectId as SubjectIdCodec<unknown>).toNative(subjectId);
51
-
52
- return yield* currentSubject(mapping, native);
53
- });
54
-
55
- const authorization = (
56
- mapping: any,
57
- current: any,
58
- auth: OAuthActionAuthorization,
59
- action: OAuthActionAuthorization["challenge"]["action"],
60
- flowId: string,
61
- commandId: string,
62
- intent: string,
63
- maximumAge: number,
64
- ) =>
65
- Effect.flatMap(CurrentOAuthTransaction, (owner) =>
66
- Effect.gen(function* () {
67
- const now = yield* owner.now(mapping.clock);
68
-
69
- const currentRequirement = snapshotOAuthSync(
70
- AuthenticationRequirement,
71
- mapping.subject.decodeActionRequirement(copiedRow(current.row), action),
72
- );
73
-
74
- const accepted = yield* validAction(
75
- auth,
76
- {
77
- moduleId: auth.challenge.moduleId,
78
- action,
79
- flowId,
80
- commandId,
81
- revision: current.revision,
82
- intent,
83
- },
84
- currentRequirement,
85
- now,
86
- maximumAge,
87
- );
88
-
89
- if (!accepted) return false;
90
-
91
- const age = Math.min(
92
- maximumAge,
93
- auth.requirement.maximumAgeMillis,
94
- currentRequirement.maximumAgeMillis,
95
- );
96
-
97
- const fresh = auth.evidence.proofs.filter(
98
- (proof) => now - DateTime.toEpochMillis(proof.verifiedAt) < age,
99
- );
100
-
101
- invariant(fresh.length > 0);
102
-
103
- const deadline = Math.min(
104
- ...fresh.map((proof) => DateTime.toEpochMillis(proof.verifiedAt) + age),
105
- );
106
-
107
- owner.postconditions.push(
108
- sql`${mapping.clock.engineNowMillis} >= ${now} and ${mapping.clock.engineNowMillis} < ${deadline}`,
109
- );
110
-
111
- return true;
112
- }),
113
- );
114
-
115
- const currentLink = (mapping: any, flow: typeof OAuthLinkPendingFlow.Type) =>
116
- Effect.gen(function* () {
117
- const current = yield* accountCurrent(mapping, flow.context.revision.subjectId);
118
-
119
- return current !== undefined && sameRevision(current.revision, flow.context.revision)
120
- ? current
121
- : undefined;
122
- });
123
-
124
- export const issueLink = (mapping: any, input: any) =>
125
- Effect.gen(function* () {
126
- const flow = snapshotOAuthSync(OAuthLinkPendingFlow, input.flow),
127
- c = flow.context;
128
-
129
- const current = yield* currentLink(mapping, flow);
130
- const auth = snapshotOAuthSync(OAuthActionAuthorization, input.authorization);
131
-
132
- if (
133
- current === undefined ||
134
- auth.challenge.moduleId !== c.moduleId ||
135
- !(yield* authorization(
136
- mapping,
137
- current,
138
- auth,
139
- "link-begin",
140
- c.flowId,
141
- c.commandId,
142
- contextStorage.encode(c),
143
- c.maximumEvidenceAgeMillis,
144
- ))
145
- )
146
- return { _tag: "Rejected" } as const;
147
-
148
- return yield* issueFlow(mapping, flow, "link");
149
- });
150
-
151
- export const preflightLink = (mapping: any, original: typeof OAuthLinkAccess.Type) =>
152
- Effect.flatMap(CurrentOAuthTransaction, (owner) =>
153
- Effect.gen(function* () {
154
- const input = snapshotOAuthSync(OAuthLinkAccess, original);
155
- // The authenticated subject is known before discovering/locking any flow.
156
- const current = yield* accountCurrent(mapping, input.subjectId);
157
- const found = yield* readFlow(mapping, input, "link");
158
- const now = yield* owner.now(mapping.clock);
159
-
160
- if (
161
- current === undefined ||
162
- found.flow === undefined ||
163
- found.row?.[mapping.flow.state] !== "Pending" ||
164
- !matchesAccess(found.flow, input)
165
- )
166
- return undefined;
167
-
168
- const flow = snapshotOAuthSync(OAuthLinkPendingFlow, found.flow as never),
169
- c = flow.context;
170
-
171
- if (
172
- c.revision.subjectId !== input.subjectId ||
173
- !sameRevision(current.revision, c.revision) ||
174
- now < c.issuedAtMillis ||
175
- now >= c.expiresAtMillis
176
- )
177
- return undefined;
178
- owner.postconditions.push(
179
- sql`${mapping.clock.engineNowMillis} >= ${c.issuedAtMillis} and ${mapping.clock.engineNowMillis} < ${c.expiresAtMillis}`,
180
- );
181
-
182
- return { current, flow, row: found.row };
183
- }),
184
- );
185
-
186
- export const claimLink = (mapping: any, input: any) =>
187
- Effect.gen(function* () {
188
- const found = yield* preflightLink(mapping, input.access);
189
- const flow = snapshotOAuthSync(OAuthLinkPendingFlow, input.flow);
190
-
191
- const auth = snapshotOAuthSync(OAuthActionAuthorization, input.authorization),
192
- c = flow.context;
193
-
194
- if (
195
- found === undefined ||
196
- linkStorage.encode(found.flow) !== linkStorage.encode(flow) ||
197
- auth.challenge.moduleId !== c.moduleId ||
198
- !(yield* authorization(
199
- mapping,
200
- found.current,
201
- auth,
202
- "link-complete",
203
- c.flowId,
204
- c.commandId,
205
- contextStorage.encode(c),
206
- c.maximumEvidenceAgeMillis,
207
- ))
208
- )
209
- return { _tag: "Rejected" } as const;
210
- const claim = yield* claimFlow(mapping, { row: found.row!, flow }, input.claimId);
211
-
212
- return claim === undefined
213
- ? ({ _tag: "Rejected" } as const)
214
- : ({ _tag: "Claimed", claim } as const);
215
- });
216
-
217
- const checkInvalidation = (mapping: any, original: SessionInvalidationWindow) => {
218
- const value = snapshotOAuthSync(SessionInvalidationWindow, original);
219
-
220
- invariant(
221
- value.trigger === "credential-change" &&
222
- value.oldAuthenticationEvidence === "rejected" &&
223
- (mapping.sessionInvalidation === "same-authority-immediate"
224
- ? value.existingSessions === "immediate" && value.maximumExposureMillis === 0
225
- : value.existingSessions === "original-absolute-expiry"),
226
- );
227
-
228
- return value;
229
- };
230
-
231
- const advance = (mapping: any, current: any, removed?: string) =>
232
- Effect.flatMap(CurrentOAuthTransaction, (owner) =>
233
- Effect.gen(function* () {
234
- const revision = mapping.subject.nextSecurityRevision(current.revision.securityRevision);
235
-
236
- invariant(
237
- typeof revision === "string" &&
238
- revision.length > 0 &&
239
- revision.length <= 256 &&
240
- revision !== current.revision.securityRevision,
241
- );
242
- const subject = mapping.subject;
243
-
244
- yield* owner.update(
245
- subject.table,
246
- { [subject.id]: current.nativeId },
247
- { [subject.securityRevision]: revision },
248
- );
249
- for (const cleanup of mapping.cleanup) {
250
- const condition = both(
251
- eq(col(cleanup.table, cleanup.subjectId), current.nativeId),
252
- cleanup.condition({
253
- subjectId: current.nativeId,
254
- revision: snapshotOAuthSync(OAuthAccountRevision, current.revision),
255
- ...(removed === undefined ? {} : { removedCredentialId: removed }),
256
- }),
257
- );
258
-
259
- yield* owner.write(owner.database.delete(cleanup.table).where(condition));
260
- owner.postconditions.push(
261
- sql`not exists(select 1 from ${cleanup.table} where ${condition})`,
262
- );
263
- }
264
-
265
- return SecurityRevision.make(revision);
266
- }),
267
- );
268
-
269
- export const settleLink = (
270
- mapping: any,
271
- input: any,
272
- allocated: { readonly credentialId: string; readonly revision: SecurityRevision },
273
- ) =>
274
- Effect.flatMap(CurrentOAuthTransaction, (owner) =>
275
- Effect.gen(function* () {
276
- const claim = snapshotOAuthSync(OAuthLinkClaim, input.claim),
277
- flow = claim.flow,
278
- c = flow.context;
279
-
280
- const current = yield* currentLink(mapping, flow);
281
-
282
- // Known subject is always locked before tuple/flow; even a rejected outcome
283
- // settles its exact original claim without adopting a newer revision.
284
- const identity =
285
- input.outcome._tag === "Verified" ? input.outcome.identity.identity : undefined;
286
-
287
- if (identity !== undefined)
288
- invariant(identity.provider === c.provider && identity.issuer === c.issuer);
289
-
290
- const tuple =
291
- identity !== undefined && current !== undefined
292
- ? yield* readTuple(mapping.ownership, identity)
293
- : undefined;
294
-
295
- const exact = yield* exactClaim(mapping, claim, "link");
296
-
297
- if (exact === undefined) return { _tag: "Rejected" } as const;
298
- let decision: any = { _tag: input.outcome._tag };
299
-
300
- if (input.outcome._tag === "Verified") {
301
- decision = { _tag: "Rejected" };
302
- const auth = snapshotOAuthSync(OAuthActionAuthorization, input.authorization);
303
-
304
- if (
305
- current !== undefined &&
306
- tuple !== undefined &&
307
- auth.challenge.moduleId === c.moduleId &&
308
- (yield* authorization(
309
- mapping,
310
- current,
311
- auth,
312
- "link-complete",
313
- c.flowId,
314
- c.commandId,
315
- contextStorage.encode(c),
316
- c.maximumEvidenceAgeMillis,
317
- ))
318
- ) {
319
- const t = mapping.ownership.tuple,
320
- cr = mapping.credential,
321
- a = mapping.authority;
322
-
323
- const login = yield* owner.read(cr.table, eq(col(cr.table, cr.identityKey), tuple.key), {
324
- limit: 1,
325
- });
326
-
327
- const previous = login.rows[0];
328
-
329
- if (
330
- tuple.row[t.state] === "Reserved" ||
331
- (tuple.row[t.state] === "Owned" &&
332
- !mapping.subjectId.equals(tuple.row[t.subjectId], current.nativeId)) ||
333
- (previous !== undefined &&
334
- (previous[cr.moduleId] !== c.moduleId ||
335
- !mapping.subjectId.equals(previous[cr.subjectId], current.nativeId) ||
336
- !cr.isActiveStatus(previous[cr.status])))
337
- )
338
- decision = { _tag: "Conflict" };
339
- else if (previous !== undefined) {
340
- const shared = current.revision.credentials.find(
341
- (item: any) => item.credentialId === previous[cr.credentialId],
342
- );
343
-
344
- if (
345
- tuple.row[t.state] !== "Owned" ||
346
- shared?.revision !== previous[cr.credentialRevision] ||
347
- !(yield* owner.check(
348
- sql`exists(select 1 from ${cr.table} where ${both(eq(col(cr.table, cr.identityKey), tuple.key), cr.activeCondition)})`,
349
- ))
350
- )
351
- decision = { _tag: "Conflict" };
352
- else
353
- decision = {
354
- _tag: "Linked",
355
- changed: false,
356
- credential: snapshotOAuthSync(OAuthCredentialSnapshot, {
357
- moduleId: c.moduleId,
358
- identity,
359
- credentialId: previous[cr.credentialId],
360
- credentialRevision: previous[cr.credentialRevision],
361
- revision: current.revision,
362
- }),
363
- };
364
- } else {
365
- checkInvalidation(mapping, input.invalidation);
366
-
367
- const values = {
368
- ...cr.encodeInsert({
369
- moduleId: c.moduleId,
370
- subjectId: current.nativeId,
371
- identityKey: tuple.key,
372
- credentialId: allocated.credentialId,
373
- credentialRevision: allocated.revision,
374
- }),
375
- [cr.moduleId]: c.moduleId,
376
- [cr.subjectId]: current.nativeId,
377
- [cr.identityKey]: tuple.key,
378
- [cr.credentialId]: allocated.credentialId,
379
- [cr.credentialRevision]: allocated.revision,
380
- };
381
-
382
- invariant(cr.isActiveStatus(values[cr.status]));
383
-
384
- const inserted = yield* owner.insert(cr.table, values, {
385
- [cr.credentialId]: allocated.credentialId,
386
- });
387
-
388
- login.rows = inserted.rows;
389
-
390
- const sharedValues = {
391
- ...a.encodeInsert({
392
- subjectId: current.nativeId,
393
- credentialId: allocated.credentialId,
394
- revision: allocated.revision,
395
- }),
396
- [a.subjectId]: current.nativeId,
397
- [a.credentialId]: allocated.credentialId,
398
- [a.revision]: allocated.revision,
399
- };
400
-
401
- invariant(a.isActiveStatus(sharedValues[a.status]));
402
-
403
- const shared = yield* owner.insert(a.table, sharedValues, {
404
- [a.subjectId]: current.nativeId,
405
- [a.credentialId]: allocated.credentialId,
406
- });
407
-
408
- current.authorities.rows = [...current.authorities.rows, ...shared.rows];
409
- owner.postconditions.push(
410
- sql`exists(select 1 from ${cr.table} where ${both(
411
- eq(col(cr.table, cr.credentialId), allocated.credentialId),
412
- cr.activeCondition,
413
- )})`,
414
- sql`exists(select 1 from ${a.table} where ${both(
415
- eq(col(a.table, a.subjectId), current.nativeId),
416
- eq(col(a.table, a.credentialId), allocated.credentialId),
417
- a.activeCondition,
418
- )})`,
419
- );
420
- if (tuple.row[t.state] === "Unowned")
421
- yield* acquireTuple(mapping.ownership, identity, tuple.key, current.nativeId);
422
- const next = yield* advance(mapping, current);
423
-
424
- const revision = snapshotOAuthSync(OAuthAccountRevision, {
425
- ...current.revision,
426
- securityRevision: next,
427
- credentials: [
428
- ...current.revision.credentials,
429
- { credentialId: allocated.credentialId, revision: allocated.revision },
430
- ],
431
- });
432
-
433
- decision = {
434
- _tag: "Linked",
435
- changed: true,
436
- credential: snapshotOAuthSync(OAuthCredentialSnapshot, {
437
- moduleId: c.moduleId,
438
- identity,
439
- credentialId: allocated.credentialId,
440
- credentialRevision: allocated.revision as never,
441
- revision,
442
- }),
443
- };
444
- }
445
- }
446
- }
447
- yield* terminalFlow(mapping, flow, decision._tag);
448
-
449
- return decision;
450
- }),
451
- );
452
-
453
- const targetCredential = (mapping: any, moduleId: string, credentialId: string, current: any) =>
454
- Effect.flatMap(CurrentOAuthTransaction, (owner) =>
455
- Effect.gen(function* () {
456
- const cr = mapping.credential;
457
-
458
- const found = yield* owner.read(cr.table, eq(col(cr.table, cr.credentialId), credentialId), {
459
- limit: 1,
460
- });
461
-
462
- const row = found.rows[0];
463
-
464
- if (
465
- row === undefined ||
466
- row[cr.moduleId] !== moduleId ||
467
- !mapping.subjectId.equals(row[cr.subjectId], current.nativeId) ||
468
- !cr.isActiveStatus(row[cr.status])
469
- )
470
- return undefined;
471
- const t = mapping.ownership.tuple;
472
-
473
- const tuple = yield* owner.read(
474
- t.table,
475
- eq(col(t.table, t.identityKey), row[cr.identityKey]),
476
- {
477
- limit: 1,
478
- },
479
- );
480
-
481
- const ownership = tuple.rows[0];
482
-
483
- if (
484
- ownership === undefined ||
485
- ownership[t.state] !== "Owned" ||
486
- !mapping.subjectId.equals(ownership[t.subjectId], current.nativeId)
487
- )
488
- return undefined;
489
-
490
- const identity = {
491
- provider: ownership[t.provider],
492
- issuer: ownership[t.issuer],
493
- subject: ownership[t.externalSubject],
494
- };
495
-
496
- invariant((yield* oauthIdentityKey(identity)) === row[cr.identityKey]);
497
-
498
- const shared = current.revision.credentials.find(
499
- (item: any) => item.credentialId === credentialId,
500
- );
501
-
502
- if (
503
- shared?.revision !== row[cr.credentialRevision] ||
504
- !(yield* owner.check(
505
- sql`exists(select 1 from ${cr.table} where ${both(eq(col(cr.table, cr.credentialId), credentialId), cr.activeCondition)})`,
506
- ))
507
- )
508
- return undefined;
509
-
510
- return snapshotOAuthSync(OAuthCredentialSnapshot, {
511
- moduleId: moduleId as never,
512
- identity,
513
- credentialId,
514
- credentialRevision: row[cr.credentialRevision],
515
- revision: current.revision,
516
- });
517
- }),
518
- );
519
-
520
- const unlinkCommand = (mapping: any, moduleId: string, commandId: string) =>
521
- Effect.flatMap(CurrentOAuthTransaction, (owner) =>
522
- owner.read(
523
- mapping.command.table,
524
- equal(mapping.command.table, {
525
- [mapping.command.moduleId]: moduleId,
526
- [mapping.command.commandId]: commandId,
527
- }),
528
- { limit: 1 },
529
- ),
530
- );
531
-
532
- export const inspectUnlink = (mapping: any, input: any) =>
533
- Effect.flatMap(CurrentOAuthTransaction, (owner) =>
534
- Effect.gen(function* () {
535
- const current = yield* accountCurrent(mapping, input.subjectId);
536
-
537
- if (current === undefined) return { _tag: "Rejected" } as const;
538
-
539
- const command = yield* unlinkCommand(mapping, input.moduleId, input.commandId),
540
- r = mapping.command;
541
-
542
- const old = command.rows[0];
543
-
544
- if (old !== undefined) {
545
- const intent = credentialStorage.decode(old[r.intentSnapshot]);
546
-
547
- if (
548
- intent.moduleId !== input.moduleId ||
549
- intent.revision.subjectId !== input.subjectId ||
550
- intent.credentialId !== input.credentialId ||
551
- !mapping.subjectId.equals(old[r.subjectId], current.nativeId) ||
552
- old[r.credentialId] !== input.credentialId
553
- )
554
- return { _tag: "Conflict" } as const;
555
- const metadata = mapping.metadata.condition(current.nativeId);
556
-
557
- if (!(yield* owner.check(metadata))) return { _tag: "Rejected" } as const;
558
- owner.postconditions.push(metadata);
559
-
560
- return { _tag: "Replay", result: unlinkedStorage.decode(old[r.decision]) } as const;
561
- }
562
-
563
- const credential = yield* targetCredential(
564
- mapping,
565
- input.moduleId,
566
- input.credentialId,
567
- current,
568
- );
569
-
570
- return credential === undefined
571
- ? ({ _tag: "Rejected" } as const)
572
- : ({ _tag: "Target", credential } as const);
573
- }),
574
- );
575
-
576
- const remainingEligibility = (mapping: any, current: any, removed: string) =>
577
- Effect.flatMap(CurrentOAuthTransaction, (owner) =>
578
- Effect.gen(function* () {
579
- invariant(mapping.eligibility.length > 0 && mapping.eligibility.length <= 32);
580
- const facts = new Map<string, OAuthEligibilityFact>();
581
-
582
- const revisions = new Map(
583
- current.revision.credentials.map((item: any) => [item.credentialId, item.revision]),
584
- );
585
-
586
- for (const descriptor of mapping.eligibility) {
587
- const observed = yield* owner.read(
588
- descriptor.table,
589
- both(
590
- eq(col(descriptor.table, descriptor.subjectId), current.nativeId),
591
- descriptor.condition(current.nativeId),
592
- ),
593
- { orderBy: col(descriptor.table, descriptor.credentialId) },
594
- );
595
-
596
- for (const row of observed.rows) {
597
- const fact = descriptor.decode(copiedRow(row)) as OAuthEligibilityFact | undefined;
598
-
599
- if (fact === undefined || fact.credentialId === removed) continue;
600
- invariant(
601
- fact.credentialId === row[descriptor.credentialId] &&
602
- fact.revision === row[descriptor.revision] &&
603
- revisions.get(fact.credentialId) === fact.revision &&
604
- fact.factors.length <= 8 &&
605
- fact.factors.every((factor) =>
606
- ["knowledge", "possession", "inherence"].includes(factor),
607
- ),
608
- );
609
- invariant(
610
- typeof fact.usablePrimary === "boolean" &&
611
- typeof fact.userVerified === "boolean" &&
612
- typeof fact.phishingResistant === "boolean",
613
- );
614
-
615
- const normalized = {
616
- credentialId: fact.credentialId,
617
- revision: fact.revision,
618
- usablePrimary: fact.usablePrimary,
619
- factors: [...new Set(fact.factors)].sort(),
620
- userVerified: fact.userVerified,
621
- phishingResistant: fact.phishingResistant,
622
- };
623
-
624
- const prior = facts.get(fact.credentialId);
625
-
626
- // oxlint-disable-next-line no-restricted-properties -- Canonical bounded eligibility fact fingerprint rejects contradictory duplicate IDs.
627
- invariant(prior === undefined || JSON.stringify(prior) === JSON.stringify(normalized));
628
- facts.set(fact.credentialId, normalized);
629
- }
630
- }
631
- const available = [...facts.values()];
632
-
633
- const requirement = snapshotOAuthSync(
634
- AuthenticationRequirement,
635
- mapping.subject.decodeAuthenticationRequirement(copiedRow(current.row)),
636
- );
637
-
638
- return available.some((fact) => fact.usablePrimary) && satisfies(available, requirement);
639
- }),
640
- );
641
-
642
- export const unlink = (mapping: any, input: any) =>
643
- Effect.flatMap(CurrentOAuthTransaction, (owner) =>
644
- Effect.gen(function* () {
645
- const expected = snapshotOAuthSync(OAuthCredentialSnapshot, input.credential);
646
-
647
- invariant(expected.moduleId === input.moduleId);
648
- const current = yield* accountCurrent(mapping, expected.revision.subjectId);
649
-
650
- if (current === undefined) return { _tag: "Rejected" } as const;
651
-
652
- const guards: ReadonlyArray<OAuthReferenceGuardDescriptor<unknown>> =
653
- mapping.connectedReferenceGuards ?? [];
654
-
655
- invariant(guards.length <= 32);
656
- let referencesGuarded = mapping.connectedReference !== undefined && guards.length > 0;
657
-
658
- if (mapping.connectedReference !== undefined)
659
- for (const guard of guards) {
660
- const condition = guard.condition({
661
- identity: snapshotOAuthSync(OAuthCredentialSnapshot.fields.identity, expected.identity),
662
- identityKey: yield* oauthIdentityKey(expected.identity),
663
- subjectId: copiedRow({ value: current.nativeId }).value,
664
- });
665
-
666
- const read = yield* owner.read(
667
- guard.table,
668
- Effect.isEffect(condition) ? yield* condition : condition,
669
- { orderBy: col(guard.table, guard.orderBy) },
670
- );
671
-
672
- if (read.rows.length === 0) referencesGuarded = false;
673
- }
674
-
675
- const command = yield* unlinkCommand(mapping, input.moduleId, input.commandId),
676
- r = mapping.command;
677
-
678
- const previous = command.rows[0];
679
-
680
- if (previous !== undefined) {
681
- if (
682
- previous[r.intentSnapshot] !== credentialStorage.encode(expected) ||
683
- previous[r.credentialId] !== expected.credentialId ||
684
- !mapping.subjectId.equals(previous[r.subjectId], current.nativeId)
685
- )
686
- return { _tag: "Conflict" } as const;
687
- const metadata = mapping.metadata.condition(current.nativeId);
688
-
689
- if (!(yield* owner.check(metadata))) return { _tag: "Rejected" } as const;
690
- owner.postconditions.push(metadata);
691
-
692
- return {
693
- _tag: "Unlinked",
694
- replayed: true,
695
- result: unlinkedStorage.decode(previous[r.decision]),
696
- } as const;
697
- }
698
-
699
- const actual = yield* targetCredential(
700
- mapping,
701
- input.moduleId,
702
- expected.credentialId,
703
- current,
704
- );
705
-
706
- if (
707
- actual === undefined ||
708
- credentialStorage.encode(actual) !== credentialStorage.encode(expected)
709
- )
710
- return { _tag: "Rejected" } as const;
711
- const auth = snapshotOAuthSync(OAuthActionAuthorization, input.authorization);
712
-
713
- if (
714
- auth.challenge.moduleId !== input.moduleId ||
715
- !(yield* authorization(
716
- mapping,
717
- current,
718
- auth,
719
- "unlink",
720
- input.commandId,
721
- input.commandId,
722
- credentialStorage.encode(expected),
723
- auth.requirement.maximumAgeMillis,
724
- ))
725
- )
726
- return { _tag: "Rejected" } as const;
727
- if (!(yield* remainingEligibility(mapping, current, expected.credentialId)))
728
- return { _tag: "LastSignInMethod" } as const;
729
-
730
- const invalidation = checkInvalidation(mapping, input.invalidation),
731
- cr = mapping.credential,
732
- a = mapping.authority;
733
-
734
- const key = yield* oauthIdentityKey(expected.identity),
735
- t = mapping.ownership.tuple;
736
-
737
- const connected = mapping.connectedReference?.({
738
- identityKey: key,
739
- subjectId: copiedRow({ value: current.nativeId }).value,
740
- });
741
-
742
- const retain =
743
- connected !== undefined && (!referencesGuarded || (yield* owner.check(connected)));
744
-
745
- if (connected !== undefined && referencesGuarded)
746
- owner.postconditions.push(retain ? connected : sql`not (${connected})`);
747
- yield* owner.remove(cr.table, { [cr.credentialId]: expected.credentialId });
748
- yield* owner.remove(a.table, {
749
- [a.subjectId]: current.nativeId,
750
- [a.credentialId]: expected.credentialId,
751
- });
752
- if (!retain) {
753
- yield* owner.update(
754
- t.table,
755
- { [t.identityKey]: key },
756
- {
757
- [t.state]: "Unowned",
758
- [t.subjectId]: null,
759
- [t.reservation]: null,
760
- [t.version]: owner.marker,
761
- },
762
- );
763
- if (mapping.ownership.mode === "separate")
764
- yield* owner.remove(mapping.ownership.external.table, {
765
- [mapping.ownership.external.identityKey]: key,
766
- });
767
- }
768
- yield* advance(mapping, current, expected.credentialId);
769
-
770
- const result = snapshotOAuthSync(OAuthUnlinked, {
771
- _tag: "Unlinked",
772
- credentialId: expected.credentialId,
773
- invalidation,
774
- });
775
-
776
- const now = yield* owner.now(mapping.clock);
777
-
778
- invariant(
779
- Number.isSafeInteger(input.retentionUntilMillis) &&
780
- input.retentionUntilMillis > now &&
781
- input.retentionUntilMillis >= input.nowMillis + 120000,
782
- );
783
- const commandKey = { [r.moduleId]: input.moduleId, [r.commandId]: input.commandId };
784
-
785
- const values = {
786
- ...r.encodeInsert({
787
- moduleId: input.moduleId,
788
- commandId: input.commandId,
789
- credential: snapshotOAuthSync(OAuthCredentialSnapshot, expected),
790
- }),
791
- ...commandKey,
792
- [r.subjectId]: current.nativeId,
793
- [r.credentialId]: expected.credentialId,
794
- [r.intentSnapshot]: credentialStorage.encode(expected),
795
- [r.decision]: unlinkedStorage.encode(result),
796
- [r.retentionUntil]: mapping.clock.encodeInstant(input.retentionUntilMillis),
797
- };
798
-
799
- const inserted = yield* owner.insert(r.table, values, commandKey);
800
-
801
- command.rows = inserted.rows;
802
-
803
- return { _tag: "Unlinked", result, replayed: false } as const;
804
- }),
805
- );