@corgelabs/corge 0.0.1 → 1.0.0

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 (341) hide show
  1. package/dist/ai/index.d.ts +3 -0
  2. package/dist/ai/index.d.ts.map +1 -0
  3. package/dist/ai/index.js +3 -0
  4. package/dist/ai/index.js.map +1 -0
  5. package/dist/ai/schema.d.ts +83 -0
  6. package/dist/ai/schema.d.ts.map +1 -0
  7. package/dist/ai/schema.js +19 -0
  8. package/dist/ai/schema.js.map +1 -0
  9. package/dist/ai/service.d.ts +14 -0
  10. package/dist/ai/service.d.ts.map +1 -0
  11. package/dist/ai/service.js +266 -0
  12. package/dist/ai/service.js.map +1 -0
  13. package/dist/ai/utils.d.ts +10 -0
  14. package/dist/ai/utils.d.ts.map +1 -0
  15. package/dist/ai/utils.js +27 -0
  16. package/dist/ai/utils.js.map +1 -0
  17. package/dist/attachments/index.d.ts +4 -0
  18. package/dist/attachments/index.d.ts.map +1 -0
  19. package/dist/attachments/index.js +4 -0
  20. package/dist/attachments/index.js.map +1 -0
  21. package/dist/attachments/routes.d.ts +75 -0
  22. package/dist/attachments/routes.d.ts.map +1 -0
  23. package/dist/attachments/routes.js +18 -0
  24. package/dist/attachments/routes.js.map +1 -0
  25. package/dist/attachments/schema.d.ts +61 -0
  26. package/dist/attachments/schema.d.ts.map +1 -0
  27. package/dist/attachments/schema.js +30 -0
  28. package/dist/attachments/schema.js.map +1 -0
  29. package/dist/attachments/service.d.ts +27 -0
  30. package/dist/attachments/service.d.ts.map +1 -0
  31. package/dist/attachments/service.js +224 -0
  32. package/dist/attachments/service.js.map +1 -0
  33. package/dist/attachments/utils.d.ts +9 -0
  34. package/dist/attachments/utils.d.ts.map +1 -0
  35. package/dist/attachments/utils.js +80 -0
  36. package/dist/attachments/utils.js.map +1 -0
  37. package/dist/auth/index.d.ts +4 -0
  38. package/dist/auth/index.d.ts.map +1 -0
  39. package/dist/auth/index.js +4 -0
  40. package/dist/auth/index.js.map +1 -0
  41. package/dist/auth/oidc.d.ts +25 -0
  42. package/dist/auth/oidc.d.ts.map +1 -0
  43. package/dist/auth/oidc.js +298 -0
  44. package/dist/auth/oidc.js.map +1 -0
  45. package/dist/auth/recovery.d.ts +41 -0
  46. package/dist/auth/recovery.d.ts.map +1 -0
  47. package/dist/auth/recovery.js +250 -0
  48. package/dist/auth/recovery.js.map +1 -0
  49. package/dist/auth/routes.d.ts +226 -0
  50. package/dist/auth/routes.d.ts.map +1 -0
  51. package/dist/auth/routes.js +96 -0
  52. package/dist/auth/routes.js.map +1 -0
  53. package/dist/auth/schema.d.ts +158 -0
  54. package/dist/auth/schema.d.ts.map +1 -0
  55. package/dist/auth/schema.js +82 -0
  56. package/dist/auth/schema.js.map +1 -0
  57. package/dist/auth/service.d.ts +96 -0
  58. package/dist/auth/service.d.ts.map +1 -0
  59. package/dist/auth/service.js +129 -0
  60. package/dist/auth/service.js.map +1 -0
  61. package/dist/auth/session.d.ts +51 -0
  62. package/dist/auth/session.d.ts.map +1 -0
  63. package/dist/auth/session.js +344 -0
  64. package/dist/auth/session.js.map +1 -0
  65. package/dist/auth/utils.d.ts +17 -0
  66. package/dist/auth/utils.d.ts.map +1 -0
  67. package/dist/auth/utils.js +96 -0
  68. package/dist/auth/utils.js.map +1 -0
  69. package/dist/bin/corge.js +547 -0
  70. package/dist/chat/context.d.ts +10 -0
  71. package/dist/chat/context.d.ts.map +1 -0
  72. package/dist/chat/context.js +221 -0
  73. package/dist/chat/context.js.map +1 -0
  74. package/dist/chat/index.d.ts +4 -0
  75. package/dist/chat/index.d.ts.map +1 -0
  76. package/dist/chat/index.js +4 -0
  77. package/dist/chat/index.js.map +1 -0
  78. package/dist/chat/routes.d.ts +160 -0
  79. package/dist/chat/routes.d.ts.map +1 -0
  80. package/dist/chat/routes.js +210 -0
  81. package/dist/chat/routes.js.map +1 -0
  82. package/dist/chat/schema.d.ts +285 -0
  83. package/dist/chat/schema.d.ts.map +1 -0
  84. package/dist/chat/schema.js +74 -0
  85. package/dist/chat/schema.js.map +1 -0
  86. package/dist/chat/service.d.ts +64 -0
  87. package/dist/chat/service.d.ts.map +1 -0
  88. package/dist/chat/service.js +279 -0
  89. package/dist/chat/service.js.map +1 -0
  90. package/dist/chat/stream.d.ts +53 -0
  91. package/dist/chat/stream.d.ts.map +1 -0
  92. package/dist/chat/stream.js +339 -0
  93. package/dist/chat/stream.js.map +1 -0
  94. package/dist/chat/utils.d.ts +61 -0
  95. package/dist/chat/utils.d.ts.map +1 -0
  96. package/dist/chat/utils.js +285 -0
  97. package/dist/chat/utils.js.map +1 -0
  98. package/dist/cli/builder.d.ts +6 -0
  99. package/dist/cli/builder.d.ts.map +1 -0
  100. package/dist/cli/builder.js +91 -0
  101. package/dist/cli/builder.js.map +1 -0
  102. package/dist/cli/deployer.d.ts +2 -0
  103. package/dist/cli/deployer.d.ts.map +1 -0
  104. package/dist/cli/deployer.js +152 -0
  105. package/dist/cli/deployer.js.map +1 -0
  106. package/dist/cli/dev-server.d.ts +2 -0
  107. package/dist/cli/dev-server.d.ts.map +1 -0
  108. package/dist/cli/dev-server.js +70 -0
  109. package/dist/cli/dev-server.js.map +1 -0
  110. package/dist/cli/env.d.ts +4 -0
  111. package/dist/cli/env.d.ts.map +1 -0
  112. package/dist/cli/env.js +68 -0
  113. package/dist/cli/env.js.map +1 -0
  114. package/dist/cli/init.d.ts +6 -0
  115. package/dist/cli/init.d.ts.map +1 -0
  116. package/dist/cli/init.js +103 -0
  117. package/dist/cli/init.js.map +1 -0
  118. package/dist/config.d.ts +262 -0
  119. package/dist/config.d.ts.map +1 -0
  120. package/dist/config.js +212 -0
  121. package/dist/config.js.map +1 -0
  122. package/dist/db/index.d.ts +3 -0
  123. package/dist/db/index.d.ts.map +1 -0
  124. package/dist/db/index.js +3 -0
  125. package/dist/db/index.js.map +1 -0
  126. package/dist/db/schema.d.ts +410 -0
  127. package/dist/db/schema.d.ts.map +1 -0
  128. package/dist/db/schema.js +294 -0
  129. package/dist/db/schema.js.map +1 -0
  130. package/dist/db/service.d.ts +42 -0
  131. package/dist/db/service.d.ts.map +1 -0
  132. package/dist/db/service.js +372 -0
  133. package/dist/db/service.js.map +1 -0
  134. package/dist/db/utils.d.ts +25 -0
  135. package/dist/db/utils.d.ts.map +1 -0
  136. package/dist/db/utils.js +188 -0
  137. package/dist/db/utils.js.map +1 -0
  138. package/dist/emails/index.d.ts +4 -0
  139. package/dist/emails/index.d.ts.map +1 -0
  140. package/dist/emails/index.js +4 -0
  141. package/dist/emails/index.js.map +1 -0
  142. package/dist/emails/routes.d.ts +191 -0
  143. package/dist/emails/routes.d.ts.map +1 -0
  144. package/dist/emails/routes.js +39 -0
  145. package/dist/emails/routes.js.map +1 -0
  146. package/dist/emails/schema.d.ts +237 -0
  147. package/dist/emails/schema.d.ts.map +1 -0
  148. package/dist/emails/schema.js +98 -0
  149. package/dist/emails/schema.js.map +1 -0
  150. package/dist/emails/service.d.ts +22 -0
  151. package/dist/emails/service.d.ts.map +1 -0
  152. package/dist/emails/service.js +337 -0
  153. package/dist/emails/service.js.map +1 -0
  154. package/dist/emails/utils.d.ts +49 -0
  155. package/dist/emails/utils.d.ts.map +1 -0
  156. package/dist/emails/utils.js +341 -0
  157. package/dist/emails/utils.js.map +1 -0
  158. package/dist/files/index.d.ts +4 -0
  159. package/dist/files/index.d.ts.map +1 -0
  160. package/dist/files/index.js +4 -0
  161. package/dist/files/index.js.map +1 -0
  162. package/dist/files/routes.d.ts +72 -0
  163. package/dist/files/routes.d.ts.map +1 -0
  164. package/dist/files/routes.js +38 -0
  165. package/dist/files/routes.js.map +1 -0
  166. package/dist/files/schema.d.ts +57 -0
  167. package/dist/files/schema.d.ts.map +1 -0
  168. package/dist/files/schema.js +39 -0
  169. package/dist/files/schema.js.map +1 -0
  170. package/dist/files/service.d.ts +27 -0
  171. package/dist/files/service.d.ts.map +1 -0
  172. package/dist/files/service.js +175 -0
  173. package/dist/files/service.js.map +1 -0
  174. package/dist/files/utils.d.ts +3 -0
  175. package/dist/files/utils.d.ts.map +1 -0
  176. package/dist/files/utils.js +63 -0
  177. package/dist/files/utils.js.map +1 -0
  178. package/dist/index.d.ts +875 -0
  179. package/dist/index.d.ts.map +1 -0
  180. package/dist/index.js +9919 -0
  181. package/dist/index.js.map +7 -0
  182. package/dist/payments/billing.d.ts +29 -0
  183. package/dist/payments/billing.d.ts.map +1 -0
  184. package/dist/payments/billing.js +378 -0
  185. package/dist/payments/billing.js.map +1 -0
  186. package/dist/payments/index.d.ts +4 -0
  187. package/dist/payments/index.d.ts.map +1 -0
  188. package/dist/payments/index.js +4 -0
  189. package/dist/payments/index.js.map +1 -0
  190. package/dist/payments/routes.d.ts +340 -0
  191. package/dist/payments/routes.d.ts.map +1 -0
  192. package/dist/payments/routes.js +56 -0
  193. package/dist/payments/routes.js.map +1 -0
  194. package/dist/payments/schema.d.ts +412 -0
  195. package/dist/payments/schema.d.ts.map +1 -0
  196. package/dist/payments/schema.js +260 -0
  197. package/dist/payments/schema.js.map +1 -0
  198. package/dist/payments/service.d.ts +31 -0
  199. package/dist/payments/service.d.ts.map +1 -0
  200. package/dist/payments/service.js +322 -0
  201. package/dist/payments/service.js.map +1 -0
  202. package/dist/payments/utils.d.ts +61 -0
  203. package/dist/payments/utils.d.ts.map +1 -0
  204. package/dist/payments/utils.js +369 -0
  205. package/dist/payments/utils.js.map +1 -0
  206. package/dist/router/guard.d.ts +19 -0
  207. package/dist/router/guard.d.ts.map +1 -0
  208. package/dist/router/guard.js +243 -0
  209. package/dist/router/guard.js.map +1 -0
  210. package/dist/router/index.d.ts +3 -0
  211. package/dist/router/index.d.ts.map +1 -0
  212. package/dist/router/index.js +3 -0
  213. package/dist/router/index.js.map +1 -0
  214. package/dist/router/schema.d.ts +103 -0
  215. package/dist/router/schema.d.ts.map +1 -0
  216. package/dist/router/schema.js +282 -0
  217. package/dist/router/schema.js.map +1 -0
  218. package/dist/router/service.d.ts +25 -0
  219. package/dist/router/service.d.ts.map +1 -0
  220. package/dist/router/service.js +339 -0
  221. package/dist/router/service.js.map +1 -0
  222. package/dist/tasks/index.d.ts +4 -0
  223. package/dist/tasks/index.d.ts.map +1 -0
  224. package/dist/tasks/index.js +4 -0
  225. package/dist/tasks/index.js.map +1 -0
  226. package/dist/tasks/routes.d.ts +54 -0
  227. package/dist/tasks/routes.d.ts.map +1 -0
  228. package/dist/tasks/routes.js +195 -0
  229. package/dist/tasks/routes.js.map +1 -0
  230. package/dist/tasks/schema.d.ts +261 -0
  231. package/dist/tasks/schema.d.ts.map +1 -0
  232. package/dist/tasks/schema.js +35 -0
  233. package/dist/tasks/schema.js.map +1 -0
  234. package/dist/tasks/service.d.ts +32 -0
  235. package/dist/tasks/service.d.ts.map +1 -0
  236. package/dist/tasks/service.js +201 -0
  237. package/dist/tasks/service.js.map +1 -0
  238. package/dist/tasks/steps.d.ts +18 -0
  239. package/dist/tasks/steps.d.ts.map +1 -0
  240. package/dist/tasks/steps.js +365 -0
  241. package/dist/tasks/steps.js.map +1 -0
  242. package/dist/tasks/utils.d.ts +75 -0
  243. package/dist/tasks/utils.d.ts.map +1 -0
  244. package/dist/tasks/utils.js +311 -0
  245. package/dist/tasks/utils.js.map +1 -0
  246. package/dist/tasks/watcher.d.ts +13 -0
  247. package/dist/tasks/watcher.d.ts.map +1 -0
  248. package/dist/tasks/watcher.js +255 -0
  249. package/dist/tasks/watcher.js.map +1 -0
  250. package/dist/tasks/worker.d.ts +28 -0
  251. package/dist/tasks/worker.d.ts.map +1 -0
  252. package/dist/tasks/worker.js +326 -0
  253. package/dist/tasks/worker.js.map +1 -0
  254. package/dist/utils/index.d.ts +3 -0
  255. package/dist/utils/index.d.ts.map +1 -0
  256. package/dist/utils/index.js +3 -0
  257. package/dist/utils/index.js.map +1 -0
  258. package/dist/utils/schema.d.ts +79 -0
  259. package/dist/utils/schema.d.ts.map +1 -0
  260. package/dist/utils/schema.js +41 -0
  261. package/dist/utils/schema.js.map +1 -0
  262. package/dist/utils/service.d.ts +32 -0
  263. package/dist/utils/service.d.ts.map +1 -0
  264. package/dist/utils/service.js +384 -0
  265. package/dist/utils/service.js.map +1 -0
  266. package/docs/SDK_SPEC.md +1645 -0
  267. package/infra/firestore.base.indexes.json +138 -0
  268. package/infra/firestore.base.rules +152 -0
  269. package/infra/storage.base.rules +8 -0
  270. package/package.json +59 -13
  271. package/src/ai/index.ts +2 -0
  272. package/src/ai/schema.ts +64 -0
  273. package/src/ai/service.ts +317 -0
  274. package/src/ai/utils.ts +38 -0
  275. package/src/attachments/index.ts +3 -0
  276. package/src/attachments/routes.ts +28 -0
  277. package/src/attachments/schema.ts +45 -0
  278. package/src/attachments/service.ts +315 -0
  279. package/src/attachments/utils.ts +115 -0
  280. package/src/auth/index.ts +3 -0
  281. package/src/auth/oidc.ts +419 -0
  282. package/src/auth/recovery.ts +365 -0
  283. package/src/auth/routes.ts +142 -0
  284. package/src/auth/schema.ts +181 -0
  285. package/src/auth/service.ts +183 -0
  286. package/src/auth/session.ts +414 -0
  287. package/src/auth/utils.ts +106 -0
  288. package/src/chat/context.ts +310 -0
  289. package/src/chat/index.ts +3 -0
  290. package/src/chat/routes.ts +264 -0
  291. package/src/chat/schema.ts +295 -0
  292. package/src/chat/service.ts +360 -0
  293. package/src/chat/stream.ts +481 -0
  294. package/src/chat/utils.ts +370 -0
  295. package/src/cli/builder.ts +93 -0
  296. package/src/cli/deployer.ts +184 -0
  297. package/src/cli/dev-server.ts +77 -0
  298. package/src/cli/env.ts +89 -0
  299. package/src/cli/init.ts +125 -0
  300. package/src/config.ts +447 -0
  301. package/src/db/index.ts +2 -0
  302. package/src/db/schema.ts +409 -0
  303. package/src/db/service.ts +477 -0
  304. package/src/db/utils.ts +250 -0
  305. package/src/emails/index.ts +3 -0
  306. package/src/emails/routes.ts +64 -0
  307. package/src/emails/schema.ts +239 -0
  308. package/src/emails/service.ts +488 -0
  309. package/src/emails/utils.ts +449 -0
  310. package/src/files/index.ts +3 -0
  311. package/src/files/routes.ts +44 -0
  312. package/src/files/schema.ts +76 -0
  313. package/src/files/service.ts +238 -0
  314. package/src/files/utils.ts +77 -0
  315. package/src/index.ts +425 -0
  316. package/src/payments/billing.ts +488 -0
  317. package/src/payments/index.ts +3 -0
  318. package/src/payments/routes.ts +92 -0
  319. package/src/payments/schema.ts +401 -0
  320. package/src/payments/service.ts +445 -0
  321. package/src/payments/utils.ts +472 -0
  322. package/src/router/guard.ts +319 -0
  323. package/src/router/index.ts +2 -0
  324. package/src/router/schema.ts +458 -0
  325. package/src/router/service.ts +453 -0
  326. package/src/tasks/index.ts +11 -0
  327. package/src/tasks/routes.ts +230 -0
  328. package/src/tasks/schema.ts +310 -0
  329. package/src/tasks/service.ts +254 -0
  330. package/src/tasks/steps.ts +472 -0
  331. package/src/tasks/utils.ts +445 -0
  332. package/src/tasks/watcher.ts +307 -0
  333. package/src/tasks/worker.ts +461 -0
  334. package/src/utils/index.ts +2 -0
  335. package/src/utils/schema.ts +69 -0
  336. package/src/utils/service.ts +461 -0
  337. package/tests/e2e.test.ts +2531 -0
  338. package/tsconfig.base.json +12 -0
  339. package/README.md +0 -2
  340. package/index.d.ts +0 -1
  341. package/index.js +0 -1
