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
@@ -0,0 +1,98 @@
1
+ /** Coerce a value that may be a Date or an ISO string back to a Date. */
2
+ function toDate(value) {
3
+ return value instanceof Date ? value : new Date(value);
4
+ }
5
+ function restoreDates(pause) {
6
+ pause.pausedAt = toDate(pause.pausedAt);
7
+ pause.expiresAt = toDate(pause.expiresAt);
8
+ if (pause.resumedAt)
9
+ pause.resumedAt = toDate(pause.resumedAt);
10
+ return pause;
11
+ }
12
+ export class LogBackedPauseStore {
13
+ log;
14
+ constructor(log) {
15
+ this.log = log;
16
+ }
17
+ async save(pause) {
18
+ await this.log.append({
19
+ executionId: pause.executionId,
20
+ type: 'pause.created',
21
+ pauseId: pause.id,
22
+ pause,
23
+ });
24
+ }
25
+ async update(id, updates) {
26
+ const pause = await this.load(id);
27
+ if (!pause)
28
+ throw new Error(`Pause not found: ${id}`);
29
+ const resumedBy = updates.resumedBy ?? 'manual';
30
+ await this.log.append({
31
+ executionId: pause.executionId,
32
+ type: 'pause.resumed',
33
+ pauseId: id,
34
+ resumedBy,
35
+ status: updates.status ?? 'resumed',
36
+ resumedAt: (updates.resumedAt ?? new Date()).toISOString(),
37
+ webhookPayload: updates.webhookPayload,
38
+ });
39
+ }
40
+ async load(id) {
41
+ const byId = this.foldPauses(await this.log.listPauses());
42
+ return byId.get(id) ?? null;
43
+ }
44
+ async loadByExecution(executionId) {
45
+ const all = this.foldPauses(await this.log.listPauses());
46
+ for (const pause of all.values()) {
47
+ if (pause.executionId === executionId && pause.status === 'waiting')
48
+ return pause;
49
+ }
50
+ return null;
51
+ }
52
+ async listActive() {
53
+ return (await this.allPauses())
54
+ .filter((p) => p.status === 'waiting')
55
+ .sort((a, b) => a.expiresAt.getTime() - b.expiresAt.getTime());
56
+ }
57
+ async listByMission(mission) {
58
+ return (await this.allPauses())
59
+ .filter((p) => p.mission === mission)
60
+ .sort((a, b) => b.pausedAt.getTime() - a.pausedAt.getTime());
61
+ }
62
+ async findExpired() {
63
+ const now = new Date();
64
+ return (await this.listActive()).filter((p) => p.expiresAt <= now);
65
+ }
66
+ /** Append-only: deletion is a no-op (history is retained in the log). */
67
+ async delete() { }
68
+ async allPauses() {
69
+ return Array.from(this.foldPauses(await this.log.listPauses()).values());
70
+ }
71
+ /**
72
+ * Fold pause events into the latest state per pause id. A `pause.created`
73
+ * seeds the state; later `pause.resumed` events apply the terminal status.
74
+ */
75
+ foldPauses(events) {
76
+ const byId = new Map();
77
+ for (const event of events) {
78
+ if (event.type === 'pause.created' && event.pause) {
79
+ byId.set(event.pauseId, restoreDates({ ...event.pause }));
80
+ }
81
+ }
82
+ // Apply resumptions after every create is loaded (events aren't ordered).
83
+ for (const event of events) {
84
+ if (event.type !== 'pause.resumed')
85
+ continue;
86
+ const pause = byId.get(event.pauseId);
87
+ if (!pause)
88
+ continue;
89
+ pause.status = event.status ?? 'resumed';
90
+ pause.resumedBy = event.resumedBy;
91
+ if (event.resumedAt)
92
+ pause.resumedAt = new Date(event.resumedAt);
93
+ if (event.webhookPayload !== undefined)
94
+ pause.webhookPayload = event.webhookPayload;
95
+ }
96
+ return byId;
97
+ }
98
+ }
@@ -46,6 +46,8 @@ export declare class PauseManager {
46
46
  private config;
47
47
  private pollTimer?;
48
48
  private isRunning;
49
+ private resuming;
50
+ private isCheckingExpired;
49
51
  constructor(config: PauseManagerConfig);
50
52
  /**
51
53
  * Create a new pause and persist state
@@ -91,6 +93,16 @@ export declare class PauseManager {
91
93
  * Get pause status summary
92
94
  */
93
95
  getStatus(): Promise<PauseStatus>;
96
+ /**
97
+ * Transition a pause to `resumed` and fire `onResume` exactly once.
98
+ *
99
+ * Returns the resumed pause, or `null` if this call lost the race (another
100
+ * resume already claimed it, or it is no longer waiting). The claim on
101
+ * `this.resuming` is synchronous — taken before any `await` — so two
102
+ * concurrent callers (e.g. the timeout poller and an inbound webhook) can't
103
+ * both pass the status check and double-fire the side-effecting tail. The
104
+ * persisted-status re-check makes resume idempotent across poll cycles too.
105
+ */
94
106
  private markResumed;
95
107
  private registerWebhookTrigger;
96
108
  private cleanupWebhooks;
@@ -12,6 +12,11 @@ export class PauseManager {
12
12
  config;
13
13
  pollTimer;
14
14
  isRunning = false;
15
+ // Pause ids currently being resumed. Claimed synchronously before any await
16
+ // so a timeout poll and an inbound webhook can't both resume the same pause.
17
+ resuming = new Set();
18
+ // Prevents a slow resume cycle from overlapping the next poll tick.
19
+ isCheckingExpired = false;
15
20
  constructor(config) {
16
21
  this.config = config;
17
22
  }
@@ -64,7 +69,15 @@ export class PauseManager {
64
69
  if (pause.status !== 'waiting') {
65
70
  throw new Error(`Pause ${pauseId} is not waiting (status: ${pause.status})`);
66
71
  }
67
- return this.markResumed(pause, 'manual');
72
+ const resumed = await this.markResumed(pause, 'manual');
73
+ if (!resumed) {
74
+ // Lost a race with another resume trigger; return the resolved state.
75
+ const current = await this.config.store.load(pauseId);
76
+ if (!current)
77
+ throw new Error(`Pause not found: ${pauseId}`);
78
+ return current;
79
+ }
80
+ return resumed;
68
81
  }
69
82
  /**
70
83
  * Resume a pause by execution ID
@@ -109,9 +122,9 @@ export class PauseManager {
109
122
  if (!webhookTrigger) {
110
123
  return false;
111
124
  }
112
- // Mark resumed with webhook payload
113
- await this.markResumed(pause, 'webhook', payload);
114
- return true;
125
+ // Mark resumed with webhook payload; false if another trigger won the race.
126
+ const resumed = await this.markResumed(pause, 'webhook', payload);
127
+ return resumed !== null;
115
128
  }
116
129
  /**
117
130
  * Start monitoring for expired pauses
@@ -141,13 +154,25 @@ export class PauseManager {
141
154
  * Check for and process expired pauses
142
155
  */
143
156
  async checkExpiredPauses() {
144
- const expired = await this.config.store.findExpired();
145
- const resumed = [];
146
- for (const pause of expired) {
147
- const updated = await this.markResumed(pause, 'timeout');
148
- resumed.push(updated);
157
+ // Overlap guard: if a prior cycle's resume callbacks are still running when
158
+ // the next poll fires, skip rather than reprocessing the same pauses.
159
+ if (this.isCheckingExpired)
160
+ return [];
161
+ this.isCheckingExpired = true;
162
+ try {
163
+ const expired = await this.config.store.findExpired();
164
+ const resumed = [];
165
+ for (const pause of expired) {
166
+ const updated = await this.markResumed(pause, 'timeout');
167
+ // null means another trigger already resumed this pause — skip it.
168
+ if (updated)
169
+ resumed.push(updated);
170
+ }
171
+ return resumed;
172
+ }
173
+ finally {
174
+ this.isCheckingExpired = false;
149
175
  }
150
- return resumed;
151
176
  }
152
177
  /**
153
178
  * Get all active pauses
@@ -184,28 +209,52 @@ export class PauseManager {
184
209
  })),
185
210
  };
186
211
  }
212
+ /**
213
+ * Transition a pause to `resumed` and fire `onResume` exactly once.
214
+ *
215
+ * Returns the resumed pause, or `null` if this call lost the race (another
216
+ * resume already claimed it, or it is no longer waiting). The claim on
217
+ * `this.resuming` is synchronous — taken before any `await` — so two
218
+ * concurrent callers (e.g. the timeout poller and an inbound webhook) can't
219
+ * both pass the status check and double-fire the side-effecting tail. The
220
+ * persisted-status re-check makes resume idempotent across poll cycles too.
221
+ */
187
222
  async markResumed(pause, resumedBy, webhookPayload) {
188
- const updates = {
189
- status: 'resumed',
190
- resumedAt: new Date(),
191
- resumedBy,
192
- };
193
- if (webhookPayload !== undefined) {
194
- updates.webhookPayload = webhookPayload;
223
+ if (this.resuming.has(pause.id)) {
224
+ return null; // another resume is already in flight for this pause
195
225
  }
196
- await this.config.store.update(pause.id, updates);
197
- // Cleanup webhook registrations
198
- await this.cleanupWebhooks(pause);
199
- const updated = await this.config.store.load(pause.id);
200
- if (!updated) {
201
- throw new Error(`Failed to load updated pause: ${pause.id}`);
226
+ this.resuming.add(pause.id);
227
+ try {
228
+ // Re-read the source of truth: a prior cycle may already have resumed it.
229
+ const current = await this.config.store.load(pause.id);
230
+ if (!current || current.status !== 'waiting') {
231
+ return null;
232
+ }
233
+ const updates = {
234
+ status: 'resumed',
235
+ resumedAt: new Date(),
236
+ resumedBy,
237
+ };
238
+ if (webhookPayload !== undefined) {
239
+ updates.webhookPayload = webhookPayload;
240
+ }
241
+ await this.config.store.update(pause.id, updates);
242
+ // Cleanup webhook registrations
243
+ await this.cleanupWebhooks(current);
244
+ const updated = await this.config.store.load(pause.id);
245
+ if (!updated) {
246
+ throw new Error(`Failed to load updated pause: ${pause.id}`);
247
+ }
248
+ this.log(`Pause ${pause.id} resumed by ${resumedBy}`);
249
+ // Trigger callback (the side-effecting pipeline tail) exactly once.
250
+ if (this.config.onResume) {
251
+ await this.config.onResume(updated);
252
+ }
253
+ return updated;
202
254
  }
203
- this.log(`Pause ${pause.id} resumed by ${resumedBy}`);
204
- // Trigger callback
205
- if (this.config.onResume) {
206
- await this.config.onResume(updated);
255
+ finally {
256
+ this.resuming.delete(pause.id);
207
257
  }
208
- return updated;
209
258
  }
210
259
  async registerWebhookTrigger(executionId, path, timeout) {
211
260
  if (!this.config.webhookServer) {
@@ -4,7 +4,7 @@
4
4
  * Stores pause state for resource-free long pauses,
5
5
  * enabling resumption after extended periods.
6
6
  */
7
- import { join } from 'node:path';
7
+ import { safeJoin } from '../utils/path.js';
8
8
  import { ensureDirectory, writeJsonFile, readJsonFile, listFiles, deleteFile, restoreDates, } from '../utils/file.js';
9
9
  /**
10
10
  * File-based pause store
@@ -17,7 +17,7 @@ export class FilePauseStore {
17
17
  this.initialized = ensureDirectory(this.baseDir);
18
18
  }
19
19
  getFilePath(id) {
20
- return join(this.baseDir, `${id}.json`);
20
+ return safeJoin(this.baseDir, `${id}.json`);
21
21
  }
22
22
  deserialize(parsed) {
23
23
  restoreDates(parsed, ['pausedAt', 'expiresAt', 'resumedAt']);
@@ -25,7 +25,9 @@ export class FilePauseStore {
25
25
  }
26
26
  async save(pause) {
27
27
  await this.initialized;
28
- await writeJsonFile(this.getFilePath(pause.id), pause);
28
+ // Pause files hold the captured variables needed to resume (so they can't
29
+ // be redacted); write owner-only (0o600) so they aren't world-readable.
30
+ await writeJsonFile(this.getFilePath(pause.id), pause, true, 0o600);
29
31
  }
30
32
  async load(id) {
31
33
  await this.initialized;
@@ -3,7 +3,7 @@ import type { ScheduleDefinition, IntervalSchedule } from '../ast/nodes.js';
3
3
  * Parse a cron expression and calculate the next run time
4
4
  *
5
5
  * Cron format: "minute hour day-of-month month day-of-week"
6
- * Supports: numbers, ranges (1-5), steps (*​/5), lists (1,3,5), and wildcards (*)
6
+ * Supports: numbers, ranges (1-5), steps (* /5), lists (1,3,5), and wildcards (*)
7
7
  */
8
8
  export declare function parseCronExpression(expression: string): CronSchedule;
9
9
  interface CronSchedule {
@@ -12,11 +12,18 @@ interface CronSchedule {
12
12
  dayOfMonth: number[];
13
13
  month: number[];
14
14
  dayOfWeek: number[];
15
+ /** True when day-of-month is not `*` (used for POSIX OR matching). */
16
+ dayOfMonthRestricted: boolean;
17
+ /** True when day-of-week is not `*` (used for POSIX OR matching). */
18
+ dayOfWeekRestricted: boolean;
15
19
  }
16
20
  /**
17
- * Calculate the next run time for a cron schedule
21
+ * Calculate the next run time for a cron schedule, evaluated against
22
+ * wall-clock time in `timeZone` (default UTC, so DST never shifts the result).
23
+ * Day-of-month and day-of-week follow POSIX: when both are restricted the
24
+ * match is their union (OR), not their intersection.
18
25
  */
19
- export declare function getNextCronRun(schedule: CronSchedule, after?: Date): Date;
26
+ export declare function getNextCronRun(schedule: CronSchedule, after?: Date, timeZone?: string): Date;
20
27
  /**
21
28
  * Convert interval schedule to milliseconds
22
29
  */