@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,453 @@
1
+ import { getRequestListener } from '@hono/node-server'
2
+ import { defineSecret } from 'firebase-functions/params'
3
+ import { onRequest } from 'firebase-functions/v2/https'
4
+ import { onTaskDispatched } from 'firebase-functions/v2/tasks'
5
+ import { Hono, type Context } from 'hono'
6
+ import { bodyLimit } from 'hono/body-limit'
7
+ import { cors } from 'hono/cors'
8
+ import { z } from 'zod'
9
+ import { coreAttachmentsRoutes, getRegisteredAttachmentTypes, registerAttachmentTypes } from '../attachments'
10
+ import { coreAuthRoutes } from '../auth'
11
+ import { coreChatRoutes, mcpRoute } from '../chat'
12
+ import { CORE_CONFIG, SECRET_NAMES, type AppBindings, type AppEnv, type DbRole } from '../config'
13
+ import { registerCollections, type EntryId } from '../db'
14
+ import { coreEmailRoutes, registerEmailTemplates } from '../emails'
15
+ import { coreFilesRoutes } from '../files'
16
+ import { Core } from '../index'
17
+ import { corePaymentRoutes, registerPlans, registerProducts } from '../payments'
18
+ import {
19
+ buildHealthRoute, buildRoutesListRoute, buildTaskCancelRoute, buildTaskSignalRoute, buildTaskStatusRoute,
20
+ TaskWaitDetachedSignal, type RouteRegistryItem, type StepRunner, type WorkflowRuntimeEntry, type WorkflowTaskPayload
21
+ } from '../tasks'
22
+ import { isRespondAsyncPreferred, mergeCoreAndDomainRoutes, RouterGuardEngine } from './guard'
23
+ import {
24
+ type CoreAppInit, type CoreRouteId, type RouteConfig, type RouteExecutionContext, type RouteMode, type WorkflowRegistryEntry
25
+ } from './schema'
26
+
27
+ export class RouterService {
28
+ readonly #core: Core
29
+ readonly #guard: RouterGuardEngine
30
+
31
+ constructor(coreClient: Core) {
32
+ this.#core = coreClient
33
+ this.#guard = new RouterGuardEngine(coreClient)
34
+ }
35
+
36
+ readonly applyRateLimit = (
37
+ ...args: Parameters<RouterGuardEngine['applyRateLimit']>
38
+ ): ReturnType<RouterGuardEngine['applyRateLimit']> => this.#guard.applyRateLimit(...args)
39
+
40
+ readonly authenticateRequest = (
41
+ ...args: Parameters<RouterGuardEngine['authenticateRequest']>
42
+ ): ReturnType<RouterGuardEngine['authenticateRequest']> => this.#guard.authenticateRequest(...args)
43
+
44
+ #createCloudFunctions = (
45
+ app: Hono<AppEnv>,
46
+ devApp: Hono<AppEnv>,
47
+ workflowRegistry: Record<string, WorkflowRuntimeEntry>
48
+ ) => {
49
+ const emailProvider = CORE_CONFIG.emails.provider
50
+ const paymentProvider = CORE_CONFIG.payments.provider
51
+ const emailSecrets: string[] = [
52
+ ...(emailProvider === 'resend' ? ['RESEND_API_KEY', 'EMAIL_WEBHOOK_SECRET'] : []),
53
+ ...(emailProvider === 'sendgrid' ? ['SENDGRID_API_KEY', 'EMAIL_WEBHOOK_SECRET'] : [])
54
+ ]
55
+ const paymentSecrets: string[] = [
56
+ ...(paymentProvider === 'stripe' ? ['STRIPE_API_KEY', 'STRIPE_WEBHOOK_SECRET', 'STRIPE_SANDBOX_API_KEY', 'STRIPE_SANDBOX_WEBHOOK_SECRET'] : []),
57
+ ...(paymentProvider === 'dodo' ? ['DODO_API_KEY', 'DODO_WEBHOOK_SECRET', 'DODO_SANDBOX_API_KEY', 'DODO_SANDBOX_WEBHOOK_SECRET'] : [])
58
+ ]
59
+ const secretNames = [...new Set<string>([...SECRET_NAMES, ...emailSecrets, ...paymentSecrets, ...CORE_CONFIG.secrets])]
60
+ const sharedOpts = {
61
+ region: CORE_CONFIG.region,
62
+ memory: CORE_CONFIG.cloudRun.memory,
63
+ cpu: CORE_CONFIG.cloudRun.cpu,
64
+ secrets: secretNames.map((name) => defineSecret(name))
65
+ }
66
+ const httpOpts = { ...sharedOpts, ...CORE_CONFIG.cloudRun.http }
67
+ const workerOpts = { ...sharedOpts, ...CORE_CONFIG.cloudRun.worker }
68
+
69
+ const httpHandler = onRequest(httpOpts, getRequestListener((req) => app.fetch(req)))
70
+ const devHttpHandler = onRequest(httpOpts, getRequestListener((req) => devApp.fetch(req)))
71
+
72
+ const workerConfig = {
73
+ ...workerOpts,
74
+ invoker: [CORE_CONFIG.serviceAccount, CORE_CONFIG.computeAccount],
75
+ retryConfig: { maxAttempts: CORE_CONFIG.tasks.maxAttempts, minBackoffSeconds: 2, maxBackoffSeconds: 20 },
76
+ rateLimits: { maxConcurrentDispatches: 1000, maxDispatchesPerSecond: 500 }
77
+ }
78
+
79
+ const tasksHandler = onTaskDispatched<WorkflowTaskPayload>(workerConfig, async (req) => {
80
+ try {
81
+ await this.#core.internals.workflows.execute({ ...req.data, isDev: false }, workflowRegistry)
82
+ } catch (err) {
83
+ if (!this.#core.errors.isStaleWorkerEpochError(err)) throw err
84
+ }
85
+ })
86
+ const devTasksHandler = onTaskDispatched<WorkflowTaskPayload>(workerConfig, async (req) => {
87
+ try {
88
+ await this.#core.internals.workflows.execute({ ...req.data, isDev: true }, workflowRegistry)
89
+ } catch (err) {
90
+ if (!this.#core.errors.isStaleWorkerEpochError(err)) throw err
91
+ }
92
+ })
93
+
94
+ const fnName = CORE_CONFIG.gcpProject
95
+ const functions: Record<string, unknown> = {
96
+ [fnName]: httpHandler,
97
+ [`${fnName}-dev`]: devHttpHandler,
98
+ [`${fnName}-tasks`]: tasksHandler,
99
+ [`${fnName}-tasks-dev`]: devTasksHandler
100
+ }
101
+
102
+ return { httpHandler, devHttpHandler, tasksHandler, devTasksHandler, functions }
103
+ }
104
+
105
+ #makeCtx = <I, P = Record<string, string>, Q = Record<string, string>>(
106
+ env: AppBindings,
107
+ userId: EntryId,
108
+ role: DbRole,
109
+ mode: RouteMode,
110
+ input: I,
111
+ step: StepRunner = this.#core.internals.workflows.createStepRunner('rest'),
112
+ c?: Context<AppEnv>,
113
+ params?: P,
114
+ query?: Q,
115
+ rawBody?: string
116
+ ): RouteExecutionContext<I, P, Q> => ({
117
+ core: new Core({ env, userId, role, step }),
118
+ c: c as Context<AppEnv>,
119
+ env,
120
+ userId,
121
+ role,
122
+ mode,
123
+ step,
124
+ allWithRollback: this.#core.internals.workflows.allWithRollback,
125
+ input,
126
+ params: (params ?? (c ? c.req.param() : {})) as P,
127
+ query: (query ?? (c ? c.req.query() : {})) as Q,
128
+ ...(rawBody !== undefined ? { rawBody } : {})
129
+ })
130
+
131
+ #respondAsync202 = (c: Context<AppEnv> | undefined, taskId: string) => {
132
+ if (c) {
133
+ c.header('Preference-Applied', 'respond-async')
134
+ c.header('Location', `/api/tasks/${taskId}`)
135
+ return c.json({ taskId, status: 'running', pollUrl: `/api/tasks/${taskId}` }, 202)
136
+ }
137
+ return { taskId, status: 'running', pollUrl: `/api/tasks/${taskId}` }
138
+ }
139
+
140
+ #executeRouteCore = async (opts: {
141
+ key: string
142
+ wfName: string
143
+ cfg: RouteConfig
144
+ mode: RouteMode
145
+ runTask: (taskInput: unknown, step: StepRunner) => Promise<unknown>
146
+ parseOut: (out: unknown) => unknown
147
+ env: AppBindings
148
+ userId: EntryId
149
+ role: DbRole
150
+ parsed: unknown
151
+ parsedParams: Record<string, string>
152
+ parsedQuery: Record<string, string>
153
+ rawBody?: string
154
+ c?: Context<AppEnv>
155
+ wait: boolean
156
+ }): Promise<unknown> => {
157
+ const { key, wfName, cfg, mode, runTask, parseOut, env, userId, role, parsed, parsedParams, parsedQuery, rawBody, c, wait } =
158
+ opts
159
+ await this.#guard.enforceGuard(c, cfg, userId, parsed, role)
160
+ const dedupeKey =
161
+ (cfg.idempotencyKey ? cfg.idempotencyKey(parsed, { userId }) : undefined) ??
162
+ (c?.req.header('idempotency-key')?.trim() || undefined)
163
+ const runWithStep = <T>(m: RouteMode, fn: (ctx: RouteExecutionContext) => Promise<T> | T): Promise<T> => {
164
+ const step = this.#core.internals.workflows.createStepRunner(m)
165
+ return this.#core.internals.context.run({ env, userId, role, step }, async () =>
166
+ fn(this.#makeCtx(env, userId, role, m, parsed, step, c, parsedParams, parsedQuery, rawBody))
167
+ )
168
+ }
169
+
170
+ if (mode === 'rest') {
171
+ const executeRest = () => runWithStep('rest', cfg.handler)
172
+ const out = dedupeKey
173
+ ? await this.#guard.withHttpIdempotency(key, dedupeKey, userId, Boolean(env.isDev), executeRest)
174
+ : await executeRest()
175
+ return parseOut(out)
176
+ }
177
+
178
+ const cached = cfg.shortCircuit ? await runWithStep('workflow', cfg.shortCircuit) : undefined
179
+ if (cached !== undefined && cached !== null) return parseOut(cached)
180
+
181
+ try {
182
+ const dispatched = await this.#core.internals.workflows.dispatch(wfName, runTask, parsed, {
183
+ mode: 'workflow',
184
+ ...(cfg.timeout !== undefined ? { timeout: cfg.timeout } : {}),
185
+ ...(dedupeKey ? { dedupeKey } : {}),
186
+ wait,
187
+ ...(wait && c ? { maxWaitMs: CORE_CONFIG.limits.maxWaitMs, signal: c.req.raw?.signal } : {}),
188
+ userId,
189
+ userRole: role
190
+ })
191
+ return !wait ? this.#respondAsync202(c, (dispatched as { taskId: string }).taskId) : parseOut(dispatched)
192
+ } catch (err) {
193
+ if (err instanceof TaskWaitDetachedSignal) return this.#respondAsync202(c, err.taskId)
194
+ throw err
195
+ }
196
+ }
197
+
198
+ #registerRouteSpecAndTools = (
199
+ key: string,
200
+ wfName: string,
201
+ mode: RouteMode,
202
+ cfg: RouteConfig,
203
+ description: string,
204
+ inputSchema: z.ZodType<any>,
205
+ runTask: (taskInput: unknown, step: StepRunner) => Promise<unknown>,
206
+ invoke: WorkflowRegistryEntry['invoke'],
207
+ workflowRegistry: Record<string, WorkflowRegistryEntry>,
208
+ routeRegistryItems: RouteRegistryItem[]
209
+ ): void => {
210
+ const method = (cfg.method ?? 'POST').toUpperCase()
211
+ const path = cfg.path ?? `/${key}`
212
+ const deadlineSeconds = cfg.timeout
213
+ ? Math.max(15, Math.ceil(this.#core.errors.parseDuration(cfg.timeout, 540_000) / 1000))
214
+ : undefined
215
+
216
+ routeRegistryItems.push({
217
+ id: key,
218
+ mode,
219
+ method,
220
+ path,
221
+ ...(mode === 'workflow' || cfg.workflowName ? { workflowName: wfName } : {}),
222
+ auth: cfg.auth ?? 'user',
223
+ description,
224
+ mcp: cfg.mcp === true,
225
+ tool: cfg.tool !== false,
226
+ ...(cfg.input ? { input: cfg.input } : {}),
227
+ ...(cfg.output ? { output: cfg.output } : {}),
228
+ ...(cfg.guard ? { guard: cfg.guard } : {})
229
+ })
230
+
231
+ if (mode === 'workflow' || cfg.workflowName) {
232
+ workflowRegistry[wfName] = {
233
+ name: wfName,
234
+ description,
235
+ input: inputSchema,
236
+ output: cfg.output,
237
+ mode,
238
+ ...(deadlineSeconds !== undefined ? { deadlineSeconds } : {}),
239
+ guard: cfg.guard,
240
+ runTask,
241
+ invoke
242
+ }
243
+ }
244
+
245
+ if (cfg.tool !== false || cfg.mcp === true || mode === 'workflow' || cfg.workflowName) {
246
+ const registerToolUnder = (toolName: string, mcp: boolean, tool: boolean) =>
247
+ this.#core.internals.registry.registerWorkflowTool(toolName, {
248
+ description,
249
+ input: inputSchema,
250
+ mcp,
251
+ tool,
252
+ execute: (inp, u) => invoke(inp, u)
253
+ })
254
+ registerToolUnder(wfName, cfg.mcp === true, cfg.tool !== false)
255
+ if (key !== wfName && cfg.tool !== false) registerToolUnder(key, false, true)
256
+ }
257
+ }
258
+
259
+ #buildRouteInvoker = (
260
+ key: string,
261
+ cfg: RouteConfig,
262
+ workflowRegistry: Record<string, WorkflowRegistryEntry>,
263
+ routeRegistryItems: RouteRegistryItem[]
264
+ ): WorkflowRegistryEntry['invoke'] => {
265
+ const mode: RouteMode = cfg.mode ?? 'rest'
266
+ const wfName = cfg.workflowName ?? key
267
+ const inputSchema = (cfg.input ?? z.object({})) as z.ZodType<any>
268
+ const description = cfg.description ?? wfName
269
+ const parseOut = (out: unknown) => (out instanceof Response ? out : cfg.output ? cfg.output.parse(out) : out)
270
+
271
+ const runTask = async (taskInput: unknown, step: StepRunner) => {
272
+ const current = new Core()
273
+ const uid = current.userId || CORE_CONFIG.auth.adminUserId
274
+ return cfg.handler(this.#makeCtx(current.env, uid, current.role, mode, taskInput, step))
275
+ }
276
+
277
+ const invoke = async (
278
+ rawInput: unknown,
279
+ userId: EntryId,
280
+ c?: Context<AppEnv>,
281
+ explicitRole?: DbRole,
282
+ opts?: { wait?: boolean; rawBody?: string }
283
+ ) => {
284
+ const env = c?.env ?? this.#core.env
285
+ const authLevel = cfg.auth ?? 'user'
286
+ const role = this.#core.internals.auth.resolveUserRole(userId, explicitRole ?? (c?.get('userRole') as DbRole | undefined))
287
+
288
+ if (authLevel !== 'public' && authLevel !== 'none' && authLevel !== false && !userId) {
289
+ return this.#core.errors.httpError(401, 'Unauthorized', 'UNAUTHORIZED')
290
+ }
291
+ if ((authLevel === 'admin' || authLevel === 'api-key') && role !== 'admin' && userId !== CORE_CONFIG.auth.adminUserId) {
292
+ return this.#core.errors.httpError(403, 'Forbidden', 'FORBIDDEN')
293
+ }
294
+
295
+ const parsedParams = cfg.params && c ? cfg.params.parse(c.req.param()) : c?.req.param() ?? {}
296
+ const parsedQuery = cfg.query && c ? cfg.query.parse(c.req.query()) : c?.req.query() ?? {}
297
+ const parsed = cfg.input ? cfg.input.parse(rawInput ?? {}) : rawInput
298
+
299
+ return this.#core.internals.context.run({ env, userId, role }, () =>
300
+ this.#executeRouteCore({ key, wfName, cfg, mode, runTask, parseOut, env, userId, role, parsed, parsedParams, parsedQuery, rawBody: opts?.rawBody, c, wait: opts?.wait !== false })
301
+ )
302
+ }
303
+
304
+ this.#registerRouteSpecAndTools(key, wfName, mode, cfg, description, inputSchema, runTask, invoke, workflowRegistry, routeRegistryItems)
305
+ return invoke
306
+ }
307
+
308
+ #mountRoute = (app: Hono<AppEnv>, cfg: RouteConfig, invoke: WorkflowRegistryEntry['invoke']): void => {
309
+ const method = (cfg.method ?? 'POST').toLowerCase() as 'get' | 'post' | 'put' | 'patch' | 'delete' | 'all'
310
+ const routePath = cfg.path ?? '/'
311
+
312
+ app[method](routePath, async (c) => {
313
+ const userId = await this.authenticateRequest(c, cfg)
314
+ const role = this.#core.internals.auth.resolveUserRole(userId, c.get('userRole') as DbRole | undefined)
315
+ let rawInput: unknown
316
+ let rawText: string | undefined
317
+ if (cfg.rawBody && method !== 'get') {
318
+ rawText = await c.req.text().catch(() => '')
319
+ if (cfg.input) {
320
+ try {
321
+ rawInput = rawText ? JSON.parse(rawText) : {}
322
+ } catch {
323
+ rawInput = {}
324
+ }
325
+ } else {
326
+ rawInput = rawText
327
+ }
328
+ } else if (cfg.input && method !== 'get') {
329
+ rawInput = await c.req.json().catch(() => ({}))
330
+ }
331
+ const wait = !(cfg.mode === 'workflow' && isRespondAsyncPreferred(c))
332
+ const out = await invoke(rawInput, userId, c, role, {
333
+ wait,
334
+ ...(rawText !== undefined ? { rawBody: rawText } : {})
335
+ })
336
+ return out instanceof Response ? out : c.json(out)
337
+ })
338
+ }
339
+
340
+ #createHonoApp = (
341
+ isDev: boolean,
342
+ allRoutes: Record<string, RouteConfig>,
343
+ routeInvokers: Map<RouteConfig, WorkflowRegistryEntry['invoke']>
344
+ ): Hono<AppEnv> => {
345
+ const app = new Hono<AppEnv>().basePath('/api')
346
+ app.use(
347
+ '*',
348
+ cors({
349
+ origin: (origin, c) => {
350
+ if (!origin) return undefined
351
+ if (
352
+ CORE_CONFIG.auth.allowedOrigins.some((p: string | RegExp) =>
353
+ typeof p === 'string' ? p === origin : p.test(origin)
354
+ )
355
+ ) {
356
+ return origin
357
+ }
358
+ try {
359
+ if (origin === new URL(c.req.url).origin) return origin
360
+ } catch {}
361
+ return null
362
+ },
363
+ credentials: true
364
+ })
365
+ )
366
+ app.use(
367
+ '*',
368
+ bodyLimit({
369
+ maxSize: CORE_CONFIG.limits.maxBodyBytes,
370
+ onError: () => this.#core.errors.httpError(413, 'Payload too large (max 25MiB)', 'PAYLOAD_TOO_LARGE')
371
+ })
372
+ )
373
+ app.onError((err) => this.#core.errors.toHttpError(err).getResponse())
374
+ app.use('*', async (c, next) => {
375
+ const env: AppBindings = { ...this.#core.env, ...(c.env ?? {}), isDev }
376
+ c.env = env
377
+ await this.#core.internals.context.run(
378
+ { env, userId: CORE_CONFIG.auth.adminUserId, role: 'admin' },
379
+ () => next()
380
+ )
381
+ })
382
+
383
+ for (const r of Object.values(allRoutes)) this.#mountRoute(app, r, routeInvokers.get(r)!)
384
+
385
+ app.all('/mcp', async (c) => {
386
+ const userId = await this.authenticateRequest(c, { path: '/mcp', auth: 'user' })
387
+ return mcpRoute(c.req.raw, userId, this.#core.internals.context.getMcpTools())
388
+ })
389
+
390
+ return app
391
+ }
392
+
393
+ createApp = (init: CoreAppInit = {}) => {
394
+ if (init.emailTemplates) registerEmailTemplates(init.emailTemplates, true)
395
+ if (init.plans) registerPlans(init.plans, true)
396
+ if (init.products) registerProducts(init.products, true)
397
+
398
+ registerCollections(init.collections ?? {}, true)
399
+ this.#core.internals.registry.registerAgents(init.agents ?? {}, true)
400
+ if (init.attachmentTypes) registerAttachmentTypes(init.attachmentTypes)
401
+ this.#core.internals.registry.registerContextCatalog(init.context ?? [], true)
402
+ this.#core.internals.registry.clearWorkflowTools()
403
+
404
+ const overrides = init.coreRouteOverrides ?? {}
405
+ const workflowRegistry: Record<string, WorkflowRegistryEntry> = {}
406
+ const routeRegistryItems: RouteRegistryItem[] = []
407
+
408
+ const attachmentTypesDesc = Object.entries(getRegisteredAttachmentTypes())
409
+ .map(([k, v]) => `${k} (${v.description})`)
410
+ .join('; ')
411
+
412
+ const rawCoreRoutes: Record<CoreRouteId, Partial<RouteConfig>> = {
413
+ health: buildHealthRoute(),
414
+ ...coreAuthRoutes,
415
+ ...coreFilesRoutes,
416
+ ...coreChatRoutes,
417
+ ...coreAttachmentsRoutes,
418
+ ...coreEmailRoutes,
419
+ ...corePaymentRoutes,
420
+ 'routes.list': buildRoutesListRoute(() => routeRegistryItems),
421
+ 'tasks.status': buildTaskStatusRoute(),
422
+ 'tasks.signal': buildTaskSignalRoute(),
423
+ 'tasks.cancel': buildTaskCancelRoute()
424
+ }
425
+
426
+ const allRoutes = mergeCoreAndDomainRoutes(
427
+ rawCoreRoutes,
428
+ overrides,
429
+ attachmentTypesDesc,
430
+ init.routes ?? {}
431
+ )
432
+ const routeInvokers = new Map<RouteConfig, WorkflowRegistryEntry['invoke']>()
433
+ for (const [id, r] of Object.entries(allRoutes)) {
434
+ routeInvokers.set(r, this.#buildRouteInvoker(id, r, workflowRegistry, routeRegistryItems))
435
+ }
436
+
437
+ this.#core.internals.workflows.registerRuntime(workflowRegistry)
438
+
439
+ const createApp = (isDev = false) => this.#createHonoApp(isDev, allRoutes, routeInvokers)
440
+ const app = createApp(false)
441
+ const devApp = createApp(true)
442
+ const cloudFunctions = this.#createCloudFunctions(app, devApp, workflowRegistry)
443
+
444
+ return {
445
+ createApp,
446
+ app,
447
+ devApp,
448
+ workflowRegistry,
449
+ agents: this.#core.internals.registry.getRegisteredAgents(),
450
+ ...cloudFunctions
451
+ }
452
+ }
453
+ }
@@ -0,0 +1,11 @@
1
+ export * from './schema'
2
+ export { TasksService } from './service'
3
+ export {
4
+ buildHealthRoute,
5
+ buildRoutesIntrospection,
6
+ buildRoutesListRoute,
7
+ buildTaskCancelRoute,
8
+ buildTaskSignalRoute,
9
+ buildTaskStatusRoute,
10
+ renderApiContractMarkdown
11
+ } from './routes'
@@ -0,0 +1,230 @@
1
+ import type { Context } from 'hono'
2
+ import type { z } from 'zod'
3
+ import { CORE_CONFIG, type AppEnv } from '../config'
4
+ import { collectionRegistry, getEffectiveRbac } from '../db'
5
+ import { core } from '../index'
6
+ import { CORE_ROUTE_DEFAULTS, type BaseRouteConfig } from '../router'
7
+ import {
8
+ signalTaskBody,
9
+ type RouteIntrospectionItem,
10
+ type RouteRegistryItem,
11
+ type RoutesIntrospectionResponse
12
+ } from './schema'
13
+ import { zodToJsonSchemaSafe } from './utils'
14
+
15
+ export {
16
+ signalTaskBody,
17
+ zodToJsonSchemaSafe,
18
+ type RouteIntrospectionItem,
19
+ type RouteRegistryItem,
20
+ type RoutesIntrospectionResponse
21
+ }
22
+
23
+ const summarizeGuards = (g?: BaseRouteConfig['guard']): string[] => {
24
+ if (!g) return []
25
+ const guards: string[] = []
26
+ if (g.checkFileRef) guards.push('checkFileRef')
27
+ if (g.checkUserId) guards.push('checkUserId')
28
+ if (g.checkCollectionField) {
29
+ guards.push(`checkCollectionField:${g.checkCollectionField.collection}.${g.checkCollectionField.param}`)
30
+ }
31
+ if (g.requireEmailVerified) guards.push('requireEmailVerified')
32
+ if (g.custom) guards.push('custom')
33
+ return guards
34
+ }
35
+
36
+ export const buildRoutesIntrospection = (
37
+ routeItems: RouteRegistryItem[],
38
+ scope: 'all' | 'custom' | 'core' = 'all'
39
+ ): RoutesIntrospectionResponse => {
40
+ const filtered = routeItems.filter((r) => {
41
+ const isCore = r.id in CORE_ROUTE_DEFAULTS
42
+ if (scope === 'core') return isCore
43
+ if (scope === 'custom') return !isCore
44
+ return true
45
+ })
46
+
47
+ const routes: RouteIntrospectionItem[] = filtered.map((r) => ({
48
+ id: r.id,
49
+ method: r.method,
50
+ path: `/api${r.path}`,
51
+ mode: r.mode,
52
+ ...(r.workflowName ? { workflowName: r.workflowName } : {}),
53
+ auth: r.auth ?? 'user',
54
+ description: r.description,
55
+ mcp: Boolean(r.mcp),
56
+ tool: r.tool !== false,
57
+ guards: summarizeGuards(r.guard),
58
+ inputSchema: zodToJsonSchemaSafe(r.input),
59
+ outputSchema: zodToJsonSchemaSafe(r.output)
60
+ }))
61
+
62
+ const collections: NonNullable<RoutesIntrospectionResponse['collections']> = {}
63
+ for (const [name, def] of collectionRegistry) {
64
+ collections[name] = {
65
+ relationType: def.userRelation?.type ?? '1:N',
66
+ rbac: getEffectiveRbac(name) as Record<string, unknown>,
67
+ schema: zodToJsonSchemaSafe(def.schema)
68
+ }
69
+ }
70
+
71
+ return {
72
+ appName: CORE_CONFIG.appName,
73
+ generatedAt: new Date().toISOString(),
74
+ routes,
75
+ collections
76
+ }
77
+ }
78
+
79
+ export const renderApiContractMarkdown = (meta: RoutesIntrospectionResponse): string => {
80
+ const coreRoutes = meta.routes.filter((r) => r.id in CORE_ROUTE_DEFAULTS)
81
+ const domainRoutes = meta.routes.filter((r) => !(r.id in CORE_ROUTE_DEFAULTS))
82
+
83
+ const renderRouteBlock = (r: RouteIntrospectionItem) => {
84
+ const lines: string[] = [
85
+ `### ${r.method} ${r.path}`,
86
+ `- **ID:** \`${r.id}\`${r.workflowName ? ` (Workflow: \`${r.workflowName}\`)` : ''}`,
87
+ `- **Mode:** \`${r.mode}\` | **Auth:** \`${String(r.auth)}\` | **Guards:** \`${r.guards.join(', ') || 'none'}\` | **MCP:** \`${r.mcp}\` | **Tool:** \`${r.tool}\``,
88
+ `- **Description:** ${r.description}`
89
+ ]
90
+ if (r.inputSchema) {
91
+ lines.push('', '**Input JSON Schema:**', '```json', JSON.stringify(r.inputSchema, null, 2), '```')
92
+ }
93
+ if (r.outputSchema) {
94
+ lines.push('', '**Output JSON Schema:**', '```json', JSON.stringify(r.outputSchema, null, 2), '```')
95
+ }
96
+ return lines.join('\n')
97
+ }
98
+
99
+ const collectionLines: string[] = []
100
+ for (const [name, col] of Object.entries(meta.collections ?? {})) {
101
+ collectionLines.push(
102
+ `### Collection: \`${name}\``,
103
+ `- **User Relation:** \`${col.relationType}\``,
104
+ `- **RBAC Matrix:** \`${JSON.stringify(col.rbac ?? {})}\``,
105
+ ...(col.schema ? ['', '```json', JSON.stringify(col.schema, null, 2), '```'] : [])
106
+ )
107
+ }
108
+
109
+ return [
110
+ `# API Contract & Schema Reference — ${meta.appName}`,
111
+ '',
112
+ `> **Generated At:** \`${meta.generatedAt}\``,
113
+ '> **Agent Instruction:** This document is deterministically generated from runtime Zod schemas and route definitions. Treat it as the single source of truth for all HTTP endpoints, payloads, authentication levels, and Firestore collection models.',
114
+ '',
115
+ '## 1. Core Protocol & Architecture Conventions',
116
+ '',
117
+ '### 1.1. Authentication & Sessions (`auth`)',
118
+ '- **`public` / `none` / `false`**: No authentication required.',
119
+ '- **`user`**: Requires `Authorization: Bearer <idToken | SDK_API_KEY>` or HTTP-only `session` cookie.',
120
+ '- **`strict`**: Requires revocation-checked session verification (`verifySessionCookie(..., true)` / `_system/auth_revocations`).',
121
+ '- **`admin`**: Requires admin role (`SDK_API_KEY` or admin user claim).',
122
+ '- **Social OAuth & Account Recovery**: Use `POST /api/auth/sign-in/social` (`google` | `apple`), `POST /api/auth/email/send-verification`, `POST /api/auth/email/verify`, `POST /api/auth/password/forgot`, `POST /api/auth/password/reset`, `POST /api/auth/recovery-email/set`, `POST /api/auth/recovery-email/verify`, `POST /api/auth/account-recovery/initiate`, and `POST /api/auth/account-recovery/complete`.',
123
+ '',
124
+ '### 1.2. Content-Addressed Files (`FileRef`)',
125
+ '- All uploaded files use content-addressed IDs formatted as `<userId>_<sha256_24>` matching `/^[A-Za-z0-9_-]{1,128}_[a-f0-9]{24}$/`.',
126
+ '- Upload files via `POST /api/files` (`multipart/form-data` field `file`) before passing `ref` or `refs` to downstream endpoints.',
127
+ '',
128
+ '### 1.3. Execution Modes (`mode: "rest"` vs `mode: "workflow"`)',
129
+ '- **`mode: "rest"`**: Synchronous `200 OK` JSON response.',
130
+ '- **`mode: "workflow"`**: Durable Cloud Tasks workflow. May return `200 OK` directly (if resolved via `shortCircuit` cache or within wait budget) **OR** `202 Accepted` (`{ taskId, status: "running", pollUrl: "/api/tasks/<taskId>" }`) when `Prefer: respond-async` / `?wait=false` is passed.',
131
+ '- Poll task progress via `GET /api/tasks/:taskId`, send signals via `POST /api/tasks/:taskId/signal`, or cancel via `DELETE /api/tasks/:taskId`.',
132
+ '',
133
+ '### 1.4. Thin-Client AI Chat Streaming (`POST /api/chats/:chatId`)',
134
+ '- Send `{ message: { id?, role: "user", content, parts?, attachments? }, agentId?, model? }` to stream SSE responses and cancel active streams via `POST /api/chats/:chatId/cancel`.',
135
+ '',
136
+ '## 2. Core Endpoints Catalog',
137
+ '',
138
+ ...coreRoutes.map(renderRouteBlock),
139
+ '',
140
+ '## 3. Domain Endpoints Catalog',
141
+ '',
142
+ ...(domainRoutes.length > 0 ? domainRoutes.map(renderRouteBlock) : ['_No custom domain routes registered._']),
143
+ '',
144
+ '## 4. Registered Firestore Collections Catalog',
145
+ '',
146
+ ...collectionLines
147
+ ].join('\n')
148
+ }
149
+
150
+ export const buildHealthRoute = () => ({
151
+ mode: 'rest' as const,
152
+ handler: async () => ({
153
+ ok: true,
154
+ status: 'healthy' as const,
155
+ service: CORE_CONFIG.appName,
156
+ env: core.env.isDev ? ('dev' as const) : ('prod' as const),
157
+ timestamp: new Date().toISOString()
158
+ })
159
+ })
160
+
161
+ export const buildRoutesListRoute = (routeItems: () => RouteRegistryItem[]) => ({
162
+ mode: 'rest' as const,
163
+ handler: async ({ c }: { c: Context<AppEnv> }) => {
164
+ const rawScope = c?.req.query('scope')
165
+ const scope: 'all' | 'custom' | 'core' =
166
+ rawScope === 'custom' || rawScope === 'core' ? rawScope : 'all'
167
+ const format = c?.req.query('format')
168
+ const accept = c?.req.header('accept') ?? ''
169
+ const meta = buildRoutesIntrospection(routeItems(), scope)
170
+
171
+ if (format === 'md' || accept.includes('text/markdown')) {
172
+ return new Response(renderApiContractMarkdown(meta), {
173
+ status: 200,
174
+ headers: { 'content-type': 'text/markdown; charset=utf-8' }
175
+ })
176
+ }
177
+ return meta
178
+ }
179
+ })
180
+
181
+ export const buildTaskStatusRoute = () => ({
182
+ mode: 'rest' as const,
183
+ handler: async ({ c }: { c: Context<AppEnv> }) => {
184
+ const taskId = c.req.param('taskId') ?? ''
185
+ const status = await core.tasks.status(taskId)
186
+ if (
187
+ status.status === 'running' ||
188
+ status.status === 'sleeping' ||
189
+ status.status === 'waiting_signal' ||
190
+ status.status === 'compensating'
191
+ ) {
192
+ return c.json(
193
+ {
194
+ taskId,
195
+ status: status.status,
196
+ ...(status.progress ? { progress: status.progress } : {}),
197
+ pollUrl: `/api/tasks/${taskId}`
198
+ },
199
+ 202
200
+ )
201
+ }
202
+ if (status.status === 'errored') {
203
+ return core.errors.httpError(502, status.error ?? 'Workflow task failed', 'WORKFLOW_ERROR')
204
+ }
205
+ if (status.status === 'cancelled') {
206
+ return core.errors.httpError(409, status.error ?? 'Workflow task cancelled', 'TASK_CANCELLED')
207
+ }
208
+ if (c.req.query('envelope') === 'true') {
209
+ return status
210
+ }
211
+ return status.response
212
+ }
213
+ })
214
+
215
+ export const buildTaskSignalRoute = () => ({
216
+ mode: 'rest' as const,
217
+ input: signalTaskBody,
218
+ handler: async ({ c, input }: { c: Context<AppEnv>; input: z.infer<typeof signalTaskBody> }) => {
219
+ const taskId = c.req.param('taskId') ?? ''
220
+ return core.tasks.signal(taskId, input.signal, input.payload)
221
+ }
222
+ })
223
+
224
+ export const buildTaskCancelRoute = () => ({
225
+ mode: 'rest' as const,
226
+ handler: async ({ c }: { c: Context<AppEnv> }) => {
227
+ const taskId = c.req.param('taskId') ?? ''
228
+ return core.tasks.cancel(taskId)
229
+ }
230
+ })