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
@@ -13,6 +13,13 @@ export function generateMockData(schema, options = {}) {
13
13
  return generateValue(schema, ctx);
14
14
  }
15
15
  function generateValue(schema, ctx) {
16
+ // Depth guard: after dereferencing, a self-referential schema becomes a
17
+ // circular object reference (not a $ref), so the only thing that stops
18
+ // infinite recursion is the depth cap. Enforce it here for every path,
19
+ // including allOf/oneOf/anyOf below.
20
+ if (ctx.depth > ctx.maxDepth) {
21
+ return null;
22
+ }
16
23
  // Handle nullable - only return null if explicitly marked nullable
17
24
  // and we're past the top level (depth > 0). Use strict equality check
18
25
  // to avoid false positives from undefined/truthy coercion.
@@ -34,12 +41,14 @@ function generateValue(schema, ctx) {
34
41
  if (schema.default !== undefined) {
35
42
  return schema.default;
36
43
  }
37
- // Check for allOf/oneOf/anyOf
44
+ // Check for allOf/oneOf/anyOf. Each recursion must increment depth so the
45
+ // depth cap can terminate self-referential combinator schemas.
46
+ const deeper = { ...ctx, depth: ctx.depth + 1 };
38
47
  if (schema.allOf && schema.allOf.length > 0) {
39
48
  // Merge all schemas
40
49
  const merged = {};
41
50
  for (const subSchema of schema.allOf) {
42
- const subValue = generateValue(subSchema, ctx);
51
+ const subValue = generateValue(subSchema, deeper);
43
52
  if (typeof subValue === 'object' && subValue !== null) {
44
53
  Object.assign(merged, subValue);
45
54
  }
@@ -47,10 +56,10 @@ function generateValue(schema, ctx) {
47
56
  return merged;
48
57
  }
49
58
  if (schema.oneOf && schema.oneOf.length > 0) {
50
- return generateValue(schema.oneOf[0], ctx);
59
+ return generateValue(schema.oneOf[0], deeper);
51
60
  }
52
61
  if (schema.anyOf && schema.anyOf.length > 0) {
53
- return generateValue(schema.anyOf[0], ctx);
62
+ return generateValue(schema.anyOf[0], deeper);
54
63
  }
55
64
  // Generate based on type
56
65
  switch (schema.type) {
@@ -1,3 +1,32 @@
1
+ /**
2
+ * Upper bound on the length of a string we will test against a `pattern`
3
+ * regex. Both the schema pattern and the response value are untrusted, so a
4
+ * pathological pattern (e.g. `(a+)+$`) over a long string can trigger
5
+ * catastrophic backtracking. Anything longer is rejected without running the
6
+ * regex, so validation always returns promptly.
7
+ */
8
+ const MAX_PATTERN_INPUT_LENGTH = 10_000;
9
+ /**
10
+ * Cache of compiled pattern regexes. Avoids recompiling the same (untrusted)
11
+ * pattern per value, and stores `null` for patterns that fail to compile so we
12
+ * skip them consistently.
13
+ */
14
+ const patternRegexCache = new Map();
15
+ function getPatternRegex(pattern) {
16
+ const cached = patternRegexCache.get(pattern);
17
+ if (cached !== undefined)
18
+ return cached;
19
+ let regex;
20
+ try {
21
+ regex = new RegExp(pattern);
22
+ }
23
+ catch {
24
+ // Invalid regex: don't crash validation, just skip the pattern check.
25
+ regex = null;
26
+ }
27
+ patternRegexCache.set(pattern, regex);
28
+ return regex;
29
+ }
1
30
  export function validateResponse(data, schema, path = '') {
2
31
  const errors = [];
3
32
  validateValue(data, schema, path, errors);
@@ -79,15 +108,26 @@ function validateString(value, schema, path, errors) {
79
108
  });
80
109
  }
81
110
  if (schema.pattern) {
82
- const regex = new RegExp(schema.pattern);
83
- if (!regex.test(value)) {
111
+ if (value.length > MAX_PATTERN_INPUT_LENGTH) {
112
+ // Refuse to run an untrusted regex over an oversized string (ReDoS guard).
84
113
  errors.push({
85
114
  path,
86
- message: `String does not match pattern`,
87
- expected: schema.pattern,
88
- actual: value,
115
+ message: `String too long to validate against pattern`,
116
+ expected: `<= ${MAX_PATTERN_INPUT_LENGTH} chars`,
117
+ actual: `${value.length} chars`,
89
118
  });
90
119
  }
120
+ else {
121
+ const regex = getPatternRegex(schema.pattern);
122
+ if (regex && !regex.test(value)) {
123
+ errors.push({
124
+ path,
125
+ message: `String does not match pattern`,
126
+ expected: schema.pattern,
127
+ actual: value,
128
+ });
129
+ }
130
+ }
91
131
  }
92
132
  }
93
133
  function validateNumber(value, schema, path, errors) {
@@ -16,7 +16,12 @@ export interface ObservabilityEvent<T = unknown> {
16
16
  mission: string;
17
17
  /** ISO timestamp */
18
18
  timestamp: string;
19
- /** Duration in milliseconds (for completed events) */
19
+ /**
20
+ * Duration in milliseconds of the operation this event reports, when the
21
+ * payload carries one. Not set by the emitter itself: a generic "time since
22
+ * the previous event of any type" value was misleading, so operation timing
23
+ * now lives in the typed payloads instead.
24
+ */
20
25
  duration?: number;
21
26
  /** Event-specific payload */
22
27
  payload: T;
@@ -227,7 +232,6 @@ export declare class ObservabilityEmitter implements EventEmitter {
227
232
  private handlers;
228
233
  private allHandlers;
229
234
  private startTime;
230
- private lastEventTime;
231
235
  constructor(executionId: string, mission: string);
232
236
  emit<T>(type: EventType, payload: T): void;
233
237
  on<T>(type: EventType, handler: EventHandler<T>): () => void;
@@ -13,24 +13,19 @@ export class ObservabilityEmitter {
13
13
  handlers = new Map();
14
14
  allHandlers = new Set();
15
15
  startTime;
16
- lastEventTime;
17
16
  constructor(executionId, mission) {
18
17
  this.executionId = executionId;
19
18
  this.mission = mission;
20
19
  this.startTime = Date.now();
21
- this.lastEventTime = this.startTime;
22
20
  }
23
21
  emit(type, payload) {
24
- const now = Date.now();
25
22
  const event = {
26
23
  type,
27
24
  executionId: this.executionId,
28
25
  mission: this.mission,
29
26
  timestamp: new Date().toISOString(),
30
- duration: now - this.lastEventTime,
31
27
  payload,
32
28
  };
33
- this.lastEventTime = now;
34
29
  // Notify specific handlers
35
30
  const typeHandlers = this.handlers.get(type);
36
31
  if (typeHandlers) {
@@ -7,6 +7,8 @@
7
7
  * - Multiple output formats
8
8
  * - Integration with event emitter
9
9
  */
10
+ import { randomBytes } from 'node:crypto';
11
+ import { redactSecrets } from '../utils/redact.js';
10
12
  // ============================================================================
11
13
  // Span Implementation
12
14
  // ============================================================================
@@ -41,7 +43,13 @@ class SpanImpl {
41
43
  }
42
44
  }
43
45
  function generateSpanId() {
44
- return Math.random().toString(36).substring(2, 10);
46
+ // CSPRNG-backed, 16 hex chars. Never all-zero (the OTel "invalid" sentinel).
47
+ for (let attempt = 0; attempt < 8; attempt++) {
48
+ const hex = randomBytes(8).toString('hex');
49
+ if (!/^0+$/.test(hex))
50
+ return hex;
51
+ }
52
+ return '1'.padEnd(16, '0');
45
53
  }
46
54
  // ============================================================================
47
55
  // Logger Implementation
@@ -73,7 +81,7 @@ class StructuredLoggerImpl {
73
81
  level,
74
82
  message,
75
83
  timestamp: new Date().toISOString(),
76
- context: { ...this.context, ...context },
84
+ context: redactSecrets({ ...this.context, ...context }),
77
85
  };
78
86
  for (const output of this.outputs) {
79
87
  try {
@@ -116,7 +124,7 @@ class StructuredLoggerImpl {
116
124
  level: 'debug',
117
125
  message: `span:end`,
118
126
  timestamp: new Date().toISOString(),
119
- context: { ...this.context, spanName: name, ...spanContext },
127
+ context: redactSecrets({ ...this.context, spanName: name, ...spanContext }),
120
128
  spanId,
121
129
  parentSpanId,
122
130
  duration,
@@ -154,13 +162,9 @@ export class ConsoleOutput {
154
162
  const prefix = `[${this.prefix}]`;
155
163
  const levelStr = entry.level.toUpperCase().padEnd(5);
156
164
  // Format context as key=value pairs
157
- const contextStr = Object.keys(entry.context).length > 0
158
- ? ` ${formatContext(entry.context)}`
159
- : '';
165
+ const contextStr = Object.keys(entry.context).length > 0 ? ` ${formatContext(entry.context)}` : '';
160
166
  // Format duration if present
161
- const durationStr = entry.duration !== undefined
162
- ? ` (${entry.duration}ms)`
163
- : '';
167
+ const durationStr = entry.duration !== undefined ? ` (${entry.duration}ms)` : '';
164
168
  const message = `${prefix} ${levelStr} ${entry.message}${contextStr}${durationStr}`;
165
169
  switch (entry.level) {
166
170
  case 'debug':
@@ -218,7 +222,10 @@ export class EventOutput {
218
222
  // Map log entries to appropriate event types based on context
219
223
  // This allows logs to flow into the event system
220
224
  if (entry.context.eventType) {
221
- this.emitter.emit(entry.context.eventType, { ...entry.context, message: entry.message });
225
+ this.emitter.emit(entry.context.eventType, {
226
+ ...entry.context,
227
+ message: entry.message,
228
+ });
222
229
  }
223
230
  }
224
231
  }
@@ -79,6 +79,13 @@ export declare class OTelEventAdapter {
79
79
  private spanBuilder;
80
80
  private spanStack;
81
81
  private eventToSpan;
82
+ /**
83
+ * Pending fetch spans keyed by source+path. Fetch start/complete events
84
+ * don't carry a correlation id, so we queue the open span ids per endpoint
85
+ * (FIFO) instead of a single shared `fetch:current` slot. This keeps
86
+ * concurrent/paginated fetches from overwriting and leaking each other.
87
+ */
88
+ private pendingFetchSpans;
82
89
  constructor(traceId?: string);
83
90
  /**
84
91
  * Process an observability event and update spans
@@ -90,6 +97,7 @@ export declare class OTelEventAdapter {
90
97
  private endStageSpan;
91
98
  private startStepSpan;
92
99
  private endStepSpan;
100
+ private fetchKey;
93
101
  private startFetchSpan;
94
102
  private endFetchSpan;
95
103
  private addEventToCurrentSpan;
@@ -7,6 +7,7 @@
7
7
  * This is a lightweight implementation that doesn't require the full OTel SDK.
8
8
  * For production use, consider using the official @opentelemetry packages.
9
9
  */
10
+ import { randomBytes } from 'node:crypto';
10
11
  // ============================================================================
11
12
  // Trace Context Management
12
13
  // ============================================================================
@@ -22,12 +23,21 @@ export function generateTraceId() {
22
23
  export function generateSpanId() {
23
24
  return randomHex(16);
24
25
  }
26
+ /**
27
+ * Generate `length` random hex characters using a CSPRNG. OTel trace/span IDs
28
+ * must be unpredictable and must never be all-zero (the all-zero ID is the
29
+ * "invalid" sentinel in the spec), so we regenerate in the vanishingly
30
+ * unlikely event of an all-zero draw.
31
+ */
25
32
  function randomHex(length) {
26
- let result = '';
27
- for (let i = 0; i < length; i++) {
28
- result += Math.floor(Math.random() * 16).toString(16);
33
+ const byteLength = Math.ceil(length / 2);
34
+ for (let attempt = 0; attempt < 8; attempt++) {
35
+ const hex = randomBytes(byteLength).toString('hex').slice(0, length);
36
+ if (!/^0+$/.test(hex))
37
+ return hex;
29
38
  }
30
- return result;
39
+ // Fallback (practically unreachable): force a non-zero leading nibble.
40
+ return '1'.padEnd(length, '0');
31
41
  }
32
42
  // ============================================================================
33
43
  // OpenTelemetry Span Builder
@@ -112,6 +122,13 @@ export class OTelEventAdapter {
112
122
  spanBuilder;
113
123
  spanStack = [];
114
124
  eventToSpan = new Map();
125
+ /**
126
+ * Pending fetch spans keyed by source+path. Fetch start/complete events
127
+ * don't carry a correlation id, so we queue the open span ids per endpoint
128
+ * (FIFO) instead of a single shared `fetch:current` slot. This keeps
129
+ * concurrent/paginated fetches from overwriting and leaking each other.
130
+ */
131
+ pendingFetchSpans = new Map();
115
132
  constructor(traceId) {
116
133
  this.spanBuilder = new SpanBuilder(traceId);
117
134
  }
@@ -119,7 +136,7 @@ export class OTelEventAdapter {
119
136
  * Process an observability event and update spans
120
137
  */
121
138
  processEvent(event) {
122
- const { type, payload } = event;
139
+ const { type } = event;
123
140
  // Map event types to span operations
124
141
  switch (type) {
125
142
  case 'mission.start':
@@ -173,6 +190,7 @@ export class OTelEventAdapter {
173
190
  error: payload.error,
174
191
  });
175
192
  this.spanStack.pop();
193
+ this.eventToSpan.delete('mission');
176
194
  }
177
195
  }
178
196
  startStageSpan(event) {
@@ -198,6 +216,7 @@ export class OTelEventAdapter {
198
216
  error: payload.error,
199
217
  });
200
218
  this.spanStack.pop();
219
+ this.eventToSpan.delete(`stage:${payload.stageIndex}`);
201
220
  }
202
221
  }
203
222
  startStepSpan(event) {
@@ -217,15 +236,20 @@ export class OTelEventAdapter {
217
236
  }
218
237
  endStepSpan(event) {
219
238
  const payload = event.payload;
220
- const spanId = this.eventToSpan.get(`step:${payload.actionName}:${payload.stepIndex}`);
239
+ const key = `step:${payload.actionName}:${payload.stepIndex}`;
240
+ const spanId = this.eventToSpan.get(key);
221
241
  if (spanId) {
222
242
  this.spanBuilder.endSpan(spanId, {
223
243
  status: payload.success ? 'OK' : 'ERROR',
224
244
  error: payload.error,
225
245
  });
226
246
  this.spanStack.pop();
247
+ this.eventToSpan.delete(key);
227
248
  }
228
249
  }
250
+ fetchKey(source, path) {
251
+ return `${source ?? ''}${path ?? ''}`;
252
+ }
229
253
  startFetchSpan(event) {
230
254
  const payload = event.payload;
231
255
  const parentSpanId = this.spanStack[this.spanStack.length - 1];
@@ -238,18 +262,29 @@ export class OTelEventAdapter {
238
262
  'reqon.source': payload.source,
239
263
  },
240
264
  });
241
- this.eventToSpan.set('fetch:current', spanId);
265
+ const key = this.fetchKey(payload.source, payload.path);
266
+ const queue = this.pendingFetchSpans.get(key);
267
+ if (queue) {
268
+ queue.push(spanId);
269
+ }
270
+ else {
271
+ this.pendingFetchSpans.set(key, [spanId]);
272
+ }
242
273
  }
243
274
  endFetchSpan(event) {
244
- const spanId = this.eventToSpan.get('fetch:current');
275
+ const payload = event.payload;
276
+ const key = this.fetchKey(payload.source, payload.path);
277
+ const queue = this.pendingFetchSpans.get(key);
278
+ const spanId = queue?.shift();
279
+ if (queue && queue.length === 0) {
280
+ this.pendingFetchSpans.delete(key);
281
+ }
245
282
  if (spanId) {
246
- const payload = event.payload;
247
283
  this.spanBuilder.endSpan(spanId, {
248
284
  status: payload.error ? 'ERROR' : 'OK',
249
285
  error: payload.error,
250
286
  attributes: payload.statusCode ? { 'http.status_code': payload.statusCode } : undefined,
251
287
  });
252
- this.eventToSpan.delete('fetch:current');
253
288
  }
254
289
  }
255
290
  addEventToCurrentSpan(event) {
@@ -115,7 +115,7 @@ export class ActionParser extends FetchParser {
115
115
  this.consume(TokenType.LBRACE, "Expected '{'");
116
116
  const mappings = [];
117
117
  while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
118
- const field = this.consumeIdentifier('Expected field name').value;
118
+ const field = this.consumeName('Expected field name').value;
119
119
  this.consume(TokenType.COLON, "Expected ':'");
120
120
  const expression = this.parseExpression();
121
121
  mappings.push({ field, expression });
@@ -202,7 +202,7 @@ export class ActionParser extends FetchParser {
202
202
  this.consume(TokenType.LBRACE, "Expected '{'");
203
203
  const mappings = [];
204
204
  while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
205
- const field = this.consumeIdentifier('Expected field name').value;
205
+ const field = this.consumeName('Expected field name').value;
206
206
  this.consume(TokenType.COLON, "Expected ':'");
207
207
  const expression = this.parseExpression();
208
208
  mappings.push({ field, expression });
@@ -21,6 +21,13 @@ export declare class ReqonParserBase {
21
21
  * This is needed because 'get', 'post', etc. are valid variable/store names.
22
22
  */
23
23
  protected consumeIdentifier(message: string): Token;
24
+ /**
25
+ * Consume a token in name/property position (after a dot, a map/transform field
26
+ * name, or the type name after `is`). Any reserved keyword's text is a valid name
27
+ * here because the position is unambiguous, so accept any non-EOF token and return
28
+ * it by value.
29
+ */
30
+ protected consumeName(message: string): Token;
24
31
  /**
25
32
  * Check if current token is an identifier (including HTTP methods as identifiers)
26
33
  */
@@ -66,6 +66,17 @@ export class ReqonParserBase {
66
66
  }
67
67
  throw this.error(message);
68
68
  }
69
+ /**
70
+ * Consume a token in name/property position (after a dot, a map/transform field
71
+ * name, or the type name after `is`). Any reserved keyword's text is a valid name
72
+ * here because the position is unambiguous, so accept any non-EOF token and return
73
+ * it by value.
74
+ */
75
+ consumeName(message) {
76
+ if (this.isAtEnd())
77
+ throw this.error(message);
78
+ return this.advance();
79
+ }
69
80
  /**
70
81
  * Check if current token is an identifier (including HTTP methods as identifiers)
71
82
  */
@@ -14,6 +14,7 @@ export interface ObjectProperty {
14
14
  value: Expression;
15
15
  }
16
16
  export declare class ReqonExpressionParser extends ReqonParserBase {
17
+ private expressionDepth;
17
18
  parseExpression(): Expression;
18
19
  parseLogicalExpression(): Expression;
19
20
  private parseTernary;
@@ -1,9 +1,22 @@
1
1
  import { TokenType } from 'vague-lang';
2
2
  import { ReqonTokenType } from '../lexer/tokens.js';
3
3
  import { ReqonParserBase } from './base.js';
4
+ // Guards unbounded recursive descent from blowing the JS stack on deeply nested
5
+ // input, surfacing a structured ParseError instead of a raw V8 RangeError.
6
+ const MAX_EXPRESSION_DEPTH = 200;
4
7
  export class ReqonExpressionParser extends ReqonParserBase {
8
+ expressionDepth = 0;
5
9
  parseExpression() {
6
- return this.parseTernary();
10
+ if (++this.expressionDepth > MAX_EXPRESSION_DEPTH) {
11
+ this.expressionDepth--;
12
+ throw this.error('Expression nesting too deep (maximum depth 200 exceeded)');
13
+ }
14
+ try {
15
+ return this.parseTernary();
16
+ }
17
+ finally {
18
+ this.expressionDepth--;
19
+ }
7
20
  }
8
21
  parseLogicalExpression() {
9
22
  return this.parseOr();
@@ -118,7 +131,7 @@ export class ReqonExpressionParser extends ReqonParserBase {
118
131
  // Check for 'is' type checking: expr is array, expr is string, etc.
119
132
  if (this.check(ReqonTokenType.IS)) {
120
133
  this.advance(); // consume 'is'
121
- const typeCheck = this.consume(TokenType.IDENTIFIER, "Expected type name after 'is'").value;
134
+ const typeCheck = this.consumeName("Expected type name after 'is'").value;
122
135
  return { type: 'IsExpression', operand: left, typeCheck };
123
136
  }
124
137
  return left;
@@ -180,7 +193,7 @@ export class ReqonExpressionParser extends ReqonParserBase {
180
193
  }
181
194
  }
182
195
  else if (this.match(TokenType.DOT)) {
183
- const name = this.consume(TokenType.IDENTIFIER, 'Expected property name').value;
196
+ const name = this.consumeName('Expected property name').value;
184
197
  if (expr.type === 'Identifier') {
185
198
  expr = { type: 'QualifiedName', parts: [expr.name, name] };
186
199
  }
@@ -251,7 +264,7 @@ export class ReqonExpressionParser extends ReqonParserBase {
251
264
  }
252
265
  // .field shorthand
253
266
  if (this.match(TokenType.DOT)) {
254
- const name = this.consumeIdentifier("Expected field name after '.'").value;
267
+ const name = this.consumeName("Expected field name after '.'").value;
255
268
  return { type: 'Identifier', name };
256
269
  }
257
270
  throw this.error(`Unexpected token: ${this.peek().value}`);
@@ -80,6 +80,7 @@ export class FetchParser extends ScheduleParser {
80
80
  let until;
81
81
  let retry;
82
82
  let since;
83
+ let backfill;
83
84
  if (this.match(TokenType.LBRACE)) {
84
85
  while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
85
86
  const key = this.parseFetchOptionKey();
@@ -103,6 +104,12 @@ export class FetchParser extends ScheduleParser {
103
104
  case 'since':
104
105
  since = this.parseSinceConfig();
105
106
  break;
107
+ case 'backfill':
108
+ backfill = this.match(TokenType.TRUE);
109
+ if (!backfill) {
110
+ this.consume(TokenType.FALSE, "Expected 'true' or 'false'");
111
+ }
112
+ break;
106
113
  default:
107
114
  throw this.error(`Unknown fetch option: ${key}`);
108
115
  }
@@ -110,7 +117,7 @@ export class FetchParser extends ScheduleParser {
110
117
  }
111
118
  this.consume(TokenType.RBRACE, "Expected '}'");
112
119
  }
113
- return { source, body, headers, paginate, until, retry, since };
120
+ return { source, body, headers, paginate, until, retry, since, backfill };
114
121
  }
115
122
  /**
116
123
  * Parse a fetch option key, handling keyword tokens that can appear as keys
@@ -243,6 +250,7 @@ export class FetchParser extends ScheduleParser {
243
250
  let backoff = 'exponential';
244
251
  let initialDelay = 1000;
245
252
  let maxDelay;
253
+ let timeout;
246
254
  while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
247
255
  const key = this.consume(TokenType.IDENTIFIER, 'Expected retry option').value;
248
256
  this.consume(TokenType.COLON, "Expected ':'");
@@ -260,10 +268,13 @@ export class FetchParser extends ScheduleParser {
260
268
  case 'maxDelay':
261
269
  maxDelay = parseInt(this.consume(TokenType.NUMBER, 'Expected number').value, 10);
262
270
  break;
271
+ case 'timeout':
272
+ timeout = parseInt(this.consume(TokenType.NUMBER, 'Expected number').value, 10);
273
+ break;
263
274
  }
264
275
  this.match(TokenType.COMMA);
265
276
  }
266
277
  this.consume(TokenType.RBRACE, "Expected '}'");
267
- return { maxAttempts, backoff, initialDelay, maxDelay };
278
+ return { maxAttempts, backoff, initialDelay, maxDelay, timeout };
268
279
  }
269
280
  }
@@ -10,5 +10,6 @@ export type { PauseState, PauseResumeTriggerState, PauseCheckpoint } from './sta
10
10
  export { generatePauseId, parseDuration, formatDuration, createPauseState, isPauseExpired, getRemainingTime, getPauseSummary, } from './state.js';
11
11
  export type { PauseStore } from './store.js';
12
12
  export { FilePauseStore, MemoryPauseStore } from './store.js';
13
+ export { LogBackedPauseStore } from './log-store.js';
13
14
  export type { PauseManagerConfig, CreatePauseOptions, PauseStatus } from './manager.js';
14
15
  export { PauseManager, createPauseManager } from './manager.js';
@@ -8,4 +8,5 @@
8
8
  */
9
9
  export { generatePauseId, parseDuration, formatDuration, createPauseState, isPauseExpired, getRemainingTime, getPauseSummary, } from './state.js';
10
10
  export { FilePauseStore, MemoryPauseStore } from './store.js';
11
+ export { LogBackedPauseStore } from './log-store.js';
11
12
  export { PauseManager, createPauseManager } from './manager.js';
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Log-backed pause store: resource-free long pauses as a view over the
3
+ * execution log.
4
+ *
5
+ * In durable mode the execution log holds the full pause record — a
6
+ * `pause.created` event carries the pause state (deadline, resume triggers,
7
+ * captured checkpoint), and `pause.resumed` records how and when it ended. A
8
+ * paused run, and the timer or webhook it waits on, are therefore reconstructable
9
+ * from the log alone, with no separate pause file. This adapter folds those
10
+ * events back into {@link PauseState}.
11
+ */
12
+ import type { ExecutionLogStore } from '../execution-log/index.js';
13
+ import type { PauseStore } from './store.js';
14
+ import type { PauseState } from './state.js';
15
+ export declare class LogBackedPauseStore implements PauseStore {
16
+ private log;
17
+ constructor(log: ExecutionLogStore);
18
+ save(pause: PauseState): Promise<void>;
19
+ update(id: string, updates: Partial<PauseState>): Promise<void>;
20
+ load(id: string): Promise<PauseState | null>;
21
+ loadByExecution(executionId: string): Promise<PauseState | null>;
22
+ listActive(): Promise<PauseState[]>;
23
+ listByMission(mission: string): Promise<PauseState[]>;
24
+ findExpired(): Promise<PauseState[]>;
25
+ /** Append-only: deletion is a no-op (history is retained in the log). */
26
+ delete(): Promise<void>;
27
+ private allPauses;
28
+ /**
29
+ * Fold pause events into the latest state per pause id. A `pause.created`
30
+ * seeds the state; later `pause.resumed` events apply the terminal status.
31
+ */
32
+ private foldPauses;
33
+ }