@yielded/auth-persistence-drizzle 0.1.0-beta.0 → 0.1.0-beta.1

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 (204) hide show
  1. package/README.md +6 -1
  2. package/dist/D1.d.mts +135 -25
  3. package/dist/D1.mjs +1 -3
  4. package/dist/D1.mjs.map +1 -1
  5. package/dist/Libsql.d.mts +192 -75
  6. package/dist/Libsql.mjs +1 -7
  7. package/dist/Libsql.mjs.map +1 -1
  8. package/dist/Mysql2.d.mts +177 -37
  9. package/dist/Mysql2.mjs +1 -3
  10. package/dist/Mysql2.mjs.map +1 -1
  11. package/dist/Pglite.d.mts +171 -30
  12. package/dist/Pglite.mjs +2 -3
  13. package/dist/Pglite.mjs.map +1 -1
  14. package/dist/Postgres.d.mts +177 -36
  15. package/dist/Postgres.mjs +2 -3
  16. package/dist/Postgres.mjs.map +1 -1
  17. package/dist/SqliteBun.d.mts +192 -74
  18. package/dist/SqliteBun.mjs +1 -5
  19. package/dist/SqliteBun.mjs.map +1 -1
  20. package/dist/SqliteDo.d.mts +194 -110
  21. package/dist/SqliteDo.mjs +2 -32
  22. package/dist/SqliteDo.mjs.map +1 -1
  23. package/dist/SqliteNode.d.mts +190 -72
  24. package/dist/SqliteNode.mjs +1 -5
  25. package/dist/SqliteNode.mjs.map +1 -1
  26. package/dist/SqliteWasm.d.mts +192 -74
  27. package/dist/SqliteWasm.mjs +1 -5
  28. package/dist/SqliteWasm.mjs.map +1 -1
  29. package/dist/drizzle/D1BatchStatements.d.mts +1 -1
  30. package/dist/drizzle/D1BatchStatements.mjs.map +1 -1
  31. package/dist/drizzle/d1-emails.mjs.map +1 -1
  32. package/dist/drizzle/d1-generated-statement.mjs.map +1 -1
  33. package/dist/drizzle/d1-identity.d.mts +1 -1
  34. package/dist/drizzle/d1-oauth.d.mts +35 -5
  35. package/dist/drizzle/d1-oauth.mjs +1 -1
  36. package/dist/drizzle/d1-password-prepared.mjs.map +1 -1
  37. package/dist/drizzle/d1-passwords.d.mts +1 -1
  38. package/dist/drizzle/d1-passwords.mjs.map +1 -1
  39. package/dist/drizzle/d1-proofs.d.mts +1 -1
  40. package/dist/drizzle/d1-proofs.mjs.map +1 -1
  41. package/dist/drizzle/d1-sessions.d.mts +3 -3
  42. package/dist/drizzle/d1-sessions.mjs.map +1 -1
  43. package/dist/drizzle/email-registration.d.mts +1 -1
  44. package/dist/drizzle/email-target.d.mts +1 -1
  45. package/dist/drizzle/email-target.mjs.map +1 -1
  46. package/dist/drizzle/model.d.mts +2 -126
  47. package/dist/drizzle/model.mjs +2 -21
  48. package/dist/drizzle/model.mjs.map +1 -1
  49. package/dist/drizzle/mysql-emails.d.mts +1 -1
  50. package/dist/drizzle/mysql-emails.mjs.map +1 -1
  51. package/dist/drizzle/mysql-identity.d.mts +1 -1
  52. package/dist/drizzle/mysql-password-prepared.d.mts +1 -1
  53. package/dist/drizzle/mysql-password-prepared.mjs.map +1 -1
  54. package/dist/drizzle/mysql-passwords.d.mts +1 -1
  55. package/dist/drizzle/mysql-passwords.mjs.map +1 -1
  56. package/dist/drizzle/mysql-proofs.d.mts +1 -1
  57. package/dist/drizzle/mysql-proofs.mjs.map +1 -1
  58. package/dist/drizzle/mysql-sessions.d.mts +1 -1
  59. package/dist/drizzle/mysql-sessions.mjs.map +1 -1
  60. package/dist/drizzle/oauth-accounts.mjs +1 -1
  61. package/dist/drizzle/oauth-connected-access.mjs +14 -12
  62. package/dist/drizzle/oauth-connected-access.mjs.map +1 -1
  63. package/dist/drizzle/oauth-connected-custody.mjs +2 -2
  64. package/dist/drizzle/oauth-connected-drivers.mjs +4 -1
  65. package/dist/drizzle/oauth-connected-drivers.mjs.map +1 -1
  66. package/dist/drizzle/oauth-connected-flow.mjs +2 -2
  67. package/dist/drizzle/oauth-connected-input.mjs +14 -1
  68. package/dist/drizzle/oauth-connected-input.mjs.map +1 -1
  69. package/dist/drizzle/oauth-connected-maintenance.mjs +1 -1
  70. package/dist/drizzle/oauth-connected-management.mjs +2 -2
  71. package/dist/drizzle/oauth-connected-model.d.mts +17 -3
  72. package/dist/drizzle/oauth-connected-model.mjs.map +1 -1
  73. package/dist/drizzle/oauth-connected-settlement.mjs +51 -41
  74. package/dist/drizzle/oauth-connected-settlement.mjs.map +1 -1
  75. package/dist/drizzle/oauth-connected-sign-in.mjs +263 -0
  76. package/dist/drizzle/oauth-connected-sign-in.mjs.map +1 -0
  77. package/dist/drizzle/oauth-connected-state.d.mts +110 -2
  78. package/dist/drizzle/oauth-connected-state.mjs +9 -4
  79. package/dist/drizzle/oauth-connected-state.mjs.map +1 -1
  80. package/dist/drizzle/oauth-connected-target.d.mts +137 -9
  81. package/dist/drizzle/oauth-connected-target.mjs +6 -2
  82. package/dist/drizzle/oauth-connected-target.mjs.map +1 -1
  83. package/dist/drizzle/oauth-drivers.mjs +1 -1
  84. package/dist/drizzle/oauth-flow.d.mts +1 -1
  85. package/dist/drizzle/oauth-flow.mjs +1 -1
  86. package/dist/drizzle/oauth-input.mjs +1 -1
  87. package/dist/drizzle/oauth-model.d.mts +1 -1
  88. package/dist/drizzle/oauth-owner.d.mts +1 -1
  89. package/dist/drizzle/oauth-registration.d.mts +2 -2
  90. package/dist/drizzle/oauth-registration.mjs +1 -1
  91. package/dist/drizzle/oauth-state.mjs +5 -4
  92. package/dist/drizzle/oauth-state.mjs.map +1 -1
  93. package/dist/drizzle/oauth-target.d.mts +2 -2
  94. package/dist/drizzle/oauth-target.mjs +1 -1
  95. package/dist/drizzle/oauth-target.mjs.map +1 -1
  96. package/dist/drizzle/passkey-write-target.d.mts +1 -1
  97. package/dist/drizzle/password-prepared-state.mjs +1 -1
  98. package/dist/drizzle/password-prepared-target.d.mts +1 -1
  99. package/dist/drizzle/password-prepared-target.mjs.map +1 -1
  100. package/dist/drizzle/password-target.d.mts +1 -1
  101. package/dist/drizzle/password-target.mjs.map +1 -1
  102. package/dist/drizzle/pg-emails.d.mts +1 -1
  103. package/dist/drizzle/pg-emails.mjs.map +1 -1
  104. package/dist/drizzle/pg-identity.d.mts +22 -0
  105. package/dist/drizzle/pg-identity.mjs +89 -0
  106. package/dist/drizzle/pg-identity.mjs.map +1 -0
  107. package/dist/drizzle/pg-password-prepared.d.mts +1 -1
  108. package/dist/drizzle/pg-password-prepared.mjs.map +1 -1
  109. package/dist/drizzle/pg-passwords.d.mts +1 -1
  110. package/dist/drizzle/pg-passwords.mjs.map +1 -1
  111. package/dist/drizzle/pg-proofs.d.mts +1 -1
  112. package/dist/drizzle/pg-proofs.mjs.map +1 -1
  113. package/dist/drizzle/pg-sessions.d.mts +1 -1
  114. package/dist/drizzle/pg-sessions.mjs.map +1 -1
  115. package/dist/drizzle/phone-model.d.mts +1 -1
  116. package/dist/drizzle/phone-target.d.mts +1 -1
  117. package/dist/drizzle/phone-target.mjs.map +1 -1
  118. package/dist/drizzle/proof-target.d.mts +1 -1
  119. package/dist/drizzle/proof-target.mjs.map +1 -1
  120. package/dist/drizzle/session-target.d.mts +1 -1
  121. package/dist/drizzle/session-target.mjs.map +1 -1
  122. package/dist/drizzle/sqlite-emails.mjs.map +1 -1
  123. package/dist/drizzle/sqlite-password-prepared.mjs.map +1 -1
  124. package/dist/drizzle/sqlite-passwords.mjs.map +1 -1
  125. package/dist/drizzle/sqlite-proofs.mjs.map +1 -1
  126. package/dist/drizzle/sqlite-sessions.mjs.map +1 -1
  127. package/dist/drizzle/standalone-guard.mjs.map +1 -1
  128. package/dist/drizzle/totp-model.d.mts +1 -1
  129. package/dist/drizzle/totp-target.d.mts +1 -1
  130. package/dist/drizzle/totp-target.mjs.map +1 -1
  131. package/dist/drizzle/transaction-mysql.mjs +1 -1
  132. package/dist/drizzle/transaction-mysql.mjs.map +1 -1
  133. package/dist/index.d.mts +2 -4
  134. package/dist/index.mjs +2 -4
  135. package/dist/internal/drizzle-postgres.mjs.map +1 -1
  136. package/dist/internal/drizzle-sqlite.mjs.map +1 -1
  137. package/package.json +1 -1
  138. package/src/D1.ts +1 -17
  139. package/src/Libsql.ts +0 -44
  140. package/src/Mysql2.ts +1 -23
  141. package/src/Pglite.ts +2 -18
  142. package/src/Postgres.ts +2 -18
  143. package/src/SqliteBun.ts +1 -38
  144. package/src/SqliteDo.ts +1 -256
  145. package/src/SqliteNode.ts +1 -38
  146. package/src/SqliteWasm.ts +1 -38
  147. package/src/drizzle/D1BatchStatements.ts +1 -1
  148. package/src/drizzle/d1-emails.ts +2 -4
  149. package/src/drizzle/d1-generated-statement.ts +1 -1
  150. package/src/drizzle/d1-password-prepared.ts +2 -5
  151. package/src/drizzle/d1-passwords.ts +2 -4
  152. package/src/drizzle/d1-proofs.ts +2 -4
  153. package/src/drizzle/d1-sessions.ts +2 -2
  154. package/src/drizzle/email-target.ts +1 -1
  155. package/src/drizzle/model.ts +1 -208
  156. package/src/drizzle/mysql-emails.ts +1 -1
  157. package/src/drizzle/mysql-password-prepared.ts +1 -1
  158. package/src/drizzle/mysql-passwords.ts +1 -1
  159. package/src/drizzle/mysql-proofs.ts +1 -1
  160. package/src/drizzle/mysql-sessions.ts +1 -1
  161. package/src/drizzle/oauth-connected-access.ts +8 -1
  162. package/src/drizzle/oauth-connected-drivers.ts +3 -0
  163. package/src/drizzle/oauth-connected-input.ts +13 -0
  164. package/src/drizzle/oauth-connected-model.ts +17 -0
  165. package/src/drizzle/oauth-connected-settlement.ts +65 -42
  166. package/src/drizzle/oauth-connected-sign-in.ts +438 -0
  167. package/src/drizzle/oauth-connected-state.ts +19 -7
  168. package/src/drizzle/oauth-connected-target.ts +10 -1
  169. package/src/drizzle/oauth-state.ts +5 -4
  170. package/src/drizzle/oauth-target.ts +1 -1
  171. package/src/drizzle/password-prepared-target.ts +1 -1
  172. package/src/drizzle/password-target.ts +1 -1
  173. package/src/drizzle/pg-emails.ts +1 -1
  174. package/src/drizzle/pg-identity.ts +301 -0
  175. package/src/drizzle/pg-password-prepared.ts +1 -1
  176. package/src/drizzle/pg-passwords.ts +1 -1
  177. package/src/drizzle/pg-proofs.ts +1 -1
  178. package/src/drizzle/pg-sessions.ts +1 -1
  179. package/src/drizzle/phone-target.ts +1 -1
  180. package/src/drizzle/proof-target.ts +1 -1
  181. package/src/drizzle/session-target.ts +1 -1
  182. package/src/drizzle/sqlite-emails.ts +1 -1
  183. package/src/drizzle/sqlite-password-prepared.ts +1 -1
  184. package/src/drizzle/sqlite-passwords.ts +1 -1
  185. package/src/drizzle/sqlite-proofs.ts +1 -1
  186. package/src/drizzle/sqlite-sessions.ts +1 -1
  187. package/src/drizzle/standalone-guard.ts +1 -1
  188. package/src/drizzle/totp-target.ts +1 -1
  189. package/src/drizzle/transaction-mysql.ts +1 -1
  190. package/src/index.ts +0 -24
  191. package/src/internal/drizzle-postgres.ts +1 -1
  192. package/src/internal/drizzle-sqlite.ts +1 -1
  193. package/dist/drizzle/mysql.d.mts +0 -69
  194. package/dist/drizzle/mysql.mjs +0 -218
  195. package/dist/drizzle/mysql.mjs.map +0 -1
  196. package/dist/drizzle/pg.d.mts +0 -80
  197. package/dist/drizzle/pg.mjs +0 -289
  198. package/dist/drizzle/pg.mjs.map +0 -1
  199. package/dist/drizzle/sqlite.d.mts +0 -77
  200. package/dist/drizzle/sqlite.mjs +0 -217
  201. package/dist/drizzle/sqlite.mjs.map +0 -1
  202. package/src/drizzle/mysql.ts +0 -687
  203. package/src/drizzle/pg.ts +0 -984
  204. package/src/drizzle/sqlite.ts +0 -705
