@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,461 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks'
2
+ import { createHash } from 'node:crypto'
3
+ import { Timestamp, type DocumentReference, type Firestore } from 'firebase-admin/firestore'
4
+ import { CORE_CONFIG, type DbRole } from '../config'
5
+ import type { EntryId } from '../db'
6
+ import type { Core } from '../index'
7
+ import type { RouteMode } from '../router'
8
+ import type { Duration } from '../utils'
9
+ import {
10
+ StaleWorkerEpochError,
11
+ type ActiveTaskExecution,
12
+ type ActiveWorkflowStore,
13
+ type DispatchWorkflowOptions,
14
+ type ExecuteWorkflowTaskOptions,
15
+ type StepRunner,
16
+ type TaskRecord,
17
+ type TaskYieldSignal,
18
+ type WorkflowContext,
19
+ type WorkflowRuntimeEntry,
20
+ type WorkflowTaskPayload
21
+ } from './schema'
22
+ import type { TaskStepsEngine } from './steps'
23
+ import {
24
+ claimTaskEpochTx,
25
+ commitTaskCompensatingTx,
26
+ commitTaskCompletionTx,
27
+ commitTaskYieldTx,
28
+ isTaskYieldSignal,
29
+ resolveTaskDeadline,
30
+ setTaskDoc,
31
+ startTaskHeartbeatTimer
32
+ } from './utils'
33
+ import type { TaskWatcherEngine } from './watcher'
34
+
35
+ const activeTaskExecutions = new Map<string, ActiveTaskExecution>()
36
+ const workflowStorage = new AsyncLocalStorage<ActiveWorkflowStore>()
37
+ let activeRegistry: Record<string, WorkflowRuntimeEntry> = {}
38
+
39
+ export class TaskWorkerEngine {
40
+ readonly #core: Core
41
+ #steps!: TaskStepsEngine
42
+ #watcher!: TaskWatcherEngine
43
+
44
+ constructor(coreClient: Core) {
45
+ this.#core = coreClient
46
+ }
47
+
48
+ bindEngines = (steps: TaskStepsEngine, watcher: TaskWatcherEngine): void => {
49
+ this.#steps = steps
50
+ this.#watcher = watcher
51
+ }
52
+
53
+ getActiveWorkflowStore = (): ActiveWorkflowStore | undefined => workflowStorage.getStore()
54
+
55
+ getContext = (): WorkflowContext => {
56
+ const store = workflowStorage.getStore()
57
+ if (store) return store.workflow
58
+ const now = Date.now()
59
+ return {
60
+ taskId: 'inline',
61
+ workflowName: 'inline',
62
+ attempt: 1,
63
+ startedAt: now,
64
+ deadlineMs: now + CORE_CONFIG.tasks.defaultDeadlineSeconds * 1000,
65
+ signal: new AbortController().signal
66
+ }
67
+ }
68
+
69
+ runWithWorkflowContext = <T>(store: ActiveWorkflowStore, fn: () => Promise<T>): Promise<T> =>
70
+ workflowStorage.run(store, fn)
71
+
72
+ registerActiveTaskExecution = (exec: ActiveTaskExecution): void => {
73
+ activeTaskExecutions.set(exec.taskId, exec)
74
+ }
75
+
76
+ unregisterActiveTaskExecution = (taskId: string, step?: StepRunner): void => {
77
+ const existing = activeTaskExecutions.get(taskId)
78
+ if (existing && (!step || existing.step === step)) activeTaskExecutions.delete(taskId)
79
+ }
80
+
81
+ getActiveTaskExecution = (taskId: string): ActiveTaskExecution | undefined => activeTaskExecutions.get(taskId)
82
+
83
+ registerRuntime = (registry: Record<string, WorkflowRuntimeEntry>): void => {
84
+ activeRegistry = registry
85
+ }
86
+
87
+ getRuntime = (): { registry: Record<string, WorkflowRuntimeEntry> } => ({ registry: activeRegistry })
88
+
89
+ invokeChildWorkflow = async (
90
+ targetName: string,
91
+ input: unknown,
92
+ options?: { timeout?: Duration; dedupeKey?: string; wait?: boolean }
93
+ ): Promise<any> => {
94
+ const wf = activeRegistry[targetName]
95
+ if (!wf) throw new Error(`Unknown child workflow: ${targetName}`)
96
+ const timeout: Duration | undefined =
97
+ options?.timeout ?? (wf.deadlineSeconds !== undefined ? `${wf.deadlineSeconds}s` : undefined)
98
+ return this.dispatch(targetName, wf.runTask as any, input, {
99
+ mode: wf.mode,
100
+ ...(timeout !== undefined ? { timeout } : {}),
101
+ ...(options?.dedupeKey ? { dedupeKey: options.dedupeKey } : {}),
102
+ wait: options?.wait !== false,
103
+ userId: this.#core.userId,
104
+ userRole: this.#core.role
105
+ })
106
+ }
107
+
108
+ #unwrapTaskRecord = <O>(res: TaskRecord<O>): O => {
109
+ if (res.status === 'completed') return res.response
110
+ if (res.status === 'cancelled') {
111
+ return this.#core.errors.httpError(409, res.error ?? 'Workflow task cancelled', 'TASK_CANCELLED')
112
+ }
113
+ if (res.nonRetryable) {
114
+ return this.#core.errors.httpError(400, res.error, res.errorCode ?? 'NON_RETRYABLE_ERROR')
115
+ }
116
+ return this.#core.errors.httpError(502, res.error, 'WORKFLOW_ERROR')
117
+ }
118
+
119
+ #dispatchInline = async <I, O>(
120
+ workflowName: string,
121
+ mode: RouteMode,
122
+ deadline: number,
123
+ userId: EntryId,
124
+ userRole: DbRole,
125
+ fn: (input: I, step: StepRunner) => Promise<O>,
126
+ params: I
127
+ ): Promise<O> => {
128
+ const env = this.#core.env
129
+ const now = Date.now()
130
+ const wfCtx: WorkflowContext = {
131
+ taskId: 'inline',
132
+ workflowName,
133
+ attempt: 1,
134
+ startedAt: now,
135
+ deadlineMs: now + deadline * 1000,
136
+ signal: new AbortController().signal
137
+ }
138
+ const step = this.#steps.createStepRunner(mode, undefined, {}, undefined, {
139
+ isDev: env.isDev,
140
+ workflowContext: wfCtx,
141
+ canYield: false
142
+ })
143
+ try {
144
+ return await this.runWithWorkflowContext({ workflow: wfCtx, step, env }, () =>
145
+ this.#core.internals.context.run({ env, userId, role: userRole, step }, () => fn(params, step))
146
+ )
147
+ } catch (err) {
148
+ await step.compensate()
149
+ if (err instanceof this.#core.errors.NonRetryableError) {
150
+ return this.#core.errors.httpError(400, err.message, err.code ?? 'NON_RETRYABLE_ERROR')
151
+ }
152
+ const msg = err instanceof Error ? err.message : String(err)
153
+ return this.#core.errors.httpError(502, msg, 'WORKFLOW_ERROR')
154
+ }
155
+ }
156
+
157
+ #initOrReuseTaskDoc = async <O>(
158
+ db: Firestore,
159
+ docRef: DocumentReference,
160
+ deterministicId: string | undefined,
161
+ workflowName: string,
162
+ params: unknown,
163
+ userId: EntryId,
164
+ userRole: DbRole
165
+ ): Promise<{ settled: true; response: O } | { settled: false; shouldDispatch: boolean }> => {
166
+ const nowMs = Date.now()
167
+ const nowIso = new Date(nowMs).toISOString()
168
+ const initialTaskDoc = {
169
+ taskId: docRef.id,
170
+ workflowName,
171
+ status: 'running',
172
+ epoch: 0,
173
+ activeRunId: null,
174
+ compensatedSteps: {},
175
+ params: (params ?? {}) as Record<string, unknown>,
176
+ steps: {},
177
+ userId,
178
+ userRole,
179
+ keepAlive: true,
180
+ lastHeartbeatAt: nowMs,
181
+ createdAt: nowIso,
182
+ updatedAt: nowIso,
183
+ expiresAt: Timestamp.fromMillis(nowMs + CORE_CONFIG.tasks.ttlMs)
184
+ }
185
+
186
+ if (!deterministicId) {
187
+ await docRef.set(initialTaskDoc)
188
+ return { settled: false, shouldDispatch: true }
189
+ }
190
+
191
+ let shouldDispatch = true
192
+ const existingOutcome = await db.runTransaction(async (tx) => {
193
+ const data = (await tx.get(docRef)).data()
194
+ if (data?.status === 'completed') return { settled: true as const, response: data.response as O }
195
+ if (['running', 'sleeping', 'waiting_signal', 'compensating'].includes(String(data?.status))) {
196
+ shouldDispatch = false
197
+ return { settled: false as const }
198
+ }
199
+ tx.set(docRef, initialTaskDoc)
200
+ return { settled: false as const }
201
+ })
202
+ return existingOutcome.settled ? existingOutcome : { settled: false, shouldDispatch }
203
+ }
204
+
205
+ dispatch = async <I, O>(
206
+ workflowName: string,
207
+ fn: (input: I, step: StepRunner) => Promise<O>,
208
+ params: I,
209
+ opts?: DispatchWorkflowOptions
210
+ ): Promise<O> => {
211
+ const env = this.#core.env
212
+ const mode = opts?.mode ?? 'workflow'
213
+ const explicitDeadline = opts?.timeout
214
+ ? Math.max(
215
+ 15,
216
+ Math.ceil(this.#core.errors.parseDuration(opts.timeout, CORE_CONFIG.tasks.defaultDeadlineSeconds * 1000) / 1000)
217
+ )
218
+ : undefined
219
+ const deadline = resolveTaskDeadline(workflowName, explicitDeadline)
220
+ const useQueue = mode !== 'rest' && CORE_CONFIG.tasks.useCloudTasks
221
+ const userId = opts?.userId ?? this.#core.userId ?? CORE_CONFIG.auth.adminUserId
222
+ const userRole = this.#core.internals.auth.resolveUserRole(userId, opts?.userRole ?? this.#core.role)
223
+
224
+ const db = this.#core.db.firestore(env.isDev)
225
+ const deterministicId = opts?.dedupeKey
226
+ ? `dedupe_${createHash('sha256').update(`${workflowName}:${opts.dedupeKey}`).digest('hex').slice(0, 24)}`
227
+ : undefined
228
+
229
+ if (!useQueue && opts?.wait !== false && !deterministicId && opts?.maxWaitMs === undefined) {
230
+ return this.#dispatchInline(workflowName, mode, deadline, userId, userRole, fn, params)
231
+ }
232
+
233
+ const docRef = deterministicId ? db.collection('_tasks').doc(deterministicId) : db.collection('_tasks').doc()
234
+ const taskId = docRef.id
235
+ const keepAlive = Boolean(deterministicId || opts?.wait === false || opts?.maxWaitMs !== undefined)
236
+ const initRes = await this.#initOrReuseTaskDoc<O>(db, docRef, deterministicId, workflowName, params, userId, userRole)
237
+ if (initRes.settled) {
238
+ return opts?.wait === false
239
+ ? ({ taskId } as unknown as O)
240
+ : await this.#steps.maybeHydrateStepValue<O>(initRes.response, Boolean(env.isDev))
241
+ }
242
+
243
+ const payload: WorkflowTaskPayload = {
244
+ taskId,
245
+ workflowName,
246
+ params: (params ?? {}) as Record<string, unknown>,
247
+ isDev: Boolean(env.isDev),
248
+ expectedEpoch: 0
249
+ }
250
+ const dispatchFn = () =>
251
+ useQueue
252
+ ? this.#watcher.enqueueBatched(payload, deadline)
253
+ : this.execute(payload, {
254
+ ...activeRegistry,
255
+ [workflowName]: { mode, deadlineSeconds: deadline, runTask: fn as any }
256
+ })
257
+
258
+ if (opts?.wait === false) {
259
+ if (initRes.shouldDispatch) void dispatchFn().catch(() => {})
260
+ return { taskId } as unknown as O
261
+ }
262
+
263
+ const record = await this.#watcher.watchTask<O>(taskId, {
264
+ isDev: Boolean(env.isDev),
265
+ docRef,
266
+ workflowName,
267
+ hedgeable: workflowName === 'scan-attachment',
268
+ keepAlive,
269
+ timeoutMs: CORE_CONFIG.tasks.timeoutMs,
270
+ ...(opts?.maxWaitMs !== undefined ? { maxWaitMs: opts.maxWaitMs } : {}),
271
+ ...(opts?.signal ? { signal: opts.signal } : {}),
272
+ ...(initRes.shouldDispatch ? { dispatch: dispatchFn } : {})
273
+ })
274
+ return this.#unwrapTaskRecord(record)
275
+ }
276
+
277
+ #commitCompletion = async (opts: {
278
+ db: Firestore
279
+ docRef: DocumentReference
280
+ baseMeta: Record<string, unknown>
281
+ taskId: string
282
+ claimedEpoch: number
283
+ response: unknown
284
+ steps: Record<string, unknown>
285
+ expiresAt: Timestamp
286
+ isDev: boolean
287
+ }): Promise<void> => {
288
+ const stored = await this.#steps.maybeOffloadStepValue(opts.docRef, '__response', opts.response, opts.isDev)
289
+ await commitTaskCompletionTx(opts.db, opts.docRef, opts.baseMeta, opts.taskId, opts.claimedEpoch, stored, opts.steps, opts.expiresAt)
290
+ }
291
+
292
+ #commitYieldAndEnqueue = async (opts: {
293
+ db: Firestore
294
+ docRef: DocumentReference
295
+ baseMeta: Record<string, unknown>
296
+ taskId: string
297
+ workflowName: string
298
+ claimedEpoch: number
299
+ err: TaskYieldSignal
300
+ effectiveParams: Record<string, unknown>
301
+ steps: Record<string, unknown>
302
+ expiresAt: Timestamp
303
+ isDev: boolean
304
+ useQueue: boolean
305
+ deadlineSeconds: number
306
+ }): Promise<void> => {
307
+ const nextEpoch = opts.claimedEpoch + 1
308
+ const nextParams =
309
+ opts.err.reason === 'continue_as_new'
310
+ ? ((opts.err.meta.newInput ?? {}) as Record<string, unknown>)
311
+ : opts.effectiveParams
312
+ const nextSteps = opts.err.reason === 'continue_as_new' ? {} : opts.steps
313
+ const nextStatus =
314
+ opts.err.reason === 'sleeping' ? 'sleeping' : opts.err.reason === 'waiting_signal' ? 'waiting_signal' : 'running'
315
+ await commitTaskYieldTx(opts.db, opts.docRef, opts.baseMeta, opts.taskId, opts.claimedEpoch, opts.err, nextEpoch, nextParams, nextSteps, nextStatus, opts.expiresAt)
316
+ if (!opts.useQueue) return
317
+ const nextPayload: WorkflowTaskPayload = {
318
+ taskId: opts.taskId,
319
+ workflowName: opts.workflowName,
320
+ params: nextParams,
321
+ isDev: opts.isDev,
322
+ expectedEpoch: nextEpoch
323
+ }
324
+ if (opts.err.reason === 'continue_as_new' || opts.err.reason === 'yield') {
325
+ await this.#watcher.enqueueBatched(nextPayload, opts.deadlineSeconds)
326
+ } else if (opts.err.reason === 'sleeping') {
327
+ await this.#watcher.enqueueBatched(nextPayload, opts.deadlineSeconds, opts.err.meta.delaySeconds ?? 1)
328
+ } else if (opts.err.reason === 'waiting_signal' && opts.err.meta.timeoutMs && opts.err.meta.timeoutMs > 0) {
329
+ await this.#watcher.enqueueBatched(nextPayload, opts.deadlineSeconds, Math.ceil(opts.err.meta.timeoutMs / 1000))
330
+ }
331
+ }
332
+
333
+ #commitErrorAndCompensate = async (opts: {
334
+ db: Firestore
335
+ docRef: DocumentReference
336
+ baseMeta: Record<string, unknown>
337
+ taskId: string
338
+ claimedEpoch: number
339
+ err: unknown
340
+ step: StepRunner
341
+ steps: Record<string, unknown>
342
+ expiresAt: Timestamp
343
+ }): Promise<void> => {
344
+ const error = opts.err instanceof Error ? opts.err.message : String(opts.err)
345
+ const nonRetryable = opts.err instanceof this.#core.errors.NonRetryableError
346
+ const errorCode =
347
+ opts.err instanceof this.#core.errors.NonRetryableError ? (opts.err.code ?? 'NON_RETRYABLE_ERROR') : undefined
348
+
349
+ await commitTaskCompensatingTx(opts.db, opts.docRef, opts.taskId, opts.claimedEpoch, opts.expiresAt)
350
+ await opts.step.compensate()
351
+ await setTaskDoc(opts.docRef, {
352
+ ...opts.baseMeta,
353
+ status: 'errored',
354
+ activeRunId: null,
355
+ error,
356
+ ...(nonRetryable ? { nonRetryable: true, errorCode } : {}),
357
+ steps: opts.steps,
358
+ updatedAt: new Date().toISOString(),
359
+ expiresAt: opts.expiresAt
360
+ })
361
+ }
362
+
363
+ #runSingleAttempt = async (
364
+ db: Firestore,
365
+ docRef: DocumentReference,
366
+ payload: WorkflowTaskPayload,
367
+ wf: WorkflowRuntimeEntry,
368
+ attempt: number,
369
+ deadlineSeconds: number,
370
+ useQueue: boolean,
371
+ canYield: boolean
372
+ ): Promise<boolean> => {
373
+ const { taskId, workflowName, isDev: rawIsDev } = payload
374
+ const isDev = Boolean(rawIsDev)
375
+ const claim = await claimTaskEpochTx(db, docRef, payload, attempt, this.#core.internals.auth.resolveUserRole)
376
+ if (claim.aborted) return true
377
+
378
+ const { claimedEpoch, startedAt, expiresAt, effectiveParams, userId, userRole, steps, signals, compensatedSteps } =
379
+ claim
380
+ const baseMeta = { taskId, workflowName, userId, userRole }
381
+ const env = { ...this.#core.env, isDev }
382
+ const controller = new AbortController()
383
+ const wfCtx: WorkflowContext = {
384
+ taskId,
385
+ workflowName,
386
+ attempt: attempt + 1,
387
+ startedAt,
388
+ deadlineMs: startedAt + deadlineSeconds * 1000,
389
+ signal: controller.signal
390
+ }
391
+ const step = this.#steps.createStepRunner(wf.mode, docRef, steps, expiresAt, {
392
+ isDev,
393
+ signals,
394
+ workflowContext: wfCtx,
395
+ canYield,
396
+ claimedEpoch,
397
+ abortController: controller,
398
+ compensatedSteps
399
+ })
400
+
401
+ this.registerActiveTaskExecution({ taskId, step, controller, claimedEpoch })
402
+ const hbTimer = startTaskHeartbeatTimer(db, docRef, taskId, claimedEpoch, expiresAt, controller)
403
+
404
+ try {
405
+ const response = await this.runWithWorkflowContext({ workflow: wfCtx, step, env }, () =>
406
+ this.#core.internals.context.run({ env, userId, role: userRole, step }, () => wf.runTask(effectiveParams, step))
407
+ )
408
+ await step.flush()
409
+ await this.#commitCompletion({ db, docRef, baseMeta, taskId, claimedEpoch, response, steps, expiresAt, isDev })
410
+ return true
411
+ } catch (err) {
412
+ if (this.#core.errors.isStaleWorkerEpochError(err)) throw err
413
+ if (isTaskYieldSignal(err)) {
414
+ await step.flush()
415
+ await this.#commitYieldAndEnqueue({ db, docRef, baseMeta, taskId, workflowName, claimedEpoch, err, effectiveParams, steps, expiresAt, isDev, useQueue, deadlineSeconds })
416
+ return true
417
+ }
418
+ if (this.#core.errors.isNonRetryableError(err) || attempt >= CORE_CONFIG.tasks.maxAttempts - 1) {
419
+ await this.#commitErrorAndCompensate({ db, docRef, baseMeta, taskId, claimedEpoch, err, step, steps, expiresAt })
420
+ return true
421
+ }
422
+ await step.flush()
423
+ await new Promise((r) => setTimeout(r, 1500 * (attempt + 1)))
424
+ return false
425
+ } finally {
426
+ clearInterval(hbTimer)
427
+ this.unregisterActiveTaskExecution(taskId, step)
428
+ }
429
+ }
430
+
431
+ execute = async (
432
+ payload: WorkflowTaskPayload,
433
+ workflowRegistry: Record<string, WorkflowRuntimeEntry> = activeRegistry,
434
+ execOpts?: ExecuteWorkflowTaskOptions
435
+ ): Promise<void> => {
436
+ const { taskId, workflowName, isDev } = payload
437
+ const db = this.#core.db.firestore(Boolean(isDev))
438
+ const docRef = db.collection('_tasks').doc(taskId)
439
+ const wf = workflowRegistry[workflowName]
440
+
441
+ if (!wf) {
442
+ await setTaskDoc(docRef, {
443
+ taskId,
444
+ workflowName,
445
+ status: 'errored',
446
+ error: `Unknown workflow: ${workflowName}`,
447
+ expiresAt: Timestamp.fromMillis(Date.now() + CORE_CONFIG.tasks.ttlMs)
448
+ })
449
+ return
450
+ }
451
+
452
+ const deadlineSeconds = resolveTaskDeadline(workflowName, wf.deadlineSeconds)
453
+ const useQueue = CORE_CONFIG.tasks.useCloudTasks
454
+ const canYield = execOpts?.canYield ?? useQueue
455
+
456
+ for (let attempt = 0; attempt < CORE_CONFIG.tasks.maxAttempts; attempt++) {
457
+ const done = await this.#runSingleAttempt(db, docRef, payload, wf, attempt, deadlineSeconds, useQueue, canYield)
458
+ if (done) return
459
+ }
460
+ }
461
+ }
@@ -0,0 +1,2 @@
1
+ export * from './schema'
2
+ export { ErrorsService, ResilienceService } from './service'
@@ -0,0 +1,69 @@
1
+ import { z } from 'zod'
2
+ import { CORE_CONFIG } from '../config'
3
+
4
+ export type Duration = number | `${number}${'ms' | 's' | 'm' | 'h' | 'd'}`
5
+
6
+ export const errorBodySchema = z.object({
7
+ error: z.string(),
8
+ code: z.string().optional(),
9
+ status: z.number().int().optional(),
10
+ retryAfterMs: z.number().int().optional()
11
+ })
12
+
13
+ export type ErrorBody = z.infer<typeof errorBodySchema>
14
+
15
+ export class NonRetryableError extends Error {
16
+ readonly nonRetryable = true as const
17
+ readonly code?: string
18
+ constructor(message: string, code?: string) {
19
+ super(message)
20
+ this.name = 'NonRetryableError'
21
+ this.code = code
22
+ }
23
+ }
24
+
25
+ export class StaleWorkerEpochError extends NonRetryableError {
26
+ readonly isStaleWorkerEpoch = true as const
27
+ readonly taskId?: string
28
+ readonly claimedEpoch?: number
29
+ readonly actualEpoch?: number
30
+ constructor(
31
+ message = 'Worker execution epoch superseded by a newer run or cancellation',
32
+ meta?: { taskId?: string; claimedEpoch?: number; actualEpoch?: number }
33
+ ) {
34
+ super(message, 'STALE_WORKER_EPOCH')
35
+ this.name = 'StaleWorkerEpochError'
36
+ this.taskId = meta?.taskId
37
+ this.claimedEpoch = meta?.claimedEpoch
38
+ this.actualEpoch = meta?.actualEpoch
39
+ }
40
+ }
41
+
42
+ export class CircuitBreakerOpenError extends NonRetryableError {
43
+ readonly isCircuitBreakerOpen = true as const
44
+ readonly breakerKey: string
45
+ constructor(breakerKey: string, message = `Circuit breaker "${breakerKey}" is OPEN`) {
46
+ super(message, 'CIRCUIT_BREAKER_OPEN')
47
+ this.name = 'CircuitBreakerOpenError'
48
+ this.breakerKey = breakerKey
49
+ }
50
+ }
51
+
52
+ export interface SingleflightOptions {
53
+ scope?: 'tenant' | 'global'
54
+ }
55
+
56
+ export const LIMITS = CORE_CONFIG.limits.buckets
57
+ export type Bucket = keyof typeof LIMITS
58
+
59
+ /** Built-in provider breakers plus any app-defined key (`[a-z0-9-]` recommended; used in a Firestore doc ID). */
60
+ export type CircuitBreakerKey = 'mistral' | 'jev' | 'vertex' | 'emails' | 'payments' | (string & {})
61
+ export type CircuitBreakerStateName = 'CLOSED' | 'OPEN' | 'HALF_OPEN'
62
+
63
+ export interface CircuitBreakerRecord {
64
+ state: CircuitBreakerStateName
65
+ failureCount: number
66
+ firstFailureAt: number
67
+ openedAt: number
68
+ updatedAt?: number
69
+ }