@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,310 @@
1
+ import type { DocumentReference } from 'firebase-admin/firestore'
2
+ import { z } from 'zod'
3
+ import type { AppBindings, DbRole } from '../config'
4
+ import type { EntryId } from '../db'
5
+ import type { AuthLevel, BaseRouteConfig, RouteMode } from '../router'
6
+ import { StaleWorkerEpochError, type Duration } from '../utils'
7
+
8
+ export { StaleWorkerEpochError }
9
+
10
+ export type TaskStatus =
11
+ | 'running'
12
+ | 'sleeping'
13
+ | 'waiting_signal'
14
+ | 'compensating'
15
+ | 'completed'
16
+ | 'errored'
17
+ | 'cancelled'
18
+
19
+ export interface TaskExecutionFence {
20
+ /** Monotonically increasing integer incremented on every worker claim, yield, hedge, or cancel transition */
21
+ epoch: number
22
+ /** Unique UUID for the currently owning worker execution attempt */
23
+ activeRunId: string | null
24
+ /** Timestamp of the latest worker heartbeat */
25
+ lastHeartbeatAt: number
26
+ }
27
+
28
+ export interface TaskProgressUpdate {
29
+ percent?: number
30
+ stage?: string
31
+ message?: string
32
+ data?: Record<string, unknown>
33
+ }
34
+
35
+ export interface TaskStatusRecord<O = unknown> {
36
+ taskId: string
37
+ workflowName: string
38
+ status: TaskStatus
39
+ userId?: EntryId
40
+ epoch?: number
41
+ activeRunId?: string | null
42
+ lastHeartbeatAt?: number
43
+ compensatedSteps?: Record<string, boolean>
44
+ progress?: TaskProgressUpdate
45
+ response?: O
46
+ error?: string
47
+ createdAt?: string
48
+ updatedAt?: string
49
+ }
50
+
51
+ export interface WorkflowContext {
52
+ taskId: string
53
+ workflowName: string
54
+ attempt: number
55
+ startedAt: number
56
+ deadlineMs: number
57
+ signal: AbortSignal
58
+ }
59
+
60
+ export interface StepRetryPolicy {
61
+ maxAttempts?: number
62
+ initialInterval?: Duration
63
+ backoffCoefficient?: number
64
+ maxInterval?: Duration
65
+ jitter?: boolean
66
+ nonRetryableErrorTypes?: string[]
67
+ }
68
+
69
+ export interface StepContext {
70
+ signal: AbortSignal
71
+ attempt: number
72
+ stepId: string
73
+ }
74
+
75
+ export interface StepOptions<T = unknown> {
76
+ retry?: number | StepRetryPolicy
77
+ timeout?: Duration
78
+ rollback?: (result: T) => Promise<unknown>
79
+ }
80
+
81
+ export interface StepMapOptions<T, R> extends StepOptions<R> {
82
+ concurrency?: number
83
+ shortCircuit?: (item: T, index: number) => Promise<R | undefined> | R | undefined
84
+ }
85
+
86
+ export type StepSpec<T> =
87
+ | ((ctx: StepContext) => Promise<T>)
88
+ | {
89
+ fn: (ctx: StepContext) => Promise<T>
90
+ rollback?: (result: T) => Promise<unknown>
91
+ retry?: number | StepRetryPolicy
92
+ timeout?: Duration
93
+ }
94
+
95
+ export type StepFn = <T>(name: string, fnOrSpec: StepSpec<T>, options?: StepOptions<T>) => Promise<T>
96
+
97
+ export interface WorkflowTargetRef<I = any, O = any> {
98
+ workflowName?: string
99
+ timeout?: Duration
100
+ handler?: (ctx: any) => Promise<O>
101
+ __input?: I
102
+ }
103
+
104
+ export interface StepRunner extends StepFn {
105
+ run: StepFn
106
+ map: <T, R>(
107
+ name: string,
108
+ items: readonly T[],
109
+ fn: (item: T, index: number, ctx: StepContext) => Promise<R>,
110
+ options?: StepMapOptions<T, R>
111
+ ) => Promise<R[]>
112
+ invoke: <I = unknown, O = unknown>(
113
+ name: string,
114
+ target: string | WorkflowTargetRef<I, O>,
115
+ input: I,
116
+ options?: { timeout?: Duration; dedupeKey?: string }
117
+ ) => Promise<O>
118
+ spawn: <I = unknown>(
119
+ name: string,
120
+ target: string | WorkflowTargetRef<I, unknown>,
121
+ input: I,
122
+ options?: { dedupeKey?: string; timeout?: Duration }
123
+ ) => Promise<{ taskId: string }>
124
+ sleep: (name: string, duration: Duration) => Promise<void>
125
+ waitForSignal: <T = unknown>(
126
+ name: string,
127
+ signalName: string,
128
+ options?: { timeout?: Duration }
129
+ ) => Promise<T>
130
+ continueAsNew: <I = unknown>(newInput: I) => Promise<never>
131
+ progress: (update: TaskProgressUpdate) => Promise<void>
132
+ flush: () => Promise<PromiseSettledResult<unknown>[]>
133
+ compensate: () => Promise<void>
134
+ onError: () => Promise<void>
135
+ }
136
+
137
+ export interface RollbackLeg<T> {
138
+ fn: () => Promise<T>
139
+ rollback?: (result: T) => Promise<unknown>
140
+ }
141
+
142
+ export interface WorkflowTaskPayload {
143
+ taskId: string
144
+ workflowName: string
145
+ params: Record<string, unknown>
146
+ isDev: boolean
147
+ expectedEpoch?: number
148
+ }
149
+
150
+ export interface WorkflowRuntimeEntry {
151
+ mode: RouteMode
152
+ deadlineSeconds?: number
153
+ runTask: (params: any, step: StepRunner) => Promise<unknown>
154
+ }
155
+
156
+ export class TaskYieldSignal extends Error {
157
+ readonly isTaskYield = true
158
+ constructor(
159
+ public readonly reason: 'sleeping' | 'waiting_signal' | 'continue_as_new' | 'yield',
160
+ public readonly meta: {
161
+ delaySeconds?: number
162
+ signalName?: string
163
+ timeoutMs?: number
164
+ newInput?: unknown
165
+ } = {}
166
+ ) {
167
+ super(`TaskYield:${reason}`)
168
+ this.name = 'TaskYieldSignal'
169
+ }
170
+ }
171
+
172
+ export interface ClaimCheckPointer {
173
+ __claimCheck: string
174
+ }
175
+
176
+ export class ReplayCompleteSignal extends Error {
177
+ readonly isReplayComplete = true as const
178
+ constructor() {
179
+ super('ReplayComplete')
180
+ this.name = 'ReplayCompleteSignal'
181
+ }
182
+ }
183
+
184
+ export interface ActiveTaskExecution {
185
+ taskId: string
186
+ step: StepRunner
187
+ controller: AbortController
188
+ claimedEpoch?: number
189
+ }
190
+
191
+ export interface ActiveWorkflowStore {
192
+ workflow: WorkflowContext
193
+ step: StepRunner
194
+ env?: AppBindings
195
+ }
196
+
197
+ export interface CreateStepRunnerOptions {
198
+ isDev?: boolean
199
+ signals?: Record<string, unknown>
200
+ workflowContext?: WorkflowContext
201
+ canYield?: boolean
202
+ claimedEpoch?: number
203
+ abortController?: AbortController
204
+ compensatedSteps?: Record<string, boolean>
205
+ replayOnly?: boolean
206
+ }
207
+
208
+ export interface DispatchWorkflowOptions {
209
+ mode?: RouteMode
210
+ timeout?: Duration
211
+ dedupeKey?: string
212
+ wait?: boolean
213
+ maxWaitMs?: number
214
+ signal?: AbortSignal
215
+ userId?: EntryId
216
+ userRole?: DbRole
217
+ }
218
+
219
+ export interface ExecuteWorkflowTaskOptions {
220
+ canYield?: boolean
221
+ }
222
+
223
+ export type TaskRecord<O = unknown> =
224
+ | { status: 'completed'; response: O }
225
+ | { status: 'errored'; error: string; errorCode?: string; nonRetryable?: boolean }
226
+ | { status: 'cancelled'; error?: string }
227
+
228
+ export class TaskWaitDetachedSignal extends Error {
229
+ readonly isTaskWaitDetached = true as const
230
+ constructor(public readonly taskId: string) {
231
+ super(`TaskWaitDetached:${taskId}`)
232
+ this.name = 'TaskWaitDetachedSignal'
233
+ }
234
+ }
235
+
236
+ export interface PendingEntry {
237
+ taskId: string
238
+ workflowName: string
239
+ docRef: DocumentReference
240
+ createdAt: number
241
+ hedged: boolean
242
+ hedgeable?: boolean
243
+ keepAlive?: boolean
244
+ unsub?: () => void
245
+ dispatch: () => Promise<void>
246
+ finish: (res: TaskRecord<any>) => void
247
+ }
248
+
249
+ export interface WatchTaskOptions {
250
+ isDev?: boolean
251
+ docRef?: DocumentReference
252
+ workflowName?: string
253
+ hedgeable?: boolean
254
+ keepAlive?: boolean
255
+ timeoutMs?: number
256
+ maxWaitMs?: number
257
+ hedgeAfterMs?: number
258
+ pollIntervalMs?: number
259
+ signal?: AbortSignal
260
+ dispatch?: () => Promise<void>
261
+ }
262
+
263
+ export interface RouteRegistryItem {
264
+ id: string
265
+ mode: RouteMode
266
+ method: string
267
+ path: string
268
+ workflowName?: string
269
+ auth?: AuthLevel
270
+ description: string
271
+ mcp?: boolean
272
+ tool?: boolean
273
+ input?: z.ZodType<any>
274
+ output?: z.ZodType<any>
275
+ guard?: BaseRouteConfig['guard']
276
+ }
277
+
278
+ export interface RouteIntrospectionItem {
279
+ id: string
280
+ method: string
281
+ path: string
282
+ mode: 'rest' | 'workflow'
283
+ workflowName?: string
284
+ auth: string | false
285
+ description: string
286
+ mcp: boolean
287
+ tool: boolean
288
+ guards: string[]
289
+ inputSchema: Record<string, unknown> | null
290
+ outputSchema: Record<string, unknown> | null
291
+ }
292
+
293
+ export interface RoutesIntrospectionResponse {
294
+ appName: string
295
+ generatedAt: string
296
+ routes: RouteIntrospectionItem[]
297
+ collections?: Record<
298
+ string,
299
+ {
300
+ relationType: string
301
+ rbac?: Record<string, unknown>
302
+ schema: Record<string, unknown> | null
303
+ }
304
+ >
305
+ }
306
+
307
+ export const signalTaskBody = z.object({
308
+ signal: z.string().min(1),
309
+ payload: z.unknown().optional()
310
+ })
@@ -0,0 +1,254 @@
1
+ import { Timestamp, type DocumentReference } from 'firebase-admin/firestore'
2
+ import { CORE_CONFIG } from '../config'
3
+ import type { Core } from '../index'
4
+ import {
5
+ StaleWorkerEpochError,
6
+ type StepRunner,
7
+ type TaskStatusRecord,
8
+ type WorkflowContext,
9
+ type WorkflowRuntimeEntry
10
+ } from './schema'
11
+ import { TaskStepsEngine } from './steps'
12
+ import { resolveTaskDeadline } from './utils'
13
+ import { TaskWatcherEngine } from './watcher'
14
+ import { TaskWorkerEngine } from './worker'
15
+
16
+ export class TasksService {
17
+ readonly #core: Core
18
+ readonly #worker: TaskWorkerEngine
19
+ readonly #steps: TaskStepsEngine
20
+ readonly #watcher: TaskWatcherEngine
21
+
22
+ constructor(coreClient: Core) {
23
+ this.#core = coreClient
24
+ this.#worker = new TaskWorkerEngine(coreClient)
25
+ this.#steps = new TaskStepsEngine(coreClient, this.#worker.invokeChildWorkflow)
26
+ this.#watcher = new TaskWatcherEngine(coreClient, this.#steps)
27
+ this.#worker.bindEngines(this.#steps, this.#watcher)
28
+ }
29
+
30
+ #useStep = (): StepRunner => {
31
+ const store = this.#worker.getActiveWorkflowStore()
32
+ return store?.step ?? this.#steps.createStepRunner('workflow')
33
+ }
34
+
35
+ #assertTaskOwnership = (data: Record<string, unknown> | undefined): void => {
36
+ if (!data) throw this.#core.errors.httpError(404, 'Task not found', 'NOT_FOUND')
37
+ const caller = this.#core.caller
38
+ if (!caller || caller.role === 'admin' || caller.userId === CORE_CONFIG.auth.adminUserId) return
39
+ if (typeof data.userId === 'string' && data.userId !== caller.userId) {
40
+ throw this.#core.errors.httpError(403, 'Forbidden', 'FORBIDDEN')
41
+ }
42
+ }
43
+
44
+ status = async <O = unknown>(taskId: string): Promise<TaskStatusRecord<O>> => {
45
+ const isDev = Boolean(this.#core.env.isDev)
46
+ const snap = await this.#core.db.firestore(isDev).collection('_tasks').doc(taskId).get()
47
+ const data = snap.exists ? (snap.data() as Record<string, unknown>) : undefined
48
+ this.#assertTaskOwnership(data)
49
+ const response =
50
+ 'response' in data! ? await this.#steps.maybeHydrateStepValue<O>(data!.response, isDev) : undefined
51
+ return {
52
+ taskId,
53
+ workflowName: String(data!.workflowName ?? ''),
54
+ status: (data!.status as TaskStatusRecord['status']) ?? 'running',
55
+ ...(typeof data!.userId === 'string' ? { userId: data!.userId } : {}),
56
+ ...(typeof data!.epoch === 'number' ? { epoch: data!.epoch } : {}),
57
+ ...('activeRunId' in data! ? { activeRunId: (data!.activeRunId as string | null) ?? null } : {}),
58
+ ...(typeof data!.lastHeartbeatAt === 'number' ? { lastHeartbeatAt: data!.lastHeartbeatAt } : {}),
59
+ ...(data!.compensatedSteps
60
+ ? { compensatedSteps: data!.compensatedSteps as Record<string, boolean> }
61
+ : {}),
62
+ ...(data!.progress ? { progress: data!.progress as TaskStatusRecord['progress'] } : {}),
63
+ ...('response' in data! ? { response: response as O } : {}),
64
+ ...(typeof data!.error === 'string' ? { error: data!.error } : {}),
65
+ ...(typeof data!.createdAt === 'string' ? { createdAt: data!.createdAt } : {}),
66
+ ...(typeof data!.updatedAt === 'string' ? { updatedAt: data!.updatedAt } : {})
67
+ }
68
+ }
69
+
70
+ signal = async (
71
+ taskId: string,
72
+ signalName: string,
73
+ payload?: unknown
74
+ ): Promise<{ taskId: string; signaled: true }> => {
75
+ const isDev = Boolean(this.#core.env.isDev)
76
+ const db = this.#core.db.firestore(isDev)
77
+ const docRef = db.collection('_tasks').doc(taskId)
78
+
79
+ const outcome = await db.runTransaction(async (tx) => {
80
+ const snap = await tx.get(docRef)
81
+ const data = snap.exists ? (snap.data() as Record<string, unknown>) : undefined
82
+ this.#assertTaskOwnership(data)
83
+
84
+ const workflowName = String(data!.workflowName ?? '')
85
+ const params = (data!.params as Record<string, unknown> | undefined) ?? {}
86
+ const signals = {
87
+ ...((data!.signals as Record<string, unknown> | undefined) ?? {}),
88
+ [signalName]: payload ?? true
89
+ }
90
+ const wasWaiting = data!.status === 'waiting_signal'
91
+ const currentEpoch = typeof data!.epoch === 'number' ? data!.epoch : 0
92
+ const nextEpoch = wasWaiting ? currentEpoch + 1 : currentEpoch
93
+
94
+ tx.set(
95
+ docRef,
96
+ {
97
+ signals,
98
+ ...(wasWaiting ? { status: 'running', waitingSignal: null, epoch: nextEpoch, activeRunId: null } : {}),
99
+ updatedAt: new Date().toISOString()
100
+ },
101
+ { merge: true }
102
+ )
103
+ return { workflowName, params, wasWaiting, nextEpoch }
104
+ })
105
+
106
+ if (outcome.wasWaiting) {
107
+ const wf = this.#worker.getRuntime().registry[outcome.workflowName]
108
+ const deadline = resolveTaskDeadline(outcome.workflowName, wf?.deadlineSeconds)
109
+ if (CORE_CONFIG.tasks.useCloudTasks) {
110
+ await this.#watcher.enqueueBatched(
111
+ {
112
+ taskId,
113
+ workflowName: outcome.workflowName,
114
+ params: outcome.params,
115
+ isDev,
116
+ expectedEpoch: outcome.nextEpoch
117
+ },
118
+ deadline
119
+ )
120
+ } else if (wf) {
121
+ void this.#worker
122
+ .execute({
123
+ taskId,
124
+ workflowName: outcome.workflowName,
125
+ params: outcome.params,
126
+ isDev,
127
+ expectedEpoch: outcome.nextEpoch
128
+ })
129
+ .catch(() => {})
130
+ }
131
+ }
132
+ return { taskId, signaled: true }
133
+ }
134
+
135
+ #replayUncompensatedSteps = async (
136
+ taskId: string,
137
+ docRef: DocumentReference,
138
+ freshData: Record<string, unknown>,
139
+ isDev: boolean
140
+ ): Promise<void> => {
141
+ const stepsMap = { ...((freshData.steps as Record<string, unknown> | undefined) ?? {}) }
142
+ const compMap = { ...((freshData.compensatedSteps as Record<string, boolean> | undefined) ?? {}) }
143
+ const workflowName = String(freshData.workflowName ?? '')
144
+ const wf: WorkflowRuntimeEntry | undefined = this.#worker.getRuntime().registry[workflowName]
145
+ if (!wf || !Object.keys(stepsMap).some((k) => !compMap[k])) return
146
+
147
+ const effectiveEnv = this.#core.env
148
+ const expiresAt = Timestamp.fromMillis(Date.now() + CORE_CONFIG.tasks.ttlMs)
149
+ const now = Date.now()
150
+ const wfCtx: WorkflowContext = {
151
+ taskId,
152
+ workflowName,
153
+ attempt: 1,
154
+ startedAt: now,
155
+ deadlineMs: now + 60_000,
156
+ signal: new AbortController().signal
157
+ }
158
+ const replayStep = this.#steps.createStepRunner(wf.mode, docRef, stepsMap, expiresAt, {
159
+ isDev,
160
+ workflowContext: wfCtx,
161
+ canYield: false,
162
+ compensatedSteps: compMap,
163
+ replayOnly: true
164
+ })
165
+ const params = (freshData.params as Record<string, unknown> | undefined) ?? {}
166
+ try {
167
+ await this.#worker.runWithWorkflowContext({ workflow: wfCtx, step: replayStep, env: effectiveEnv }, () =>
168
+ this.#core.internals.context.run(
169
+ { env: effectiveEnv, userId: this.#core.userId, role: this.#core.role, step: replayStep },
170
+ () => wf.runTask(params, replayStep)
171
+ )
172
+ )
173
+ } catch {}
174
+ await replayStep.compensate()
175
+ }
176
+
177
+ cancel = async (taskId: string): Promise<{ taskId: string; status: 'cancelled' }> => {
178
+ const isDev = Boolean(this.#core.env.isDev)
179
+ const db = this.#core.db.firestore(isDev)
180
+ const docRef = db.collection('_tasks').doc(taskId)
181
+ const preActiveExec = this.#worker.getActiveTaskExecution(taskId)
182
+
183
+ const txResult = await db.runTransaction(async (tx) => {
184
+ const snap = await tx.get(docRef)
185
+ const data = snap.exists ? (snap.data() as Record<string, unknown>) : undefined
186
+ this.#assertTaskOwnership(data)
187
+
188
+ const currentStatus = data!.status as string | undefined
189
+ if (currentStatus === 'completed' || currentStatus === 'errored' || currentStatus === 'cancelled') {
190
+ return { alreadyTerminal: true as const, data: data! }
191
+ }
192
+
193
+ const currentEpoch = typeof data!.epoch === 'number' ? data!.epoch : 0
194
+ const nextEpoch = currentEpoch + 1
195
+ tx.set(
196
+ docRef,
197
+ {
198
+ status: 'compensating',
199
+ epoch: nextEpoch,
200
+ activeRunId: null,
201
+ updatedAt: new Date().toISOString()
202
+ },
203
+ { merge: true }
204
+ )
205
+ return { alreadyTerminal: false as const, nextEpoch, data: data! }
206
+ })
207
+
208
+ if (!txResult.alreadyTerminal) {
209
+ const activeExec = preActiveExec ?? this.#worker.getActiveTaskExecution(taskId)
210
+ if (activeExec) {
211
+ activeExec.controller.abort(
212
+ new StaleWorkerEpochError(`Task ${taskId} cancelled by caller`, {
213
+ taskId,
214
+ claimedEpoch: activeExec.claimedEpoch,
215
+ actualEpoch: txResult.nextEpoch
216
+ })
217
+ )
218
+ await activeExec.step.compensate()
219
+ }
220
+
221
+ const freshSnap = await docRef.get().catch(() => null)
222
+ const freshData = (freshSnap?.data() as Record<string, unknown> | undefined) ?? txResult.data
223
+ await this.#replayUncompensatedSteps(taskId, docRef, freshData, isDev)
224
+
225
+ await docRef.set(
226
+ {
227
+ status: 'cancelled',
228
+ activeRunId: null,
229
+ error: 'Cancelled by caller',
230
+ updatedAt: new Date().toISOString()
231
+ },
232
+ { merge: true }
233
+ )
234
+ }
235
+
236
+ return { taskId, status: 'cancelled' }
237
+ }
238
+
239
+ get internals() {
240
+ return {
241
+ createStepRunner: this.#steps.createStepRunner,
242
+ allWithRollback: this.#steps.allWithRollback,
243
+ getContext: this.#worker.getContext,
244
+ useStep: this.#useStep,
245
+ getActiveWorkflowStore: this.#worker.getActiveWorkflowStore,
246
+ dispatch: this.#worker.dispatch,
247
+ execute: this.#worker.execute,
248
+ registerRuntime: this.#worker.registerRuntime,
249
+ getRuntime: this.#worker.getRuntime,
250
+ watchTask: this.#watcher.watchTask,
251
+ getActiveWatcherCount: this.#watcher.getActiveWatcherCount
252
+ }
253
+ }
254
+ }