reqon-dsl 0.3.0 → 0.4.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 (122) hide show
  1. package/README.md +23 -3
  2. package/dist/ast/nodes.d.ts +8 -0
  3. package/dist/auth/circuit-breaker.d.ts +11 -0
  4. package/dist/auth/circuit-breaker.js +83 -12
  5. package/dist/auth/credentials.d.ts +6 -1
  6. package/dist/auth/credentials.js +12 -4
  7. package/dist/auth/oauth2-provider.js +13 -3
  8. package/dist/auth/rate-limiter.d.ts +8 -1
  9. package/dist/auth/rate-limiter.js +30 -10
  10. package/dist/auth/token-store.js +8 -1
  11. package/dist/cli.d.ts +11 -1
  12. package/dist/cli.js +65 -6
  13. package/dist/config/constants.d.ts +15 -4
  14. package/dist/config/constants.js +15 -4
  15. package/dist/control/server.d.ts +17 -0
  16. package/dist/control/server.js +82 -5
  17. package/dist/control/types.d.ts +6 -0
  18. package/dist/debug/cli-debugger.js +8 -3
  19. package/dist/execution/store.js +2 -2
  20. package/dist/execution-log/events.d.ts +125 -0
  21. package/dist/execution-log/events.js +17 -0
  22. package/dist/execution-log/fold.d.ts +38 -0
  23. package/dist/execution-log/fold.js +54 -0
  24. package/dist/execution-log/index.d.ts +18 -0
  25. package/dist/execution-log/index.js +6 -0
  26. package/dist/execution-log/postgres-store.d.ts +36 -0
  27. package/dist/execution-log/postgres-store.js +108 -0
  28. package/dist/execution-log/resume.d.ts +11 -0
  29. package/dist/execution-log/resume.js +5 -0
  30. package/dist/execution-log/sqlite-store.d.ts +16 -0
  31. package/dist/execution-log/sqlite-store.js +101 -0
  32. package/dist/execution-log/store.d.ts +72 -0
  33. package/dist/execution-log/store.js +182 -0
  34. package/dist/index.d.ts +4 -3
  35. package/dist/index.js +4 -3
  36. package/dist/interpreter/context.d.ts +15 -0
  37. package/dist/interpreter/context.js +3 -0
  38. package/dist/interpreter/evaluator.js +38 -8
  39. package/dist/interpreter/executor.d.ts +63 -1
  40. package/dist/interpreter/executor.js +406 -30
  41. package/dist/interpreter/fetch-handler.d.ts +39 -1
  42. package/dist/interpreter/fetch-handler.js +84 -15
  43. package/dist/interpreter/http.d.ts +31 -2
  44. package/dist/interpreter/http.js +187 -26
  45. package/dist/interpreter/index.d.ts +3 -3
  46. package/dist/interpreter/index.js +3 -3
  47. package/dist/interpreter/pagination.d.ts +1 -1
  48. package/dist/interpreter/pagination.js +7 -1
  49. package/dist/interpreter/step-handlers/for-handler.d.ts +3 -0
  50. package/dist/interpreter/step-handlers/for-handler.js +18 -3
  51. package/dist/interpreter/step-handlers/match-handler.js +5 -2
  52. package/dist/interpreter/step-handlers/store-handler.d.ts +7 -1
  53. package/dist/interpreter/step-handlers/store-handler.js +25 -16
  54. package/dist/interpreter/step-handlers/validate-handler.js +4 -1
  55. package/dist/interpreter/step-handlers/webhook-handler.d.ts +1 -0
  56. package/dist/interpreter/step-handlers/webhook-handler.js +13 -3
  57. package/dist/interpreter/store-manager.d.ts +1 -1
  58. package/dist/interpreter/store-manager.js +5 -1
  59. package/dist/loader/index.js +5 -8
  60. package/dist/mcp/sandbox.d.ts +41 -0
  61. package/dist/mcp/sandbox.js +76 -0
  62. package/dist/mcp/server.js +62 -9
  63. package/dist/oas/loader.d.ts +13 -1
  64. package/dist/oas/loader.js +25 -3
  65. package/dist/oas/mock-generator.js +13 -4
  66. package/dist/oas/validator.js +45 -5
  67. package/dist/observability/events.d.ts +6 -2
  68. package/dist/observability/events.js +0 -5
  69. package/dist/observability/logger.js +17 -10
  70. package/dist/observability/otel.d.ts +8 -0
  71. package/dist/observability/otel.js +45 -10
  72. package/dist/parser/action-parser.js +2 -2
  73. package/dist/parser/base.d.ts +7 -0
  74. package/dist/parser/base.js +11 -0
  75. package/dist/parser/expressions.d.ts +1 -0
  76. package/dist/parser/expressions.js +17 -4
  77. package/dist/parser/fetch-parser.js +13 -2
  78. package/dist/pause/index.d.ts +1 -0
  79. package/dist/pause/index.js +1 -0
  80. package/dist/pause/log-store.d.ts +33 -0
  81. package/dist/pause/log-store.js +98 -0
  82. package/dist/pause/manager.d.ts +12 -0
  83. package/dist/pause/manager.js +77 -28
  84. package/dist/pause/store.js +5 -3
  85. package/dist/scheduler/cron-parser.d.ts +10 -3
  86. package/dist/scheduler/cron-parser.js +227 -48
  87. package/dist/scheduler/scheduler.js +56 -22
  88. package/dist/stores/factory.d.ts +6 -0
  89. package/dist/stores/factory.js +11 -1
  90. package/dist/stores/file.js +9 -17
  91. package/dist/stores/memory.js +3 -12
  92. package/dist/stores/postgrest.d.ts +28 -0
  93. package/dist/stores/postgrest.js +84 -37
  94. package/dist/sync/index.d.ts +3 -2
  95. package/dist/sync/index.js +2 -1
  96. package/dist/sync/log-store.d.ts +30 -0
  97. package/dist/sync/log-store.js +45 -0
  98. package/dist/sync/store.js +1 -1
  99. package/dist/trace/index.d.ts +2 -0
  100. package/dist/trace/index.js +1 -0
  101. package/dist/trace/log-view.d.ts +57 -0
  102. package/dist/trace/log-view.js +76 -0
  103. package/dist/trace/recorder.d.ts +5 -1
  104. package/dist/trace/recorder.js +19 -6
  105. package/dist/trace/store.d.ts +6 -0
  106. package/dist/trace/store.js +47 -22
  107. package/dist/utils/deep-merge.d.ts +10 -0
  108. package/dist/utils/deep-merge.js +23 -0
  109. package/dist/utils/file.d.ts +13 -4
  110. package/dist/utils/file.js +70 -12
  111. package/dist/utils/index.d.ts +1 -1
  112. package/dist/utils/index.js +1 -1
  113. package/dist/utils/long-timeout.d.ts +19 -0
  114. package/dist/utils/long-timeout.js +33 -0
  115. package/dist/utils/path.d.ts +22 -1
  116. package/dist/utils/path.js +46 -1
  117. package/dist/utils/redact.d.ts +22 -0
  118. package/dist/utils/redact.js +42 -0
  119. package/dist/webhook/server.d.ts +9 -0
  120. package/dist/webhook/server.js +115 -30
  121. package/dist/webhook/types.d.ts +9 -1
  122. package/package.json +22 -4
