@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
@@ -1,687 +0,0 @@
1
- import { AuthStore, EmailOtpRejected } from "@yielded/auth/AuthStore";
2
- import { AuthRateLimited, AuthStoreError, InvalidRegistration } from "@yielded/auth/Errors";
3
- import {
4
- InvalidOAuthState,
5
- OAuthStateDecisions,
6
- OAuthStateStore,
7
- type OAuthState,
8
- } from "@yielded/auth/OAuth";
9
- import { AuthStoreDecisions, reportPersistenceFailure } from "@yielded/auth/Persistence";
10
- import {
11
- timingSafeStringEqual,
12
- VerifiedEmail,
13
- type ConsumeChallenge,
14
- type ConsumeRegistration,
15
- type NewChallenge,
16
- type NewRegistration,
17
- type TokenDigest,
18
- } from "@yielded/auth/Schema";
19
- /* oxlint-disable no-explicit-any -- Drizzle's generic query builders lose the concrete consumer table through a runtime column map. Assertions stay in this adapter. */
20
- import { and, eq, gt, sql, type AnyRelations, type InferSelectModel } from "drizzle-orm";
21
- import type { EffectMysql2Database } from "drizzle-orm/effect-mysql2";
22
- import type { AnyMySqlTable, MySqlColumn } from "drizzle-orm/mysql-core";
23
- import { Cause, Context, DateTime, Duration, Effect, Schema } from "effect";
24
- import type { SqlError } from "effect/unstable/sql/SqlError";
25
-
26
- import {
27
- column,
28
- requireStandaloneConsume,
29
- updateValues,
30
- type AuthTables,
31
- type AuthStoreTables,
32
- type ChallengeConsumeDecision,
33
- type ConsumeDecision,
34
- type OAuthStateTables,
35
- } from "./model";
36
- import type { SuppliedService } from "./SuppliedService";
37
-
38
- type Database = EffectMysql2Database<AnyRelations>;
39
- type RuntimeDatabase = EffectMysql2Database<any> & Parameters<typeof requireStandaloneConsume>[0];
40
- type TransactionOf<D extends Database> = Parameters<Parameters<D["transaction"]>[0]>[0];
41
-
42
- const selected = <T extends AnyMySqlTable>(row: unknown): InferSelectModel<T> =>
43
- row as InferSelectModel<T>;
44
-
45
- const readError = () => AuthStoreError.make({ message: "Auth store read failed" });
46
- const writeError = () => AuthStoreError.make({ message: "Auth store write failed" });
47
- const isStoreFailure = Schema.is(AuthStoreError);
48
-
49
- const mapStoreFailure =
50
- (failure: () => AuthStoreError) =>
51
- <A, E, R>(effect: Effect.Effect<A, E, R>): Effect.Effect<A, AuthStoreError, R> =>
52
- reportPersistenceFailure(effect, isStoreFailure).pipe(
53
- Effect.catchCause((cause) => Effect.failCause(Cause.map(cause, failure))),
54
- );
55
-
56
- const mapStoreReadFailure = mapStoreFailure(readError);
57
- const mapStoreWriteFailure = mapStoreFailure(writeError);
58
-
59
- /** MySQL/InnoDB implementation. Multi-step decisions stay values until commit. */
60
- export const makeMysqlAuthStoreServices = <C extends AnyMySqlTable, R extends AnyMySqlTable>(
61
- database: Database,
62
- mapping: AuthStoreTables<C, R>,
63
- ) => {
64
- const db = database as RuntimeDatabase;
65
- const challengeTable = mapping.challenge.table;
66
- const ck = mapping.challenge.columns;
67
-
68
- const c = Object.fromEntries(
69
- Object.entries(ck).map(([name, key]) => [name, column(challengeTable, key)]),
70
- ) as Record<keyof typeof ck, MySqlColumn>;
71
-
72
- const registrationTable = mapping.registration.table;
73
- const rk = mapping.registration.columns;
74
-
75
- const r = Object.fromEntries(
76
- Object.entries(rk).map(([name, key]) => [name, column(registrationTable, key)]),
77
- ) as Record<keyof typeof rk, MySqlColumn>;
78
-
79
- const issueChallenge = Effect.fn("DrizzleMysqlAuthStore.issueChallenge")(function* (
80
- input: NewChallenge,
81
- ) {
82
- const result = yield* Effect.suspend(() =>
83
- db.transaction((tx) =>
84
- Effect.gen(function* () {
85
- // A locking read of a missing InnoDB key takes a gap lock. Concurrent
86
- // first issuances would then deadlock when each transaction inserts.
87
- // The no-op upsert serializes on the declared series/digest unique
88
- // indexes before the row is read and evaluated below.
89
- yield* tx
90
- .insert(challengeTable as any)
91
- .values(
92
- mapping.challenge.encodeInsert(input, {
93
- namespace: mapping.challenge.identifierNamespace,
94
- failedAttempts: 0,
95
- consumed: false,
96
- }) as any,
97
- )
98
- .onDuplicateKeyUpdate({
99
- set: updateValues<C>([[ck.challengeId, c.challengeId]]) as any,
100
- });
101
-
102
- const rows = yield* tx
103
- .select()
104
- .from(challengeTable as any)
105
- .where(
106
- and(
107
- eq(c.namespace, mapping.challenge.identifierNamespace),
108
- eq(c.value, input.email),
109
- eq(c.purpose, input.purpose),
110
- ),
111
- )
112
- .for("update")
113
- .limit(1);
114
-
115
- // A digest collision against another series is not a replay and must
116
- // never overwrite that row.
117
- if (rows[0] === undefined)
118
- return yield* AuthStoreError.make({ message: "Challenge digest conflict" });
119
- const existing = yield* mapping.challenge.decode(selected<C>(rows[0]));
120
- const now = yield* DateTime.now;
121
-
122
- const exactReplay =
123
- existing.challenge.challengeId === input.challengeId &&
124
- timingSafeStringEqual(existing.challenge.tokenDigest, input.tokenDigest) &&
125
- existing.challenge.email === input.email &&
126
- existing.challenge.purpose === input.purpose &&
127
- existing.challenge.otpKeyId === input.otpKeyId &&
128
- timingSafeStringEqual(existing.challenge.otpDigest, input.otpDigest) &&
129
- DateTime.toEpochMillis(existing.challenge.issuedAt) ===
130
- DateTime.toEpochMillis(input.issuedAt) &&
131
- DateTime.toEpochMillis(existing.challenge.expiresAt) ===
132
- DateTime.toEpochMillis(input.expiresAt) &&
133
- existing.challenge.attemptLimit === input.attemptLimit &&
134
- Duration.toMillis(existing.challenge.resendCooldown) ===
135
- Duration.toMillis(input.resendCooldown);
136
-
137
- // Retrying an identical still-live request is idempotent. In
138
- // particular, it does not reset the persisted wrong-attempt budget.
139
- if (
140
- exactReplay &&
141
- !existing.consumed &&
142
- DateTime.toEpochMillis(existing.challenge.expiresAt) > DateTime.toEpochMillis(now)
143
- )
144
- return { _tag: "issued" } as const;
145
- if (exactReplay) return { _tag: "limited", retryAfterSeconds: 1 } as const;
146
- if (
147
- existing.challenge.challengeId === input.challengeId ||
148
- DateTime.toEpochMillis(input.issuedAt) <=
149
- DateTime.toEpochMillis(existing.challenge.issuedAt)
150
- )
151
- return { _tag: "limited", retryAfterSeconds: 1 } as const;
152
-
153
- const eligibleAt =
154
- DateTime.toEpochMillis(existing.challenge.issuedAt) +
155
- Duration.toMillis(input.resendCooldown);
156
-
157
- if (!existing.consumed && DateTime.toEpochMillis(now) < eligibleAt) {
158
- return {
159
- _tag: "limited",
160
- retryAfterSeconds: Math.max(
161
- 1,
162
- Math.ceil((eligibleAt - DateTime.toEpochMillis(now)) / 1000),
163
- ),
164
- } as const;
165
- }
166
-
167
- const failedAttempts =
168
- DateTime.toEpochMillis(existing.challenge.expiresAt) > DateTime.toEpochMillis(now)
169
- ? existing.failedAttempts
170
- : 0;
171
-
172
- yield* tx
173
- .update(challengeTable as any)
174
- .set(
175
- mapping.challenge.encodeUpdate(input, {
176
- namespace: mapping.challenge.identifierNamespace,
177
- failedAttempts,
178
- consumed: false,
179
- }) as any,
180
- )
181
- .where(
182
- and(
183
- eq(c.namespace, mapping.challenge.identifierNamespace),
184
- eq(c.value, input.email),
185
- eq(c.purpose, input.purpose),
186
- ),
187
- );
188
-
189
- return { _tag: "issued" } as const;
190
- }),
191
- ),
192
- ).pipe(mapStoreWriteFailure);
193
-
194
- if (result._tag === "limited")
195
- return yield* AuthRateLimited.make({ retryAfterSeconds: result.retryAfterSeconds });
196
- });
197
-
198
- const consumeChallengeDecision = Effect.fn("DrizzleMysqlAuthStore.consumeChallengeDecision")(
199
- function* (
200
- input: ConsumeChallenge,
201
- ): Effect.fn.Return<ChallengeConsumeDecision, AuthStoreError> {
202
- return yield* Effect.suspend(() =>
203
- db.transaction((tx) =>
204
- Effect.gen(function* () {
205
- const rows = yield* tx
206
- .select()
207
- .from(challengeTable as any)
208
- .where(eq(c.tokenDigest, input.tokenDigest))
209
- .for("update")
210
- .limit(1);
211
-
212
- if (rows[0] === undefined)
213
- return { _tag: "rejected", reason: "missing_challenge" } as const;
214
- const row = yield* mapping.challenge.decode(selected<C>(rows[0]));
215
- const now = yield* DateTime.now;
216
-
217
- if (row.consumed) return { _tag: "rejected", reason: "consumed_challenge" } as const;
218
- if (DateTime.toEpochMillis(row.challenge.expiresAt) <= DateTime.toEpochMillis(now))
219
- return { _tag: "rejected", reason: "expired_challenge" } as const;
220
- if (row.failedAttempts >= row.challenge.attemptLimit)
221
- return { _tag: "rejected", reason: "attempts_exhausted" } as const;
222
- const candidate = input.otpDigests[row.challenge.otpKeyId];
223
-
224
- const matches =
225
- candidate !== undefined && timingSafeStringEqual(candidate, row.challenge.otpDigest);
226
-
227
- yield* tx
228
- .update(challengeTable as any)
229
- .set(
230
- updateValues<C>(
231
- matches
232
- ? [[ck.consumed, true]]
233
- : [[ck.failedAttempts, sql`${c.failedAttempts} + 1`]],
234
- ) as any,
235
- )
236
- .where(eq(c.tokenDigest, input.tokenDigest));
237
-
238
- return matches
239
- ? ({
240
- _tag: "accepted",
241
- value: VerifiedEmail.make({
242
- email: row.challenge.email,
243
- purpose: row.challenge.purpose,
244
- }),
245
- } as const)
246
- : ({ _tag: "rejected", reason: "bad_code" } as const);
247
- }),
248
- ),
249
- ).pipe(mapStoreWriteFailure);
250
- },
251
- );
252
-
253
- const consumeChallenge = Effect.fn("DrizzleMysqlAuthStore.consumeChallenge")(function* (
254
- input: ConsumeChallenge,
255
- ) {
256
- yield* requireStandaloneConsume(db);
257
- const decision = yield* consumeChallengeDecision(input);
258
-
259
- return decision._tag === "accepted"
260
- ? decision.value
261
- : yield* EmailOtpRejected.make({ reason: decision.reason });
262
- });
263
-
264
- const issueRegistration = Effect.fn("DrizzleMysqlAuthStore.issueRegistration")(function* (
265
- input: NewRegistration,
266
- ) {
267
- yield* Effect.suspend(() =>
268
- db
269
- .insert(registrationTable as any)
270
- .values(mapping.registration.encodeInsert(input, false) as any),
271
- ).pipe(mapStoreWriteFailure);
272
- });
273
-
274
- const inspectRegistration = Effect.fn("DrizzleMysqlAuthStore.inspectRegistration")(function* (
275
- digest: TokenDigest,
276
- ) {
277
- const now = yield* DateTime.now;
278
-
279
- const rows = yield* Effect.suspend(() =>
280
- db
281
- .select()
282
- .from(registrationTable as any)
283
- .where(
284
- and(
285
- eq(r.tokenDigest, digest),
286
- eq(r.consumed, false),
287
- gt(r.expiresAt, mapping.registration.encodeInstant(now)),
288
- ),
289
- )
290
- .limit(1),
291
- ).pipe(mapStoreReadFailure);
292
-
293
- if (rows[0] === undefined) return yield* InvalidRegistration.make();
294
-
295
- return (yield* Effect.suspend(() => mapping.registration.decode(selected<R>(rows[0]))).pipe(
296
- mapStoreReadFailure,
297
- )).registration;
298
- });
299
-
300
- const consumeRegistrationDecision = Effect.fn(
301
- "DrizzleMysqlAuthStore.consumeRegistrationDecision",
302
- )(function* (
303
- input: ConsumeRegistration,
304
- ): Effect.fn.Return<ConsumeDecision<void>, AuthStoreError> {
305
- return yield* Effect.suspend(() =>
306
- db.transaction((tx) =>
307
- Effect.gen(function* () {
308
- const rows = yield* tx
309
- .select()
310
- .from(registrationTable as any)
311
- .where(eq(r.tokenDigest, input.tokenDigest))
312
- .for("update")
313
- .limit(1);
314
-
315
- if (rows[0] === undefined) return { _tag: "rejected" } as const;
316
- const row = yield* mapping.registration.decode(selected<R>(rows[0]));
317
- const now = yield* DateTime.now;
318
-
319
- if (
320
- row.consumed ||
321
- DateTime.toEpochMillis(row.registration.expiresAt) <= DateTime.toEpochMillis(now)
322
- )
323
- return { _tag: "rejected" } as const;
324
- yield* tx
325
- .update(registrationTable as any)
326
- .set(updateValues<R>([[rk.consumed, true]]) as any)
327
- .where(eq(r.tokenDigest, input.tokenDigest));
328
-
329
- return { _tag: "accepted", value: undefined } as const;
330
- }),
331
- ),
332
- ).pipe(mapStoreWriteFailure);
333
- });
334
-
335
- const consumeRegistration = Effect.fn("DrizzleMysqlAuthStore.consumeRegistration")(function* (
336
- input: ConsumeRegistration,
337
- ) {
338
- yield* requireStandaloneConsume(db);
339
- const decision = yield* consumeRegistrationDecision(input);
340
-
341
- if (decision._tag === "rejected") return yield* InvalidRegistration.make();
342
- });
343
-
344
- return {
345
- authStore: AuthStore.of({
346
- issueChallenge,
347
- consumeChallenge,
348
- issueRegistration,
349
- inspectRegistration,
350
- consumeRegistration,
351
- }),
352
- decisions: {
353
- consumeChallenge: consumeChallengeDecision,
354
- consumeRegistration: consumeRegistrationDecision,
355
- },
356
- } as const;
357
- };
358
-
359
- export const makeMysqlOAuthStateServices = <S extends AnyMySqlTable>(
360
- database: Database,
361
- mapping: OAuthStateTables<S>,
362
- ) => {
363
- const db = database as RuntimeDatabase;
364
- const stateTable = mapping.oauthState.table;
365
- const sk = mapping.oauthState.columns;
366
-
367
- const s = Object.fromEntries(
368
- Object.entries(sk).map(([name, key]) => [name, column(stateTable, key)]),
369
- ) as Record<keyof typeof sk, MySqlColumn>;
370
-
371
- const issue = Effect.fn("DrizzleMysqlOAuthStateStore.issue")(function* (input: OAuthState) {
372
- yield* Effect.suspend(() =>
373
- db.insert(stateTable as any).values(mapping.oauthState.encodeInsert(input, false) as any),
374
- ).pipe(mapStoreWriteFailure);
375
- });
376
-
377
- const consumeDecision = Effect.fn("DrizzleMysqlOAuthStateStore.consumeDecision")(function* (
378
- digest: TokenDigest,
379
- ): Effect.fn.Return<ConsumeDecision<OAuthState>, AuthStoreError> {
380
- return yield* Effect.suspend(() =>
381
- db.transaction((tx) =>
382
- Effect.gen(function* () {
383
- const rows = yield* tx
384
- .select()
385
- .from(stateTable as any)
386
- .where(eq(s.stateDigest, digest))
387
- .for("update")
388
- .limit(1);
389
-
390
- if (rows[0] === undefined) return { _tag: "rejected" } as const;
391
- const row = yield* mapping.oauthState.decode(selected<S>(rows[0]));
392
- const now = yield* DateTime.now;
393
-
394
- if (
395
- row.consumed ||
396
- DateTime.toEpochMillis(row.state.expiresAt) <= DateTime.toEpochMillis(now)
397
- )
398
- return { _tag: "rejected" } as const;
399
- yield* tx
400
- .update(stateTable as any)
401
- .set(updateValues<S>([[sk.consumed, true]]) as any)
402
- .where(eq(s.stateDigest, digest));
403
-
404
- return { _tag: "accepted", value: row.state } as const;
405
- }),
406
- ),
407
- ).pipe(mapStoreWriteFailure);
408
- });
409
-
410
- const consume = Effect.fn("DrizzleMysqlOAuthStateStore.consume")(function* (digest: TokenDigest) {
411
- yield* requireStandaloneConsume(db);
412
- const decision = yield* consumeDecision(digest);
413
-
414
- return decision._tag === "accepted" ? decision.value : yield* InvalidOAuthState.make();
415
- });
416
-
417
- return {
418
- oauthStateStore: OAuthStateStore.of({ issue, consume }),
419
- decisions: { consumeOAuthState: consumeDecision },
420
- } as const;
421
- };
422
-
423
- export const makeMysqlAuthServices = <
424
- C extends AnyMySqlTable,
425
- R extends AnyMySqlTable,
426
- S extends AnyMySqlTable,
427
- >(
428
- database: Database,
429
- mapping: AuthTables<C, R, S>,
430
- ) => {
431
- const auth = makeMysqlAuthStoreServices(database, mapping);
432
- const oauth = makeMysqlOAuthStateServices(database, mapping);
433
-
434
- return { ...auth, ...oauth, decisions: { ...auth.decisions, ...oauth.decisions } } as const;
435
- };
436
-
437
- /**
438
- * Owns the native outer commit. Keep rejection as a decision value inside the
439
- * owner; interruption before commit rolls back. This adapter never retries a
440
- * deadlock inside the caller's transaction.
441
- */
442
- export function coordinateMysqlAuthStoreTransaction<
443
- D extends Database,
444
- C extends AnyMySqlTable,
445
- R extends AnyMySqlTable,
446
- A,
447
- E,
448
- Requirements,
449
- DatabaseError,
450
- DatabaseRequirements,
451
- >(
452
- acquire: Effect.Effect<D, DatabaseError, DatabaseRequirements>,
453
- options: { readonly mapping: AuthStoreTables<C, R>; readonly transaction?: never },
454
- body: Effect.Effect<A, E, Requirements>,
455
- ): Effect.Effect<
456
- A,
457
- E | DatabaseError | SqlError,
458
- Exclude<Requirements, AuthStoreDecisions> | DatabaseRequirements
459
- >;
460
-
461
- export function coordinateMysqlAuthStoreTransaction<
462
- D extends Database,
463
- C extends AnyMySqlTable,
464
- R extends AnyMySqlTable,
465
- A,
466
- E,
467
- Requirements,
468
- DatabaseError,
469
- DatabaseRequirements,
470
- TxId,
471
- TxShape,
472
- >(
473
- acquire: Effect.Effect<D, DatabaseError, DatabaseRequirements>,
474
- options: {
475
- readonly mapping: AuthStoreTables<C, R>;
476
- readonly transaction: SuppliedService<TxId, NoInfer<TransactionOf<D>>, TxShape>;
477
- },
478
- body: Effect.Effect<A, E, Requirements>,
479
- ): Effect.Effect<
480
- A,
481
- E | DatabaseError | SqlError,
482
- Exclude<Requirements, AuthStoreDecisions | TxId> | DatabaseRequirements
483
- >;
484
-
485
- export function coordinateMysqlAuthStoreTransaction<
486
- D extends Database,
487
- C extends AnyMySqlTable,
488
- R extends AnyMySqlTable,
489
- A,
490
- E,
491
- Requirements,
492
- DatabaseError,
493
- DatabaseRequirements,
494
- TxId,
495
- TxShape,
496
- >(
497
- acquire: Effect.Effect<D, DatabaseError, DatabaseRequirements>,
498
- options: {
499
- readonly mapping: AuthStoreTables<C, R>;
500
- readonly transaction?: SuppliedService<TxId, NoInfer<TransactionOf<D>>, TxShape>;
501
- },
502
- body: Effect.Effect<A, E, Requirements>,
503
- ) {
504
- return Effect.flatMap(acquire, (database) =>
505
- database.transaction<A, E, Exclude<Requirements, AuthStoreDecisions>>((transaction) => {
506
- const decisions = makeMysqlAuthStoreServices(transaction as never, options.mapping).decisions;
507
- const provided = Context.make(AuthStoreDecisions, decisions);
508
- const work = Effect.provideContext(body, provided);
509
-
510
- return options.transaction === undefined
511
- ? work
512
- : Effect.provideService(
513
- work,
514
- options.transaction,
515
- options.transaction.of(transaction as TransactionOf<D>),
516
- );
517
- }),
518
- );
519
- }
520
-
521
- export function coordinateMysqlOAuthStateTransaction<
522
- D extends Database,
523
- S extends AnyMySqlTable,
524
- A,
525
- E,
526
- Requirements,
527
- DatabaseError,
528
- DatabaseRequirements,
529
- >(
530
- acquire: Effect.Effect<D, DatabaseError, DatabaseRequirements>,
531
- options: { readonly mapping: OAuthStateTables<S>; readonly transaction?: never },
532
- body: Effect.Effect<A, E, Requirements>,
533
- ): Effect.Effect<
534
- A,
535
- E | DatabaseError | SqlError,
536
- Exclude<Requirements, OAuthStateDecisions> | DatabaseRequirements
537
- >;
538
-
539
- export function coordinateMysqlOAuthStateTransaction<
540
- D extends Database,
541
- S extends AnyMySqlTable,
542
- A,
543
- E,
544
- Requirements,
545
- DatabaseError,
546
- DatabaseRequirements,
547
- TxId,
548
- TxShape,
549
- >(
550
- acquire: Effect.Effect<D, DatabaseError, DatabaseRequirements>,
551
- options: {
552
- readonly mapping: OAuthStateTables<S>;
553
- readonly transaction: SuppliedService<TxId, NoInfer<TransactionOf<D>>, TxShape>;
554
- },
555
- body: Effect.Effect<A, E, Requirements>,
556
- ): Effect.Effect<
557
- A,
558
- E | DatabaseError | SqlError,
559
- Exclude<Requirements, OAuthStateDecisions | TxId> | DatabaseRequirements
560
- >;
561
-
562
- export function coordinateMysqlOAuthStateTransaction<
563
- D extends Database,
564
- S extends AnyMySqlTable,
565
- A,
566
- E,
567
- Requirements,
568
- DatabaseError,
569
- DatabaseRequirements,
570
- TxId,
571
- TxShape,
572
- >(
573
- acquire: Effect.Effect<D, DatabaseError, DatabaseRequirements>,
574
- options: {
575
- readonly mapping: OAuthStateTables<S>;
576
- readonly transaction?: SuppliedService<TxId, NoInfer<TransactionOf<D>>, TxShape>;
577
- },
578
- body: Effect.Effect<A, E, Requirements>,
579
- ) {
580
- return Effect.flatMap(acquire, (database) =>
581
- database.transaction<A, E, Exclude<Requirements, OAuthStateDecisions>>((transaction) => {
582
- const decisions = makeMysqlOAuthStateServices(
583
- transaction as never,
584
- options.mapping,
585
- ).decisions;
586
-
587
- const provided = Context.make(OAuthStateDecisions, decisions);
588
- const work = Effect.provideContext(body, provided);
589
-
590
- return options.transaction === undefined
591
- ? work
592
- : Effect.provideService(
593
- work,
594
- options.transaction,
595
- options.transaction.of(transaction as TransactionOf<D>),
596
- );
597
- }),
598
- );
599
- }
600
-
601
- export function coordinateMysqlAuthTransaction<
602
- D extends Database,
603
- C extends AnyMySqlTable,
604
- R extends AnyMySqlTable,
605
- S extends AnyMySqlTable,
606
- A,
607
- E,
608
- Requirements,
609
- DatabaseError,
610
- DatabaseRequirements,
611
- >(
612
- acquire: Effect.Effect<D, DatabaseError, DatabaseRequirements>,
613
- options: { readonly mapping: AuthTables<C, R, S>; readonly transaction?: never },
614
- body: Effect.Effect<A, E, Requirements>,
615
- ): Effect.Effect<
616
- A,
617
- E | DatabaseError | SqlError,
618
- Exclude<Requirements, AuthStoreDecisions | OAuthStateDecisions> | DatabaseRequirements
619
- >;
620
-
621
- export function coordinateMysqlAuthTransaction<
622
- D extends Database,
623
- C extends AnyMySqlTable,
624
- R extends AnyMySqlTable,
625
- S extends AnyMySqlTable,
626
- A,
627
- E,
628
- Requirements,
629
- DatabaseError,
630
- DatabaseRequirements,
631
- TxId,
632
- TxShape,
633
- >(
634
- acquire: Effect.Effect<D, DatabaseError, DatabaseRequirements>,
635
- options: {
636
- readonly mapping: AuthTables<C, R, S>;
637
- readonly transaction: SuppliedService<TxId, NoInfer<TransactionOf<D>>, TxShape>;
638
- },
639
- body: Effect.Effect<A, E, Requirements>,
640
- ): Effect.Effect<
641
- A,
642
- E | DatabaseError | SqlError,
643
- Exclude<Requirements, AuthStoreDecisions | OAuthStateDecisions | TxId> | DatabaseRequirements
644
- >;
645
-
646
- export function coordinateMysqlAuthTransaction<
647
- D extends Database,
648
- C extends AnyMySqlTable,
649
- R extends AnyMySqlTable,
650
- S extends AnyMySqlTable,
651
- A,
652
- E,
653
- Requirements,
654
- DatabaseError,
655
- DatabaseRequirements,
656
- TxId,
657
- TxShape,
658
- >(
659
- acquire: Effect.Effect<D, DatabaseError, DatabaseRequirements>,
660
- options: {
661
- readonly mapping: AuthTables<C, R, S>;
662
- readonly transaction?: SuppliedService<TxId, NoInfer<TransactionOf<D>>, TxShape>;
663
- },
664
- body: Effect.Effect<A, E, Requirements>,
665
- ) {
666
- return Effect.flatMap(acquire, (database) =>
667
- database.transaction<A, E, Exclude<Requirements, AuthStoreDecisions | OAuthStateDecisions>>(
668
- (transaction) => {
669
- const decisions = makeMysqlAuthServices(transaction as never, options.mapping).decisions;
670
-
671
- const provided = Context.make(AuthStoreDecisions, decisions).pipe(
672
- Context.add(OAuthStateDecisions, decisions),
673
- );
674
-
675
- const work = Effect.provideContext(body, provided);
676
-
677
- return options.transaction === undefined
678
- ? work
679
- : Effect.provideService(
680
- work,
681
- options.transaction,
682
- options.transaction.of(transaction as TransactionOf<D>),
683
- );
684
- },
685
- ),
686
- );
687
- }