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.
- package/README.md +23 -3
- package/dist/ast/nodes.d.ts +8 -0
- package/dist/auth/circuit-breaker.d.ts +11 -0
- package/dist/auth/circuit-breaker.js +83 -12
- package/dist/auth/credentials.d.ts +6 -1
- package/dist/auth/credentials.js +12 -4
- package/dist/auth/oauth2-provider.js +13 -3
- package/dist/auth/rate-limiter.d.ts +8 -1
- package/dist/auth/rate-limiter.js +30 -10
- package/dist/auth/token-store.js +8 -1
- package/dist/cli.d.ts +11 -1
- package/dist/cli.js +65 -6
- package/dist/config/constants.d.ts +15 -4
- package/dist/config/constants.js +15 -4
- package/dist/control/server.d.ts +17 -0
- package/dist/control/server.js +82 -5
- package/dist/control/types.d.ts +6 -0
- package/dist/debug/cli-debugger.js +8 -3
- package/dist/execution/store.js +2 -2
- package/dist/execution-log/events.d.ts +125 -0
- package/dist/execution-log/events.js +17 -0
- package/dist/execution-log/fold.d.ts +38 -0
- package/dist/execution-log/fold.js +54 -0
- package/dist/execution-log/index.d.ts +18 -0
- package/dist/execution-log/index.js +6 -0
- package/dist/execution-log/postgres-store.d.ts +36 -0
- package/dist/execution-log/postgres-store.js +108 -0
- package/dist/execution-log/resume.d.ts +11 -0
- package/dist/execution-log/resume.js +5 -0
- package/dist/execution-log/sqlite-store.d.ts +16 -0
- package/dist/execution-log/sqlite-store.js +101 -0
- package/dist/execution-log/store.d.ts +72 -0
- package/dist/execution-log/store.js +182 -0
- package/dist/index.d.ts +4 -3
- package/dist/index.js +4 -3
- package/dist/interpreter/context.d.ts +15 -0
- package/dist/interpreter/context.js +3 -0
- package/dist/interpreter/evaluator.js +38 -8
- package/dist/interpreter/executor.d.ts +63 -1
- package/dist/interpreter/executor.js +406 -30
- package/dist/interpreter/fetch-handler.d.ts +39 -1
- package/dist/interpreter/fetch-handler.js +84 -15
- package/dist/interpreter/http.d.ts +31 -2
- package/dist/interpreter/http.js +187 -26
- package/dist/interpreter/index.d.ts +3 -3
- package/dist/interpreter/index.js +3 -3
- package/dist/interpreter/pagination.d.ts +1 -1
- package/dist/interpreter/pagination.js +7 -1
- package/dist/interpreter/step-handlers/for-handler.d.ts +3 -0
- package/dist/interpreter/step-handlers/for-handler.js +18 -3
- package/dist/interpreter/step-handlers/match-handler.js +5 -2
- package/dist/interpreter/step-handlers/store-handler.d.ts +7 -1
- package/dist/interpreter/step-handlers/store-handler.js +25 -16
- package/dist/interpreter/step-handlers/validate-handler.js +4 -1
- package/dist/interpreter/step-handlers/webhook-handler.d.ts +1 -0
- package/dist/interpreter/step-handlers/webhook-handler.js +13 -3
- package/dist/interpreter/store-manager.d.ts +1 -1
- package/dist/interpreter/store-manager.js +5 -1
- package/dist/loader/index.js +5 -8
- package/dist/mcp/sandbox.d.ts +41 -0
- package/dist/mcp/sandbox.js +76 -0
- package/dist/mcp/server.js +62 -9
- package/dist/oas/loader.d.ts +13 -1
- package/dist/oas/loader.js +25 -3
- package/dist/oas/mock-generator.js +13 -4
- package/dist/oas/validator.js +45 -5
- package/dist/observability/events.d.ts +6 -2
- package/dist/observability/events.js +0 -5
- package/dist/observability/logger.js +17 -10
- package/dist/observability/otel.d.ts +8 -0
- package/dist/observability/otel.js +45 -10
- package/dist/parser/action-parser.js +2 -2
- package/dist/parser/base.d.ts +7 -0
- package/dist/parser/base.js +11 -0
- package/dist/parser/expressions.d.ts +1 -0
- package/dist/parser/expressions.js +17 -4
- package/dist/parser/fetch-parser.js +13 -2
- package/dist/pause/index.d.ts +1 -0
- package/dist/pause/index.js +1 -0
- package/dist/pause/log-store.d.ts +33 -0
- package/dist/pause/log-store.js +98 -0
- package/dist/pause/manager.d.ts +12 -0
- package/dist/pause/manager.js +77 -28
- package/dist/pause/store.js +5 -3
- package/dist/scheduler/cron-parser.d.ts +10 -3
- package/dist/scheduler/cron-parser.js +227 -48
- package/dist/scheduler/scheduler.js +56 -22
- package/dist/stores/factory.d.ts +6 -0
- package/dist/stores/factory.js +11 -1
- package/dist/stores/file.js +9 -17
- package/dist/stores/memory.js +3 -12
- package/dist/stores/postgrest.d.ts +28 -0
- package/dist/stores/postgrest.js +84 -37
- package/dist/sync/index.d.ts +3 -2
- package/dist/sync/index.js +2 -1
- package/dist/sync/log-store.d.ts +30 -0
- package/dist/sync/log-store.js +45 -0
- package/dist/sync/store.js +1 -1
- package/dist/trace/index.d.ts +2 -0
- package/dist/trace/index.js +1 -0
- package/dist/trace/log-view.d.ts +57 -0
- package/dist/trace/log-view.js +76 -0
- package/dist/trace/recorder.d.ts +5 -1
- package/dist/trace/recorder.js +19 -6
- package/dist/trace/store.d.ts +6 -0
- package/dist/trace/store.js +47 -22
- package/dist/utils/deep-merge.d.ts +10 -0
- package/dist/utils/deep-merge.js +23 -0
- package/dist/utils/file.d.ts +13 -4
- package/dist/utils/file.js +70 -12
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.js +1 -1
- package/dist/utils/long-timeout.d.ts +19 -0
- package/dist/utils/long-timeout.js +33 -0
- package/dist/utils/path.d.ts +22 -1
- package/dist/utils/path.js +46 -1
- package/dist/utils/redact.d.ts +22 -0
- package/dist/utils/redact.js +42 -0
- package/dist/webhook/server.d.ts +9 -0
- package/dist/webhook/server.js +115 -30
- package/dist/webhook/types.d.ts +9 -1
- package/package.json +22 -4
package/dist/trace/store.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* enabling replay and step-by-step inspection of past runs.
|
|
6
6
|
*/
|
|
7
7
|
import { join } from 'node:path';
|
|
8
|
+
import { safeJoin } from '../utils/path.js';
|
|
8
9
|
import { ensureDirectory, writeJsonFile, readJsonFile, listFiles, deleteFile, restoreDates, restoreDatesInArray, } from '../utils/file.js';
|
|
9
10
|
/**
|
|
10
11
|
* File-based trace store
|
|
@@ -13,12 +14,15 @@ import { ensureDirectory, writeJsonFile, readJsonFile, listFiles, deleteFile, re
|
|
|
13
14
|
export class FileTraceStore {
|
|
14
15
|
baseDir;
|
|
15
16
|
initialized;
|
|
17
|
+
/** Per-trace append serialization to prevent concurrent index collisions. */
|
|
18
|
+
appendChain = new Map();
|
|
16
19
|
constructor(baseDir = '.reqon-data/traces') {
|
|
17
20
|
this.baseDir = baseDir;
|
|
18
21
|
this.initialized = ensureDirectory(this.baseDir);
|
|
19
22
|
}
|
|
20
23
|
getTraceDir(id) {
|
|
21
|
-
|
|
24
|
+
// Confine the (DSL-influenced) trace id to the traces base directory.
|
|
25
|
+
return safeJoin(this.baseDir, id);
|
|
22
26
|
}
|
|
23
27
|
getMetadataPath(id) {
|
|
24
28
|
return join(this.getTraceDir(id), 'metadata.json');
|
|
@@ -53,13 +57,23 @@ export class FileTraceStore {
|
|
|
53
57
|
metadata: trace.metadata,
|
|
54
58
|
snapshotCount: trace.snapshots.length,
|
|
55
59
|
};
|
|
56
|
-
|
|
57
|
-
//
|
|
60
|
+
// Write all snapshots first, then metadata last. Metadata's snapshotCount
|
|
61
|
+
// acts as a commit pointer: a crash mid-save leaves either no metadata or
|
|
62
|
+
// the previous one, never a metadata that claims more snapshots than exist.
|
|
58
63
|
for (let i = 0; i < trace.snapshots.length; i++) {
|
|
59
|
-
await writeJsonFile(this.getSnapshotPath(trace.id, i), trace.snapshots[i]);
|
|
64
|
+
await writeJsonFile(this.getSnapshotPath(trace.id, i), trace.snapshots[i], true, 0o600);
|
|
60
65
|
}
|
|
66
|
+
await writeJsonFile(this.getMetadataPath(trace.id), metadata, true, 0o600);
|
|
61
67
|
}
|
|
62
68
|
async appendSnapshot(traceId, snapshot) {
|
|
69
|
+
// Serialize appends per trace so two concurrent calls can't compute the
|
|
70
|
+
// same next index and clobber each other's snapshot file.
|
|
71
|
+
const run = (this.appendChain.get(traceId) ?? Promise.resolve()).then(() => this.doAppendSnapshot(traceId, snapshot));
|
|
72
|
+
// Keep the chain alive even if this append rejects.
|
|
73
|
+
this.appendChain.set(traceId, run.catch(() => { }));
|
|
74
|
+
return run;
|
|
75
|
+
}
|
|
76
|
+
async doAppendSnapshot(traceId, snapshot) {
|
|
63
77
|
await this.initialized;
|
|
64
78
|
const traceDir = this.getTraceDir(traceId);
|
|
65
79
|
await ensureDirectory(traceDir);
|
|
@@ -70,32 +84,34 @@ export class FileTraceStore {
|
|
|
70
84
|
if (metadata && typeof metadata.snapshotCount === 'number') {
|
|
71
85
|
snapshotCount = metadata.snapshotCount;
|
|
72
86
|
}
|
|
73
|
-
//
|
|
74
|
-
|
|
75
|
-
//
|
|
87
|
+
// Write the snapshot first, then commit the new count to metadata so a
|
|
88
|
+
// crash between the two leaves an uncommitted (ignored) snapshot, never a
|
|
89
|
+
// count that points past what's on disk.
|
|
90
|
+
await writeJsonFile(this.getSnapshotPath(traceId, snapshotCount), snapshot, true, 0o600);
|
|
76
91
|
if (metadata) {
|
|
77
92
|
metadata.snapshotCount = snapshotCount + 1;
|
|
78
|
-
await writeJsonFile(metadataPath, metadata);
|
|
93
|
+
await writeJsonFile(metadataPath, metadata, true, 0o600);
|
|
79
94
|
}
|
|
80
95
|
}
|
|
81
96
|
async load(id) {
|
|
82
97
|
await this.initialized;
|
|
83
|
-
const
|
|
84
|
-
if (!
|
|
98
|
+
const raw = await this.readMetadataRaw(id);
|
|
99
|
+
if (!raw)
|
|
85
100
|
return null;
|
|
86
|
-
|
|
101
|
+
const { snapshotCount, metadata } = raw;
|
|
102
|
+
// Load exactly the committed number of snapshots, by index. Globbing
|
|
103
|
+
// snapshot_*.json would silently include a half-written extra file or
|
|
104
|
+
// miss nothing about a gap; loading by the committed count guarantees the
|
|
105
|
+
// replayed trace matches what save/append actually committed.
|
|
87
106
|
const snapshots = [];
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
if (
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
snapshots.push(this.deserializeSnapshot(parsed));
|
|
94
|
-
}
|
|
107
|
+
for (let i = 0; i < snapshotCount; i++) {
|
|
108
|
+
const parsed = await readJsonFile(this.getSnapshotPath(id, i));
|
|
109
|
+
if (!parsed) {
|
|
110
|
+
throw new Error(`Trace ${id} is inconsistent: metadata claims ${snapshotCount} snapshots ` +
|
|
111
|
+
`but snapshot ${i} is missing.`);
|
|
95
112
|
}
|
|
113
|
+
snapshots.push(this.deserializeSnapshot(parsed));
|
|
96
114
|
}
|
|
97
|
-
// Sort by index
|
|
98
|
-
snapshots.sort((a, b) => a.index - b.index);
|
|
99
115
|
return {
|
|
100
116
|
...metadata,
|
|
101
117
|
snapshots,
|
|
@@ -144,13 +160,22 @@ export class FileTraceStore {
|
|
|
144
160
|
return traces[0] ?? null;
|
|
145
161
|
}
|
|
146
162
|
async getMetadata(id) {
|
|
163
|
+
const raw = await this.readMetadataRaw(id);
|
|
164
|
+
return raw ? raw.metadata : null;
|
|
165
|
+
}
|
|
166
|
+
/** Read the metadata file, returning the committed snapshot count and the
|
|
167
|
+
* trace metadata (snapshotCount stripped). */
|
|
168
|
+
async readMetadataRaw(id) {
|
|
147
169
|
await this.initialized;
|
|
148
170
|
const parsed = await readJsonFile(this.getMetadataPath(id));
|
|
149
171
|
if (!parsed)
|
|
150
172
|
return null;
|
|
151
173
|
restoreDates(parsed, ['startedAt', 'completedAt']);
|
|
152
|
-
const { snapshotCount
|
|
153
|
-
return
|
|
174
|
+
const { snapshotCount, ...rest } = parsed;
|
|
175
|
+
return {
|
|
176
|
+
snapshotCount: typeof snapshotCount === 'number' ? snapshotCount : 0,
|
|
177
|
+
metadata: rest,
|
|
178
|
+
};
|
|
154
179
|
}
|
|
155
180
|
}
|
|
156
181
|
/**
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deep-merge for store upserts.
|
|
3
|
+
*
|
|
4
|
+
* A shallow `{ ...existing, ...incoming }` clobbers nested objects: merging
|
|
5
|
+
* `{ profile: { city: 'LA' } }` over `{ profile: { name: 'Alice' } }` drops
|
|
6
|
+
* `name`. deepMerge recurses into plain objects so sibling nested fields
|
|
7
|
+
* survive. Arrays and primitives are replaced wholesale (the incoming value
|
|
8
|
+
* wins), and neither input is mutated.
|
|
9
|
+
*/
|
|
10
|
+
export declare function deepMerge(target: Record<string, unknown>, source: Record<string, unknown>): Record<string, unknown>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deep-merge for store upserts.
|
|
3
|
+
*
|
|
4
|
+
* A shallow `{ ...existing, ...incoming }` clobbers nested objects: merging
|
|
5
|
+
* `{ profile: { city: 'LA' } }` over `{ profile: { name: 'Alice' } }` drops
|
|
6
|
+
* `name`. deepMerge recurses into plain objects so sibling nested fields
|
|
7
|
+
* survive. Arrays and primitives are replaced wholesale (the incoming value
|
|
8
|
+
* wins), and neither input is mutated.
|
|
9
|
+
*/
|
|
10
|
+
function isPlainObject(value) {
|
|
11
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
12
|
+
return false;
|
|
13
|
+
const proto = Object.getPrototypeOf(value);
|
|
14
|
+
return proto === Object.prototype || proto === null;
|
|
15
|
+
}
|
|
16
|
+
export function deepMerge(target, source) {
|
|
17
|
+
const out = { ...target };
|
|
18
|
+
for (const [key, value] of Object.entries(source)) {
|
|
19
|
+
const existing = out[key];
|
|
20
|
+
out[key] = isPlainObject(existing) && isPlainObject(value) ? deepMerge(existing, value) : value;
|
|
21
|
+
}
|
|
22
|
+
return out;
|
|
23
|
+
}
|
package/dist/utils/file.d.ts
CHANGED
|
@@ -11,12 +11,21 @@ export declare function ensureParentDirectory(filePath: string): Promise<void>;
|
|
|
11
11
|
*/
|
|
12
12
|
export declare function serialize(data: unknown, pretty?: boolean): string;
|
|
13
13
|
/**
|
|
14
|
-
*
|
|
14
|
+
* Atomically write a file: write to a temp file, fsync it, then rename over
|
|
15
|
+
* the target. A crash or full disk mid-write leaves the original file intact
|
|
16
|
+
* rather than truncating it (the rename is atomic on POSIX/NTFS).
|
|
15
17
|
*/
|
|
16
|
-
export declare function
|
|
18
|
+
export declare function writeFileAtomic(filePath: string, content: string, mode?: number): Promise<void>;
|
|
19
|
+
/** Synchronous counterpart to {@link writeFileAtomic} for flush/exit paths. */
|
|
20
|
+
export declare function writeFileAtomicSync(filePath: string, content: string): void;
|
|
17
21
|
/**
|
|
18
|
-
*
|
|
19
|
-
|
|
22
|
+
* Write JSON data to a file atomically (durable against mid-write crashes).
|
|
23
|
+
*/
|
|
24
|
+
export declare function writeJsonFile(filePath: string, data: unknown, pretty?: boolean, mode?: number): Promise<void>;
|
|
25
|
+
/**
|
|
26
|
+
* Read JSON data from a file.
|
|
27
|
+
* Returns null only if the file is absent; throws if it exists but is corrupt
|
|
28
|
+
* so a truncated/partial file is never silently treated as "empty".
|
|
20
29
|
*/
|
|
21
30
|
export declare function readJsonFile<T>(filePath: string): Promise<T | null>;
|
|
22
31
|
/**
|
package/dist/utils/file.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
import { mkdir, readFile,
|
|
1
|
+
import { mkdir, readFile, access, readdir, unlink, rename, open } from 'node:fs/promises';
|
|
2
|
+
import { openSync, writeSync, fsyncSync, closeSync, renameSync, unlinkSync } from 'node:fs';
|
|
2
3
|
import { dirname, join } from 'node:path';
|
|
4
|
+
// Monotonic counter to keep concurrent temp-file names unique within a process.
|
|
5
|
+
let tmpCounter = 0;
|
|
3
6
|
/**
|
|
4
7
|
* Ensure a directory exists, creating it if necessary
|
|
5
8
|
*/
|
|
@@ -24,22 +27,79 @@ export function serialize(data, pretty = true) {
|
|
|
24
27
|
return pretty ? JSON.stringify(data, null, 2) : JSON.stringify(data);
|
|
25
28
|
}
|
|
26
29
|
/**
|
|
27
|
-
*
|
|
30
|
+
* Atomically write a file: write to a temp file, fsync it, then rename over
|
|
31
|
+
* the target. A crash or full disk mid-write leaves the original file intact
|
|
32
|
+
* rather than truncating it (the rename is atomic on POSIX/NTFS).
|
|
28
33
|
*/
|
|
29
|
-
export async function
|
|
30
|
-
|
|
34
|
+
export async function writeFileAtomic(filePath, content, mode) {
|
|
35
|
+
const tmpPath = `${filePath}.${process.pid}.${tmpCounter++}.tmp`;
|
|
36
|
+
const handle = await open(tmpPath, 'w', mode);
|
|
37
|
+
try {
|
|
38
|
+
await handle.writeFile(content, 'utf-8');
|
|
39
|
+
await handle.sync();
|
|
40
|
+
}
|
|
41
|
+
finally {
|
|
42
|
+
await handle.close();
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
await rename(tmpPath, filePath);
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
await unlink(tmpPath).catch(() => { });
|
|
49
|
+
throw err;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/** Synchronous counterpart to {@link writeFileAtomic} for flush/exit paths. */
|
|
53
|
+
export function writeFileAtomicSync(filePath, content) {
|
|
54
|
+
const tmpPath = `${filePath}.${process.pid}.${tmpCounter++}.tmp`;
|
|
55
|
+
const fd = openSync(tmpPath, 'w');
|
|
56
|
+
try {
|
|
57
|
+
writeSync(fd, content);
|
|
58
|
+
fsyncSync(fd);
|
|
59
|
+
}
|
|
60
|
+
finally {
|
|
61
|
+
closeSync(fd);
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
renameSync(tmpPath, filePath);
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
try {
|
|
68
|
+
unlinkSync(tmpPath);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
// best-effort cleanup
|
|
72
|
+
}
|
|
73
|
+
throw err;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Write JSON data to a file atomically (durable against mid-write crashes).
|
|
78
|
+
*/
|
|
79
|
+
export async function writeJsonFile(filePath, data, pretty = true, mode) {
|
|
80
|
+
await writeFileAtomic(filePath, serialize(data, pretty), mode);
|
|
31
81
|
}
|
|
32
82
|
/**
|
|
33
|
-
* Read JSON data from a file
|
|
34
|
-
* Returns null if file
|
|
83
|
+
* Read JSON data from a file.
|
|
84
|
+
* Returns null only if the file is absent; throws if it exists but is corrupt
|
|
85
|
+
* so a truncated/partial file is never silently treated as "empty".
|
|
35
86
|
*/
|
|
36
87
|
export async function readJsonFile(filePath) {
|
|
88
|
+
let content;
|
|
89
|
+
try {
|
|
90
|
+
content = await readFile(filePath, 'utf-8');
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
if (err.code === 'ENOENT') {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
throw err;
|
|
97
|
+
}
|
|
37
98
|
try {
|
|
38
|
-
const content = await readFile(filePath, 'utf-8');
|
|
39
99
|
return JSON.parse(content);
|
|
40
100
|
}
|
|
41
|
-
catch {
|
|
42
|
-
|
|
101
|
+
catch (err) {
|
|
102
|
+
throw new Error(`Corrupt JSON in ${filePath}: ${err.message}`, { cause: err });
|
|
43
103
|
}
|
|
44
104
|
}
|
|
45
105
|
/**
|
|
@@ -48,9 +108,7 @@ export async function readJsonFile(filePath) {
|
|
|
48
108
|
export async function listFiles(dir, extension) {
|
|
49
109
|
try {
|
|
50
110
|
const entries = await readdir(dir);
|
|
51
|
-
const filtered = extension
|
|
52
|
-
? entries.filter((f) => f.endsWith(extension))
|
|
53
|
-
: entries;
|
|
111
|
+
const filtered = extension ? entries.filter((f) => f.endsWith(extension)) : entries;
|
|
54
112
|
return filtered.map((f) => join(dir, f));
|
|
55
113
|
}
|
|
56
114
|
catch {
|
package/dist/utils/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { sleep } from './async.js';
|
|
2
|
-
export { extractNestedValue, traversePath } from './path.js';
|
|
2
|
+
export { extractNestedValue, traversePath, safeJoin, sanitizeSegment, PathTraversalError, } from './path.js';
|
|
3
3
|
export { type Logger, ConsoleLogger, SilentLogger, createLogger } from './logger.js';
|
|
4
4
|
export { ensureDirectory, ensureParentDirectory, serialize, writeJsonFile, readJsonFile, listFiles, deleteFile, restoreDates, restoreDatesInArray, } from './file.js';
|
|
5
5
|
export { isRecord, isObject, isArrayOf, isString, isNumber, isBoolean, isDefined, isPresent, getProperty, getNestedProperty, hasProperty, hasTypedProperty, } from './type-guards.js';
|
package/dist/utils/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { sleep } from './async.js';
|
|
2
|
-
export { extractNestedValue, traversePath } from './path.js';
|
|
2
|
+
export { extractNestedValue, traversePath, safeJoin, sanitizeSegment, PathTraversalError, } from './path.js';
|
|
3
3
|
export { ConsoleLogger, SilentLogger, createLogger } from './logger.js';
|
|
4
4
|
export { ensureDirectory, ensureParentDirectory, serialize, writeJsonFile, readJsonFile, listFiles, deleteFile, restoreDates, restoreDatesInArray, } from './file.js';
|
|
5
5
|
export { isRecord, isObject, isArrayOf, isString, isNumber, isBoolean, isDefined, isPresent, getProperty, getNestedProperty, hasProperty, hasTypedProperty, } from './type-guards.js';
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Timers for delays beyond Node's 32-bit setTimeout limit.
|
|
3
|
+
*
|
|
4
|
+
* Node stores the delay in a 32-bit signed int: anything over 2_147_483_647 ms
|
|
5
|
+
* (~24.8 days) overflows and the callback fires almost immediately. A multi-week
|
|
6
|
+
* webhook wait or pause would therefore time out at once. `setLongTimeout`
|
|
7
|
+
* chains native timeouts so arbitrarily long delays fire at the right time.
|
|
8
|
+
*/
|
|
9
|
+
/** Largest delay (ms) Node's setTimeout handles without overflowing. */
|
|
10
|
+
export declare const MAX_TIMEOUT_MS = 2147483647;
|
|
11
|
+
export interface LongTimeout {
|
|
12
|
+
/** Cancel the pending timeout. */
|
|
13
|
+
clear(): void;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Schedule `callback` to run after `delayMs`, correctly handling delays larger
|
|
17
|
+
* than {@link MAX_TIMEOUT_MS} by chaining native timeouts.
|
|
18
|
+
*/
|
|
19
|
+
export declare function setLongTimeout(callback: () => void, delayMs: number): LongTimeout;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Timers for delays beyond Node's 32-bit setTimeout limit.
|
|
3
|
+
*
|
|
4
|
+
* Node stores the delay in a 32-bit signed int: anything over 2_147_483_647 ms
|
|
5
|
+
* (~24.8 days) overflows and the callback fires almost immediately. A multi-week
|
|
6
|
+
* webhook wait or pause would therefore time out at once. `setLongTimeout`
|
|
7
|
+
* chains native timeouts so arbitrarily long delays fire at the right time.
|
|
8
|
+
*/
|
|
9
|
+
/** Largest delay (ms) Node's setTimeout handles without overflowing. */
|
|
10
|
+
export const MAX_TIMEOUT_MS = 2_147_483_647;
|
|
11
|
+
/**
|
|
12
|
+
* Schedule `callback` to run after `delayMs`, correctly handling delays larger
|
|
13
|
+
* than {@link MAX_TIMEOUT_MS} by chaining native timeouts.
|
|
14
|
+
*/
|
|
15
|
+
export function setLongTimeout(callback, delayMs) {
|
|
16
|
+
let remaining = Math.max(0, delayMs);
|
|
17
|
+
let timer;
|
|
18
|
+
const schedule = () => {
|
|
19
|
+
if (remaining <= MAX_TIMEOUT_MS) {
|
|
20
|
+
timer = setTimeout(callback, remaining);
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
remaining -= MAX_TIMEOUT_MS;
|
|
24
|
+
timer = setTimeout(schedule, MAX_TIMEOUT_MS);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
schedule();
|
|
28
|
+
return {
|
|
29
|
+
clear() {
|
|
30
|
+
clearTimeout(timer);
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
package/dist/utils/path.d.ts
CHANGED
|
@@ -7,6 +7,27 @@
|
|
|
7
7
|
*/
|
|
8
8
|
export declare function extractNestedValue(data: Record<string, unknown>, path: string): unknown;
|
|
9
9
|
/**
|
|
10
|
-
*
|
|
10
|
+
* Error thrown when a path segment would escape its base directory.
|
|
11
11
|
*/
|
|
12
|
+
export declare class PathTraversalError extends Error {
|
|
13
|
+
readonly segment: string;
|
|
14
|
+
readonly baseDir: string;
|
|
15
|
+
constructor(segment: string, baseDir: string);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Safely join an untrusted path segment under a base directory, asserting the
|
|
19
|
+
* result stays confined inside that base. Guards against `../`, absolute paths,
|
|
20
|
+
* and embedded separators in store names / IDs.
|
|
21
|
+
*
|
|
22
|
+
* @throws {PathTraversalError} if the resolved path escapes baseDir
|
|
23
|
+
*/
|
|
24
|
+
export declare function safeJoin(baseDir: string, ...segments: string[]): string;
|
|
25
|
+
/**
|
|
26
|
+
* Sanitize a single untrusted path segment (e.g. a store name or record ID) so
|
|
27
|
+
* it cannot traverse directories. Rejects empty, separator-bearing, or
|
|
28
|
+
* traversal segments rather than silently mangling them.
|
|
29
|
+
*
|
|
30
|
+
* @throws {PathTraversalError} if the segment is unsafe
|
|
31
|
+
*/
|
|
32
|
+
export declare function sanitizeSegment(segment: string, baseDir?: string): string;
|
|
12
33
|
export declare function traversePath(parts: string[], current: unknown, fallbackLookup?: (key: string) => unknown): unknown;
|
package/dist/utils/path.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Utility functions for path traversal and value extraction
|
|
3
3
|
*/
|
|
4
|
+
import { join, resolve, sep } from 'node:path';
|
|
4
5
|
/**
|
|
5
6
|
* Extract a nested value from an object using dot notation path
|
|
6
7
|
* @example extractNestedValue({ a: { b: 1 } }, 'a.b') // => 1
|
|
@@ -19,8 +20,52 @@ export function extractNestedValue(data, path) {
|
|
|
19
20
|
return value;
|
|
20
21
|
}
|
|
21
22
|
/**
|
|
22
|
-
*
|
|
23
|
+
* Error thrown when a path segment would escape its base directory.
|
|
23
24
|
*/
|
|
25
|
+
export class PathTraversalError extends Error {
|
|
26
|
+
segment;
|
|
27
|
+
baseDir;
|
|
28
|
+
constructor(segment, baseDir) {
|
|
29
|
+
super(`Path segment "${segment}" escapes base directory "${baseDir}"`);
|
|
30
|
+
this.segment = segment;
|
|
31
|
+
this.baseDir = baseDir;
|
|
32
|
+
this.name = 'PathTraversalError';
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Safely join an untrusted path segment under a base directory, asserting the
|
|
37
|
+
* result stays confined inside that base. Guards against `../`, absolute paths,
|
|
38
|
+
* and embedded separators in store names / IDs.
|
|
39
|
+
*
|
|
40
|
+
* @throws {PathTraversalError} if the resolved path escapes baseDir
|
|
41
|
+
*/
|
|
42
|
+
export function safeJoin(baseDir, ...segments) {
|
|
43
|
+
const base = resolve(baseDir);
|
|
44
|
+
const joined = resolve(base, ...segments);
|
|
45
|
+
// Confined if it equals the base or sits inside it (base + separator prefix).
|
|
46
|
+
if (joined !== base && !joined.startsWith(base + sep)) {
|
|
47
|
+
throw new PathTraversalError(join(...segments), baseDir);
|
|
48
|
+
}
|
|
49
|
+
return joined;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Sanitize a single untrusted path segment (e.g. a store name or record ID) so
|
|
53
|
+
* it cannot traverse directories. Rejects empty, separator-bearing, or
|
|
54
|
+
* traversal segments rather than silently mangling them.
|
|
55
|
+
*
|
|
56
|
+
* @throws {PathTraversalError} if the segment is unsafe
|
|
57
|
+
*/
|
|
58
|
+
export function sanitizeSegment(segment, baseDir = '.') {
|
|
59
|
+
if (segment.length === 0 ||
|
|
60
|
+
segment === '.' ||
|
|
61
|
+
segment === '..' ||
|
|
62
|
+
segment.includes('/') ||
|
|
63
|
+
segment.includes('\\') ||
|
|
64
|
+
segment.includes('\0')) {
|
|
65
|
+
throw new PathTraversalError(segment, baseDir);
|
|
66
|
+
}
|
|
67
|
+
return segment;
|
|
68
|
+
}
|
|
24
69
|
export function traversePath(parts, current, fallbackLookup) {
|
|
25
70
|
let value = current;
|
|
26
71
|
for (let i = 0; i < parts.length; i++) {
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Secret redaction for logs, error messages, and durable trace files.
|
|
3
|
+
*
|
|
4
|
+
* Uses a denylist of sensitive key-name patterns: any object property whose
|
|
5
|
+
* key looks like a credential is replaced with {@link REDACTED}. This is a
|
|
6
|
+
* best-effort guard — it cannot catch a raw secret stored under an innocuous
|
|
7
|
+
* key — so callers should also avoid persisting cleartext credentials.
|
|
8
|
+
*/
|
|
9
|
+
export declare const REDACTED = "[REDACTED]";
|
|
10
|
+
/** True when a key name looks like it holds a credential. */
|
|
11
|
+
export declare function isSensitiveKey(key: string): boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Return a deep copy of `value` with any credential-looking properties
|
|
14
|
+
* replaced by {@link REDACTED}. Non-objects are returned unchanged. Cycles are
|
|
15
|
+
* handled, and the input is never mutated.
|
|
16
|
+
*/
|
|
17
|
+
export declare function redactSecrets<T>(value: T, seen?: WeakSet<object>): T;
|
|
18
|
+
/**
|
|
19
|
+
* Redact a named value for logging: if the name itself looks sensitive the
|
|
20
|
+
* whole value is hidden; otherwise nested credential properties are redacted.
|
|
21
|
+
*/
|
|
22
|
+
export declare function redactNamedValue(name: string, value: unknown): unknown;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Secret redaction for logs, error messages, and durable trace files.
|
|
3
|
+
*
|
|
4
|
+
* Uses a denylist of sensitive key-name patterns: any object property whose
|
|
5
|
+
* key looks like a credential is replaced with {@link REDACTED}. This is a
|
|
6
|
+
* best-effort guard — it cannot catch a raw secret stored under an innocuous
|
|
7
|
+
* key — so callers should also avoid persisting cleartext credentials.
|
|
8
|
+
*/
|
|
9
|
+
export const REDACTED = '[REDACTED]';
|
|
10
|
+
/** Key names that should never have their value logged or persisted in clear. */
|
|
11
|
+
const SENSITIVE_KEY = /(pass(word|wd)?|secret|token|auth(orization)?|api[-_]?key|access[-_]?key|client[-_]?secret|credential|cookie|session|bearer|private[-_]?key)/i;
|
|
12
|
+
/** True when a key name looks like it holds a credential. */
|
|
13
|
+
export function isSensitiveKey(key) {
|
|
14
|
+
return SENSITIVE_KEY.test(key);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Return a deep copy of `value` with any credential-looking properties
|
|
18
|
+
* replaced by {@link REDACTED}. Non-objects are returned unchanged. Cycles are
|
|
19
|
+
* handled, and the input is never mutated.
|
|
20
|
+
*/
|
|
21
|
+
export function redactSecrets(value, seen = new WeakSet()) {
|
|
22
|
+
if (value === null || typeof value !== 'object')
|
|
23
|
+
return value;
|
|
24
|
+
if (seen.has(value))
|
|
25
|
+
return value;
|
|
26
|
+
seen.add(value);
|
|
27
|
+
if (Array.isArray(value)) {
|
|
28
|
+
return value.map((item) => redactSecrets(item, seen));
|
|
29
|
+
}
|
|
30
|
+
const out = {};
|
|
31
|
+
for (const [key, val] of Object.entries(value)) {
|
|
32
|
+
out[key] = isSensitiveKey(key) ? REDACTED : redactSecrets(val, seen);
|
|
33
|
+
}
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Redact a named value for logging: if the name itself looks sensitive the
|
|
38
|
+
* whole value is hidden; otherwise nested credential properties are redacted.
|
|
39
|
+
*/
|
|
40
|
+
export function redactNamedValue(name, value) {
|
|
41
|
+
return isSensitiveKey(name) ? REDACTED : redactSecrets(value);
|
|
42
|
+
}
|
package/dist/webhook/server.d.ts
CHANGED
|
@@ -45,6 +45,8 @@ export declare class WebhookServer {
|
|
|
45
45
|
* Wait for webhook events
|
|
46
46
|
*/
|
|
47
47
|
waitForEvents(registrationId: string, timeout?: number): Promise<WaitResult>;
|
|
48
|
+
/** Remove a single waiter, dropping the registration's set when empty. */
|
|
49
|
+
private removePendingWait;
|
|
48
50
|
/**
|
|
49
51
|
* Unregister a webhook endpoint
|
|
50
52
|
*/
|
|
@@ -69,6 +71,13 @@ export declare class WebhookServer {
|
|
|
69
71
|
* Read request body
|
|
70
72
|
*/
|
|
71
73
|
private readBody;
|
|
74
|
+
/** True if a host string is a loopback address. */
|
|
75
|
+
private isLoopback;
|
|
76
|
+
/**
|
|
77
|
+
* Validate the shared secret from Authorization bearer, X-Webhook-Token
|
|
78
|
+
* header, or a `token` query param.
|
|
79
|
+
*/
|
|
80
|
+
private authorized;
|
|
72
81
|
/**
|
|
73
82
|
* Extract headers from request
|
|
74
83
|
*/
|