@@ -7,6 +7,7 @@
7
7
  import { createServer } from 'node:http';
8
8
  import { parse as parseUrl } from 'node:url';
9
9
  import { randomUUID } from 'node:crypto';
10
+ import { setLongTimeout } from '../utils/long-timeout.js';
10
11
  import { MemoryWebhookStore } from './store.js';
11
12
  import { WEBHOOK_DEFAULTS } from '../config/index.js';
12
13
  /**
@@ -19,6 +20,8 @@ export class WebhookServer {
19
20
  store;
20
21
  callbacks;
21
22
  server;
23
+ // Multiple concurrent waiters may await the same registration; each gets its
24
+ // own entry so a second waiter can't clobber the first's timer/promise.
22
25
  pendingWaits = new Map();
23
26
  cleanupInterval;
24
27
  running = false;
@@ -29,6 +32,8 @@ export class WebhookServer {
29
32
  baseUrl: config.baseUrl ?? `http://localhost:${config.port ?? WEBHOOK_DEFAULTS.PORT}`,
30
33
  defaultTimeout: config.defaultTimeout ?? WEBHOOK_DEFAULTS.DEFAULT_TIMEOUT_MS,
31
34
  verbose: config.verbose ?? false,
35
+ secret: config.secret ?? '',
36
+ maxBodyBytes: config.maxBodyBytes ?? WEBHOOK_DEFAULTS.MAX_BODY_BYTES,
32
37
  };
33
38
  this.store = store ?? new MemoryWebhookStore();
34
39
  this.callbacks = callbacks;
@@ -39,8 +44,14 @@ export class WebhookServer {
39
44
  async start() {
40
45
  if (this.running)
41
46
  return;
47
+ // Warn loudly if exposing an unauthenticated webhook server off-host.
48
+ if (!this.isLoopback(this.config.host) && !this.config.secret) {
49
+ console.warn(`[Webhook] WARNING: binding to ${this.config.host} with no secret — ` +
50
+ `anyone who can reach the port can inject events.`);
51
+ }
42
52
  return new Promise((resolve, reject) => {
43
53
  this.server = createServer((req, res) => this.handleRequest(req, res));
54
+ this.server.setTimeout(WEBHOOK_DEFAULTS.SOCKET_TIMEOUT_MS);
44
55
  this.server.on('error', (error) => {
45
56
  reject(error);
46
57
  });
@@ -65,13 +76,15 @@ export class WebhookServer {
65
76
  this.cleanupInterval = undefined;
66
77
  }
67
78
  // Cancel all pending waits
68
- for (const [id, pending] of this.pendingWaits) {
69
- clearTimeout(pending.timeoutId);
70
- pending.resolve({
71
- success: false,
72
- events: [],
73
- error: 'Server shutting down',
74
- });
79
+ for (const [, waiters] of this.pendingWaits) {
80
+ for (const pending of waiters) {
81
+ pending.timer.clear();
82
+ pending.resolve({
83
+ success: false,
84
+ events: [],
85
+ error: 'Server shutting down',
86
+ });
87
+ }
75
88
  }
76
89
  this.pendingWaits.clear();
77
90
  // Close server
@@ -134,10 +147,11 @@ export class WebhookServer {
134
147
  return { success: true, events };
135
148
  }
136
149
  // Wait for more events
137
- const waitTimeout = timeout ?? (registration.expiresAt.getTime() - Date.now());
150
+ const waitTimeout = timeout ?? registration.expiresAt.getTime() - Date.now();
138
151
  return new Promise((resolve) => {
139
- const timeoutId = setTimeout(() => {
140
- this.pendingWaits.delete(registrationId);
152
+ const pending = { registrationId, resolve, timer: undefined };
153
+ pending.timer = setLongTimeout(() => {
154
+ this.removePendingWait(registrationId, pending);
141
155
  this.store.getEvents(registrationId).then((events) => {
142
156
  resolve({
143
157
  success: events.length >= registration.expectedEvents,
@@ -146,23 +160,36 @@ export class WebhookServer {
146
160
  });
147
161
  });
148
162
  }, waitTimeout);
149
- this.pendingWaits.set(registrationId, {
150
- registrationId,
151
- resolve,
152
- timeoutId,
153
- });
163
+ let waiters = this.pendingWaits.get(registrationId);
164
+ if (!waiters) {
165
+ waiters = new Set();
166
+ this.pendingWaits.set(registrationId, waiters);
167
+ }
168
+ waiters.add(pending);
154
169
  });
155
170
  }
171
+ /** Remove a single waiter, dropping the registration's set when empty. */
172
+ removePendingWait(registrationId, pending) {
173
+ const waiters = this.pendingWaits.get(registrationId);
174
+ if (!waiters)
175
+ return;
176
+ waiters.delete(pending);
177
+ if (waiters.size === 0) {
178
+ this.pendingWaits.delete(registrationId);
179
+ }
180
+ }
156
181
  /**
157
182
  * Unregister a webhook endpoint
158
183
  */
