reqon-dsl 0.2.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 (388) hide show
  1. package/.claude/settings.local.json +31 -0
  2. package/.claude/skills/api-integration.md +125 -0
  3. package/.claude/skills/database-schema.md +51 -0
  4. package/.claude/skills/dsl-design.md +80 -0
  5. package/.claude/skills/property-testing.md +143 -0
  6. package/.claude/skills/reqon/SKILL.md +44 -0
  7. package/.claude/skills/reqon/references/examples.md +206 -0
  8. package/.claude/skills/reqon/references/syntax.md +263 -0
  9. package/.claude/skills/vscode-extension.md +113 -0
  10. package/.github/dependabot.yml +32 -0
  11. package/.github/pull_request_template.md +21 -0
  12. package/.github/workflows/ci.yml +174 -0
  13. package/.github/workflows/release.yml +73 -0
  14. package/CLAUDE.md +72 -0
  15. package/CONTRIBUTING.md +161 -0
  16. package/README.md +235 -0
  17. package/TODO.md +51 -0
  18. package/dist/ast/index.d.ts +1 -0
  19. package/dist/ast/index.js +1 -0
  20. package/dist/ast/nodes.d.ts +237 -0
  21. package/dist/ast/nodes.js +12 -0
  22. package/dist/auth/auth.test.d.ts +1 -0
  23. package/dist/auth/auth.test.js +255 -0
  24. package/dist/auth/circuit-breaker.d.ts +115 -0
  25. package/dist/auth/circuit-breaker.js +267 -0
  26. package/dist/auth/credentials.d.ts +91 -0
  27. package/dist/auth/credentials.js +169 -0
  28. package/dist/auth/index.d.ts +5 -0
  29. package/dist/auth/index.js +8 -0
  30. package/dist/auth/oauth2-provider.d.ts +41 -0
  31. package/dist/auth/oauth2-provider.js +131 -0
  32. package/dist/auth/rate-limiter.d.ts +61 -0
  33. package/dist/auth/rate-limiter.js +380 -0
  34. package/dist/auth/token-store.d.ts +30 -0
  35. package/dist/auth/token-store.js +148 -0
  36. package/dist/auth/types.d.ts +142 -0
  37. package/dist/auth/types.js +1 -0
  38. package/dist/cli.d.ts +2 -0
  39. package/dist/cli.js +270 -0
  40. package/dist/errors/errors.test.d.ts +1 -0
  41. package/dist/errors/errors.test.js +165 -0
  42. package/dist/errors/index.d.ts +83 -0
  43. package/dist/errors/index.js +159 -0
  44. package/dist/execution/execution.test.d.ts +1 -0
  45. package/dist/execution/execution.test.js +246 -0
  46. package/dist/execution/index.d.ts +4 -0
  47. package/dist/execution/index.js +2 -0
  48. package/dist/execution/state.d.ts +136 -0
  49. package/dist/execution/state.js +82 -0
  50. package/dist/execution/store.d.ts +52 -0
  51. package/dist/execution/store.js +120 -0
  52. package/dist/index.d.ts +27 -0
  53. package/dist/index.js +57 -0
  54. package/dist/integration.test.d.ts +1 -0
  55. package/dist/integration.test.js +168 -0
  56. package/dist/interpreter/context.d.ts +15 -0
  57. package/dist/interpreter/context.js +29 -0
  58. package/dist/interpreter/evaluator.d.ts +5 -0
  59. package/dist/interpreter/evaluator.js +223 -0
  60. package/dist/interpreter/evaluator.test.d.ts +1 -0
  61. package/dist/interpreter/evaluator.test.js +512 -0
  62. package/dist/interpreter/executor.d.ts +131 -0
  63. package/dist/interpreter/executor.js +663 -0
  64. package/dist/interpreter/fetch-handler.d.ts +43 -0
  65. package/dist/interpreter/fetch-handler.js +203 -0
  66. package/dist/interpreter/http.d.ts +57 -0
  67. package/dist/interpreter/http.js +210 -0
  68. package/dist/interpreter/http.test.d.ts +1 -0
  69. package/dist/interpreter/http.test.js +299 -0
  70. package/dist/interpreter/index.d.ts +7 -0
  71. package/dist/interpreter/index.js +7 -0
  72. package/dist/interpreter/pagination.d.ts +63 -0
  73. package/dist/interpreter/pagination.js +155 -0
  74. package/dist/interpreter/progress.test.d.ts +1 -0
  75. package/dist/interpreter/progress.test.js +216 -0
  76. package/dist/interpreter/schema-matcher.d.ts +16 -0
  77. package/dist/interpreter/schema-matcher.js +136 -0
  78. package/dist/interpreter/schema-matcher.test.d.ts +1 -0
  79. package/dist/interpreter/schema-matcher.test.js +122 -0
  80. package/dist/interpreter/signals.d.ts +57 -0
  81. package/dist/interpreter/signals.js +73 -0
  82. package/dist/interpreter/step-handlers/for-handler.d.ts +17 -0
  83. package/dist/interpreter/step-handlers/for-handler.js +51 -0
  84. package/dist/interpreter/step-handlers/index.d.ts +8 -0
  85. package/dist/interpreter/step-handlers/index.js +8 -0
  86. package/dist/interpreter/step-handlers/map-handler.d.ts +10 -0
  87. package/dist/interpreter/step-handlers/map-handler.js +20 -0
  88. package/dist/interpreter/step-handlers/match-handler.d.ts +27 -0
  89. package/dist/interpreter/step-handlers/match-handler.js +61 -0
  90. package/dist/interpreter/step-handlers/store-handler.d.ts +13 -0
  91. package/dist/interpreter/step-handlers/store-handler.js +66 -0
  92. package/dist/interpreter/step-handlers/types.d.ts +15 -0
  93. package/dist/interpreter/step-handlers/types.js +1 -0
  94. package/dist/interpreter/step-handlers/validate-handler.d.ts +10 -0
  95. package/dist/interpreter/step-handlers/validate-handler.js +26 -0
  96. package/dist/interpreter/step-handlers/webhook-handler.d.ts +36 -0
  97. package/dist/interpreter/step-handlers/webhook-handler.js +104 -0
  98. package/dist/lexer/index.d.ts +10 -0
  99. package/dist/lexer/index.js +12 -0
  100. package/dist/lexer/lexer.d.ts +24 -0
  101. package/dist/lexer/lexer.js +264 -0
  102. package/dist/lexer/lexer.test.d.ts +1 -0
  103. package/dist/lexer/lexer.test.js +259 -0
  104. package/dist/lexer/tokens.d.ts +69 -0
  105. package/dist/lexer/tokens.js +146 -0
  106. package/dist/loader/index.d.ts +36 -0
  107. package/dist/loader/index.js +220 -0
  108. package/dist/loader/loader.test.d.ts +1 -0
  109. package/dist/loader/loader.test.js +287 -0
  110. package/dist/oas/index.d.ts +4 -0
  111. package/dist/oas/index.js +2 -0
  112. package/dist/oas/loader.d.ts +21 -0
  113. package/dist/oas/loader.js +82 -0
  114. package/dist/oas/oas.test.d.ts +1 -0
  115. package/dist/oas/oas.test.js +218 -0
  116. package/dist/oas/validator.d.ts +12 -0
  117. package/dist/oas/validator.js +227 -0
  118. package/dist/parser/base.d.ts +33 -0
  119. package/dist/parser/base.js +97 -0
  120. package/dist/parser/expressions.d.ts +27 -0
  121. package/dist/parser/expressions.js +248 -0
  122. package/dist/parser/expressions.test.d.ts +1 -0
  123. package/dist/parser/expressions.test.js +378 -0
  124. package/dist/parser/index.d.ts +3 -0
  125. package/dist/parser/index.js +3 -0
  126. package/dist/parser/match.test.d.ts +1 -0
  127. package/dist/parser/match.test.js +254 -0
  128. package/dist/parser/parser.d.ts +68 -0
  129. package/dist/parser/parser.js +1229 -0
  130. package/dist/parser/parser.test.d.ts +1 -0
  131. package/dist/parser/parser.test.js +333 -0
  132. package/dist/parser/schedule.test.d.ts +1 -0
  133. package/dist/parser/schedule.test.js +241 -0
  134. package/dist/plugin.d.ts +35 -0
  135. package/dist/plugin.js +68 -0
  136. package/dist/scheduler/cron-parser.d.ts +32 -0
  137. package/dist/scheduler/cron-parser.js +198 -0
  138. package/dist/scheduler/cron-parser.test.d.ts +1 -0
  139. package/dist/scheduler/cron-parser.test.js +188 -0
  140. package/dist/scheduler/index.d.ts +3 -0
  141. package/dist/scheduler/index.js +2 -0
  142. package/dist/scheduler/scheduler.d.ts +81 -0
  143. package/dist/scheduler/scheduler.js +376 -0
  144. package/dist/scheduler/types.d.ts +65 -0
  145. package/dist/scheduler/types.js +1 -0
  146. package/dist/stores/factory.d.ts +36 -0
  147. package/dist/stores/factory.js +73 -0
  148. package/dist/stores/file.d.ts +60 -0
  149. package/dist/stores/file.js +173 -0
  150. package/dist/stores/file.test.d.ts +1 -0
  151. package/dist/stores/file.test.js +165 -0
  152. package/dist/stores/index.d.ts +6 -0
  153. package/dist/stores/index.js +5 -0
  154. package/dist/stores/memory.d.ts +19 -0
  155. package/dist/stores/memory.js +51 -0
  156. package/dist/stores/memory.test.d.ts +1 -0
  157. package/dist/stores/memory.test.js +157 -0
  158. package/dist/stores/postgrest.d.ts +55 -0
  159. package/dist/stores/postgrest.js +217 -0
  160. package/dist/stores/stores.test.d.ts +1 -0
  161. package/dist/stores/stores.test.js +158 -0
  162. package/dist/stores/types.d.ts +31 -0
  163. package/dist/stores/types.js +26 -0
  164. package/dist/sync/index.d.ts +4 -0
  165. package/dist/sync/index.js +2 -0
  166. package/dist/sync/state.d.ts +69 -0
  167. package/dist/sync/state.js +66 -0
  168. package/dist/sync/store.d.ts +49 -0
  169. package/dist/sync/store.js +93 -0
  170. package/dist/sync/sync.test.d.ts +1 -0
  171. package/dist/sync/sync.test.js +221 -0
  172. package/dist/utils/async.d.ts +7 -0
  173. package/dist/utils/async.js +9 -0
  174. package/dist/utils/file.d.ts +38 -0
  175. package/dist/utils/file.js +92 -0
  176. package/dist/utils/index.d.ts +4 -0
  177. package/dist/utils/index.js +4 -0
  178. package/dist/utils/logger.d.ts +34 -0
  179. package/dist/utils/logger.js +39 -0
  180. package/dist/utils/path.d.ts +12 -0
  181. package/dist/utils/path.js +41 -0
  182. package/dist/webhook/index.d.ts +8 -0
  183. package/dist/webhook/index.js +7 -0
  184. package/dist/webhook/server.d.ts +84 -0
  185. package/dist/webhook/server.js +319 -0
  186. package/dist/webhook/store.d.ts +67 -0
  187. package/dist/webhook/store.js +193 -0
  188. package/dist/webhook/types.d.ts +88 -0
  189. package/dist/webhook/types.js +6 -0
  190. package/docusaurus/README.md +41 -0
  191. package/docusaurus/docs/advanced/execution-state.md +283 -0
  192. package/docusaurus/docs/advanced/extending-reqon.md +388 -0
  193. package/docusaurus/docs/advanced/multi-file-missions.md +250 -0
  194. package/docusaurus/docs/advanced/parallel-execution.md +353 -0
  195. package/docusaurus/docs/api-reference.md +443 -0
  196. package/docusaurus/docs/authentication/api-key.md +339 -0
  197. package/docusaurus/docs/authentication/basic.md +276 -0
  198. package/docusaurus/docs/authentication/bearer.md +282 -0
  199. package/docusaurus/docs/authentication/oauth2.md +317 -0
  200. package/docusaurus/docs/authentication/overview.md +251 -0
  201. package/docusaurus/docs/cli.md +229 -0
  202. package/docusaurus/docs/core-concepts/actions.md +286 -0
  203. package/docusaurus/docs/core-concepts/missions.md +264 -0
  204. package/docusaurus/docs/core-concepts/schemas.md +353 -0
  205. package/docusaurus/docs/core-concepts/sources.md +339 -0
  206. package/docusaurus/docs/core-concepts/stores.md +332 -0
  207. package/docusaurus/docs/dsl-syntax/expressions.md +361 -0
  208. package/docusaurus/docs/dsl-syntax/fetch.md +293 -0
  209. package/docusaurus/docs/dsl-syntax/for-loops.md +324 -0
  210. package/docusaurus/docs/dsl-syntax/map.md +345 -0
  211. package/docusaurus/docs/dsl-syntax/match.md +387 -0
  212. package/docusaurus/docs/dsl-syntax/pipelines.md +397 -0
  213. package/docusaurus/docs/dsl-syntax/validate.md +401 -0
  214. package/docusaurus/docs/error-handling/dead-letter-queues.md +399 -0
  215. package/docusaurus/docs/error-handling/flow-control.md +337 -0
  216. package/docusaurus/docs/error-handling/retry-strategies.md +368 -0
  217. package/docusaurus/docs/examples.md +488 -0
  218. package/docusaurus/docs/getting-started.md +256 -0
  219. package/docusaurus/docs/http/circuit-breaker.md +401 -0
  220. package/docusaurus/docs/http/incremental-sync.md +394 -0
  221. package/docusaurus/docs/http/pagination.md +361 -0
  222. package/docusaurus/docs/http/rate-limiting.md +383 -0
  223. package/docusaurus/docs/http/requests.md +328 -0
  224. package/docusaurus/docs/http/retry.md +402 -0
  225. package/docusaurus/docs/intro.md +90 -0
  226. package/docusaurus/docs/openapi/loading-specs.md +305 -0
  227. package/docusaurus/docs/openapi/operation-calls.md +314 -0
  228. package/docusaurus/docs/openapi/overview.md +212 -0
  229. package/docusaurus/docs/openapi/response-validation.md +344 -0
  230. package/docusaurus/docs/scheduling/cron.md +305 -0
  231. package/docusaurus/docs/scheduling/daemon-mode.md +317 -0
  232. package/docusaurus/docs/scheduling/intervals.md +289 -0
  233. package/docusaurus/docs/scheduling/overview.md +231 -0
  234. package/docusaurus/docs/stores/custom-adapters.md +376 -0
  235. package/docusaurus/docs/stores/file.md +236 -0
  236. package/docusaurus/docs/stores/memory.md +193 -0
  237. package/docusaurus/docs/stores/overview.md +274 -0
  238. package/docusaurus/docs/stores/postgrest.md +316 -0
  239. package/docusaurus/docusaurus.config.ts +148 -0
  240. package/docusaurus/package-lock.json +18029 -0
  241. package/docusaurus/package.json +47 -0
  242. package/docusaurus/sidebars.ts +155 -0
  243. package/docusaurus/src/components/HomepageFeatures/index.tsx +105 -0
  244. package/docusaurus/src/components/HomepageFeatures/styles.module.css +12 -0
  245. package/docusaurus/src/css/custom.css +169 -0
  246. package/docusaurus/src/pages/index.module.css +48 -0
  247. package/docusaurus/src/pages/index.tsx +110 -0
  248. package/docusaurus/src/pages/markdown-page.md +7 -0
  249. package/docusaurus/static/.nojekyll +0 -0
  250. package/docusaurus/static/img/docusaurus-social-card.jpg +0 -0
  251. package/docusaurus/static/img/docusaurus.png +0 -0
  252. package/docusaurus/static/img/favicon.ico +0 -0
  253. package/docusaurus/static/img/logo.svg +10 -0
  254. package/docusaurus/static/img/undraw_docusaurus_mountain.svg +171 -0
  255. package/docusaurus/static/img/undraw_docusaurus_react.svg +170 -0
  256. package/docusaurus/static/img/undraw_docusaurus_tree.svg +40 -0
  257. package/docusaurus/tsconfig.json +8 -0
  258. package/examples/README.md +112 -0
  259. package/examples/error-handling/README.md +150 -0
  260. package/examples/error-handling/payment-processor.vague +287 -0
  261. package/examples/github-sync/README.md +74 -0
  262. package/examples/github-sync/fetch-issues.vague +47 -0
  263. package/examples/github-sync/fetch-prs.vague +40 -0
  264. package/examples/github-sync/mission.vague +101 -0
  265. package/examples/github-sync/normalize.vague +70 -0
  266. package/examples/jsonplaceholder/README.md +28 -0
  267. package/examples/jsonplaceholder/posts.vague +48 -0
  268. package/examples/petstore/README.md +35 -0
  269. package/examples/petstore/openapi.yaml +97 -0
  270. package/examples/petstore/sync.vague +52 -0
  271. package/examples/temporal-comparison/README.md +297 -0
  272. package/examples/temporal-comparison/reconciliation.vague +355 -0
  273. package/examples/temporal-comparison/temporal/activities/index.ts +8 -0
  274. package/examples/temporal-comparison/temporal/activities/shipstation.ts +225 -0
  275. package/examples/temporal-comparison/temporal/activities/shopify.ts +257 -0
  276. package/examples/temporal-comparison/temporal/activities/storage.ts +198 -0
  277. package/examples/temporal-comparison/temporal/activities/stripe.ts +169 -0
  278. package/examples/temporal-comparison/temporal/activities/validation.ts +205 -0
  279. package/examples/temporal-comparison/temporal/client/schedule.ts +218 -0
  280. package/examples/temporal-comparison/temporal/config/retry.ts +63 -0
  281. package/examples/temporal-comparison/temporal/types/index.ts +129 -0
  282. package/examples/temporal-comparison/temporal/workers/main.ts +130 -0
  283. package/examples/temporal-comparison/temporal/workflows/orderReconciliation.ts +262 -0
  284. package/examples/xero/README.md +88 -0
  285. package/examples/xero/invoices.vague +189 -0
  286. package/package.json +40 -0
  287. package/src/api-integration.test.ts +954 -0
  288. package/src/ast/index.ts +1 -0
  289. package/src/ast/nodes.ts +310 -0
  290. package/src/auth/auth.test.ts +326 -0
  291. package/src/auth/circuit-breaker.test.ts +390 -0
  292. package/src/auth/circuit-breaker.ts +379 -0
  293. package/src/auth/credentials.test.ts +273 -0
  294. package/src/auth/credentials.ts +246 -0
  295. package/src/auth/index.ts +40 -0
  296. package/src/auth/oauth2-provider.ts +177 -0
  297. package/src/auth/rate-limiter.ts +459 -0
  298. package/src/auth/token-store.ts +177 -0
  299. package/src/auth/types.ts +159 -0
  300. package/src/benchmark/e2e.bench.ts +288 -0
  301. package/src/benchmark/evaluator.bench.ts +331 -0
  302. package/src/benchmark/fixtures.ts +295 -0
  303. package/src/benchmark/index.ts +108 -0
  304. package/src/benchmark/lexer.bench.ts +69 -0
  305. package/src/benchmark/parser.bench.ts +103 -0
  306. package/src/benchmark/resilience.bench.ts +193 -0
  307. package/src/benchmark/store.bench.ts +147 -0
  308. package/src/benchmark/utils.ts +230 -0
  309. package/src/cli.ts +313 -0
  310. package/src/errors/errors.test.ts +234 -0
  311. package/src/errors/index.ts +223 -0
  312. package/src/execution/execution.test.ts +307 -0
  313. package/src/execution/index.ts +21 -0
  314. package/src/execution/state.ts +207 -0
  315. package/src/execution/store.ts +188 -0
  316. package/src/index.ts +169 -0
  317. package/src/integration.test.ts +192 -0
  318. package/src/interpreter/context.ts +57 -0
  319. package/src/interpreter/evaluator.test.ts +796 -0
  320. package/src/interpreter/evaluator.ts +245 -0
  321. package/src/interpreter/executor.ts +946 -0
  322. package/src/interpreter/fetch-handler.ts +302 -0
  323. package/src/interpreter/http.test.ts +423 -0
  324. package/src/interpreter/http.ts +308 -0
  325. package/src/interpreter/index.ts +32 -0
  326. package/src/interpreter/pagination.ts +207 -0
  327. package/src/interpreter/progress.test.ts +276 -0
  328. package/src/interpreter/schema-matcher.test.ts +160 -0
  329. package/src/interpreter/schema-matcher.ts +168 -0
  330. package/src/interpreter/signals.ts +73 -0
  331. package/src/interpreter/step-handlers/for-handler.ts +65 -0
  332. package/src/interpreter/step-handlers/index.ts +17 -0
  333. package/src/interpreter/step-handlers/map-handler.ts +24 -0
  334. package/src/interpreter/step-handlers/match-handler.ts +101 -0
  335. package/src/interpreter/step-handlers/store-handler.ts +78 -0
  336. package/src/interpreter/step-handlers/types.ts +17 -0
  337. package/src/interpreter/step-handlers/validate-handler.ts +30 -0
  338. package/src/interpreter/step-handlers/webhook-handler.ts +142 -0
  339. package/src/lexer/index.ts +18 -0
  340. package/src/lexer/lexer.test.ts +316 -0
  341. package/src/lexer/tokens.ts +179 -0
  342. package/src/loader/index.ts +288 -0
  343. package/src/loader/loader.test.ts +360 -0
  344. package/src/oas/index.ts +4 -0
  345. package/src/oas/loader.ts +126 -0
  346. package/src/oas/oas.test.ts +254 -0
  347. package/src/oas/validator.ts +299 -0
  348. package/src/parser/base.ts +124 -0
  349. package/src/parser/expressions.test.ts +525 -0
  350. package/src/parser/expressions.ts +314 -0
  351. package/src/parser/index.ts +3 -0
  352. package/src/parser/match.test.ts +296 -0
  353. package/src/parser/parser.test.ts +739 -0
  354. package/src/parser/parser.ts +1469 -0
  355. package/src/parser/schedule.test.ts +287 -0
  356. package/src/parser/webhook.test.ts +248 -0
  357. package/src/plugin.ts +83 -0
  358. package/src/scheduler/cron-parser.test.ts +236 -0
  359. package/src/scheduler/cron-parser.ts +236 -0
  360. package/src/scheduler/index.ts +10 -0
  361. package/src/scheduler/scheduler.ts +443 -0
  362. package/src/scheduler/types.ts +71 -0
  363. package/src/stores/factory.ts +104 -0
  364. package/src/stores/file.test.ts +276 -0
  365. package/src/stores/file.ts +211 -0
  366. package/src/stores/index.ts +6 -0
  367. package/src/stores/memory.test.ts +238 -0
  368. package/src/stores/memory.ts +63 -0
  369. package/src/stores/postgrest.test.ts +488 -0
  370. package/src/stores/postgrest.ts +263 -0
  371. package/src/stores/stores.test.ts +197 -0
  372. package/src/stores/types.ts +58 -0
  373. package/src/sync/index.ts +16 -0
  374. package/src/sync/state.ts +126 -0
  375. package/src/sync/store.ts +139 -0
  376. package/src/sync/sync.test.ts +271 -0
  377. package/src/utils/async.ts +10 -0
  378. package/src/utils/file.ts +106 -0
  379. package/src/utils/index.ts +14 -0
  380. package/src/utils/logger.ts +53 -0
  381. package/src/utils/path.ts +47 -0
  382. package/src/webhook/index.ts +15 -0
  383. package/src/webhook/server.test.ts +253 -0
  384. package/src/webhook/server.ts +389 -0
  385. package/src/webhook/store.ts +239 -0
  386. package/src/webhook/types.ts +93 -0
  387. package/tsconfig.json +17 -0
  388. package/vitest.config.ts +39 -0
