@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,872 +0,0 @@
1
- import { type OAuthAccountRevision, snapshotOAuthSync } from "@yielded/auth/OAuth";
2
- import * as M from "@yielded/auth/OAuth";
3
- import { AuthenticationRequirement } from "@yielded/auth/Sessions";
4
- /* oxlint-disable no-explicit-any -- private table erasure retains declared callback error/requirements channels. */
5
- import { and, eq, isNull, sql, type SQL, type Table } from "drizzle-orm";
6
- import type { EffectDrizzleQueryError } from "drizzle-orm/effect-core";
7
- import { DateTime, Effect, Schema } from "effect";
8
-
9
- import type {
10
- OAuthConnectedAuthorityMapping,
11
- OAuthConnectedClientRegistrationTable,
12
- OAuthConnectedCohortTable,
13
- OAuthConnectedMapping,
14
- OAuthConnectedPolicyInput,
15
- OAuthConnectedRevocationMapping,
16
- } from "./oauth-connected-model";
17
- import { currentSubject } from "./oauth-flow";
18
- import {
19
- both,
20
- col,
21
- copiedRow,
22
- equal,
23
- CurrentOAuthTransaction,
24
- type OAuthOwner,
25
- type Row,
26
- } from "./oauth-owner";
27
- import {
28
- digest,
29
- invariant,
30
- oauthIdentityKey,
31
- sameRevision,
32
- satisfies,
33
- storage,
34
- unavailable,
35
- } from "./oauth-state";
36
-
37
- export type Authority = OAuthConnectedAuthorityMapping<any, any, any, any, any, any, any> & {
38
- readonly client: Partial<Pick<OAuthConnectedClientRegistrationTable<any>, "encodeInsert">>;
39
- readonly cohort: Partial<Pick<OAuthConnectedCohortTable<any>, "encodeInsert">>;
40
- };
41
-
42
- export type Mapping = OAuthConnectedMapping<
43
- any,
44
- any,
45
- any,
46
- any,
47
- any,
48
- any,
49
- any,
50
- any,
51
- any,
52
- any,
53
- any,
54
- any
55
- >;
56
-
57
- export type Revocations = OAuthConnectedRevocationMapping<any, any, any, any, any, any, any, any>;
58
- export type Current = NonNullable<Effect.Success<ReturnType<typeof currentSubject>>>;
59
- export const contextStorage = storage(M.OAuthConnectedTransactionContext);
60
- export const flowStorage = storage(M.OAuthConnectedPendingFlow);
61
- export const claimStorage = storage(M.OAuthConnectedClaim);
62
- export const tokenContextStorage = storage(M.OAuthConnectedTokenContext);
63
- export const sealedStorage = storage(M.OAuthConnectedSealedTokens);
64
- export const grantStorage = storage(M.OAuthConnectedStoredGrant);
65
-
66
- export const admissionStorage = storage(
67
- Schema.Struct({
68
- grant: M.OAuthConnectedStoredGrant,
69
- authorization: M.OAuthConnectedUseAuthorization,
70
- }),
71
- );
72
-
73
- export const commandStorage = storage(
74
- Schema.Struct({
75
- context: M.OAuthConnectedTokenContext,
76
- originalDigest: Schema.String.check(Schema.isPattern(/^[A-Za-z0-9_-]{43}$/)),
77
- revocationJobId: Schema.optionalKey(Schema.String.check(Schema.isMaxLength(256))),
78
- }),
79
- );
80
-
81
- export const disconnectStorage = storage(M.OAuthConnectedDisconnectGrant);
82
- export const refreshStorage = storage(M.OAuthConnectedRefreshClaim);
83
- export const jobStorage = storage(M.OAuthConnectedRevocationJob);
84
- export const summaryStorage = storage(M.OAuthConnectedSummary);
85
- export const disconnectedStorage = storage(M.OAuthConnectedDisconnected);
86
- const strings = storage(Schema.Array(Schema.String));
87
-
88
- const safeInteger = Schema.Int.check(
89
- Schema.isBetween({ minimum: 0, maximum: Number.MAX_SAFE_INTEGER }),
90
- );
91
-
92
- const orderParser = Schema.decodeSync(safeInteger);
93
- const canonicalOrder = Schema.String.check(Schema.isPattern(/^(0|[1-9][0-9]{0,15})$/));
94
- const orderTextParser = Schema.decodeSync(canonicalOrder);
95
-
96
- export const orderNumber = (value: string) => {
97
- const number = orderParser(Number(orderTextParser(value)));
98
-
99
- invariant(String(number) === value);
100
-
101
- return number;
102
- };
103
-
104
- export const nativeOrder = (mapping: Authority, value: unknown) =>
105
- orderParser(mapping.order.decode(value));
106
-
107
- const encoder = new TextEncoder();
108
- const decoder = new TextDecoder("utf-8", { fatal: true });
109
-
110
- const key = Effect.fnUntraced(function* (domain: string, fields: ReadonlyArray<string>) {
111
- for (const value of fields) invariant(decoder.decode(encoder.encode(value)) === value);
112
-
113
- return "v1:" + (yield* digest(strings.encode([domain, ...fields])));
114
- });
115
-
116
- export const clientKey = (configuration: M.OAuthConnectedConfiguration) =>
117
- key("effect-auth/oauth-connected-client/v1", [
118
- configuration.provider,
119
- configuration.issuer,
120
- configuration.profile.clientRegistrationId,
121
- ]);
122
-
123
- export const cohortKey = (client: string, identity: string) =>
124
- key("effect-auth/oauth-connected-cohort/v1", [client, identity]);
125
-
126
- export const initialGeneration = (cohort: string) =>
127
- digest(cohort).pipe(
128
- Effect.map((value) => M.OAuthConnectedTarget.fields.cohortGeneration.make("initial:" + value)),
129
- );
130
-
131
- export const configuration = (flow: M.OAuthConnectedPendingFlow): M.OAuthConnectedConfiguration =>
132
- snapshotOAuthSync(M.OAuthConnectedConfiguration, flow.context);
133
-
134
- export const retainedUntil = (mapping: Authority, now: number) => {
135
- invariant(
136
- Number.isSafeInteger(mapping.retentionMillis) &&
137
- mapping.retentionMillis >= 120000 &&
138
- mapping.retentionMillis <= 2592000000,
139
- );
140
-
141
- return orderParser(now + mapping.retentionMillis);
142
- };
143
-
144
- export const rowCount = Effect.fn("oauthConnected.rowCount")(function* (table: Table, where: SQL) {
145
- const owner = yield* CurrentOAuthTransaction;
146
-
147
- const rows = yield* (
148
- owner.database
149
- .select({ value: sql<number>`count(*)`.mapWith(Number) })
150
- .from(table)
151
- .where(where) as Effect.Effect<ReadonlyArray<{ value: number }>, EffectDrizzleQueryError>
152
- ).pipe(Effect.mapError(unavailable));
153
-
154
- invariant(rows.length === 1);
155
-
156
- return orderParser(rows[0]!.value);
157
- });
158
-
159
- export const countCondition = (table: Table, where: SQL, count: number) =>
160
- sql`(select count(*) from ${table} where ${where}) = ${count}`;
161
-
162
- export const current = Effect.fn("oauthConnected.current")(function* (
163
- mapping: Mapping,
164
- subject: OAuthAccountRevision["subjectId"],
165
- ) {
166
- const native = yield* mapping.subjectId.toNative(subject);
167
-
168
- return yield* currentSubject(mapping, native);
169
- });
170
-
171
- export const nativeCopy = <N>(value: N): N => copiedRow({ value }).value as N;
172
-
173
- const policyData = (input: OAuthConnectedPolicyInput<any>): OAuthConnectedPolicyInput<any> =>
174
- Object.freeze({
175
- ...input,
176
- subjectId: nativeCopy(input.subjectId),
177
- revision: snapshotOAuthSync(M.OAuthConnectedUseAuthorization.fields.revision, input.revision),
178
- ...(input.kind === "sign-in"
179
- ? {
180
- credential: snapshotOAuthSync(M.OAuthCredentialSnapshot, input.credential),
181
- configuration: snapshotOAuthSync(M.OAuthConnectedConfiguration, input.configuration),
182
- }
183
- : {
184
- authorization:
185
- input.kind === "action"
186
- ? snapshotOAuthSync(M.OAuthConnectedActionAuthorization, input.authorization)
187
- : snapshotOAuthSync(M.OAuthConnectedUseAuthorization, input.authorization),
188
- ...(input.kind === "action"
189
- ? {
190
- configuration: snapshotOAuthSync(
191
- M.OAuthConnectedConfiguration,
192
- input.configuration,
193
- ),
194
- }
195
- : {}),
196
- }),
197
- ...(input.grant === undefined
198
- ? {}
199
- : { grant: snapshotOAuthSync(M.OAuthConnectedTokenContext, input.grant) }),
200
- }) as OAuthConnectedPolicyInput<any>;
201
-
202
- export const jobTable = (mapping: Authority) => {
203
- const direct = (mapping as Partial<Revocations>).job;
204
- const configured = (mapping as Partial<Mapping>).revocation;
205
-
206
- return direct ?? (configured?.mode === "cohort" ? configured.job : undefined);
207
- };
208
-
209
- export const policy = Effect.fn("oauthConnected.policy")(function* (
210
- mapping: Mapping,
211
- input: OAuthConnectedPolicyInput<any>,
212
- ) {
213
- const owner = yield* CurrentOAuthTransaction;
214
-
215
- invariant((mapping.policy.guards?.length ?? 0) <= 32);
216
- for (const guard of mapping.policy.guards ?? []) {
217
- const read = yield* owner.read(guard.table, guard.condition(policyData(input)), {
218
- orderBy: col(guard.table, guard.orderBy),
219
- });
220
-
221
- invariant(read.rows.length > 0);
222
- }
223
- const condition = mapping.policy.condition(policyData(input));
224
- const accepted = yield* owner.check(condition);
225
-
226
- if (accepted) owner.postconditions.push(condition);
227
-
228
- return accepted;
229
- });
230
-
231
- export const action = Effect.fn("oauthConnected.action")(function* (
232
- mapping: Mapping,
233
- found: Current,
234
- authorization: M.OAuthConnectedActionAuthorization,
235
- operation: "issue" | "claim" | "settle" | "disconnect",
236
- expected: {
237
- readonly moduleId: string;
238
- readonly flowId: string;
239
- readonly commandId: string;
240
- readonly revision: OAuthAccountRevision;
241
- readonly intent: string;
242
- readonly maximumAgeMillis: number;
243
- readonly configuration: M.OAuthConnectedConfiguration;
244
- readonly grant?: M.OAuthConnectedTokenContext;
245
- },
246
- ) {
247
- const owner = yield* CurrentOAuthTransaction;
248
-
249
- const challenge = authorization.challenge,
250
- evidence = authorization.evidence;
251
-
252
- const name =
253
- operation === "issue"
254
- ? "connected-begin"
255
- : operation === "disconnect"
256
- ? "connected-disconnect"
257
- : "connected-complete";
258
-
259
- const intentDigest = yield* digest(expected.intent);
260
-
261
- const binding = yield* digest(
262
- strings.encode([
263
- "effect-auth/oauth-connected-action/v1",
264
- expected.moduleId,
265
- name,
266
- expected.flowId,
267
- expected.commandId,
268
- intentDigest,
269
- ]),
270
- );
271
-
272
- if (
273
- challenge.moduleId !== expected.moduleId ||
274
- challenge.action !== name ||
275
- challenge.flowId !== expected.flowId ||
276
- challenge.commandId !== expected.commandId ||
277
- challenge.intentDigest !== intentDigest ||
278
- challenge.bindingDigest !== binding ||
279
- evidence.flowId !== expected.flowId ||
280
- evidence.bindingDigest !== binding ||
281
- !sameRevision(challenge.revision, expected.revision) ||
282
- !sameRevision(evidence.revision, expected.revision) ||
283
- !sameRevision(found.revision, expected.revision)
284
- )
285
- return false;
286
- const now = yield* owner.now(mapping.clock);
287
-
288
- const requirement = snapshotOAuthSync(
289
- AuthenticationRequirement,
290
- mapping.subject.decodeActionRequirement(copiedRow(found.row), name),
291
- );
292
-
293
- const revisions = new Map(
294
- found.revision.credentials.map((item) => [item.credentialId, item.revision]),
295
- );
296
-
297
- if (
298
- evidence.proofs.some(
299
- (proof) =>
300
- !revisions.has(proof.credentialId) || DateTime.toEpochMillis(proof.verifiedAt) > now,
301
- )
302
- )
303
- return false;
304
- for (const required of [authorization.requirement, requirement]) {
305
- const age = Math.min(expected.maximumAgeMillis, required.maximumAgeMillis);
306
-
307
- const fresh = evidence.proofs.filter(
308
- (proof) => now - DateTime.toEpochMillis(proof.verifiedAt) < age,
309
- );
310
-
311
- if (!satisfies(fresh, required)) return false;
312
- owner.postconditions.push(
313
- sql`${mapping.clock.engineNowMillis} >= ${now} and ${both(...fresh.map((proof) => sql`${mapping.clock.engineNowMillis} < ${DateTime.toEpochMillis(proof.verifiedAt) + age}`))}`,
314
- );
315
- }
316
-
317
- return yield* policy(mapping, {
318
- subjectId: found.nativeId,
319
- revision: found.revision,
320
- kind: "action",
321
- operation,
322
- authorization,
323
- configuration: expected.configuration,
324
- ...(expected.grant === undefined ? {} : { grant: expected.grant }),
325
- });
326
- });
327
-
328
- export const useAuthority = Effect.fn("oauthConnected.useAuthority")(function* (
329
- mapping: Mapping,
330
- authorization: M.OAuthConnectedUseAuthorization,
331
- kind: "metadata" | "use",
332
- grant?: M.OAuthConnectedTokenContext,
333
- ) {
334
- const owner = yield* CurrentOAuthTransaction;
335
- const found = yield* current(mapping, authorization.revision.subjectId);
336
- const now = yield* owner.now(mapping.clock);
337
-
338
- if (
339
- found === undefined ||
340
- authorization.purpose !== kind ||
341
- !sameRevision(found.revision, authorization.revision) ||
342
- now >= authorization.expiresAtMillis ||
343
- (grant !== undefined &&
344
- (grant.moduleId !== authorization.moduleId ||
345
- grant.subjectId !== authorization.revision.subjectId ||
346
- (authorization.grantId !== undefined && authorization.grantId !== grant.grantId) ||
347
- (authorization.profileKey !== undefined &&
348
- authorization.profileKey !== grant.configuration.profile.key)))
349
- )
350
- return undefined;
351
- if (
352
- !(yield* policy(mapping, {
353
- subjectId: found.nativeId,
354
- revision: found.revision,
355
- kind,
356
- authorization,
357
- ...(grant === undefined ? {} : { grant }),
358
- }))
359
- )
360
- return undefined;
361
- owner.postconditions.push(
362
- sql`${mapping.clock.engineNowMillis} >= ${now} and ${mapping.clock.engineNowMillis} < ${authorization.expiresAtMillis}`,
363
- );
364
-
365
- return found;
366
- });
367
-
368
- export const scopeKey = (provider: string, issuer: string) =>
369
- key("effect-auth/oauth-connected-provider-issuer/v1", [provider, issuer]).pipe(
370
- Effect.map((value) => "scope:" + value),
371
- );
372
-
373
- export interface ScopeAnchor {
374
- readonly id: string;
375
- row: Row;
376
- }
377
-
378
- export interface ClientAnchor {
379
- readonly id: string;
380
- row: Row;
381
- counter: number;
382
- readonly scope: ScopeAnchor;
383
- }
384
-
385
- const scopeCache = new WeakMap<OAuthOwner, Map<Table, Map<string, ScopeAnchor>>>();
386
- const clientCache = new WeakMap<OAuthOwner, Map<Table, Map<string, ClientAnchor>>>();
387
-
388
- const anchors = Effect.fn("oauthConnected.anchors")(function* <A>(
389
- cache: WeakMap<OAuthOwner, Map<Table, Map<string, A>>>,
390
- table: Table,
391
- ) {
392
- const owner = yield* CurrentOAuthTransaction;
393
- let tables = cache.get(owner);
394
-
395
- if (tables === undefined) {
396
- tables = new Map();
397
- cache.set(owner, tables);
398
- }
399
- let rows = tables.get(table);
400
-
401
- if (rows === undefined) {
402
- rows = new Map();
403
- tables.set(table, rows);
404
- }
405
-
406
- return rows;
407
- });
408
-
409
- /** Empty registration ID is impossible in the public profile label schema. The
410
- * distinct 52-byte key domain shares the physical table, never a client counter. */
411
- export const scope = Effect.fn("oauthConnected.scope")(function* (
412
- mapping: Authority,
413
- config: M.OAuthConnectedConfiguration,
414
- create: boolean,
415
- ) {
416
- const owner = yield* CurrentOAuthTransaction;
417
-
418
- const c = mapping.client,
419
- id = yield* scopeKey(config.provider, config.issuer),
420
- cache = yield* anchors(scopeCache, c.table);
421
-
422
- const existing = cache.get(id);
423
-
424
- if (existing !== undefined) return existing;
425
- const read = yield* owner.read(c.table, equal(c.table, { [c.clientKey]: id }), { limit: 1 });
426
-
427
- if (read.rows.length === 0) {
428
- const related = [
429
- mapping.flow,
430
- mapping.grant,
431
- mapping.cohort,
432
- ...(jobTable(mapping) === undefined ? [] : [jobTable(mapping)!]),
433
- ];
434
-
435
- invariant(
436
- yield* owner.check(
437
- both(
438
- sql`not exists(select 1 from ${c.table} where ${owner.exact(c.table, { [c.provider]: config.provider, [c.issuer]: config.issuer })})`,
439
- ...related.map(
440
- (v) =>
441
- sql`not exists(select 1 from ${v.table} where not exists(select 1 from ${c.table} where ${col(c.table, c.clientKey)} = ${col(v.table, v.clientKey)}))`,
442
- ),
443
- ),
444
- ),
445
- );
446
- if (!create) return undefined;
447
- invariant(c.encodeInsert !== undefined);
448
-
449
- const inserted = yield* owner.insert(
450
- c.table,
451
- {
452
- ...c.encodeInsert(snapshotOAuthSync(M.OAuthConnectedConfiguration, config)),
453
- [c.clientKey]: id,
454
- [c.provider]: config.provider,
455
- [c.issuer]: config.issuer,
456
- [c.clientRegistrationId]: "",
457
- [c.counter]: mapping.order.encode(0),
458
- [c.version]: owner.marker,
459
- },
460
- { [c.clientKey]: id },
461
- true,
462
- );
463
-
464
- read.rows = inserted.rows;
465
- }
466
- const row = read.rows[0]!;
467
-
468
- invariant(
469
- row[c.provider] === config.provider &&
470
- row[c.issuer] === config.issuer &&
471
- row[c.clientRegistrationId] === "" &&
472
- nativeOrder(mapping, row[c.counter]) === 0,
473
- );
474
- const found = { id, row };
475
-
476
- cache.set(id, found);
477
-
478
- return found;
479
- });
480
-
481
- export const touchScope = Effect.fn("oauthConnected.touchScope")(function* (
482
- mapping: Authority,
483
- found: ScopeAnchor,
484
- ) {
485
- const owner = yield* CurrentOAuthTransaction;
486
- const c = mapping.client;
487
-
488
- yield* owner.update(
489
- c.table,
490
- { [c.clientKey]: found.id, [c.version]: found.row[c.version] },
491
- { [c.version]: owner.marker },
492
- );
493
- found.row = { ...found.row, [c.version]: owner.marker };
494
- });
495
-
496
- export const readScope = Effect.fn("oauthConnected.readScope")(function* (
497
- mapping: Authority,
498
- provider: string,
499
- issuer: string,
500
- ) {
501
- const owner = yield* CurrentOAuthTransaction;
502
-
503
- const c = mapping.client,
504
- id = yield* scopeKey(provider, issuer),
505
- cache = yield* anchors(scopeCache, c.table),
506
- cached = cache.get(id);
507
-
508
- if (cached !== undefined) return cached;
509
-
510
- const read = yield* owner.read(c.table, equal(c.table, { [c.clientKey]: id }), { limit: 1 }),
511
- row = read.rows[0];
512
-
513
- invariant(
514
- row !== undefined &&
515
- row[c.provider] === provider &&
516
- row[c.issuer] === issuer &&
517
- row[c.clientRegistrationId] === "" &&
518
- nativeOrder(mapping, row[c.counter]) === 0,
519
- );
520
- const found = { id, row };
521
-
522
- cache.set(id, found);
523
-
524
- return found;
525
- });
526
-
527
- export const lockClientById = Effect.fn("oauthConnected.lockClientById")(function* (
528
- mapping: Authority,
529
- id: string,
530
- ) {
531
- const owner = yield* CurrentOAuthTransaction;
532
-
533
- const c = mapping.client,
534
- cache = yield* anchors(clientCache, c.table),
535
- cached = cache.get(id);
536
-
537
- if (cached !== undefined) return cached;
538
-
539
- const discovery = yield* owner.read(c.table, equal(c.table, { [c.clientKey]: id }), {
540
- limit: 1,
541
- observe: false,
542
- lock: false,
543
- }),
544
- initial = discovery.rows[0];
545
-
546
- invariant(initial !== undefined);
547
- const scope = yield* readScope(mapping, initial[c.provider], initial[c.issuer]);
548
-
549
- const read = yield* owner.read(c.table, equal(c.table, { [c.clientKey]: id }), { limit: 1 }),
550
- row = read.rows[0];
551
-
552
- invariant(
553
- row !== undefined &&
554
- row[c.clientRegistrationId] !== "" &&
555
- id ===
556
- (yield* key("effect-auth/oauth-connected-client/v1", [
557
- row[c.provider],
558
- row[c.issuer],
559
- row[c.clientRegistrationId],
560
- ])) &&
561
- scope.id === (yield* scopeKey(row[c.provider], row[c.issuer])),
562
- );
563
- const found = { id, row, counter: nativeOrder(mapping, row[c.counter]), scope };
564
-
565
- cache.set(id, found);
566
-
567
- return found;
568
- });
569
-
570
- export const heldScope = Effect.fnUntraced(function* (
571
- mapping: Authority,
572
- provider: string,
573
- issuer: string,
574
- ) {
575
- const cache = yield* anchors(scopeCache, mapping.client.table);
576
-
577
- return cache.get(yield* scopeKey(provider, issuer));
578
- });
579
-
580
- /** Cleanup obtains every scope then every client before entering any tuple. */
581
- export const prelockClients = Effect.fn("oauthConnected.prelockClients")(function* (
582
- mapping: Authority,
583
- ids: ReadonlyArray<string>,
584
- releaseScopes: ReadonlyArray<{ readonly provider: string; readonly issuer: string }> = [],
585
- ) {
586
- const owner = yield* CurrentOAuthTransaction;
587
-
588
- const unique = [...new Set(ids)].sort(),
589
- c = mapping.client;
590
-
591
- invariant(unique.length <= 4000);
592
- const scopes = new Map<string, { provider: string; issuer: string; required: boolean }>();
593
-
594
- for (const value of releaseScopes)
595
- scopes.set(yield* scopeKey(value.provider, value.issuer), { ...value, required: false });
596
- for (const id of unique) {
597
- const read = yield* owner.read(c.table, equal(c.table, { [c.clientKey]: id }), {
598
- limit: 1,
599
- lock: false,
600
- observe: false,
601
- }),
602
- row = read.rows[0];
603
-
604
- invariant(row !== undefined);
605
- scopes.set(yield* scopeKey(row[c.provider], row[c.issuer]), {
606
- provider: row[c.provider],
607
- issuer: row[c.issuer],
608
- required: true,
609
- });
610
- }
611
- for (const [id, { provider, issuer, required }] of [...scopes].sort(([a], [b]) =>
612
- a < b ? -1 : a > b ? 1 : 0,
613
- )) {
614
- if (required) yield* readScope(mapping, provider, issuer);
615
- else {
616
- const read = yield* owner.read(c.table, equal(c.table, { [c.clientKey]: id }), { limit: 1 }),
617
- row = read.rows[0];
618
-
619
- if (row !== undefined) {
620
- invariant(
621
- row[c.provider] === provider &&
622
- row[c.issuer] === issuer &&
623
- row[c.clientRegistrationId] === "" &&
624
- nativeOrder(mapping, row[c.counter]) === 0,
625
- );
626
- (yield* anchors(scopeCache, c.table)).set(id, { id, row });
627
- }
628
- }
629
- }
630
- for (const id of unique) yield* lockClientById(mapping, id);
631
- });
632
-
633
- export const client = Effect.fn("oauthConnected.client")(function* (
634
- mapping: Authority,
635
- config: M.OAuthConnectedConfiguration,
636
- create: boolean,
637
- ) {
638
- const owner = yield* CurrentOAuthTransaction;
639
-
640
- const c = mapping.client,
641
- id = yield* clientKey(config),
642
- where = { [c.clientKey]: id };
643
-
644
- const heldScope = yield* scope(mapping, config, create);
645
-
646
- if (heldScope === undefined) return undefined;
647
-
648
- const cache = yield* anchors(clientCache, c.table),
649
- cached = cache.get(id);
650
-
651
- if (cached !== undefined) return cached;
652
- let read = yield* owner.read(c.table, equal(c.table, where), { limit: 1 });
653
-
654
- if (read.rows.length === 0) {
655
- const related = [
656
- mapping.flow,
657
- mapping.grant,
658
- mapping.cohort,
659
- ...(jobTable(mapping) === undefined ? [] : [jobTable(mapping)!]),
660
- ];
661
-
662
- invariant(
663
- yield* owner.check(
664
- both(
665
- ...related.map(
666
- (v) =>
667
- sql`not exists(select 1 from ${v.table} where ${eq(col(v.table, v.clientKey), id)})`,
668
- ),
669
- ),
670
- ),
671
- );
672
- }
673
- if (read.rows.length === 0 && create) {
674
- const inserted = yield* owner.insert(
675
- c.table,
676
- {
677
- ...(invariant(c.encodeInsert !== undefined),
678
- c.encodeInsert(snapshotOAuthSync(M.OAuthConnectedConfiguration, config))),
679
- ...where,
680
- [c.provider]: config.provider,
681
- [c.issuer]: config.issuer,
682
- [c.clientRegistrationId]: config.profile.clientRegistrationId,
683
- [c.counter]: mapping.order.encode(0),
684
- [c.version]: owner.marker,
685
- },
686
- where,
687
- true,
688
- );
689
-
690
- read.rows = inserted.rows;
691
- }
692
- const row = read.rows[0];
693
-
694
- if (row === undefined) return undefined;
695
- invariant(
696
- row[c.provider] === config.provider &&
697
- row[c.issuer] === config.issuer &&
698
- row[c.clientRegistrationId] === config.profile.clientRegistrationId,
699
- );
700
- const found = { id, row, counter: nativeOrder(mapping, row[c.counter]), scope: heldScope };
701
-
702
- cache.set(id, found);
703
-
704
- return found;
705
- });
706
-
707
- export const nextOrder = Effect.fn("oauthConnected.nextOrder")(function* (
708
- mapping: Authority,
709
- found: NonNullable<Effect.Success<ReturnType<typeof client>>>,
710
- ) {
711
- const owner = yield* CurrentOAuthTransaction;
712
-
713
- const next = orderParser(found.counter + 1),
714
- c = mapping.client;
715
-
716
- yield* owner.update(
717
- c.table,
718
- { [c.clientKey]: found.id, [c.counter]: mapping.order.encode(found.counter) },
719
- { [c.counter]: mapping.order.encode(next), [c.version]: owner.marker },
720
- );
721
- found.counter = next;
722
-
723
- return next;
724
- });
725
-
726
- export const cohort = Effect.fn("oauthConnected.cohort")(function* (
727
- mapping: Authority,
728
- clientId: string,
729
- identityId: string,
730
- create: boolean,
731
- ) {
732
- const owner = yield* CurrentOAuthTransaction;
733
-
734
- const c = mapping.cohort,
735
- id = yield* cohortKey(clientId, identityId),
736
- where = { [c.cohortKey]: id };
737
-
738
- let read = yield* owner.read(c.table, equal(c.table, where), { limit: 1 });
739
-
740
- if (read.rows.length === 0) {
741
- const related = [
742
- mapping.flow,
743
- mapping.grant,
744
- ...(jobTable(mapping) === undefined ? [] : [jobTable(mapping)!]),
745
- ];
746
-
747
- invariant(
748
- yield* owner.check(
749
- both(
750
- ...related.map(
751
- (v) =>
752
- sql`not exists(select 1 from ${v.table} where ${eq(col(v.table, v.cohortKey), id)})`,
753
- ),
754
- ),
755
- ),
756
- );
757
- }
758
- if (read.rows.length === 0 && create) {
759
- const inserted = yield* owner.insert(
760
- c.table,
761
- {
762
- ...(invariant(c.encodeInsert !== undefined),
763
- c.encodeInsert({ clientKey: clientId, identityKey: identityId })),
764
- ...where,
765
- [c.clientKey]: clientId,
766
- [c.identityKey]: identityId,
767
- [c.generation]: yield* initialGeneration(id),
768
- [c.cutoff]: mapping.order.encode(0),
769
- [c.state]: "Open",
770
- [c.version]: owner.marker,
771
- },
772
- where,
773
- true,
774
- );
775
-
776
- read.rows = inserted.rows;
777
- }
778
- const row = read.rows[0];
779
-
780
- if (row === undefined)
781
- return {
782
- id,
783
- row: undefined,
784
- generation: yield* initialGeneration(id),
785
- cutoff: 0,
786
- blocked: false,
787
- };
788
- invariant(
789
- row[c.clientKey] === clientId &&
790
- row[c.identityKey] === identityId &&
791
- ["Open", "Blocked"].includes(row[c.state]),
792
- );
793
-
794
- const generation = snapshotOAuthSync(
795
- M.OAuthConnectedTarget.fields.cohortGeneration,
796
- row[c.generation],
797
- );
798
-
799
- return {
800
- id,
801
- row,
802
- generation,
803
- cutoff: nativeOrder(mapping, row[c.cutoff]),
804
- blocked: row[c.state] === "Blocked",
805
- };
806
- });
807
-
808
- export const readGrant = Effect.fn("oauthConnected.readGrant")(function* (
809
- mapping: Authority,
810
- moduleId: string,
811
- grantId: string,
812
- lock = true,
813
- ) {
814
- const owner = yield* CurrentOAuthTransaction;
815
- const g = mapping.grant;
816
-
817
- const read = yield* owner.read(
818
- g.table,
819
- equal(g.table, { [g.moduleId]: moduleId, [g.grantId]: grantId }),
820
- { limit: 1, lock, admissionOnly: lock, observe: false },
821
- );
822
-
823
- const row = read.rows[0];
824
-
825
- if (row === undefined) return undefined;
826
- if (lock) owner.observations.push(read);
827
- const context = tokenContextStorage.decode(row[g.context]);
828
- const native = yield* mapping.subjectId.toNative(context.subjectId);
829
-
830
- invariant(
831
- context.moduleId === moduleId &&
832
- context.grantId === grantId &&
833
- mapping.subjectId.equals(native, row[g.subjectId]) &&
834
- row[g.identityKey] === (yield* oauthIdentityKey(context.identity)) &&
835
- row[g.clientKey] === (yield* clientKey(context.configuration)) &&
836
- row[g.cohortKey] === (yield* cohortKey(row[g.clientKey], row[g.identityKey])) &&
837
- row[g.profileKey] === context.configuration.profile.key &&
838
- row[g.grantVersion] === context.grantVersion &&
839
- row[g.tokenVersion] === context.tokenVersion &&
840
- row[g.cohortGeneration] === context.cohortGeneration,
841
- );
842
-
843
- return {
844
- row,
845
- context,
846
- native,
847
- sealed: row[g.sealed] === null ? undefined : sealedStorage.decode(row[g.sealed]),
848
- };
849
- });
850
-
851
- export const unknownWork = (mapping: Authority, clientId: string | SQL, cutoff?: number) => {
852
- const f = mapping.flow;
853
-
854
- return both(
855
- eq(col(f.table, f.clientKey), clientId),
856
- eq(col(f.table, f.work), "Unresolved"),
857
- isNull(col(f.table, f.cohortKey)),
858
- cutoff === undefined
859
- ? undefined
860
- : sql`${col(f.table, f.claimOrder)} <= ${mapping.order.encode(cutoff)}`,
861
- );
862
- };
863
-
864
- export const cohortWork = (mapping: Authority, id: string | SQL) => {
865
- const f = mapping.flow,
866
- g = mapping.grant;
867
-
868
- return both(
869
- sql`not exists(select 1 from ${f.table} where ${and(eq(col(f.table, f.cohortKey), id), eq(col(f.table, f.work), "Unresolved"))})`,
870
- sql`not exists(select 1 from ${g.table} where ${and(eq(col(g.table, g.cohortKey), id), eq(col(g.table, g.refreshWork), "Unresolved"))})`,
871
- );
872
- };