159
184
  async unregister(registrationId) {
160
185
  await this.store.deleteRegistration(registrationId);
161
186
  await this.store.deleteEvents(registrationId);
162
- // Cancel pending wait if any
163
- const pending = this.pendingWaits.get(registrationId);
164
- if (pending) {
165
- clearTimeout(pending.timeoutId);
187
+ // Cancel any pending waits for this registration.
188
+ const waiters = this.pendingWaits.get(registrationId);
189
+ if (waiters) {
190
+ for (const pending of waiters) {
191
+ pending.timer.clear();
192
+ }
166
193
  this.pendingWaits.delete(registrationId);
167
194
  }
168
195
  this.log(`Unregistered webhook: ${registrationId}`);
@@ -211,11 +238,31 @@ export class WebhookServer {
211
238
  await this.store.deleteRegistration(registration.id);
212
239
  return;
213
240
  }
214
- // Parse request body
241
+ // Require the shared secret if one is configured.
242
+ if (this.config.secret && !this.authorized(req, url.query)) {
243
+ res.writeHead(401, { 'Content-Type': 'application/json' });
244
+ res.end(JSON.stringify({ error: 'Unauthorized' }));
245
+ return;
246
+ }
247
+ // Read the request body with a hard size cap to prevent an OOM from a
248
+ // large or slow-drip POST.
215
249
  let rawBody = '';
216
- let body = null;
217
250
  try {
218
251
  rawBody = await this.readBody(req);
252
+ }
253
+ catch (error) {
254
+ if (error.code === 'BODY_TOO_LARGE') {
255
+ res.writeHead(413, { 'Content-Type': 'application/json' });
256
+ res.end(JSON.stringify({ error: 'Request body too large' }));
257
+ return;
258
+ }
259
+ res.writeHead(400, { 'Content-Type': 'application/json' });
260
+ res.end(JSON.stringify({ error: 'Failed to read request body' }));
261
+ return;
262
+ }
263
+ // Parse request body
264
+ let body = null;
265
+ try {
219
266
  if (rawBody) {
220
267
  const contentType = req.headers['content-type'] ?? '';
221
268
  if (contentType.includes('application/json')) {
@@ -229,7 +276,7 @@ export class WebhookServer {
229
276
  }
230
277
  }
231
278
  }
232
- catch (error) {
279
+ catch {
233
280
  body = rawBody;
234
281
  }
235
282
  // Create event
@@ -253,12 +300,14 @@ export class WebhookServer {
253
300
  if (registration.receivedEvents >= registration.expectedEvents) {
254
301
  const events = await this.store.getEvents(registration.id);
255
302
  this.callbacks.onRegistrationComplete?.(registration, events);
256
- // Resolve pending wait
257
- const pending = this.pendingWaits.get(registration.id);
258
- if (pending) {
259
- clearTimeout(pending.timeoutId);
303
+ // Resolve every pending waiter for this registration.
304
+ const waiters = this.pendingWaits.get(registration.id);
305
+ if (waiters) {
260
306
  this.pendingWaits.delete(registration.id);
261
- pending.resolve({ success: true, events });
307
+ for (const pending of waiters) {
308
+ pending.timer.clear();
309
+ pending.resolve({ success: true, events });
310
+ }
262
311
  }
263
312
  }
264
313
  // Send response
@@ -274,17 +323,53 @@ export class WebhookServer {
274
323
  * Read request body
275
324
  */
276
325
  readBody(req) {
326
+ const limit = this.config.maxBodyBytes;
277
327
  return new Promise((resolve, reject) => {
278
- let data = '';
328
+ const chunks = [];
329
+ let size = 0;
330
+ let aborted = false;
279
331
  req.on('data', (chunk) => {
280
- data += chunk;
332
+ if (aborted)
333
+ return;
334
+ size += chunk.length;
335
+ if (size > limit) {
336
+ // Stop accumulating (memory stays bounded) and reject; the handler
337
+ // responds 413. We don't destroy the socket here so the response
338
+ // can flush first.
339
+ aborted = true;
340
+ const err = new Error('Request body too large');
341
+ err.code = 'BODY_TOO_LARGE';
342
+ reject(err);
343
+ return;
344
+ }
345
+ chunks.push(chunk);
281
346
  });
282
347
  req.on('end', () => {
283
- resolve(data);
348
+ resolve(Buffer.concat(chunks).toString('utf-8'));
284
349
  });
285
350
  req.on('error', reject);
286
351
  });
287
352
  }
353
+ /** True if a host string is a loopback address. */
354
+ isLoopback(host) {
355
+ return host === 'localhost' || host === '127.0.0.1' || host === '::1';
356
+ }
357
+ /**
358
+ * Validate the shared secret from Authorization bearer, X-Webhook-Token
359
+ * header, or a `token` query param.
360
+ */
361
+ authorized(req, query) {
362
+ const secret = this.config.secret;
363
+ const auth = req.headers.authorization;
364
+ if (auth === `Bearer ${secret}`)
365
+ return true;
366
+ const tokenHeader = req.headers['x-webhook-token'];
367
+ if (tokenHeader === secret)
368
+ return true;
369
+ if (typeof query.token === 'string' && query.token === secret)
370
+ return true;
371
+ return false;
372
+ }
288
373
  /**
289
374
  * Extract headers from request
290
375
  */
@@ -51,7 +51,7 @@ export interface WebhookEvent {
51
51
  export interface WebhookServerConfig {
52
52
  /** Port to listen on (default: 3000) */
53
53
  port?: number;
54
- /** Host to bind to (default: '0.0.0.0') */
54
+ /** Host to bind to (default: '127.0.0.1' — loopback only) */
55
55
  host?: string;
56
56
  /** Base URL for webhook endpoints (e.g., 'https://example.com/webhooks') */
57
57
  baseUrl?: string;
@@ -59,6 +59,14 @@ export interface WebhookServerConfig {
59
59
  defaultTimeout?: number;
60
60
  /** Enable verbose logging */
61
61
  verbose?: boolean;
62
+ /**
63
+ * Shared secret required on inbound webhook requests. When set, a request
64
+ * must present it via `Authorization: Bearer <secret>`, `X-Webhook-Token`,
65
+ * or a `token` query param, or it is rejected with 401.
66
+ */
67
+ secret?: string;
68
+ /** Maximum accepted request body size in bytes (default 1 MiB) */
69
+ maxBodyBytes?: number;
62
70
  }
63
71
  /**
64
72
  * Callbacks for webhook server events
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reqon-dsl",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "A DSL framework for fetch, map, validate pipelines - built on Vague",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -10,11 +10,15 @@
10
10
  "reqon-mcp": "dist/mcp/server.js"
11
11
  },
12
12
  "scripts": {
13
- "build": "tsc",
14
- "dev": "tsc --watch",
13
+ "build": "tsc -p tsconfig.build.json",
14
+ "dev": "tsc -p tsconfig.build.json --watch",
15
+ "typecheck": "tsc --noEmit",
15
16
  "test": "vitest",
16
17
  "test:run": "vitest run",
17
18
  "test:coverage": "vitest run --coverage",
19
+ "test:integration": "RUN_NETWORK_TESTS=1 vitest run src/api-integration.test.ts",
20
+ "test:crash": "vitest run src/durability/",
21
+ "test:pg": "vitest run src/execution-log/postgres-store.test.ts",
18
22
  "lint": "eslint src/",
19
23
  "lint:fix": "eslint src/ --fix",
20
24
  "format": "prettier --write src/",
@@ -40,7 +44,7 @@
40
44
  "README.md"
41
45
  ],
42
46
  "engines": {
43
- "node": ">=18"
47
+ "node": ">=20"
44
48
  },
45
49
  "repository": {
46
50
  "type": "git",
@@ -68,6 +72,18 @@
68
72
  "vague-lang": "^3.3.0",
69
73
  "zod": "^4.2.1"
70
74
  },
75
+ "peerDependencies": {
76
+ "better-sqlite3": ">=11",
77
+ "pg": ">=8"
78
+ },
79
+ "peerDependenciesMeta": {
80
+ "better-sqlite3": {
81
+ "optional": true
82
+ },
83
+ "pg": {
84
+ "optional": true
85
+ }
86
+ },
71
87
  "lint-staged": {
72
88
  "src/**/*.{ts,tsx}": [
73
89
  "eslint --fix",
@@ -78,9 +94,11 @@
78
94
  "@eslint/js": "^9.17.0",
79
95
  "@types/node": "^25.0.3",
80
96
  "@vitest/coverage-v8": "^4.0.16",
97
+ "better-sqlite3": "^12.11.1",
81
98
  "eslint": "^9.17.0",
82
99
  "husky": "^9.1.7",
83
100
  "lint-staged": "^16.2.7",
101
+ "pg": "^8.22.0",
84
102
  "prettier": "^3.4.2",
85
103
  "typescript": "^5.9.3",
86
104
  "typescript-eslint": "^8.18.2",