@@ -0,0 +1,253 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { WebhookServer } from './server.js';
3
+ import { MemoryWebhookStore } from './store.js';
4
+
5
+ describe('WebhookServer', () => {
6
+ let server: WebhookServer;
7
+ let store: MemoryWebhookStore;
8
+
9
+ beforeEach(() => {
10
+ store = new MemoryWebhookStore();
11
+ server = new WebhookServer(
12
+ { port: 0, verbose: false }, // Use port 0 for dynamic port assignment
13
+ store
14
+ );
15
+ });
16
+
17
+ afterEach(async () => {
18
+ if (server.isRunning()) {
19
+ await server.stop();
20
+ }
21
+ });
22
+
23
+ describe('start/stop', () => {
24
+ it('should start and stop the server', async () => {
25
+ // Server should not be running initially
26
+ expect(server.isRunning()).toBe(false);
27
+
28
+ // Note: Port 0 might not work in all environments, so we skip actual start
29
+ // In a real test environment, you'd start the server and verify
30
+ });
31
+ });
32
+
33
+ describe('register', () => {
34
+ it('should register a webhook endpoint', async () => {
35
+ const registration = await server.register('exec-123', {
36
+ path: '/test/callback',
37
+ timeout: 60000,
38
+ expectedEvents: 2,
39
+ });
40
+
41
+ expect(registration.id).toBeDefined();
42
+ expect(registration.executionId).toBe('exec-123');
43
+ expect(registration.path).toBe('/test/callback');
44
+ expect(registration.expectedEvents).toBe(2);
45
+ expect(registration.receivedEvents).toBe(0);
46
+ });
47
+
48
+ it('should auto-generate path if not provided', async () => {
49
+ const registration = await server.register('exec-456');
50
+
51
+ expect(registration.path).toContain('/webhook/exec-456/');
52
+ });
53
+
54
+ it('should set expiration based on timeout', async () => {
55
+ const before = Date.now();
56
+ const registration = await server.register('exec-789', {
57
+ timeout: 120000, // 2 minutes
58
+ });
59
+ const after = Date.now();
60
+
61
+ const expectedMin = before + 120000;
62
+ const expectedMax = after + 120000;
63
+
64
+ expect(registration.expiresAt.getTime()).toBeGreaterThanOrEqual(expectedMin);
65
+ expect(registration.expiresAt.getTime()).toBeLessThanOrEqual(expectedMax);
66
+ });
67
+ });
68
+
69
+ describe('unregister', () => {
70
+ it('should unregister a webhook endpoint', async () => {
71
+ const registration = await server.register('exec-123');
72
+
73
+ await server.unregister(registration.id);
74
+
75
+ const loaded = await store.getRegistration(registration.id);
76
+ expect(loaded).toBeUndefined();
77
+ });
78
+ });
79
+
80
+ describe('getWebhookUrl', () => {
81
+ it('should return the full webhook URL', async () => {
82
+ const serverWithUrl = new WebhookServer(
83
+ { port: 8080, baseUrl: 'https://example.com' },
84
+ store
85
+ );
86
+
87
+ const registration = await serverWithUrl.register('exec-123', {
88
+ path: '/webhooks/test',
89
+ });
90
+
91
+ const url = serverWithUrl.getWebhookUrl(registration);
92
+ expect(url).toBe('https://example.com/webhooks/test');
93
+ });
94
+ });
95
+ });
96
+
97
+ describe('MemoryWebhookStore', () => {
98
+ let store: MemoryWebhookStore;
99
+
100
+ beforeEach(() => {
101
+ store = new MemoryWebhookStore();
102
+ });
103
+
104
+ describe('registrations', () => {
105
+ it('should save and retrieve a registration', async () => {
106
+ const registration = {
107
+ id: 'reg-1',
108
+ executionId: 'exec-1',
109
+ path: '/test',
110
+ createdAt: new Date(),
111
+ expiresAt: new Date(Date.now() + 60000),
112
+ expectedEvents: 1,
113
+ receivedEvents: 0,
114
+ };
115
+
116
+ await store.saveRegistration(registration);
117
+ const loaded = await store.getRegistration('reg-1');
118
+
119
+ expect(loaded).toEqual(registration);
120
+ });
121
+
122
+ it('should find registration by path', async () => {
123
+ const registration = {
124
+ id: 'reg-2',
125
+ executionId: 'exec-2',
126
+ path: '/webhooks/callback',
127
+ createdAt: new Date(),
128
+ expiresAt: new Date(Date.now() + 60000),
129
+ expectedEvents: 1,
130
+ receivedEvents: 0,
131
+ };
132
+
133
+ await store.saveRegistration(registration);
134
+ const found = await store.getRegistrationByPath('/webhooks/callback');
135
+
136
+ expect(found?.id).toBe('reg-2');
137
+ });
138
+
139
+ it('should delete a registration', async () => {
140
+ const registration = {
141
+ id: 'reg-3',
142
+ executionId: 'exec-3',
143
+ path: '/test3',
144
+ createdAt: new Date(),
145
+ expiresAt: new Date(Date.now() + 60000),
146
+ expectedEvents: 1,
147
+ receivedEvents: 0,
148
+ };
149
+
150
+ await store.saveRegistration(registration);
151
+ await store.deleteRegistration('reg-3');
152
+
153
+ const loaded = await store.getRegistration('reg-3');
154
+ expect(loaded).toBeUndefined();
155
+ });
156
+
157
+ it('should list all registrations', async () => {
158
+ await store.saveRegistration({
159
+ id: 'reg-a',
160
+ executionId: 'exec-a',
161
+ path: '/a',
162
+ createdAt: new Date(),
163
+ expiresAt: new Date(Date.now() + 60000),
164
+ expectedEvents: 1,
165
+ receivedEvents: 0,
166
+ });
167
+
168
+ await store.saveRegistration({
169
+ id: 'reg-b',
170
+ executionId: 'exec-b',
171
+ path: '/b',
172
+ createdAt: new Date(),
173
+ expiresAt: new Date(Date.now() + 60000),
174
+ expectedEvents: 1,
175
+ receivedEvents: 0,
176
+ });
177
+
178
+ const list = await store.listRegistrations();
179
+ expect(list).toHaveLength(2);
180
+ });
181
+ });
182
+
183
+ describe('events', () => {
184
+ it('should save and retrieve events', async () => {
185
+ const event = {
186
+ id: 'evt-1',
187
+ registrationId: 'reg-1',
188
+ receivedAt: new Date(),
189
+ method: 'POST',
190
+ headers: { 'content-type': 'application/json' },
191
+ body: { data: 'test' },
192
+ rawBody: '{"data":"test"}',
193
+ query: {},
194
+ };
195
+
196
+ await store.saveEvent(event);
197
+ const events = await store.getEvents('reg-1');
198
+
199
+ expect(events).toHaveLength(1);
200
+ expect(events[0]).toEqual(event);
201
+ });
202
+
203
+ it('should delete events for a registration', async () => {
204
+ await store.saveEvent({
205
+ id: 'evt-2',
206
+ registrationId: 'reg-2',
207
+ receivedAt: new Date(),
208
+ method: 'POST',
209
+ headers: {},
210
+ body: {},
211
+ rawBody: '',
212
+ query: {},
213
+ });
214
+
215
+ await store.deleteEvents('reg-2');
216
+ const events = await store.getEvents('reg-2');
217
+
218
+ expect(events).toHaveLength(0);
219
+ });
220
+ });
221
+
222
+ describe('cleanup', () => {
223
+ it('should clean up expired registrations', async () => {
224
+ // Create an expired registration
225
+ await store.saveRegistration({
226
+ id: 'expired',
227
+ executionId: 'exec-expired',
228
+ path: '/expired',
229
+ createdAt: new Date(Date.now() - 120000),
230
+ expiresAt: new Date(Date.now() - 60000), // Expired 1 minute ago
231
+ expectedEvents: 1,
232
+ receivedEvents: 0,
233
+ });
234
+
235
+ // Create a valid registration
236
+ await store.saveRegistration({
237
+ id: 'valid',
238
+ executionId: 'exec-valid',
239
+ path: '/valid',
240
+ createdAt: new Date(),
241
+ expiresAt: new Date(Date.now() + 60000), // Expires in 1 minute
242
+ expectedEvents: 1,
243
+ receivedEvents: 0,
244
+ });
245
+
246
+ const cleaned = await store.cleanupExpired();
247
+
248
+ expect(cleaned).toBe(1);
249
+ expect(await store.getRegistration('expired')).toBeUndefined();
250
+ expect(await store.getRegistration('valid')).toBeDefined();
251
+ });
252
+ });
253
+ });
@@ -0,0 +1,389 @@
1
+ /**
2
+ * Webhook Server
3
+ *
4
+ * HTTP server for receiving webhook callbacks.
5
+ * Supports dynamic registration of webhook endpoints.
6
+ */
7
+
8
+ import { createServer, type Server, type IncomingMessage, type ServerResponse } from 'node:http';
9
+ import { parse as parseUrl } from 'node:url';
10
+ import { randomUUID } from 'node:crypto';
11
+ import type {
12
+ WebhookServerConfig,
13
+ WebhookServerCallbacks,
14
+ WebhookRegistration,
15
+ WebhookEvent,
16
+ WaitResult,
17
+ } from './types.js';
18
+ import type { WebhookStore } from './store.js';
19
+ import { MemoryWebhookStore } from './store.js';
20
+
21
+ /**
22
+ * Pending wait request
23
+ */
24
+ interface PendingWait {
25
+ registrationId: string;
26
+ resolve: (result: WaitResult) => void;
27
+ timeoutId: ReturnType<typeof setTimeout>;
28
+ }
29
+
30
+ /**
31
+ * Webhook Server
32
+ *
33
+ * Provides HTTP endpoints for receiving webhook callbacks.
34
+ */
35
+ export class WebhookServer {
36
+ private config: Required<WebhookServerConfig>;
37
+ private store: WebhookStore;
38
+ private callbacks: WebhookServerCallbacks;
39
+ private server?: Server;
40
+ private pendingWaits: Map<string, PendingWait> = new Map();
41
+ private cleanupInterval?: ReturnType<typeof setInterval>;
42
+ private running = false;
43
+
44
+ constructor(
45
+ config: WebhookServerConfig = {},
46
+ store?: WebhookStore,
47
+ callbacks: WebhookServerCallbacks = {}
48
+ ) {
49
+ this.config = {
50
+ port: config.port ?? 3000,
51
+ host: config.host ?? '0.0.0.0',
52
+ baseUrl: config.baseUrl ?? `http://localhost:${config.port ?? 3000}`,
53
+ defaultTimeout: config.defaultTimeout ?? 300000, // 5 minutes
54
+ verbose: config.verbose ?? false,
55
+ };
56
+ this.store = store ?? new MemoryWebhookStore();
57
+ this.callbacks = callbacks;
58
+ }
59
+
60
+ /**
61
+ * Start the webhook server
62
+ */
63
+ async start(): Promise<void> {
64
+ if (this.running) return;
65
+
66
+ return new Promise((resolve, reject) => {
67
+ this.server = createServer((req, res) => this.handleRequest(req, res));
68
+
69
+ this.server.on('error', (error) => {
70
+ reject(error);
71
+ });
72
+
73
+ this.server.listen(this.config.port, this.config.host, () => {
74
+ this.running = true;
75
+ this.log(`Webhook server listening on ${this.config.host}:${this.config.port}`);
76
+
77
+ // Start cleanup interval (every minute)
78
+ this.cleanupInterval = setInterval(() => this.cleanup(), 60000);
79
+
80
+ resolve();
81
+ });
82
+ });
83
+ }
84
+
85
+ /**
86
+ * Stop the webhook server
87
+ */
88
+ async stop(): Promise<void> {
89
+ if (!this.running) return;
90
+
91
+ // Clear cleanup interval
92
+ if (this.cleanupInterval) {
93
+ clearInterval(this.cleanupInterval);
94
+ this.cleanupInterval = undefined;
95
+ }
96
+
97
+ // Cancel all pending waits
98
+ for (const [id, pending] of this.pendingWaits) {
99
+ clearTimeout(pending.timeoutId);
100
+ pending.resolve({
101
+ success: false,
102
+ events: [],
103
+ error: 'Server shutting down',
104
+ });
105
+ }
106
+ this.pendingWaits.clear();
107
+
108
+ // Close server
109
+ return new Promise((resolve) => {
110
+ if (this.server) {
111
+ this.server.close(() => {
112
+ this.running = false;
113
+ this.log('Webhook server stopped');
114
+ resolve();
115
+ });
116
+ } else {
117
+ resolve();
118
+ }
119
+ });
120
+ }
121
+
122
+ /**
123
+ * Register a webhook endpoint
124
+ */
125
+ async register(
126
+ executionId: string,
127
+ options: {
128
+ path?: string;
129
+ timeout?: number;
130
+ expectedEvents?: number;
131
+ filter?: string;
132
+ } = {}
133
+ ): Promise<WebhookRegistration> {
134
+ const id = randomUUID();
135
+ const timeout = options.timeout ?? this.config.defaultTimeout;
136
+ const path = options.path ?? `/webhook/${executionId}/${id}`;
137
+
138
+ const registration: WebhookRegistration = {
139
+ id,
140
+ executionId,
141
+ path,
142
+ createdAt: new Date(),
143
+ expiresAt: new Date(Date.now() + timeout),
144
+ expectedEvents: options.expectedEvents ?? 1,
145
+ receivedEvents: 0,
146
+ filter: options.filter,
147
+ };
148
+
149
+ await this.store.saveRegistration(registration);
150
+ this.callbacks.onRegistrationCreated?.(registration);
151
+ this.log(`Registered webhook: ${path} (expires: ${registration.expiresAt.toISOString()})`);
152
+
153
+ return registration;
154
+ }
155
+
156
+ /**
157
+ * Get the full URL for a webhook endpoint
158
+ */
159
+ getWebhookUrl(registration: WebhookRegistration): string {
160
+ return `${this.config.baseUrl}${registration.path}`;
161
+ }
162
+
163
+ /**
164
+ * Wait for webhook events
165
+ */
166
+ async waitForEvents(
167
+ registrationId: string,
168
+ timeout?: number
169
+ ): Promise<WaitResult> {
170
+ const registration = await this.store.getRegistration(registrationId);
171
+ if (!registration) {
172
+ return {
173
+ success: false,
174
+ events: [],
175
+ error: `Registration not found: ${registrationId}`,
176
+ };
177
+ }
178
+
179
+ // Check if already received enough events
180
+ const events = await this.store.getEvents(registrationId);
181
+ if (events.length >= registration.expectedEvents) {
182
+ return { success: true, events };
183
+ }
184
+
185
+ // Wait for more events
186
+ const waitTimeout = timeout ?? (registration.expiresAt.getTime() - Date.now());
187
+
188
+ return new Promise((resolve) => {
189
+ const timeoutId = setTimeout(() => {
190
+ this.pendingWaits.delete(registrationId);
191
+ this.store.getEvents(registrationId).then((events) => {
192
+ resolve({
193
+ success: events.length >= registration.expectedEvents,
194
+ events,
195
+ timedOut: true,
196
+ });
197
+ });
198
+ }, waitTimeout);
199
+
200
+ this.pendingWaits.set(registrationId, {
201
+ registrationId,
202
+ resolve,
203
+ timeoutId,
204
+ });
205
+ });
206
+ }
207
+
208
+ /**
209
+ * Unregister a webhook endpoint
210
+ */
211
+ async unregister(registrationId: string): Promise<void> {
212
+ await this.store.deleteRegistration(registrationId);
213
+ await this.store.deleteEvents(registrationId);
214
+
215
+ // Cancel pending wait if any
216
+ const pending = this.pendingWaits.get(registrationId);
217
+ if (pending) {
218
+ clearTimeout(pending.timeoutId);
219
+ this.pendingWaits.delete(registrationId);
220
+ }
221
+
222
+ this.log(`Unregistered webhook: ${registrationId}`);
223
+ }
224
+
225
+ /**
226
+ * Check if the server is running
227
+ */
228
+ isRunning(): boolean {
229
+ return this.running;
230
+ }
231
+
232
+ /**
233
+ * Get the server port
234
+ */
235
+ getPort(): number {
236
+ return this.config.port;
237
+ }
238
+
239
+ /**
240
+ * Get the base URL
241
+ */
242
+ getBaseUrl(): string {
243
+ return this.config.baseUrl;
244
+ }
245
+
246
+ /**
247
+ * Handle incoming HTTP request
248
+ */
249
+ private async handleRequest(req: IncomingMessage, res: ServerResponse): Promise<void> {
250
+ const url = parseUrl(req.url ?? '/', true);
251
+ const path = url.pathname ?? '/';
252
+
253
+ // Health check endpoint
254
+ if (path === '/health' || path === '/_health') {
255
+ res.writeHead(200, { 'Content-Type': 'application/json' });
256
+ res.end(JSON.stringify({ status: 'ok', timestamp: new Date().toISOString() }));
257
+ return;
258
+ }
259
+
260
+ // Find matching registration
261
+ const registration = await this.store.getRegistrationByPath(path);
262
+ if (!registration) {
263
+ res.writeHead(404, { 'Content-Type': 'application/json' });
264
+ res.end(JSON.stringify({ error: 'Not found', path }));
265
+ return;
266
+ }
267
+
268
+ // Check if registration is expired
269
+ if (registration.expiresAt < new Date()) {
270
+ res.writeHead(410, { 'Content-Type': 'application/json' });
271
+ res.end(JSON.stringify({ error: 'Webhook registration expired' }));
272
+ await this.store.deleteRegistration(registration.id);
273
+ return;
274
+ }
275
+
276
+ // Parse request body
277
+ let rawBody = '';
278
+ let body: unknown = null;
279
+
280
+ try {
281
+ rawBody = await this.readBody(req);
282
+ if (rawBody) {
283
+ const contentType = req.headers['content-type'] ?? '';
284
+ if (contentType.includes('application/json')) {
285
+ body = JSON.parse(rawBody);
286
+ } else if (contentType.includes('application/x-www-form-urlencoded')) {
287
+ body = Object.fromEntries(new URLSearchParams(rawBody));
288
+ } else {
289
+ body = rawBody;
290
+ }
291
+ }
292
+ } catch (error) {
293
+ body = rawBody;
294
+ }
295
+
296
+ // Create event
297
+ const event: WebhookEvent = {
298
+ id: randomUUID(),
299
+ registrationId: registration.id,
300
+ receivedAt: new Date(),
301
+ method: req.method ?? 'POST',
302
+ headers: this.extractHeaders(req),
303
+ body,
304
+ rawBody,
305
+ query: url.query as Record<string, string>,
306
+ };
307
+
308
+ // Save event
309
+ await this.store.saveEvent(event);
310
+ registration.receivedEvents++;
311
+ await this.store.saveRegistration(registration);
312
+
313
+ this.log(`Webhook received: ${path} (${registration.receivedEvents}/${registration.expectedEvents})`);
314
+ this.callbacks.onWebhookReceived?.(event);
315
+
316
+ // Check if all expected events received
317
+ if (registration.receivedEvents >= registration.expectedEvents) {
318
+ const events = await this.store.getEvents(registration.id);
319
+ this.callbacks.onRegistrationComplete?.(registration, events);
320
+
321
+ // Resolve pending wait
322
+ const pending = this.pendingWaits.get(registration.id);
323
+ if (pending) {
324
+ clearTimeout(pending.timeoutId);
325
+ this.pendingWaits.delete(registration.id);
326
+ pending.resolve({ success: true, events });
327
+ }
328
+ }
329
+
330
+ // Send response
331
+ res.writeHead(200, { 'Content-Type': 'application/json' });
332
+ res.end(JSON.stringify({
333
+ success: true,
334
+ eventId: event.id,
335
+ received: registration.receivedEvents,
336
+ expected: registration.expectedEvents,
337
+ }));
338
+ }
339
+
340
+ /**
341
+ * Read request body
342
+ */
343
+ private readBody(req: IncomingMessage): Promise<string> {
344
+ return new Promise((resolve, reject) => {
345
+ let data = '';
346
+ req.on('data', (chunk) => {
347
+ data += chunk;
348
+ });
349
+ req.on('end', () => {
350
+ resolve(data);
351
+ });
352
+ req.on('error', reject);
353
+ });
354
+ }
355
+
356
+ /**
357
+ * Extract headers from request
358
+ */
359
+ private extractHeaders(req: IncomingMessage): Record<string, string> {
360
+ const headers: Record<string, string> = {};
361
+ for (const [key, value] of Object.entries(req.headers)) {
362
+ if (typeof value === 'string') {
363
+ headers[key] = value;
364
+ } else if (Array.isArray(value)) {
365
+ headers[key] = value.join(', ');
366
+ }
367
+ }
368
+ return headers;
369
+ }
370
+
371
+ /**
372
+ * Clean up expired registrations
373
+ */
374
+ private async cleanup(): Promise<void> {
375
+ const cleaned = await this.store.cleanupExpired();
376
+ if (cleaned > 0) {
377
+ this.log(`Cleaned up ${cleaned} expired webhook registration(s)`);
378
+ }
379
+ }
380
+
381
+ /**
382
+ * Log message if verbose mode enabled
383
+ */
384
+ private log(message: string): void {
385
+ if (this.config.verbose) {
386
+ console.log(`[Webhook] ${message}`);
387
+ }
388
+ }
389
+ }