@corgelabs/corge 0.0.1 → 1.0.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 (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 +542 -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 +97 -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 +58 -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 +121 -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,183 @@
1
+ import { CORE_CONFIG } from '../config'
2
+ import { getRegisteredCollectionNames, type EntryId } from '../db'
3
+ import type { Core } from '../index'
4
+ import type { StepRunner } from '../tasks'
5
+ import { AuthOidcEngine } from './oidc'
6
+ import { AuthRecoveryEngine } from './recovery'
7
+ import {
8
+ eraseUserInput,
9
+ eraseUserOutput,
10
+ type AuthUser,
11
+ type CompleteAccountRecoveryInput,
12
+ type EraseUserInput,
13
+ type EraseUserOutput,
14
+ type ForgotPasswordInput,
15
+ type InitiateAccountRecoveryInput,
16
+ type ResetPasswordInput,
17
+ type SetRecoveryEmailInput,
18
+ type SocialSignInInput,
19
+ type VerifyEmailInput,
20
+ type VerifyRecoveryEmailInput
21
+ } from './schema'
22
+ import { AuthSessionEngine, tokenCache } from './session'
23
+ import { hashToken, mapUserRecord, resolveUserRole } from './utils'
24
+
25
+ export class AuthService {
26
+ readonly #core: Core
27
+ readonly #session: AuthSessionEngine
28
+ readonly #oidc: AuthOidcEngine
29
+ readonly #recovery: AuthRecoveryEngine
30
+
31
+ constructor(coreClient: Core) {
32
+ this.#core = coreClient
33
+ this.#session = new AuthSessionEngine(coreClient)
34
+ this.#oidc = new AuthOidcEngine(coreClient, this.#session)
35
+ this.#recovery = new AuthRecoveryEngine(coreClient, this.#session)
36
+ }
37
+
38
+ signInSocial = (raw: SocialSignInInput): Promise<{ user: AuthUser; sessionCookie: string }> =>
39
+ this.#oidc.signInSocial(raw)
40
+
41
+ sendVerificationEmail = (userId?: EntryId): Promise<{ sent: true; expiresInMinutes: number }> =>
42
+ this.#recovery.sendVerificationEmail(userId)
43
+
44
+ verifyEmail = (raw: VerifyEmailInput): Promise<{ verified: true; userId: string }> =>
45
+ this.#recovery.verifyEmail(raw)
46
+
47
+ forgotPassword = (raw: ForgotPasswordInput): Promise<{ sent: true }> => this.#recovery.forgotPassword(raw)
48
+
49
+ resetPassword = (raw: ResetPasswordInput): Promise<{ reset: true }> => this.#recovery.resetPassword(raw)
50
+
51
+ setRecoveryEmail = (
52
+ raw: SetRecoveryEmailInput,
53
+ userId?: EntryId
54
+ ): Promise<{ recoveryEmail: string; recoveryEmailVerified: false; sent: true }> =>
55
+ this.#recovery.setRecoveryEmail(raw, userId)
56
+
57
+ verifyRecoveryEmail = (
58
+ raw: VerifyRecoveryEmailInput,
59
+ userId?: EntryId
60
+ ): Promise<{ recoveryEmail: string; recoveryEmailVerified: true; verified: true }> =>
61
+ this.#recovery.verifyRecoveryEmail(raw, userId)
62
+
63
+ initiateAccountRecovery = (raw: InitiateAccountRecoveryInput): Promise<{ initiated: true }> =>
64
+ this.#recovery.initiateAccountRecovery(raw)
65
+
66
+ completeAccountRecovery = (raw: CompleteAccountRecoveryInput): Promise<{ user: AuthUser; sessionCookie: string }> =>
67
+ this.#recovery.completeAccountRecovery(raw)
68
+
69
+ getUser = async (uid: EntryId): Promise<AuthUser | undefined> => {
70
+ try {
71
+ const [u, meta] = await Promise.all([
72
+ this.#session.getAuth().getUser(uid),
73
+ this.#session.getUserAuthMeta(uid, this.#core.env.isDev)
74
+ ])
75
+ return mapUserRecord(u, meta)
76
+ } catch {
77
+ return undefined
78
+ }
79
+ }
80
+
81
+ setClaims = async (uid: EntryId, claims: Record<string, unknown> | null): Promise<void> => {
82
+ await this.#session.getAuth().setCustomUserClaims(uid, claims)
83
+ this.#session.evictUserTokens(uid)
84
+ await this.#session.recordRevocation(uid, this.#core.env.isDev)
85
+ }
86
+
87
+ listUsers = async (includeDeleted = false): Promise<AuthUser[]> => {
88
+ const { users } = await this.#session.getAuth().listUsers(1000)
89
+ return users.map((u) => mapUserRecord(u)).filter((u) => includeDeleted || !u.deleted)
90
+ }
91
+
92
+ blockUser = async (
93
+ userId: EntryId = this.#core.userId,
94
+ blocked = true
95
+ ): Promise<{ userId: EntryId; blocked: boolean; deleted: boolean }> => {
96
+ const auth = this.#session.getAuth()
97
+ this.#session.evictUserTokens(userId)
98
+ await Promise.all([
99
+ blocked
100
+ ? this.#session.recordRevocation(userId, this.#core.env.isDev)
101
+ : this.#session.clearUserRevocation(userId, this.#core.env.isDev),
102
+ ...(blocked ? [auth.revokeRefreshTokens(userId).catch(() => {})] : []),
103
+ auth.updateUser(userId, { disabled: blocked }).catch(() => {})
104
+ ])
105
+ return { userId, blocked, deleted: blocked }
106
+ }
107
+
108
+ #triggerFirestoreBackup = async (userId: string): Promise<string> => {
109
+ const projectId = this.#core.config.gcpProject
110
+ const dbId = this.#core.env.isDev ? this.#core.config.firestoreDbs.dev : this.#core.config.firestoreDbs.prod
111
+ const bucketName = this.#core.env.isDev
112
+ ? this.#core.config.storageBuckets.dev
113
+ : this.#core.config.storageBuckets.prod
114
+ const outputUriPrefix = `gs://${bucketName}/backups/${dbId}-${userId}-${Date.now()}`
115
+ const accessToken = await this.#core.internals.registry.getGcpAccessToken()
116
+ const res = await fetch(
117
+ `https://firestore.googleapis.com/v1/projects/${projectId}/databases/${dbId}:exportDocuments`,
118
+ {
119
+ method: 'POST',
120
+ headers: { Authorization: `Bearer ${accessToken}`, 'content-type': 'application/json' },
121
+ body: JSON.stringify({ outputUriPrefix, collectionIds: getRegisteredCollectionNames() })
122
+ }
123
+ )
124
+ if (!res.ok) throw new Error(`Firestore exportDocuments failed (${res.status}): ${await res.text()}`)
125
+ const data = (await res.json()) as { name?: string }
126
+ return data.name ?? outputUriPrefix
127
+ }
128
+
129
+ eraseUser = async (raw: EraseUserInput, step: StepRunner = this.#core.step): Promise<EraseUserOutput> => {
130
+ const { userId, backupBeforeErase } = eraseUserInput.parse(raw)
131
+ const backupOperation = backupBeforeErase
132
+ ? await step('backup', () => this.#triggerFirestoreBackup(userId))
133
+ : undefined
134
+
135
+ const { erasedFiles, erasedChats, erasedCollections } =
136
+ await this.#core.internals.registry.cascadeEraseUserCollections(userId, step)
137
+
138
+ const erasedUser = await step('erase-user', async () => {
139
+ this.#session.evictUserTokens(userId)
140
+ await Promise.all([
141
+ this.#session.recordRevocation(userId, this.#core.env.isDev),
142
+ this.#session.getAuth().deleteUser(userId).catch(() => {})
143
+ ])
144
+ return true
145
+ })
146
+
147
+ return eraseUserOutput.parse({
148
+ userId,
149
+ ...(backupOperation ? { backupOperation } : {}),
150
+ erasedFiles,
151
+ erasedChats,
152
+ ...(Object.keys(erasedCollections).length ? { erasedCollections } : {}),
153
+ erasedUser
154
+ })
155
+ }
156
+
157
+ get internals() {
158
+ const getAuthWarm = this.#session.getAuthWarm
159
+ return {
160
+ verifyToken: this.#session.verifyToken,
161
+ hashToken,
162
+ resolveUserRole,
163
+ tokenCache,
164
+ setTokenCache: this.#session.setTokenCache,
165
+ recordRevocation: this.#session.recordRevocation,
166
+ syncRevocations: this.#session.syncRevocations,
167
+ getLastDevEmail: this.#recovery.getLastDevEmail,
168
+ registerTestOidcPublicKey: this.#oidc.registerTestOidcPublicKey,
169
+ get sessionCookie() {
170
+ return CORE_CONFIG.auth.sessionCookie
171
+ },
172
+ get authWarm() {
173
+ return getAuthWarm()
174
+ },
175
+ mintCustomToken: this.#session.mintCustomToken,
176
+ createSession: this.#session.createSession,
177
+ signUpEmail: this.#oidc.signUpEmail,
178
+ signInEmail: this.#oidc.signInEmail,
179
+ revokeSession: this.#session.revokeSession,
180
+ setSessionCookie: this.#session.setSessionCookie
181
+ }
182
+ }
183
+ }
@@ -0,0 +1,414 @@
1
+ import { getAuth, type Auth } from 'firebase-admin/auth'
2
+ import { setCookie } from 'hono/cookie'
3
+ import type { Context } from 'hono'
4
+ import { CORE_CONFIG, type AppEnv, type DbRole } from '../config'
5
+ import type { EntryId } from '../db'
6
+ import type { Core } from '../index'
7
+ import type { AuthUser, CachedTokenEntry, UserAuthMetaRecord } from './schema'
8
+ import { fetchIdentityToolkit, normalizeDate, parseLinkedProviders, resolveTokenRole } from './utils'
9
+
10
+ type InternalVerifier = { signatureVerifier?: { keyFetcher?: { fetchPublicKeys?: () => Promise<unknown> } } }
11
+ let authWarmPromise: Promise<unknown> | undefined
12
+
13
+ const REVOCATION_SYNC_TTL_MS = 3_000
14
+ const AUTH_REVOCATIONS_DOC_PATH = '_system/auth_revocations'
15
+
16
+ export const tokenCache = new Map<string, CachedTokenEntry>()
17
+
18
+ interface RevocationWatermarkState {
19
+ lastSyncedAt: number
20
+ revokedUsers: Record<string, number>
21
+ }
22
+
23
+ const revocationWatermarks: [RevocationWatermarkState, RevocationWatermarkState] = [
24
+ { lastSyncedAt: 0, revokedUsers: {} },
25
+ { lastSyncedAt: 0, revokedUsers: {} }
26
+ ]
27
+
28
+ let bgSyncTimer: ReturnType<typeof setInterval> | null = null
29
+
30
+ export class AuthSessionEngine {
31
+ readonly #core: Core
32
+
33
+ constructor(coreClient: Core) {
34
+ this.#core = coreClient
35
+ }
36
+
37
+ getAuth = (): Auth => {
38
+ this.#core.internals.registry.ensureFirebaseApp()
39
+ return getAuth()
40
+ }
41
+
42
+ getAuthWarm = (): Promise<unknown> => {
43
+ if (!authWarmPromise) {
44
+ const auth = this.getAuth()
45
+ const rawAuth = auth as unknown as {
46
+ idTokenVerifier?: InternalVerifier
47
+ sessionCookieVerifier?: InternalVerifier
48
+ }
49
+ authWarmPromise = Promise.all([
50
+ rawAuth.idTokenVerifier?.signatureVerifier?.keyFetcher?.fetchPublicKeys?.(),
51
+ rawAuth.sessionCookieVerifier?.signatureVerifier?.keyFetcher?.fetchPublicKeys?.(),
52
+ auth.getUser(CORE_CONFIG.auth.adminUserId).catch(() => {})
53
+ ]).catch(() => {})
54
+ }
55
+ return authWarmPromise
56
+ }
57
+
58
+ resolveDefaultIsDev = (isDev?: boolean): boolean => isDev ?? this.#core.env.isDev
59
+
60
+ #ensureBackgroundRevocationSync = (): void => {
61
+ if (bgSyncTimer) return
62
+ bgSyncTimer = setInterval(() => {
63
+ if (tokenCache.size === 0) {
64
+ if (bgSyncTimer) {
65
+ clearInterval(bgSyncTimer)
66
+ bgSyncTimer = null
67
+ }
68
+ return
69
+ }
70
+ const defaultIsDev = this.#core.env.isDev
71
+ for (const isDevSlot of [false, true] as const) {
72
+ const idx = isDevSlot ? 1 : 0
73
+ if (isDevSlot === defaultIsDev || revocationWatermarks[idx].lastSyncedAt > 0) {
74
+ void this.syncRevocations(isDevSlot, true)
75
+ }
76
+ }
77
+ }, 2_000)
78
+ bgSyncTimer.unref?.()
79
+ }
80
+
81
+ setTokenCache = (
82
+ token: string,
83
+ uid: EntryId,
84
+ role: DbRole = uid === CORE_CONFIG.auth.adminUserId ? 'admin' : 'user',
85
+ exp = Date.now() + CORE_CONFIG.auth.tokenCacheTtlMs,
86
+ strictVerified = false,
87
+ emailVerified = false
88
+ ): void => {
89
+ const existing = tokenCache.get(token)
90
+ if (!existing && tokenCache.size >= CORE_CONFIG.auth.tokenCacheMaxSize) {
91
+ tokenCache.delete(tokenCache.keys().next().value!)
92
+ }
93
+ tokenCache.set(token, {
94
+ uid,
95
+ role,
96
+ exp,
97
+ cachedAt: Date.now(),
98
+ strictVerified: Boolean(strictVerified || existing?.strictVerified),
99
+ emailVerified: Boolean(emailVerified || existing?.emailVerified)
100
+ })
101
+ this.#ensureBackgroundRevocationSync()
102
+ }
103
+
104
+ evictUserTokens = (uid: EntryId, revokedAt?: number): void => {
105
+ for (const [k, v] of tokenCache) {
106
+ if (v.uid === uid && (revokedAt === undefined || v.cachedAt <= revokedAt)) {
107
+ tokenCache.delete(k)
108
+ }
109
+ }
110
+ }
111
+
112
+ recordRevocation = async (uid: EntryId, isDev?: boolean): Promise<number> => {
113
+ let revokedAt = Date.now()
114
+ this.evictUserTokens(uid)
115
+ const ttlMs = CORE_CONFIG.auth.tokenCacheTtlMs
116
+ const targetIsDev = this.resolveDefaultIsDev(isDev)
117
+ const localState = revocationWatermarks[targetIsDev ? 1 : 0]
118
+ localState.revokedUsers[uid] = revokedAt
119
+ localState.lastSyncedAt = revokedAt
120
+
121
+ const db = this.#core.db.firestore(targetIsDev)
122
+ const docRef = db.doc(AUTH_REVOCATIONS_DOC_PATH)
123
+ try {
124
+ await db.runTransaction(async (tx) => {
125
+ const snap = await tx.get(docRef)
126
+ const now = Date.now()
127
+ revokedAt = now
128
+ const data = snap.data() as { revokedUsers?: Record<string, number> } | undefined
129
+ const existing = data?.revokedUsers ?? {}
130
+ const pruned: Record<string, number> = {}
131
+ const cutoff = now - ttlMs
132
+ for (const [k, ts] of Object.entries(existing)) {
133
+ if (typeof ts === 'number' && ts > cutoff) pruned[k] = ts
134
+ }
135
+ pruned[uid] = now
136
+ tx.set(docRef, { revokedUsers: pruned, updatedAt: now })
137
+ })
138
+ localState.revokedUsers[uid] = revokedAt
139
+ localState.lastSyncedAt = revokedAt
140
+ } catch {}
141
+ return revokedAt
142
+ }
143
+
144
+ clearUserRevocation = async (uid: EntryId, isDev?: boolean): Promise<void> => {
145
+ const targetIsDev = this.resolveDefaultIsDev(isDev)
146
+ const localState = revocationWatermarks[targetIsDev ? 1 : 0]
147
+ delete localState.revokedUsers[uid]
148
+ localState.lastSyncedAt = Date.now()
149
+
150
+ const db = this.#core.db.firestore(targetIsDev)
151
+ const docRef = db.doc(AUTH_REVOCATIONS_DOC_PATH)
152
+ try {
153
+ await db.runTransaction(async (tx) => {
154
+ const snap = await tx.get(docRef)
155
+ const data = snap.data() as { revokedUsers?: Record<string, number> } | undefined
156
+ if (!data?.revokedUsers || !(uid in data.revokedUsers)) return
157
+ const next = { ...data.revokedUsers }
158
+ delete next[uid]
159
+ tx.set(docRef, { revokedUsers: next, updatedAt: Date.now() })
160
+ })
161
+ } catch {}
162
+ }
163
+
164
+ syncRevocations = async (isDev?: boolean, force = false): Promise<Record<string, number>> => {
165
+ const targetIsDev = this.resolveDefaultIsDev(isDev)
166
+ const state = revocationWatermarks[targetIsDev ? 1 : 0]
167
+ const now = Date.now()
168
+ if (!force && state.lastSyncedAt > 0 && now - state.lastSyncedAt < REVOCATION_SYNC_TTL_MS) {
169
+ return state.revokedUsers
170
+ }
171
+ return this.#core.internals.resilience.singleflight(
172
+ `auth:revocations:${targetIsDev ? 'dev' : 'prod'}`,
173
+ async () => {
174
+ try {
175
+ const snap = await this.#core.db.firestore(targetIsDev).doc(AUTH_REVOCATIONS_DOC_PATH).get()
176
+ const remote = (snap.data()?.revokedUsers as Record<string, number> | undefined) ?? {}
177
+ state.revokedUsers = remote
178
+ state.lastSyncedAt = Date.now()
179
+ for (const [uid, revokedAt] of Object.entries(remote)) {
180
+ if (typeof revokedAt === 'number') this.evictUserTokens(uid, revokedAt)
181
+ }
182
+ } catch {
183
+ state.lastSyncedAt = Date.now()
184
+ }
185
+ return state.revokedUsers
186
+ },
187
+ { scope: 'global' }
188
+ )
189
+ }
190
+
191
+ verifyToken = async (
192
+ token: string,
193
+ isCookie: boolean,
194
+ checkRevoked: boolean,
195
+ isDev?: boolean
196
+ ): Promise<{ uid: EntryId; role: DbRole; emailVerified: boolean }> => {
197
+ const targetIsDev = this.resolveDefaultIsDev(isDev)
198
+ const state = revocationWatermarks[targetIsDev ? 1 : 0]
199
+ let hit = tokenCache.get(token)
200
+ if (hit && hit.exp > Date.now()) {
201
+ if (Date.now() - state.lastSyncedAt >= REVOCATION_SYNC_TTL_MS) {
202
+ void this.syncRevocations(targetIsDev)
203
+ }
204
+ const revokedAt = state.revokedUsers[hit.uid]
205
+ if (typeof revokedAt === 'number' && revokedAt >= hit.cachedAt) {
206
+ this.evictUserTokens(hit.uid, revokedAt)
207
+ hit = undefined
208
+ }
209
+ }
210
+ if (hit && hit.exp > Date.now() && (!checkRevoked || hit.strictVerified)) {
211
+ return { uid: hit.uid, role: hit.role, emailVerified: Boolean(hit.emailVerified) }
212
+ }
213
+ return this.#core.internals.resilience.singleflight(
214
+ `auth:${isCookie ? 'c' : 'b'}:${checkRevoked ? '1' : '0'}:${token}`,
215
+ async () => {
216
+ const auth = this.getAuth()
217
+ const d = isCookie
218
+ ? await auth.verifySessionCookie(token, checkRevoked)
219
+ : await auth.verifyIdToken(token, checkRevoked)
220
+ const role = resolveTokenRole(d as unknown as Record<string, unknown>, d.uid)
221
+ const emailVerified = Boolean(d.email_verified)
222
+ this.setTokenCache(
223
+ token,
224
+ d.uid,
225
+ role,
226
+ Math.min(d.exp * 1000, Date.now() + CORE_CONFIG.auth.tokenCacheTtlMs),
227
+ checkRevoked,
228
+ emailVerified
229
+ )
230
+ return { uid: d.uid, role, emailVerified }
231
+ }
232
+ )
233
+ }
234
+
235
+ getUserAuthMeta = async (uid: EntryId, isDev?: boolean): Promise<UserAuthMetaRecord | undefined> => {
236
+ if (!uid) return undefined
237
+ try {
238
+ const snap = await this.#core.db
239
+ .firestore(this.resolveDefaultIsDev(isDev))
240
+ .collection('user_auth_meta')
241
+ .doc(uid)
242
+ .get()
243
+ if (!snap.exists) return undefined
244
+ const raw = snap.data() as Record<string, unknown> | undefined
245
+ if (!raw || raw.deleted === true) return undefined
246
+ const linkedProviders = parseLinkedProviders(raw.linkedProviders)
247
+ return {
248
+ ...(typeof raw.email === 'string' || raw.email === null ? { email: raw.email } : {}),
249
+ ...(typeof raw.emailVerified === 'boolean' ? { emailVerified: raw.emailVerified } : {}),
250
+ ...(typeof raw.name === 'string' || raw.name === null ? { name: raw.name } : {}),
251
+ ...(typeof raw.avatarUrl === 'string' || raw.avatarUrl === null ? { avatarUrl: raw.avatarUrl } : {}),
252
+ ...(typeof raw.recoveryEmail === 'string' || raw.recoveryEmail === null
253
+ ? { recoveryEmail: raw.recoveryEmail }
254
+ : {}),
255
+ ...(typeof raw.recoveryEmailVerified === 'boolean'
256
+ ? { recoveryEmailVerified: raw.recoveryEmailVerified }
257
+ : {}),
258
+ ...(linkedProviders.length > 0 ? { linkedProviders } : {}),
259
+ ...(Array.isArray(raw.providerKeys) ? { providerKeys: raw.providerKeys.map(String) } : {}),
260
+ createdAt: normalizeDate(raw.createdAt),
261
+ updatedAt: normalizeDate(raw.updatedAt)
262
+ }
263
+ } catch {
264
+ return undefined
265
+ }
266
+ }
267
+
268
+ upsertUserAuthMeta = async (
269
+ uid: EntryId,
270
+ patch: Partial<UserAuthMetaRecord>,
271
+ isDev?: boolean
272
+ ): Promise<UserAuthMetaRecord> => {
273
+ const db = this.#core.db.firestore(this.resolveDefaultIsDev(isDev))
274
+ const docRef = db.collection('user_auth_meta').doc(uid)
275
+ const now = new Date()
276
+ let merged: UserAuthMetaRecord = { ...patch, updatedAt: now }
277
+ await db.runTransaction(async (tx) => {
278
+ const snap = await tx.get(docRef)
279
+ const existing = snap.exists ? (snap.data() as Record<string, unknown>) : undefined
280
+ const createdAt = existing?.createdAt ? normalizeDate(existing.createdAt, now) : now
281
+ const existingProviders = parseLinkedProviders(existing?.linkedProviders, now)
282
+ const nextProviders = patch.linkedProviders ?? existingProviders
283
+ const providerKeys = Array.from(
284
+ new Set([
285
+ ...(Array.isArray(existing?.providerKeys) ? existing.providerKeys.map(String) : []),
286
+ ...(patch.providerKeys ?? []),
287
+ ...nextProviders.map((p) => `${p.provider}:${p.providerUserId}`)
288
+ ])
289
+ )
290
+ const nextDoc: Record<string, unknown> = {
291
+ id: uid,
292
+ deleted: false,
293
+ email: patch.email !== undefined ? patch.email : (existing?.email ?? null),
294
+ emailVerified: patch.emailVerified !== undefined ? patch.emailVerified : Boolean(existing?.emailVerified),
295
+ name: patch.name !== undefined ? patch.name : (existing?.name ?? null),
296
+ avatarUrl: patch.avatarUrl !== undefined ? patch.avatarUrl : (existing?.avatarUrl ?? null),
297
+ recoveryEmail: patch.recoveryEmail !== undefined ? patch.recoveryEmail : (existing?.recoveryEmail ?? null),
298
+ recoveryEmailVerified:
299
+ patch.recoveryEmailVerified !== undefined
300
+ ? patch.recoveryEmailVerified
301
+ : Boolean(existing?.recoveryEmailVerified),
302
+ linkedProviders: nextProviders,
303
+ providerKeys,
304
+ createdAt,
305
+ updatedAt: now
306
+ }
307
+ tx.set(docRef, nextDoc, { merge: true })
308
+ merged = {
309
+ email: nextDoc.email as string | null,
310
+ emailVerified: nextDoc.emailVerified as boolean,
311
+ name: nextDoc.name as string | null,
312
+ avatarUrl: nextDoc.avatarUrl as string | null,
313
+ recoveryEmail: nextDoc.recoveryEmail as string | null,
314
+ recoveryEmailVerified: nextDoc.recoveryEmailVerified as boolean,
315
+ linkedProviders: nextProviders,
316
+ providerKeys,
317
+ createdAt,
318
+ updatedAt: now
319
+ }
320
+ })
321
+ return merged
322
+ }
323
+
324
+ setSessionCookie = (c: Context<AppEnv>, sessionCookie: string): void => {
325
+ setCookie(c, CORE_CONFIG.auth.sessionCookie, sessionCookie, {
326
+ httpOnly: true,
327
+ secure: c.req.header('x-forwarded-proto') === 'https' || c.req.url.startsWith('https://'),
328
+ path: '/',
329
+ sameSite: 'Lax',
330
+ maxAge: Math.floor(CORE_CONFIG.auth.sessionExpiresInMs / 1000)
331
+ })
332
+ }
333
+
334
+ mintCustomToken = async (
335
+ uid = `anon_${crypto.randomUUID().replace(/-/g, '').slice(0, 16)}`
336
+ ): Promise<{ uid: string; customToken: string }> => ({
337
+ uid,
338
+ customToken: await this.getAuth().createCustomToken(uid)
339
+ })
340
+
341
+ createSession = async (
342
+ idToken: string,
343
+ profile?: { email?: string; name?: string; avatarUrl?: string; emailVerified?: boolean }
344
+ ): Promise<{ user: AuthUser; sessionCookie: string }> => {
345
+ const auth = this.getAuth()
346
+ const cacheTtl = CORE_CONFIG.auth.tokenCacheTtlMs
347
+ const sessionExpiresIn = CORE_CONFIG.auth.sessionExpiresInMs
348
+ const now = new Date()
349
+ const [ { decoded, meta }, sessionCookie ] = await Promise.all([
350
+ auth.verifyIdToken(idToken, true).then(async (d) => {
351
+ const role = resolveTokenRole(d as unknown as Record<string, unknown>, d.uid)
352
+ const emailVerified = Boolean(d.email_verified || profile?.emailVerified)
353
+ this.setTokenCache(idToken, d.uid, role, Math.min(d.exp * 1000, Date.now() + cacheTtl), true, emailVerified)
354
+ const updates: { displayName?: string; photoURL?: string } = {}
355
+ if (profile?.name && d.name !== profile.name) updates.displayName = profile.name
356
+ if (profile?.avatarUrl && d.picture !== profile.avatarUrl) updates.photoURL = profile.avatarUrl
357
+ const [m] = await Promise.all([
358
+ this.getUserAuthMeta(d.uid, this.#core.env.isDev),
359
+ Object.keys(updates).length > 0 ? auth.updateUser(d.uid, updates).catch(() => {}) : Promise.resolve()
360
+ ])
361
+ return { decoded: d, meta: m }
362
+ }),
363
+ auth.createSessionCookie(idToken, { expiresIn: sessionExpiresIn })
364
+ ])
365
+ const role = resolveTokenRole(decoded as unknown as Record<string, unknown>, decoded.uid)
366
+ const emailVerified = Boolean(decoded.email_verified || profile?.emailVerified || meta?.emailVerified)
367
+ const avatarUrl = profile?.avatarUrl ?? (decoded.picture as string | undefined) ?? meta?.avatarUrl
368
+ const user: AuthUser = {
369
+ id: decoded.uid,
370
+ email: decoded.email ?? profile?.email ?? meta?.email ?? null,
371
+ emailVerified,
372
+ name: profile?.name ?? (decoded.name as string | undefined) ?? meta?.name ?? null,
373
+ ...(avatarUrl !== undefined ? { avatarUrl } : {}),
374
+ ...(meta?.recoveryEmail !== undefined ? { recoveryEmail: meta.recoveryEmail } : {}),
375
+ ...(meta?.recoveryEmailVerified !== undefined ? { recoveryEmailVerified: meta.recoveryEmailVerified } : {}),
376
+ ...(meta?.linkedProviders?.length ? { linkedProviders: meta.linkedProviders } : {}),
377
+ ...(decoded.role || decoded.admin ? { customClaims: { role } } : {}),
378
+ deleted: false,
379
+ createdAt: meta?.createdAt ?? now,
380
+ updatedAt: now
381
+ }
382
+ this.setTokenCache(sessionCookie, user.id, role, Date.now() + cacheTtl, true, emailVerified)
383
+ return { user, sessionCookie }
384
+ }
385
+
386
+ mintSessionForUid = async (
387
+ uid: EntryId,
388
+ profile?: { email?: string; name?: string; avatarUrl?: string; emailVerified?: boolean }
389
+ ): Promise<{ user: AuthUser; sessionCookie: string }> => {
390
+ const customToken = await this.getAuth().createCustomToken(uid)
391
+ const { ok, data } = await fetchIdentityToolkit<{ idToken?: string; error?: { message?: string } }>(
392
+ 'accounts:signInWithCustomToken',
393
+ this.#core.env.WEB_API_KEY,
394
+ { token: customToken, returnSecureToken: true }
395
+ )
396
+ if (!ok || !data.idToken) {
397
+ const msg = data.error?.message ?? 'CUSTOM_TOKEN_EXCHANGE_FAILED'
398
+ return this.#core.errors.httpError(401, msg, msg)
399
+ }
400
+ return this.createSession(data.idToken, profile)
401
+ }
402
+
403
+ revokeSession = async (sessionCookie: string, deleteUser = false): Promise<void> => {
404
+ const auth = this.getAuth()
405
+ const uid = tokenCache.get(sessionCookie)?.uid ?? (await auth.verifySessionCookie(sessionCookie)).sub
406
+ this.evictUserTokens(uid)
407
+ tokenCache.delete(sessionCookie)
408
+ await Promise.all([
409
+ this.recordRevocation(uid, this.#core.env.isDev),
410
+ auth.revokeRefreshTokens(uid).catch(() => {}),
411
+ ...(deleteUser ? [auth.updateUser(uid, { disabled: true }).catch(() => {})] : [])
412
+ ])
413
+ }
414
+ }