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.
- package/.claude/settings.local.json +31 -0
- package/.claude/skills/api-integration.md +125 -0
- package/.claude/skills/database-schema.md +51 -0
- package/.claude/skills/dsl-design.md +80 -0
- package/.claude/skills/property-testing.md +143 -0
- package/.claude/skills/reqon/SKILL.md +44 -0
- package/.claude/skills/reqon/references/examples.md +206 -0
- package/.claude/skills/reqon/references/syntax.md +263 -0
- package/.claude/skills/vscode-extension.md +113 -0
- package/.github/dependabot.yml +32 -0
- package/.github/pull_request_template.md +21 -0
- package/.github/workflows/ci.yml +174 -0
- package/.github/workflows/release.yml +73 -0
- package/CLAUDE.md +72 -0
- package/CONTRIBUTING.md +161 -0
- package/README.md +235 -0
- package/TODO.md +51 -0
- package/dist/ast/index.d.ts +1 -0
- package/dist/ast/index.js +1 -0
- package/dist/ast/nodes.d.ts +237 -0
- package/dist/ast/nodes.js +12 -0
- package/dist/auth/auth.test.d.ts +1 -0
- package/dist/auth/auth.test.js +255 -0
- package/dist/auth/circuit-breaker.d.ts +115 -0
- package/dist/auth/circuit-breaker.js +267 -0
- package/dist/auth/credentials.d.ts +91 -0
- package/dist/auth/credentials.js +169 -0
- package/dist/auth/index.d.ts +5 -0
- package/dist/auth/index.js +8 -0
- package/dist/auth/oauth2-provider.d.ts +41 -0
- package/dist/auth/oauth2-provider.js +131 -0
- package/dist/auth/rate-limiter.d.ts +61 -0
- package/dist/auth/rate-limiter.js +380 -0
- package/dist/auth/token-store.d.ts +30 -0
- package/dist/auth/token-store.js +148 -0
- package/dist/auth/types.d.ts +142 -0
- package/dist/auth/types.js +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +270 -0
- package/dist/errors/errors.test.d.ts +1 -0
- package/dist/errors/errors.test.js +165 -0
- package/dist/errors/index.d.ts +83 -0
- package/dist/errors/index.js +159 -0
- package/dist/execution/execution.test.d.ts +1 -0
- package/dist/execution/execution.test.js +246 -0
- package/dist/execution/index.d.ts +4 -0
- package/dist/execution/index.js +2 -0
- package/dist/execution/state.d.ts +136 -0
- package/dist/execution/state.js +82 -0
- package/dist/execution/store.d.ts +52 -0
- package/dist/execution/store.js +120 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +57 -0
- package/dist/integration.test.d.ts +1 -0
- package/dist/integration.test.js +168 -0
- package/dist/interpreter/context.d.ts +15 -0
- package/dist/interpreter/context.js +29 -0
- package/dist/interpreter/evaluator.d.ts +5 -0
- package/dist/interpreter/evaluator.js +223 -0
- package/dist/interpreter/evaluator.test.d.ts +1 -0
- package/dist/interpreter/evaluator.test.js +512 -0
- package/dist/interpreter/executor.d.ts +131 -0
- package/dist/interpreter/executor.js +663 -0
- package/dist/interpreter/fetch-handler.d.ts +43 -0
- package/dist/interpreter/fetch-handler.js +203 -0
- package/dist/interpreter/http.d.ts +57 -0
- package/dist/interpreter/http.js +210 -0
- package/dist/interpreter/http.test.d.ts +1 -0
- package/dist/interpreter/http.test.js +299 -0
- package/dist/interpreter/index.d.ts +7 -0
- package/dist/interpreter/index.js +7 -0
- package/dist/interpreter/pagination.d.ts +63 -0
- package/dist/interpreter/pagination.js +155 -0
- package/dist/interpreter/progress.test.d.ts +1 -0
- package/dist/interpreter/progress.test.js +216 -0
- package/dist/interpreter/schema-matcher.d.ts +16 -0
- package/dist/interpreter/schema-matcher.js +136 -0
- package/dist/interpreter/schema-matcher.test.d.ts +1 -0
- package/dist/interpreter/schema-matcher.test.js +122 -0
- package/dist/interpreter/signals.d.ts +57 -0
- package/dist/interpreter/signals.js +73 -0
- package/dist/interpreter/step-handlers/for-handler.d.ts +17 -0
- package/dist/interpreter/step-handlers/for-handler.js +51 -0
- package/dist/interpreter/step-handlers/index.d.ts +8 -0
- package/dist/interpreter/step-handlers/index.js +8 -0
- package/dist/interpreter/step-handlers/map-handler.d.ts +10 -0
- package/dist/interpreter/step-handlers/map-handler.js +20 -0
- package/dist/interpreter/step-handlers/match-handler.d.ts +27 -0
- package/dist/interpreter/step-handlers/match-handler.js +61 -0
- package/dist/interpreter/step-handlers/store-handler.d.ts +13 -0
- package/dist/interpreter/step-handlers/store-handler.js +66 -0
- package/dist/interpreter/step-handlers/types.d.ts +15 -0
- package/dist/interpreter/step-handlers/types.js +1 -0
- package/dist/interpreter/step-handlers/validate-handler.d.ts +10 -0
- package/dist/interpreter/step-handlers/validate-handler.js +26 -0
- package/dist/interpreter/step-handlers/webhook-handler.d.ts +36 -0
- package/dist/interpreter/step-handlers/webhook-handler.js +104 -0
- package/dist/lexer/index.d.ts +10 -0
- package/dist/lexer/index.js +12 -0
- package/dist/lexer/lexer.d.ts +24 -0
- package/dist/lexer/lexer.js +264 -0
- package/dist/lexer/lexer.test.d.ts +1 -0
- package/dist/lexer/lexer.test.js +259 -0
- package/dist/lexer/tokens.d.ts +69 -0
- package/dist/lexer/tokens.js +146 -0
- package/dist/loader/index.d.ts +36 -0
- package/dist/loader/index.js +220 -0
- package/dist/loader/loader.test.d.ts +1 -0
- package/dist/loader/loader.test.js +287 -0
- package/dist/oas/index.d.ts +4 -0
- package/dist/oas/index.js +2 -0
- package/dist/oas/loader.d.ts +21 -0
- package/dist/oas/loader.js +82 -0
- package/dist/oas/oas.test.d.ts +1 -0
- package/dist/oas/oas.test.js +218 -0
- package/dist/oas/validator.d.ts +12 -0
- package/dist/oas/validator.js +227 -0
- package/dist/parser/base.d.ts +33 -0
- package/dist/parser/base.js +97 -0
- package/dist/parser/expressions.d.ts +27 -0
- package/dist/parser/expressions.js +248 -0
- package/dist/parser/expressions.test.d.ts +1 -0
- package/dist/parser/expressions.test.js +378 -0
- package/dist/parser/index.d.ts +3 -0
- package/dist/parser/index.js +3 -0
- package/dist/parser/match.test.d.ts +1 -0
- package/dist/parser/match.test.js +254 -0
- package/dist/parser/parser.d.ts +68 -0
- package/dist/parser/parser.js +1229 -0
- package/dist/parser/parser.test.d.ts +1 -0
- package/dist/parser/parser.test.js +333 -0
- package/dist/parser/schedule.test.d.ts +1 -0
- package/dist/parser/schedule.test.js +241 -0
- package/dist/plugin.d.ts +35 -0
- package/dist/plugin.js +68 -0
- package/dist/scheduler/cron-parser.d.ts +32 -0
- package/dist/scheduler/cron-parser.js +198 -0
- package/dist/scheduler/cron-parser.test.d.ts +1 -0
- package/dist/scheduler/cron-parser.test.js +188 -0
- package/dist/scheduler/index.d.ts +3 -0
- package/dist/scheduler/index.js +2 -0
- package/dist/scheduler/scheduler.d.ts +81 -0
- package/dist/scheduler/scheduler.js +376 -0
- package/dist/scheduler/types.d.ts +65 -0
- package/dist/scheduler/types.js +1 -0
- package/dist/stores/factory.d.ts +36 -0
- package/dist/stores/factory.js +73 -0
- package/dist/stores/file.d.ts +60 -0
- package/dist/stores/file.js +173 -0
- package/dist/stores/file.test.d.ts +1 -0
- package/dist/stores/file.test.js +165 -0
- package/dist/stores/index.d.ts +6 -0
- package/dist/stores/index.js +5 -0
- package/dist/stores/memory.d.ts +19 -0
- package/dist/stores/memory.js +51 -0
- package/dist/stores/memory.test.d.ts +1 -0
- package/dist/stores/memory.test.js +157 -0
- package/dist/stores/postgrest.d.ts +55 -0
- package/dist/stores/postgrest.js +217 -0
- package/dist/stores/stores.test.d.ts +1 -0
- package/dist/stores/stores.test.js +158 -0
- package/dist/stores/types.d.ts +31 -0
- package/dist/stores/types.js +26 -0
- package/dist/sync/index.d.ts +4 -0
- package/dist/sync/index.js +2 -0
- package/dist/sync/state.d.ts +69 -0
- package/dist/sync/state.js +66 -0
- package/dist/sync/store.d.ts +49 -0
- package/dist/sync/store.js +93 -0
- package/dist/sync/sync.test.d.ts +1 -0
- package/dist/sync/sync.test.js +221 -0
- package/dist/utils/async.d.ts +7 -0
- package/dist/utils/async.js +9 -0
- package/dist/utils/file.d.ts +38 -0
- package/dist/utils/file.js +92 -0
- package/dist/utils/index.d.ts +4 -0
- package/dist/utils/index.js +4 -0
- package/dist/utils/logger.d.ts +34 -0
- package/dist/utils/logger.js +39 -0
- package/dist/utils/path.d.ts +12 -0
- package/dist/utils/path.js +41 -0
- package/dist/webhook/index.d.ts +8 -0
- package/dist/webhook/index.js +7 -0
- package/dist/webhook/server.d.ts +84 -0
- package/dist/webhook/server.js +319 -0
- package/dist/webhook/store.d.ts +67 -0
- package/dist/webhook/store.js +193 -0
- package/dist/webhook/types.d.ts +88 -0
- package/dist/webhook/types.js +6 -0
- package/docusaurus/README.md +41 -0
- package/docusaurus/docs/advanced/execution-state.md +283 -0
- package/docusaurus/docs/advanced/extending-reqon.md +388 -0
- package/docusaurus/docs/advanced/multi-file-missions.md +250 -0
- package/docusaurus/docs/advanced/parallel-execution.md +353 -0
- package/docusaurus/docs/api-reference.md +443 -0
- package/docusaurus/docs/authentication/api-key.md +339 -0
- package/docusaurus/docs/authentication/basic.md +276 -0
- package/docusaurus/docs/authentication/bearer.md +282 -0
- package/docusaurus/docs/authentication/oauth2.md +317 -0
- package/docusaurus/docs/authentication/overview.md +251 -0
- package/docusaurus/docs/cli.md +229 -0
- package/docusaurus/docs/core-concepts/actions.md +286 -0
- package/docusaurus/docs/core-concepts/missions.md +264 -0
- package/docusaurus/docs/core-concepts/schemas.md +353 -0
- package/docusaurus/docs/core-concepts/sources.md +339 -0
- package/docusaurus/docs/core-concepts/stores.md +332 -0
- package/docusaurus/docs/dsl-syntax/expressions.md +361 -0
- package/docusaurus/docs/dsl-syntax/fetch.md +293 -0
- package/docusaurus/docs/dsl-syntax/for-loops.md +324 -0
- package/docusaurus/docs/dsl-syntax/map.md +345 -0
- package/docusaurus/docs/dsl-syntax/match.md +387 -0
- package/docusaurus/docs/dsl-syntax/pipelines.md +397 -0
- package/docusaurus/docs/dsl-syntax/validate.md +401 -0
- package/docusaurus/docs/error-handling/dead-letter-queues.md +399 -0
- package/docusaurus/docs/error-handling/flow-control.md +337 -0
- package/docusaurus/docs/error-handling/retry-strategies.md +368 -0
- package/docusaurus/docs/examples.md +488 -0
- package/docusaurus/docs/getting-started.md +256 -0
- package/docusaurus/docs/http/circuit-breaker.md +401 -0
- package/docusaurus/docs/http/incremental-sync.md +394 -0
- package/docusaurus/docs/http/pagination.md +361 -0
- package/docusaurus/docs/http/rate-limiting.md +383 -0
- package/docusaurus/docs/http/requests.md +328 -0
- package/docusaurus/docs/http/retry.md +402 -0
- package/docusaurus/docs/intro.md +90 -0
- package/docusaurus/docs/openapi/loading-specs.md +305 -0
- package/docusaurus/docs/openapi/operation-calls.md +314 -0
- package/docusaurus/docs/openapi/overview.md +212 -0
- package/docusaurus/docs/openapi/response-validation.md +344 -0
- package/docusaurus/docs/scheduling/cron.md +305 -0
- package/docusaurus/docs/scheduling/daemon-mode.md +317 -0
- package/docusaurus/docs/scheduling/intervals.md +289 -0
- package/docusaurus/docs/scheduling/overview.md +231 -0
- package/docusaurus/docs/stores/custom-adapters.md +376 -0
- package/docusaurus/docs/stores/file.md +236 -0
- package/docusaurus/docs/stores/memory.md +193 -0
- package/docusaurus/docs/stores/overview.md +274 -0
- package/docusaurus/docs/stores/postgrest.md +316 -0
- package/docusaurus/docusaurus.config.ts +148 -0
- package/docusaurus/package-lock.json +18029 -0
- package/docusaurus/package.json +47 -0
- package/docusaurus/sidebars.ts +155 -0
- package/docusaurus/src/components/HomepageFeatures/index.tsx +105 -0
- package/docusaurus/src/components/HomepageFeatures/styles.module.css +12 -0
- package/docusaurus/src/css/custom.css +169 -0
- package/docusaurus/src/pages/index.module.css +48 -0
- package/docusaurus/src/pages/index.tsx +110 -0
- package/docusaurus/src/pages/markdown-page.md +7 -0
- package/docusaurus/static/.nojekyll +0 -0
- package/docusaurus/static/img/docusaurus-social-card.jpg +0 -0
- package/docusaurus/static/img/docusaurus.png +0 -0
- package/docusaurus/static/img/favicon.ico +0 -0
- package/docusaurus/static/img/logo.svg +10 -0
- package/docusaurus/static/img/undraw_docusaurus_mountain.svg +171 -0
- package/docusaurus/static/img/undraw_docusaurus_react.svg +170 -0
- package/docusaurus/static/img/undraw_docusaurus_tree.svg +40 -0
- package/docusaurus/tsconfig.json +8 -0
- package/examples/README.md +112 -0
- package/examples/error-handling/README.md +150 -0
- package/examples/error-handling/payment-processor.vague +287 -0
- package/examples/github-sync/README.md +74 -0
- package/examples/github-sync/fetch-issues.vague +47 -0
- package/examples/github-sync/fetch-prs.vague +40 -0
- package/examples/github-sync/mission.vague +101 -0
- package/examples/github-sync/normalize.vague +70 -0
- package/examples/jsonplaceholder/README.md +28 -0
- package/examples/jsonplaceholder/posts.vague +48 -0
- package/examples/petstore/README.md +35 -0
- package/examples/petstore/openapi.yaml +97 -0
- package/examples/petstore/sync.vague +52 -0
- package/examples/temporal-comparison/README.md +297 -0
- package/examples/temporal-comparison/reconciliation.vague +355 -0
- package/examples/temporal-comparison/temporal/activities/index.ts +8 -0
- package/examples/temporal-comparison/temporal/activities/shipstation.ts +225 -0
- package/examples/temporal-comparison/temporal/activities/shopify.ts +257 -0
- package/examples/temporal-comparison/temporal/activities/storage.ts +198 -0
- package/examples/temporal-comparison/temporal/activities/stripe.ts +169 -0
- package/examples/temporal-comparison/temporal/activities/validation.ts +205 -0
- package/examples/temporal-comparison/temporal/client/schedule.ts +218 -0
- package/examples/temporal-comparison/temporal/config/retry.ts +63 -0
- package/examples/temporal-comparison/temporal/types/index.ts +129 -0
- package/examples/temporal-comparison/temporal/workers/main.ts +130 -0
- package/examples/temporal-comparison/temporal/workflows/orderReconciliation.ts +262 -0
- package/examples/xero/README.md +88 -0
- package/examples/xero/invoices.vague +189 -0
- package/package.json +40 -0
- package/src/api-integration.test.ts +954 -0
- package/src/ast/index.ts +1 -0
- package/src/ast/nodes.ts +310 -0
- package/src/auth/auth.test.ts +326 -0
- package/src/auth/circuit-breaker.test.ts +390 -0
- package/src/auth/circuit-breaker.ts +379 -0
- package/src/auth/credentials.test.ts +273 -0
- package/src/auth/credentials.ts +246 -0
- package/src/auth/index.ts +40 -0
- package/src/auth/oauth2-provider.ts +177 -0
- package/src/auth/rate-limiter.ts +459 -0
- package/src/auth/token-store.ts +177 -0
- package/src/auth/types.ts +159 -0
- package/src/benchmark/e2e.bench.ts +288 -0
- package/src/benchmark/evaluator.bench.ts +331 -0
- package/src/benchmark/fixtures.ts +295 -0
- package/src/benchmark/index.ts +108 -0
- package/src/benchmark/lexer.bench.ts +69 -0
- package/src/benchmark/parser.bench.ts +103 -0
- package/src/benchmark/resilience.bench.ts +193 -0
- package/src/benchmark/store.bench.ts +147 -0
- package/src/benchmark/utils.ts +230 -0
- package/src/cli.ts +313 -0
- package/src/errors/errors.test.ts +234 -0
- package/src/errors/index.ts +223 -0
- package/src/execution/execution.test.ts +307 -0
- package/src/execution/index.ts +21 -0
- package/src/execution/state.ts +207 -0
- package/src/execution/store.ts +188 -0
- package/src/index.ts +169 -0
- package/src/integration.test.ts +192 -0
- package/src/interpreter/context.ts +57 -0
- package/src/interpreter/evaluator.test.ts +796 -0
- package/src/interpreter/evaluator.ts +245 -0
- package/src/interpreter/executor.ts +946 -0
- package/src/interpreter/fetch-handler.ts +302 -0
- package/src/interpreter/http.test.ts +423 -0
- package/src/interpreter/http.ts +308 -0
- package/src/interpreter/index.ts +32 -0
- package/src/interpreter/pagination.ts +207 -0
- package/src/interpreter/progress.test.ts +276 -0
- package/src/interpreter/schema-matcher.test.ts +160 -0
- package/src/interpreter/schema-matcher.ts +168 -0
- package/src/interpreter/signals.ts +73 -0
- package/src/interpreter/step-handlers/for-handler.ts +65 -0
- package/src/interpreter/step-handlers/index.ts +17 -0
- package/src/interpreter/step-handlers/map-handler.ts +24 -0
- package/src/interpreter/step-handlers/match-handler.ts +101 -0
- package/src/interpreter/step-handlers/store-handler.ts +78 -0
- package/src/interpreter/step-handlers/types.ts +17 -0
- package/src/interpreter/step-handlers/validate-handler.ts +30 -0
- package/src/interpreter/step-handlers/webhook-handler.ts +142 -0
- package/src/lexer/index.ts +18 -0
- package/src/lexer/lexer.test.ts +316 -0
- package/src/lexer/tokens.ts +179 -0
- package/src/loader/index.ts +288 -0
- package/src/loader/loader.test.ts +360 -0
- package/src/oas/index.ts +4 -0
- package/src/oas/loader.ts +126 -0
- package/src/oas/oas.test.ts +254 -0
- package/src/oas/validator.ts +299 -0
- package/src/parser/base.ts +124 -0
- package/src/parser/expressions.test.ts +525 -0
- package/src/parser/expressions.ts +314 -0
- package/src/parser/index.ts +3 -0
- package/src/parser/match.test.ts +296 -0
- package/src/parser/parser.test.ts +739 -0
- package/src/parser/parser.ts +1469 -0
- package/src/parser/schedule.test.ts +287 -0
- package/src/parser/webhook.test.ts +248 -0
- package/src/plugin.ts +83 -0
- package/src/scheduler/cron-parser.test.ts +236 -0
- package/src/scheduler/cron-parser.ts +236 -0
- package/src/scheduler/index.ts +10 -0
- package/src/scheduler/scheduler.ts +443 -0
- package/src/scheduler/types.ts +71 -0
- package/src/stores/factory.ts +104 -0
- package/src/stores/file.test.ts +276 -0
- package/src/stores/file.ts +211 -0
- package/src/stores/index.ts +6 -0
- package/src/stores/memory.test.ts +238 -0
- package/src/stores/memory.ts +63 -0
- package/src/stores/postgrest.test.ts +488 -0
- package/src/stores/postgrest.ts +263 -0
- package/src/stores/stores.test.ts +197 -0
- package/src/stores/types.ts +58 -0
- package/src/sync/index.ts +16 -0
- package/src/sync/state.ts +126 -0
- package/src/sync/store.ts +139 -0
- package/src/sync/sync.test.ts +271 -0
- package/src/utils/async.ts +10 -0
- package/src/utils/file.ts +106 -0
- package/src/utils/index.ts +14 -0
- package/src/utils/logger.ts +53 -0
- package/src/utils/path.ts +47 -0
- package/src/webhook/index.ts +15 -0
- package/src/webhook/server.test.ts +253 -0
- package/src/webhook/server.ts +389 -0
- package/src/webhook/store.ts +239 -0
- package/src/webhook/types.ts +93 -0
- package/tsconfig.json +17 -0
- 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
|
+
}
|