@@ -7,7 +7,7 @@ import * as C from "./oauth-connected-custody";
7
7
  import * as F from "./oauth-connected-flow";
8
8
  import * as S from "./oauth-connected-state";
9
9
  import { both, col, equal, CurrentOAuthTransaction } from "./oauth-owner";
10
- import { invariant, sameIdentity } from "./oauth-state";
10
+ import { invariant, sameIdentity, oauthIdentityKey } from "./oauth-state";
11
11
 
12
12
  export const validMetadata = (context: M.OAuthConnectedTokenContext, now: number) => {
13
13
  const m = context.metadata,
@@ -35,6 +35,67 @@ export const validMetadata = (context: M.OAuthConnectedTokenContext, now: number
35
35
  );
36
36
  };
37
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 = oauthIdentityKey(token.identity);
50
+ const clientKey = S.clientKey(token.configuration);
51
+ const cohortKey = 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
+
38
99
  export const settle = Effect.fn("oauthConnected.settle")(function* (
39
100
  mapping: S.Mapping,
40
101
  input: F.Input<"settle">,
@@ -123,7 +184,8 @@ export const settle = Effect.fn("oauthConnected.settle")(function* (
123
184
 
124
185
  if (claimed === undefined) return { _tag: "Rejected" } as const;
125
186
  invariant(
126
- validMetadata(token, claimed.now) &&
187
+ token.exchangeOrder === input.claim.order &&
188
+ validMetadata(token, claimed.now) &&
127
189
  token.metadata.obtainedAtMillis >= input.claim.claimedAtMillis,
128
190
  );
129
191
  const reconnect = c.reconnect;
@@ -177,46 +239,7 @@ export const settle = Effect.fn("oauthConnected.settle")(function* (
177
239
  if (tuple.row[t.state] === "Unowned")
178
240
  yield* F.acquireTuple(mapping.ownership, token.identity, tuple.key, S.nativeCopy(native));
179
241
 
180
- const values = {
181
- ...g.encodeInsert({
182
- grant: snapshotOAuthSync(M.OAuthConnectedStoredGrant, grant),
183
- subjectId: S.nativeCopy(native),
184
- }),
185
- [g.moduleId]: c.moduleId,
186
- [g.grantId]: c.grantId,
187
- [g.subjectId]: native,
188
- [g.identityKey]: tuple.key,
189
- [g.activeIdentityKey]: tuple.key,
190
- [g.clientKey]: cl.id,
191
- [g.cohortKey]: co.id,
192
- [g.profileKey]: c.profile.key,
193
- [g.grantVersion]: token.grantVersion,
194
- [g.tokenVersion]: token.tokenVersion,
195
- [g.cohortGeneration]: token.cohortGeneration,
196
- [g.state]: "Active",
197
- [g.version]: owner.marker,
198
- [g.context]: S.tokenContextStorage.encode(token),
199
- [g.sealed]: S.sealedStorage.encode(grant.sealed),
200
- [g.summary]: S.summaryStorage.encode(C.summary(token)),
201
- [g.revocationJobId]: null,
202
- [g.refreshWork]: "None",
203
- [g.refreshClaim]: null,
204
- [g.refreshClaimExpiresAt]: null,
205
- [g.retentionUntil]: mapping.clock.encodeInstant(
206
- S.retainedUntil(
207
- mapping,
208
- Math.max(
209
- claimed.now,
210
- token.metadata.useUntilMillis,
211
- token.metadata.refreshUseUntilMillis ?? 0,
212
- ),
213
- ),
214
- ),
215
- };
216
-
217
- if (original === undefined) {
218
- yield* owner.insert(g.table, values, { [g.moduleId]: c.moduleId, [g.grantId]: c.grantId });
219
- } else yield* owner.update(g.table, { [g.moduleId]: c.moduleId, [g.grantId]: c.grantId }, values);
242
+ yield* activateGrant(mapping, grant, native, claimed.now, original !== undefined);
220
243
  yield* F.terminal(mapping, input.claim, "Connected", "Resolved", { cohort: co.id });
221
244
  owner.postconditions.push(
222
245
  sql`${mapping.clock.engineNowMillis} >= ${claimed.now} and ${mapping.clock.engineNowMillis} < ${Math.min(input.claim.claimExpiresAtMillis, token.metadata.useUntilMillis)}`,
@@ -0,0 +1,438 @@
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]: 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] !== 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] === 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]: 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 = (
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 S.clientKey(reservation.configuration) === S.clientKey(previous.configuration)
253
+ ? generation >= oldGeneration &&
254
+ S.orderNumber(reservation.order) > S.orderNumber(previous.exchangeOrder)
255
+ : generation > oldGeneration;
256
+ };
257
+
258
+ export const inspectSignIn = Effect.fn("oauthConnected.inspectSignIn")(function* (
259
+ mapping: S.Mapping,
260
+ input: F.Input<"inspectSignIn">,
261
+ ) {
262
+ const found = yield* credential(mapping, input.reservation, input.credential);
263
+
264
+ if (found === undefined) return { _tag: "Rejected" } as const;
265
+ const r = input.reservation;
266
+ const cl = yield* S.client(mapping, r.configuration, false);
267
+
268
+ if (cl === undefined) return { _tag: "Rejected" } as const;
269
+ const tuple = yield* F.inspectTuple(mapping, input.credential.identity);
270
+ const t = mapping.ownership.tuple;
271
+
272
+ if (
273
+ tuple.row?.[t.state] !== "Owned" ||
274
+ !mapping.subjectId.equals(tuple.row[t.subjectId], found.nativeId)
275
+ )
276
+ return { _tag: "Rejected" } as const;
277
+ const co = yield* S.cohort(mapping, cl.id, tuple.key, false);
278
+
279
+ const previous = yield* slot(
280
+ mapping,
281
+ r.claim.flow.context.moduleId,
282
+ found.nativeId,
283
+ tuple.key,
284
+ r.configuration.profile.key,
285
+ );
286
+
287
+ const held = yield* exact(mapping, r);
288
+
289
+ if (held === undefined) return { _tag: "Rejected" } as const;
290
+
291
+ const quarantine =
292
+ !held.active ||
293
+ co.blocked ||
294
+ S.orderNumber(r.order) <= co.cutoff ||
295
+ previous?.row[mapping.grant.refreshWork] === "Unresolved" ||
296
+ (previous !== undefined && !canReplace(r, previous.context));
297
+
298
+ return snapshotOAuthSync(M.OAuthSignInAccessInspection, {
299
+ _tag: quarantine ? "Quarantine" : "Target",
300
+ grantId: previous?.context.grantId ?? input.grantId,
301
+ cohortGeneration: co.generation,
302
+ ...(previous === undefined
303
+ ? {}
304
+ : { previous: snapshotOAuthSync(M.OAuthConnectedTarget, previous.context) }),
305
+ });
306
+ });
307
+
308
+ export const settleSignIn = Effect.fn("oauthConnected.settleSignIn")(function* (
309
+ mapping: S.Mapping,
310
+ input: F.Input<"settleSignIn">,
311
+ ) {
312
+ const owner = yield* CurrentOAuthTransaction;
313
+ const r = input.reservation;
314
+ const out = input.outcome;
315
+ const found = out._tag === "Verified" ? yield* credential(mapping, r, out.credential) : undefined;
316
+ const cl = yield* S.client(mapping, r.configuration, false);
317
+
318
+ if (cl === undefined) return { _tag: "Rejected" } as const;
319
+ const f = mapping.flow;
320
+
321
+ if (out._tag !== "Verified") {
322
+ const held = yield* exact(mapping, r);
323
+
324
+ if (held === undefined) return { _tag: "Rejected" } as const;
325
+ yield* owner.update(f.table, held.key, {
326
+ [f.state]: out._tag,
327
+ [f.work]: out._tag === "Cancelled" ? "Resolved" : "Unresolved",
328
+ [f.version]: owner.marker,
329
+ });
330
+
331
+ return { _tag: out._tag };
332
+ }
333
+ const token = out.grant.context;
334
+
335
+ invariant(
336
+ token.moduleId === r.claim.flow.context.moduleId &&
337
+ token.subjectId === out.credential.revision.subjectId &&
338
+ sameIdentity(token.identity, out.credential.identity) &&
339
+ configurationStorage.encode(token.configuration) ===
340
+ configurationStorage.encode(r.configuration),
341
+ );
342
+ const native = yield* mapping.subjectId.toNative(token.subjectId);
343
+ const tuple = yield* F.readTuple(mapping.ownership, token.identity);
344
+ const t = mapping.ownership.tuple;
345
+
346
+ const owned =
347
+ tuple.row[t.state] === "Owned" && mapping.subjectId.equals(tuple.row[t.subjectId], native);
348
+
349
+ const co = yield* S.cohort(mapping, cl.id, tuple.key, true);
350
+ const previous = yield* S.readGrant(mapping, token.moduleId, token.grantId);
351
+
352
+ const activeSlot = yield* slot(
353
+ mapping,
354
+ token.moduleId,
355
+ native,
356
+ tuple.key,
357
+ token.configuration.profile.key,
358
+ );
359
+
360
+ const held = yield* exact(mapping, r);
361
+
362
+ if (held === undefined) return { _tag: "Rejected" } as const;
363
+ invariant(
364
+ token.exchangeOrder === r.order &&
365
+ validMetadata(token, held.now) &&
366
+ token.metadata.obtainedAtMillis >= r.claim.claimedAtMillis,
367
+ );
368
+
369
+ const target =
370
+ out.previous === undefined
371
+ ? previous === undefined
372
+ : previous !== undefined &&
373
+ F.sameTarget(out.previous, previous.context) &&
374
+ sameIdentity(previous.context.identity, token.identity) &&
375
+ previous.row[mapping.grant.refreshWork] !== "Unresolved" &&
376
+ canReplace(r, previous.context);
377
+
378
+ const previousSafe = yield* C.noFormerOwner(mapping, tuple.key, cl.id, native);
379
+
380
+ if (!owned || !previousSafe) {
381
+ yield* owner.update(f.table, held.key, { [f.state]: "Conflict", [f.version]: owner.marker });
382
+
383
+ return { _tag: "Conflict" } as const;
384
+ }
385
+ if (
386
+ out.quarantine ||
387
+ found === undefined ||
388
+ !held.active ||
389
+ !target ||
390
+ (activeSlot !== undefined && activeSlot.context.grantId !== token.grantId) ||
391
+ co.blocked ||
392
+ S.orderNumber(r.order) <= co.cutoff ||
393
+ co.generation !== token.cohortGeneration ||
394
+ token.metadata.useUntilMillis <= held.now ||
395
+ token.configuration.profile.issuance !== "active"
396
+ ) {
397
+ if (token.configuration.profile.revocation === "unsupported") {
398
+ // No remote cleanup can run for this profile. Discard the losing material;
399
+ // it must neither replace nor revoke the currently installed grant.
400
+ yield* owner.update(f.table, held.key, {
401
+ [f.state]: "Rejected",
402
+ [f.work]: "Resolved",
403
+ [f.subjectId]: native,
404
+ [f.cohortKey]: co.id,
405
+ [f.custody]: null,
406
+ [f.version]: owner.marker,
407
+ });
408
+ } else {
409
+ yield* C.fence(mapping, cl, co);
410
+ if (out.cleanup === undefined) return yield* unavailable();
411
+ yield* C.storeJob(mapping, out.cleanup, token, native, held.now);
412
+ yield* owner.update(f.table, held.key, {
413
+ [f.state]: "Quarantined",
414
+ [f.work]: "Resolved",
415
+ [f.subjectId]: native,
416
+ [f.cohortKey]: co.id,
417
+ [f.custody]: null,
418
+ [f.version]: owner.marker,
419
+ });
420
+ }
421
+
422
+ return { _tag: "Rejected" } as const;
423
+ }
424
+ yield* activateGrant(mapping, out.grant, native, held.now, previous !== undefined);
425
+ yield* owner.update(f.table, held.key, {
426
+ [f.state]: "Connected",
427
+ [f.work]: "Resolved",
428
+ [f.subjectId]: native,
429
+ [f.cohortKey]: co.id,
430
+ [f.custody]: null,
431
+ [f.version]: owner.marker,
432
+ });
433
+ owner.postconditions.push(
434
+ sql`${mapping.clock.engineNowMillis} >= ${held.now} and ${mapping.clock.engineNowMillis} < ${Math.min(r.claim.claimExpiresAtMillis, token.metadata.useUntilMillis)}`,
435
+ );
436
+
437
+ return { _tag: "Connected", grant: out.grant } as const;
438
+ });
@@ -173,13 +173,25 @@ const policyData = (input: OAuthConnectedPolicyInput<any>): OAuthConnectedPolicy
173
173
  ...input,
174
174
  subjectId: nativeCopy(input.subjectId),
175
175
  revision: snapshotOAuthSync(M.OAuthConnectedUseAuthorization.fields.revision, input.revision),
176
- authorization:
177
- input.kind === "action"
178
- ? snapshotOAuthSync(M.OAuthConnectedActionAuthorization, input.authorization)
179
- : snapshotOAuthSync(M.OAuthConnectedUseAuthorization, input.authorization),
180
- ...(input.kind === "action"
181
- ? { configuration: snapshotOAuthSync(M.OAuthConnectedConfiguration, input.configuration) }
182
- : {}),
176
+ ...(input.kind === "sign-in"
177
+ ? {
178
+ credential: snapshotOAuthSync(M.OAuthCredentialSnapshot, input.credential),
179
+ configuration: snapshotOAuthSync(M.OAuthConnectedConfiguration, input.configuration),
180
+ }
181
+ : {
182
+ authorization:
183
+ input.kind === "action"
184
+ ? snapshotOAuthSync(M.OAuthConnectedActionAuthorization, input.authorization)
185
+ : snapshotOAuthSync(M.OAuthConnectedUseAuthorization, input.authorization),
186
+ ...(input.kind === "action"
187
+ ? {
188
+ configuration: snapshotOAuthSync(
189
+ M.OAuthConnectedConfiguration,
190
+ input.configuration,
191
+ ),
192
+ }
193
+ : {}),
194
+ }),
183
195
  ...(input.grant === undefined
184
196
  ? {}
185
197
  : { grant: snapshotOAuthSync(M.OAuthConnectedTokenContext, input.grant) }),
@@ -10,7 +10,7 @@ import {
10
10
  } from "@yielded/auth/OAuth";
11
11
  /* oxlint-disable no-explicit-any -- concrete adapters retain the native database, table and ID types. */
12
12
  import { Effect, Layer, Schema } from "effect";
13
- import type { Statement } from "effect/unstable/sql/Statement";
13
+ import type { Statement } from "effect/sql/Statement";
14
14
 
15
15
  import type { PersistenceMappingError } from "./model";
16
16
  import * as A from "./oauth-connected-access";
@@ -19,6 +19,7 @@ import { connectedInputs, connectedRevocationInputs } from "./oauth-connected-in
19
19
  import * as Maintenance from "./oauth-connected-maintenance";
20
20
  import * as Management from "./oauth-connected-management";
21
21
  import { settle } from "./oauth-connected-settlement";
22
+ import * as SignIn from "./oauth-connected-sign-in";
22
23
  import * as S from "./oauth-connected-state";
23
24
  import { capturedOAuthService } from "./oauth-input";
24
25
  import { CurrentOAuthTransaction } from "./oauth-owner";
@@ -124,6 +125,14 @@ export const makeConnected = (
124
125
  ): OAuthConnectedPersistence["Service"] =>
125
126
  capturedOAuthService<OAuthConnectedPersistence["Service"]>(
126
127
  {
128
+ claimSignIn: mutation(mapping, execute, SignIn.claimSignIn, M.OAuthSignInAccessClaim),
129
+ inspectSignIn: read(mapping, execute, SignIn.inspectSignIn),
130
+ settleSignIn: mutation(
131
+ mapping,
132
+ execute,
133
+ SignIn.settleSignIn,
134
+ M.OAuthConnectedSettlementDecision,
135
+ ),
127
136
  capture: read(mapping, execute, F.capture),
128
137
  issue: mutation(mapping, execute, F.issue, M.OAuthConnectedIssueDecision),
129
138
  preflight: read(mapping, execute, F.preflight),
@@ -10,7 +10,8 @@ import {
10
10
  snapshotOAuthSync,
11
11
  } from "@yielded/auth/OAuth";
12
12
  import type { AuthenticationRequirement } from "@yielded/auth/Sessions";
13
- import { DateTime, Effect, Encoding, Schema } from "effect";
13
+ import { DateTime, Effect, Schema } from "effect";
14
+ import { Base64Url } from "effect/encoding";
14
15
 
15
16
  const encoder = new TextEncoder();
16
17
  const decoder = new TextDecoder("utf-8", { fatal: true });
@@ -21,8 +22,8 @@ export const invariant: (value: unknown) => asserts value = (value) => {
21
22
  if (!value) throw unavailable();
22
23
  };
23
24
 
24
- export const nonce = () => Encoding.encodeBase64Url(randomBytes(32));
25
- export const digest = (value: string) => Encoding.encodeBase64Url(sha256(encoder.encode(value)));
25
+ export const nonce = () => Base64Url.encode(randomBytes(32));
26
+ export const digest = (value: string) => Base64Url.encode(sha256(encoder.encode(value)));
26
27
 
27
28
  /** Capture configuration/callback references while preserving Drizzle table, SQL
28
29
  * and Effect objects. Never freeze or mutate the consumer's original graph. */
@@ -84,7 +85,7 @@ export const oauthIdentityKey = (input: typeof OAuthExternalIdentity.Type): stri
84
85
  offset += 4 + value.length;
85
86
  }
86
87
 
87
- return "v1:" + Encoding.encodeBase64Url(sha256(packed));
88
+ return "v1:" + Base64Url.encode(sha256(packed));
88
89
  };
89
90
 
90
91
  export const sameIdentity = (
@@ -28,7 +28,7 @@ import type { SecurityRevision } from "@yielded/auth/Sessions";
28
28
  /* oxlint-disable no-explicit-any -- concrete driver entry points restore table/database generics. */
29
29
  import { eq, sql } from "drizzle-orm";
30
30
  import { type Context, Effect, Layer } from "effect";
31
- import type { Statement } from "effect/unstable/sql/Statement";
31
+ import type { Statement } from "effect/sql/Statement";
32
32
 
33
33
  import type { PersistenceMappingError } from "./model";
34
34
  import {
@@ -13,7 +13,7 @@ import {
13
13
  import { PasswordUnavailable, type PasswordPreparedPersistence } from "@yielded/auth/Password";
14
14
  /* oxlint-disable no-explicit-any -- concrete driver modules restore native table/database types. */
15
15
  import { type Context, Cause, Effect, Layer } from "effect";
16
- import type * as SqlError from "effect/unstable/sql/SqlError";
16
+ import type * as SqlError from "effect/sql/SqlError";
17
17
 
18
18
  import { makeSqlPasswordPreparedPersistence } from "./password-prepared-sql";
19
19
  import type { PasswordSqlDatabase } from "./password-sql";