@@ -0,0 +1,365 @@
1
+ import { randomBytes, randomInt } from 'node:crypto'
2
+ import type { UserRecord } from 'firebase-admin/auth'
3
+ import type { EntryId } from '../db'
4
+ import type { Core } from '../index'
5
+ import {
6
+ completeAccountRecoveryBody,
7
+ forgotPasswordBody,
8
+ initiateAccountRecoveryBody,
9
+ resetPasswordBody,
10
+ setRecoveryEmailBody,
11
+ verifyEmailBody,
12
+ verifyRecoveryEmailBody,
13
+ type AuthEmailPurpose,
14
+ type AuthTokenPurpose,
15
+ type AuthUser,
16
+ type CompleteAccountRecoveryInput,
17
+ type ForgotPasswordInput,
18
+ type InitiateAccountRecoveryInput,
19
+ type ResetPasswordInput,
20
+ type SetRecoveryEmailInput,
21
+ type TransactionalEmailPayload,
22
+ type VerifyEmailInput,
23
+ type VerifyRecoveryEmailInput
24
+ } from './schema'
25
+ import type { AuthSessionEngine } from './session'
26
+ import { hashToken, normalizeDate, safeEqualHex } from './utils'
27
+
28
+ export class AuthRecoveryEngine {
29
+ readonly #core: Core
30
+ readonly #session: AuthSessionEngine
31
+
32
+ constructor(coreClient: Core, session: AuthSessionEngine) {
33
+ this.#core = coreClient
34
+ this.#session = session
35
+ }
36
+
37
+ getLastDevEmail = (to: string, purpose: AuthEmailPurpose): TransactionalEmailPayload | undefined =>
38
+ this.#core.internals.emails.getLastDevEmail(to, purpose)
39
+
40
+ #issueAuthToken = async (
41
+ userId: EntryId,
42
+ purpose: AuthTokenPurpose,
43
+ targetEmail: string,
44
+ expiresInMinutes = this.#core.config.emails.otpTtlMinutes
45
+ ): Promise<TransactionalEmailPayload> => {
46
+ const normalizedTarget = targetEmail.trim().toLowerCase()
47
+ const code = String(randomInt(100_000, 1_000_000))
48
+ const token = randomBytes(32).toString('base64url')
49
+ const tokenHash = hashToken(code)
50
+ const urlTokenHash = hashToken(token)
51
+ const now = new Date()
52
+ const expiresAt = new Date(now.getTime() + expiresInMinutes * 60_000)
53
+ const docId = `${userId}_${purpose}`
54
+
55
+ await this.#core.db
56
+ .firestore()
57
+ .collection('auth_tokens')
58
+ .doc(docId)
59
+ .set({
60
+ id: docId,
61
+ userId,
62
+ purpose,
63
+ targetEmail: normalizedTarget,
64
+ tokenHash,
65
+ urlTokenHash,
66
+ attempts: 0,
67
+ expiresAt,
68
+ consumedAt: null,
69
+ deleted: false,
70
+ createdAt: now,
71
+ updatedAt: now
72
+ })
73
+
74
+ const payload: TransactionalEmailPayload = {
75
+ to: normalizedTarget,
76
+ purpose,
77
+ code,
78
+ token,
79
+ expiresInMinutes,
80
+ userId
81
+ }
82
+ await this.#core.internals.emails.dispatchAuthEmail(payload)
83
+ return payload
84
+ }
85
+
86
+ #consumeAuthToken = async (
87
+ userId: EntryId,
88
+ purpose: AuthTokenPurpose,
89
+ expectedTargetEmail: string,
90
+ codeOrToken: string
91
+ ): Promise<void> => {
92
+ const db = this.#core.db.firestore()
93
+ const docRef = db.collection('auth_tokens').doc(`${userId}_${purpose}`)
94
+ const maxAttempts = this.#core.config.emails.maxVerifyAttempts
95
+ const normalizedExpected = expectedTargetEmail.trim().toLowerCase()
96
+ const candidateHash = hashToken(codeOrToken)
97
+
98
+ type ConsumeOutcome = 'valid' | 'missing_or_consumed' | 'expired' | 'max_attempts' | 'mismatch'
99
+
100
+ const outcome = await db.runTransaction<ConsumeOutcome>(async (tx) => {
101
+ const snap = await tx.get(docRef)
102
+ if (!snap.exists) return 'missing_or_consumed'
103
+ const data = snap.data() as Record<string, unknown> | undefined
104
+ if (!data || data.deleted === true || data.consumedAt != null) return 'missing_or_consumed'
105
+
106
+ const attempts = typeof data.attempts === 'number' ? data.attempts : 0
107
+ if (attempts >= maxAttempts) return 'max_attempts'
108
+
109
+ const now = new Date()
110
+ if (normalizeDate(data.expiresAt, new Date(0)).getTime() <= now.getTime()) {
111
+ tx.update(docRef, { deleted: true, updatedAt: now })
112
+ return 'expired'
113
+ }
114
+
115
+ const targetEmail = String(data.targetEmail ?? '').trim().toLowerCase()
116
+ const tokenHash = String(data.tokenHash ?? '')
117
+ const urlTokenHash = typeof data.urlTokenHash === 'string' ? data.urlTokenHash : ''
118
+ const matches =
119
+ targetEmail === normalizedExpected &&
120
+ (safeEqualHex(candidateHash, tokenHash) ||
121
+ (Boolean(urlTokenHash) && safeEqualHex(candidateHash, urlTokenHash)))
122
+
123
+ if (!matches) {
124
+ const nextAttempts = attempts + 1
125
+ const exhausted = nextAttempts >= maxAttempts
126
+ tx.update(docRef, {
127
+ attempts: nextAttempts,
128
+ ...(exhausted ? { deleted: true, consumedAt: now } : {}),
129
+ updatedAt: now
130
+ })
131
+ return exhausted ? 'max_attempts' : 'mismatch'
132
+ }
133
+
134
+ tx.update(docRef, { consumedAt: now, deleted: true, updatedAt: now })
135
+ return 'valid'
136
+ })
137
+
138
+ if (outcome === 'valid') return
139
+ if (outcome === 'expired') this.#core.errors.httpError(400, 'Verification token has expired', 'TOKEN_EXPIRED')
140
+ if (outcome === 'max_attempts') {
141
+ this.#core.errors.httpError(
142
+ 400,
143
+ 'Maximum verification attempts exceeded; token invalidated',
144
+ 'TOKEN_MAX_ATTEMPTS_EXCEEDED'
145
+ )
146
+ }
147
+ this.#core.errors.httpError(400, 'Invalid or expired verification code', 'INVALID_TOKEN')
148
+ }
149
+
150
+ #requireUserByEmail = async (
151
+ email: string,
152
+ errMessage: string,
153
+ errCode: string,
154
+ rejectDisabled = false
155
+ ): Promise<{ user: UserRecord; normalizedEmail: string }> => {
156
+ const normalizedEmail = email.trim().toLowerCase()
157
+ const user = await this.#session
158
+ .getAuth()
159
+ .getUserByEmail(normalizedEmail)
160
+ .catch(() => undefined)
161
+ if (!user || !user.email || (rejectDisabled && user.disabled)) {
162
+ return this.#core.errors.httpError(400, errMessage, errCode)
163
+ }
164
+ return { user, normalizedEmail }
165
+ }
166
+
167
+ #revokeAllUserSessions = async (uid: EntryId): Promise<void> => {
168
+ this.#session.evictUserTokens(uid)
169
+ await Promise.all([
170
+ this.#session.recordRevocation(uid, this.#core.env.isDev),
171
+ this.#session
172
+ .getAuth()
173
+ .revokeRefreshTokens(uid)
174
+ .catch(() => {})
175
+ ])
176
+ }
177
+
178
+ sendVerificationEmail = async (
179
+ userId: EntryId = this.#core.userId
180
+ ): Promise<{ sent: true; expiresInMinutes: number }> => {
181
+ if (!userId) return this.#core.errors.httpError(401, 'Unauthorized', 'UNAUTHORIZED')
182
+ const user = await this.#session
183
+ .getAuth()
184
+ .getUser(userId)
185
+ .catch(() => undefined)
186
+ if (!user || !user.email) {
187
+ return this.#core.errors.httpError(400, 'User does not have a primary email address', 'MISSING_EMAIL')
188
+ }
189
+ const expiresInMinutes = this.#core.config.emails.otpTtlMinutes
190
+ await this.#issueAuthToken(user.uid, 'verify_email', user.email, expiresInMinutes)
191
+ return { sent: true, expiresInMinutes }
192
+ }
193
+
194
+ #consumeAndUpsertMeta = async (
195
+ uid: EntryId,
196
+ purpose: AuthTokenPurpose,
197
+ targetEmail: string,
198
+ codeOrToken: string,
199
+ metaPatch: Parameters<AuthSessionEngine['upsertUserAuthMeta']>[1]
200
+ ): Promise<void> => {
201
+ await this.#consumeAuthToken(uid, purpose, targetEmail, codeOrToken)
202
+ await this.#session.upsertUserAuthMeta(uid, metaPatch, this.#core.env.isDev)
203
+ }
204
+
205
+ verifyEmail = async (raw: VerifyEmailInput): Promise<{ verified: true; userId: string }> => {
206
+ const { email, codeOrToken } = verifyEmailBody.parse(raw)
207
+ const { user, normalizedEmail } = await this.#requireUserByEmail(
208
+ email,
209
+ 'Invalid email or verification token',
210
+ 'INVALID_TOKEN'
211
+ )
212
+ await this.#consumeAndUpsertMeta(user.uid, 'verify_email', user.email!, codeOrToken, {
213
+ email: normalizedEmail,
214
+ emailVerified: true
215
+ })
216
+ await this.#session.getAuth().updateUser(user.uid, { emailVerified: true })
217
+ this.#session.evictUserTokens(user.uid)
218
+ return { verified: true, userId: user.uid }
219
+ }
220
+
221
+ forgotPassword = async (raw: ForgotPasswordInput): Promise<{ sent: true }> => {
222
+ const { email } = forgotPasswordBody.parse(raw)
223
+ const normalizedEmail = email.trim().toLowerCase()
224
+ const user = await this.#session
225
+ .getAuth()
226
+ .getUserByEmail(normalizedEmail)
227
+ .catch(() => undefined)
228
+ if (user && user.email && !user.disabled) {
229
+ await this.#issueAuthToken(user.uid, 'reset_password', user.email, this.#core.config.emails.otpTtlMinutes)
230
+ }
231
+ return { sent: true }
232
+ }
233
+
234
+ resetPassword = async (raw: ResetPasswordInput): Promise<{ reset: true }> => {
235
+ const { email, codeOrToken, newPassword } = resetPasswordBody.parse(raw)
236
+ const { user } = await this.#requireUserByEmail(email, 'Invalid email or reset token', 'INVALID_TOKEN')
237
+ await this.#consumeAuthToken(user.uid, 'reset_password', user.email!, codeOrToken)
238
+ await this.#session.getAuth().updateUser(user.uid, { password: newPassword })
239
+ await this.#revokeAllUserSessions(user.uid)
240
+ return { reset: true }
241
+ }
242
+
243
+ setRecoveryEmail = async (
244
+ raw: SetRecoveryEmailInput,
245
+ userId: EntryId = this.#core.userId
246
+ ): Promise<{ recoveryEmail: string; recoveryEmailVerified: false; sent: true }> => {
247
+ if (!userId) return this.#core.errors.httpError(401, 'Unauthorized', 'UNAUTHORIZED')
248
+ const { recoveryEmail } = setRecoveryEmailBody.parse(raw)
249
+ const normalizedRecovery = recoveryEmail.trim().toLowerCase()
250
+ const user = await this.#session
251
+ .getAuth()
252
+ .getUser(userId)
253
+ .catch(() => undefined)
254
+ if (!user) return this.#core.errors.httpError(404, 'User not found', 'NOT_FOUND')
255
+
256
+ const primaryEmail = (user.email ?? '').trim().toLowerCase()
257
+ if (primaryEmail && normalizedRecovery === primaryEmail) {
258
+ return this.#core.errors.httpError(
259
+ 400,
260
+ 'Recovery email must be different from primary email',
261
+ 'INVALID_RECOVERY_EMAIL'
262
+ )
263
+ }
264
+
265
+ await this.#session.upsertUserAuthMeta(
266
+ user.uid,
267
+ {
268
+ ...(primaryEmail ? { email: primaryEmail } : {}),
269
+ recoveryEmail: normalizedRecovery,
270
+ recoveryEmailVerified: false
271
+ },
272
+ this.#core.env.isDev
273
+ )
274
+ await this.#issueAuthToken(
275
+ user.uid,
276
+ 'verify_recovery_email',
277
+ normalizedRecovery,
278
+ this.#core.config.emails.otpTtlMinutes
279
+ )
280
+ return { recoveryEmail: normalizedRecovery, recoveryEmailVerified: false, sent: true }
281
+ }
282
+
283
+ verifyRecoveryEmail = async (
284
+ raw: VerifyRecoveryEmailInput,
285
+ userId: EntryId = this.#core.userId
286
+ ): Promise<{ recoveryEmail: string; recoveryEmailVerified: true; verified: true }> => {
287
+ if (!userId) return this.#core.errors.httpError(401, 'Unauthorized', 'UNAUTHORIZED')
288
+ const { codeOrToken } = verifyRecoveryEmailBody.parse(raw)
289
+ const meta = await this.#session.getUserAuthMeta(userId, this.#core.env.isDev)
290
+ if (!meta?.recoveryEmail) {
291
+ return this.#core.errors.httpError(
292
+ 400,
293
+ 'No recovery email configured for this account',
294
+ 'MISSING_RECOVERY_EMAIL'
295
+ )
296
+ }
297
+ await this.#consumeAndUpsertMeta(userId, 'verify_recovery_email', meta.recoveryEmail, codeOrToken, {
298
+ recoveryEmailVerified: true
299
+ })
300
+ return { recoveryEmail: meta.recoveryEmail, recoveryEmailVerified: true, verified: true }
301
+ }
302
+
303
+ initiateAccountRecovery = async (raw: InitiateAccountRecoveryInput): Promise<{ initiated: true }> => {
304
+ const { primaryEmail, recoveryEmail } = initiateAccountRecoveryBody.parse(raw)
305
+ const normPrimary = primaryEmail.trim().toLowerCase()
306
+ const normRecovery = recoveryEmail.trim().toLowerCase()
307
+ const user = await this.#session
308
+ .getAuth()
309
+ .getUserByEmail(normPrimary)
310
+ .catch(() => undefined)
311
+ if (user && !user.disabled && (user.email ?? '').trim().toLowerCase() === normPrimary) {
312
+ const meta = await this.#session.getUserAuthMeta(user.uid, this.#core.env.isDev)
313
+ if (meta?.recoveryEmail === normRecovery && meta.recoveryEmailVerified === true) {
314
+ await this.#issueAuthToken(
315
+ user.uid,
316
+ 'account_recovery',
317
+ normRecovery,
318
+ this.#core.config.emails.recoveryOtpTtlMinutes
319
+ )
320
+ }
321
+ }
322
+ return { initiated: true }
323
+ }
324
+
325
+ completeAccountRecovery = async (
326
+ raw: CompleteAccountRecoveryInput
327
+ ): Promise<{ user: AuthUser; sessionCookie: string }> => {
328
+ const { primaryEmail, recoveryEmail, codeOrToken, newPrimaryEmail, newPassword } =
329
+ completeAccountRecoveryBody.parse(raw)
330
+ const normPrimary = primaryEmail.trim().toLowerCase()
331
+ const normRecovery = recoveryEmail.trim().toLowerCase()
332
+ const normNewPrimary = newPrimaryEmail.trim().toLowerCase()
333
+
334
+ const { user } = await this.#requireUserByEmail(
335
+ normPrimary,
336
+ 'Invalid account recovery request or token',
337
+ 'INVALID_RECOVERY_REQUEST',
338
+ true
339
+ )
340
+ if ((user.email ?? '').trim().toLowerCase() !== normPrimary) {
341
+ return this.#core.errors.httpError(400, 'Invalid account recovery request or token', 'INVALID_RECOVERY_REQUEST')
342
+ }
343
+
344
+ const meta = await this.#session.getUserAuthMeta(user.uid, this.#core.env.isDev)
345
+ if (!meta || meta.recoveryEmail !== normRecovery || meta.recoveryEmailVerified !== true) {
346
+ return this.#core.errors.httpError(400, 'Invalid account recovery request or token', 'INVALID_RECOVERY_REQUEST')
347
+ }
348
+
349
+ await this.#consumeAndUpsertMeta(user.uid, 'account_recovery', normRecovery, codeOrToken, {
350
+ email: normNewPrimary,
351
+ emailVerified: true
352
+ })
353
+ await this.#session.getAuth().updateUser(user.uid, {
354
+ email: normNewPrimary,
355
+ emailVerified: true,
356
+ ...(newPassword ? { password: newPassword } : {})
357
+ })
358
+ await this.#revokeAllUserSessions(user.uid)
359
+
360
+ return this.#session.mintSessionForUid(user.uid, {
361
+ email: normNewPrimary,
362
+ emailVerified: true
363
+ })
364
+ }
365
+ }
@@ -0,0 +1,142 @@
1
+ import { deleteCookie, getCookie } from 'hono/cookie'
2
+ import type { Context } from 'hono'
3
+ import type { z } from 'zod'
4
+ import { CORE_CONFIG, type AppEnv, type DbRole } from '../config'
5
+ import type { EntryId } from '../db'
6
+ import { core } from '../index'
7
+ import type { StepRunner } from '../tasks'
8
+ import {
9
+ blockUserBody,
10
+ completeAccountRecoveryBody,
11
+ customTokenBody,
12
+ emailSignInBody,
13
+ emailSignUpBody,
14
+ eraseUserInput,
15
+ eraseUserOutput,
16
+ forgotPasswordBody,
17
+ initiateAccountRecoveryBody,
18
+ resetPasswordBody,
19
+ sendVerificationEmailBody,
20
+ sessionBody,
21
+ setRecoveryEmailBody,
22
+ signOutBody,
23
+ socialSignInInput,
24
+ verifyEmailBody,
25
+ verifyRecoveryEmailBody,
26
+ type AuthUser,
27
+ type EraseUserInput
28
+ } from './schema'
29
+
30
+ const respondWithSession = async (
31
+ c: Context<AppEnv>,
32
+ p: Promise<{ user: AuthUser; sessionCookie: string }>
33
+ ): Promise<AuthUser> => {
34
+ const { user, sessionCookie } = await p
35
+ core.internals.auth.setSessionCookie(c, sessionCookie)
36
+ return user
37
+ }
38
+
39
+ export const coreAuthRoutes = {
40
+ 'auth.custom-token': {
41
+ input: customTokenBody,
42
+ handler: async ({
43
+ input,
44
+ userId,
45
+ role
46
+ }: {
47
+ input: z.infer<typeof customTokenBody>
48
+ userId: EntryId
49
+ role: DbRole
50
+ }) => {
51
+ if (input.uid && role !== 'admin' && userId !== CORE_CONFIG.auth.adminUserId) {
52
+ return core.errors.httpError(403, 'Admin privileges required to mint token for specific UID', 'FORBIDDEN')
53
+ }
54
+ return core.internals.auth.mintCustomToken(input.uid)
55
+ }
56
+ },
57
+ 'auth.session.create': {
58
+ input: sessionBody,
59
+ handler: async ({ c, input }: { c: Context<AppEnv>; input: z.infer<typeof sessionBody> }) =>
60
+ respondWithSession(c, core.internals.auth.createSession(input.idToken)).catch(() =>
61
+ core.errors.httpError(401, 'Invalid ID token', 'INVALID_ID_TOKEN')
62
+ )
63
+ },
64
+ 'auth.session.get': {
65
+ handler: async ({ userId }: { userId: EntryId }) => ({ userId })
66
+ },
67
+ 'auth.sign-up.email': {
68
+ input: emailSignUpBody,
69
+ handler: async ({ c, input }: { c: Context<AppEnv>; input: z.infer<typeof emailSignUpBody> }) =>
70
+ respondWithSession(c, core.internals.auth.signUpEmail(input.email, input.password, input.name))
71
+ },
72
+ 'auth.sign-in.email': {
73
+ input: emailSignInBody,
74
+ handler: async ({ c, input }: { c: Context<AppEnv>; input: z.infer<typeof emailSignInBody> }) =>
75
+ respondWithSession(c, core.internals.auth.signInEmail(input.email, input.password))
76
+ },
77
+ 'auth.sign-in.social': {
78
+ input: socialSignInInput,
79
+ handler: async ({ c, input }: { c: Context<AppEnv>; input: z.infer<typeof socialSignInInput> }) =>
80
+ respondWithSession(c, core.auth.signInSocial(input))
81
+ },
82
+ 'auth.email.send-verification': {
83
+ input: sendVerificationEmailBody,
84
+ handler: async ({ userId }: { userId: EntryId }) => core.auth.sendVerificationEmail(userId)
85
+ },
86
+ 'auth.email.verify': {
87
+ input: verifyEmailBody,
88
+ handler: async ({ input }: { input: z.infer<typeof verifyEmailBody> }) => core.auth.verifyEmail(input)
89
+ },
90
+ 'auth.password.forgot': {
91
+ input: forgotPasswordBody,
92
+ handler: async ({ input }: { input: z.infer<typeof forgotPasswordBody> }) => core.auth.forgotPassword(input)
93
+ },
94
+ 'auth.password.reset': {
95
+ input: resetPasswordBody,
96
+ handler: async ({ input }: { input: z.infer<typeof resetPasswordBody> }) => core.auth.resetPassword(input)
97
+ },
98
+ 'auth.recovery-email.set': {
99
+ input: setRecoveryEmailBody,
100
+ handler: async ({ userId, input }: { userId: EntryId; input: z.infer<typeof setRecoveryEmailBody> }) =>
101
+ core.auth.setRecoveryEmail(input, userId)
102
+ },
103
+ 'auth.recovery-email.verify': {
104
+ input: verifyRecoveryEmailBody,
105
+ handler: async ({ userId, input }: { userId: EntryId; input: z.infer<typeof verifyRecoveryEmailBody> }) =>
106
+ core.auth.verifyRecoveryEmail(input, userId)
107
+ },
108
+ 'auth.account-recovery.initiate': {
109
+ input: initiateAccountRecoveryBody,
110
+ handler: async ({ input }: { input: z.infer<typeof initiateAccountRecoveryBody> }) =>
111
+ core.auth.initiateAccountRecovery(input)
112
+ },
113
+ 'auth.account-recovery.complete': {
114
+ input: completeAccountRecoveryBody,
115
+ handler: async ({ c, input }: { c: Context<AppEnv>; input: z.infer<typeof completeAccountRecoveryBody> }) =>
116
+ respondWithSession(c, core.auth.completeAccountRecovery(input))
117
+ },
118
+ 'auth.users.list': {
119
+ handler: async () => core.auth.listUsers()
120
+ },
121
+ 'auth.users.block': {
122
+ input: blockUserBody,
123
+ handler: async ({ userId, input }: { userId: EntryId; input: z.infer<typeof blockUserBody> }) =>
124
+ core.auth.blockUser(input.userId ?? userId, input.blocked)
125
+ },
126
+ 'auth.users.delete': {
127
+ mode: 'workflow' as const,
128
+ workflowName: 'erase-user',
129
+ input: eraseUserInput,
130
+ output: eraseUserOutput,
131
+ handler: async ({ input, step }: { input: EraseUserInput; step: StepRunner }) => core.auth.eraseUser(input, step)
132
+ },
133
+ 'auth.sign-out': {
134
+ input: signOutBody,
135
+ handler: async ({ c, input }: { c: Context<AppEnv>; input: z.infer<typeof signOutBody> }) => {
136
+ const cookie = getCookie(c, CORE_CONFIG.auth.sessionCookie)
137
+ if (cookie) await core.internals.auth.revokeSession(cookie, Boolean(input.deleteUser)).catch(() => {})
138
+ deleteCookie(c, CORE_CONFIG.auth.sessionCookie, { path: '/' })
139
+ return { ok: true }
140
+ }
141
+ }
142
+ }
@@ -0,0 +1,181 @@
1
+ import { z } from 'zod'
2
+ import {
3
+ CORE_CONFIG,
4
+ type AuthEmailPurpose,
5
+ type AuthTokenPurpose,
6
+ type DbRole,
7
+ type TransactionalEmailPayload
8
+ } from '../config'
9
+ import { entryId, type BaseEntryFields, type EntryId } from '../db'
10
+
11
+ export { type AuthEmailPurpose, type AuthTokenPurpose, type TransactionalEmailPayload }
12
+
13
+ export const SESSION_COOKIE = CORE_CONFIG.auth.sessionCookie
14
+ export const SESSION_EXPIRES_IN = CORE_CONFIG.auth.sessionExpiresInMs
15
+ export const ADMIN_USER_ID: EntryId = CORE_CONFIG.auth.adminUserId
16
+
17
+ export type OAuthProvider = 'google' | 'apple'
18
+
19
+ export interface LinkedProvider {
20
+ provider: OAuthProvider
21
+ providerUserId: string
22
+ email?: string | null
23
+ linkedAt: Date
24
+ }
25
+
26
+ export interface CachedTokenEntry {
27
+ uid: EntryId
28
+ role: DbRole
29
+ exp: number
30
+ cachedAt: number
31
+ strictVerified: boolean
32
+ emailVerified?: boolean
33
+ }
34
+
35
+ export interface UserAuthMetaRecord {
36
+ email?: string | null
37
+ emailVerified?: boolean
38
+ name?: string | null
39
+ avatarUrl?: string | null
40
+ recoveryEmail?: string | null
41
+ recoveryEmailVerified?: boolean
42
+ linkedProviders?: LinkedProvider[]
43
+ providerKeys?: string[]
44
+ createdAt?: Date
45
+ updatedAt?: Date
46
+ }
47
+
48
+ export interface VerifiedOidcClaims {
49
+ sub: string
50
+ email: string | null
51
+ emailVerified: boolean
52
+ name?: string
53
+ picture?: string
54
+ }
55
+
56
+ export interface AuthUser {
57
+ id: EntryId
58
+ email: string | null
59
+ emailVerified: boolean
60
+ name: string | null
61
+ avatarUrl?: string | null
62
+ /** Secondary recovery email (stored normalized/lowercase) */
63
+ recoveryEmail?: string | null
64
+ /** True only after the user verifies ownership of `recoveryEmail` via OTP */
65
+ recoveryEmailVerified?: boolean
66
+ linkedProviders?: LinkedProvider[]
67
+ customClaims?: Record<string, unknown>
68
+ deleted: boolean
69
+ createdAt: Date
70
+ updatedAt: Date
71
+ }
72
+
73
+ export interface AuthTokenDoc extends BaseEntryFields {
74
+ userId: EntryId
75
+ purpose: AuthTokenPurpose
76
+ /** Target email address (primary email or recovery email) */
77
+ targetEmail: string
78
+ /** SHA-256 hex digest of the 6-digit OTP code or 32-byte URL token */
79
+ tokenHash: string
80
+ urlTokenHash?: string
81
+ /** Max 5 failed verification attempts before automatic invalidation */
82
+ attempts: number
83
+ expiresAt: Date
84
+ consumedAt?: Date | null
85
+ }
86
+
87
+ export const eraseUserInput = z.object({
88
+ userId: entryId,
89
+ backupBeforeErase: z.boolean().default(false)
90
+ })
91
+ export const eraseUserOutput = z.object({
92
+ userId: entryId,
93
+ backupOperation: z.string().optional(),
94
+ erasedFiles: z.number(),
95
+ erasedChats: z.number(),
96
+ erasedCollections: z.record(z.string(), z.number()).optional(),
97
+ erasedUser: z.boolean()
98
+ })
99
+ export type EraseUserInput = z.input<typeof eraseUserInput>
100
+ export type EraseUserOutput = z.infer<typeof eraseUserOutput>
101
+
102
+ export const customTokenBody = z
103
+ .object({ uid: entryId.refine((id) => id !== CORE_CONFIG.auth.adminUserId, 'Reserved UID').optional() })
104
+ .default({})
105
+ export const sessionBody = z.object({ idToken: z.string().min(1) })
106
+ export const emailSignUpBody = z.object({
107
+ email: z.string().email(),
108
+ password: z.string().min(8).max(128),
109
+ name: z.string().min(1).max(120).optional()
110
+ })
111
+ export const emailSignInBody = z.object({ email: z.string().email(), password: z.string().min(1).max(128) })
112
+
113
+ export const socialSignInInput = z.object({
114
+ provider: z.enum(['google', 'apple']),
115
+ /** OIDC ID token returned by Apple Sign-In (iOS/Capacitor/Web) or Google Sign-In */
116
+ idToken: z.string().min(20),
117
+ /** Optional raw nonce for Apple Sign-In replay protection */
118
+ nonce: z.string().optional(),
119
+ /** Optional access token (Google) */
120
+ accessToken: z.string().optional(),
121
+ /**
122
+ * Apple iOS/Capacitor quirk: Apple ONLY sends the user's `name` on the VERY FIRST authorization.
123
+ * Accept optional first-time profile data from the client and persist it immediately.
124
+ */
125
+ profile: z
126
+ .object({
127
+ name: z.string().min(1).max(128).optional(),
128
+ avatarUrl: z.string().url().optional()
129
+ })
130
+ .optional()
131
+ })
132
+ export type SocialSignInInput = z.infer<typeof socialSignInInput>
133
+
134
+ export const sendVerificationEmailBody = z.object({}).default({})
135
+ export const verifyEmailBody = z.object({
136
+ email: z.string().email(),
137
+ codeOrToken: z.string().min(4).max(512)
138
+ })
139
+ export type VerifyEmailInput = z.infer<typeof verifyEmailBody>
140
+
141
+ export const forgotPasswordBody = z.object({
142
+ email: z.string().email()
143
+ })
144
+ export type ForgotPasswordInput = z.infer<typeof forgotPasswordBody>
145
+
146
+ export const resetPasswordBody = z.object({
147
+ email: z.string().email(),
148
+ codeOrToken: z.string().min(4).max(512),
149
+ newPassword: z.string().min(8).max(128)
150
+ })
151
+ export type ResetPasswordInput = z.infer<typeof resetPasswordBody>
152
+
153
+ export const setRecoveryEmailBody = z.object({
154
+ recoveryEmail: z.string().email()
155
+ })
156
+ export type SetRecoveryEmailInput = z.infer<typeof setRecoveryEmailBody>
157
+
158
+ export const verifyRecoveryEmailBody = z.object({
159
+ codeOrToken: z.string().min(4).max(512)
160
+ })
161
+ export type VerifyRecoveryEmailInput = z.infer<typeof verifyRecoveryEmailBody>
162
+
163
+ export const initiateAccountRecoveryBody = z.object({
164
+ primaryEmail: z.string().email(),
165
+ recoveryEmail: z.string().email()
166
+ })
167
+ export type InitiateAccountRecoveryInput = z.infer<typeof initiateAccountRecoveryBody>
168
+
169
+ export const completeAccountRecoveryBody = z.object({
170
+ primaryEmail: z.string().email(),
171
+ recoveryEmail: z.string().email(),
172
+ codeOrToken: z.string().min(4).max(512),
173
+ newPrimaryEmail: z.string().email(),
174
+ newPassword: z.string().min(8).max(128).optional()
175
+ })
176
+ export type CompleteAccountRecoveryInput = z.infer<typeof completeAccountRecoveryBody>
177
+
178
+ export const blockUserBody = z
179
+ .object({ userId: entryId.optional(), blocked: z.boolean().default(true) })
180
+ .default({ blocked: true })
181
+ export const signOutBody = z.object({ deleteUser: z.boolean().optional() }).default({})