@showrun/core 0.1.0 → 0.1.1
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/dist/__tests__/config.test.d.ts +2 -0
- package/dist/__tests__/config.test.d.ts.map +1 -0
- package/dist/__tests__/config.test.js +164 -0
- package/dist/__tests__/httpReplay.test.d.ts +2 -0
- package/dist/__tests__/httpReplay.test.d.ts.map +1 -0
- package/dist/__tests__/httpReplay.test.js +306 -0
- package/dist/__tests__/requestSnapshot.test.d.ts +2 -0
- package/dist/__tests__/requestSnapshot.test.d.ts.map +1 -0
- package/dist/__tests__/requestSnapshot.test.js +323 -0
- package/dist/browserLauncher.d.ts.map +1 -1
- package/dist/browserLauncher.js +7 -1
- package/dist/config.d.ts +82 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +255 -0
- package/dist/dsl/interpreter.d.ts +9 -0
- package/dist/dsl/interpreter.d.ts.map +1 -1
- package/dist/dsl/interpreter.js +9 -3
- package/dist/dsl/stepHandlers.d.ts +7 -0
- package/dist/dsl/stepHandlers.d.ts.map +1 -1
- package/dist/dsl/stepHandlers.js +81 -0
- package/dist/dsl/types.d.ts +6 -0
- package/dist/dsl/types.d.ts.map +1 -1
- package/dist/dsl/validation.d.ts.map +1 -1
- package/dist/dsl/validation.js +7 -0
- package/dist/httpReplay.d.ts +43 -0
- package/dist/httpReplay.d.ts.map +1 -0
- package/dist/httpReplay.js +102 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/jsonPackValidator.d.ts.map +1 -1
- package/dist/jsonPackValidator.js +12 -3
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +4 -0
- package/dist/requestSnapshot.d.ts +91 -0
- package/dist/requestSnapshot.d.ts.map +1 -0
- package/dist/requestSnapshot.js +200 -0
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +189 -10
- package/dist/types.d.ts +5 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP-only execution engine for request snapshots.
|
|
3
|
+
*
|
|
4
|
+
* When every `network_replay` step in a flow has a valid snapshot and no
|
|
5
|
+
* DOM extraction steps exist, the flow can be executed purely via HTTP
|
|
6
|
+
* requests — no browser needed.
|
|
7
|
+
*/
|
|
8
|
+
import { isSnapshotStale, validateResponse, applyOverrides, } from './requestSnapshot.js';
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Constants
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
/** Default timeout for HTTP replay requests (30 seconds). */
|
|
13
|
+
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// HTTP-only compatibility check
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
/** Step types that require DOM access for data extraction (force browser mode). */
|
|
18
|
+
const DOM_EXTRACTION_STEPS = new Set(['extract_text', 'extract_title', 'extract_attribute']);
|
|
19
|
+
/**
|
|
20
|
+
* Check whether a flow can run in HTTP-only mode.
|
|
21
|
+
*
|
|
22
|
+
* Requirements:
|
|
23
|
+
* 1. Every `network_replay` step has a corresponding, non-stale snapshot.
|
|
24
|
+
* 2. No DOM extraction steps exist in the flow.
|
|
25
|
+
*/
|
|
26
|
+
export function isFlowHttpCompatible(steps, snapshots) {
|
|
27
|
+
if (!snapshots)
|
|
28
|
+
return false;
|
|
29
|
+
// Check for DOM extraction steps
|
|
30
|
+
for (const step of steps) {
|
|
31
|
+
if (DOM_EXTRACTION_STEPS.has(step.type)) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
// Check that every network_replay step has a valid snapshot
|
|
36
|
+
const replaySteps = steps.filter((s) => s.type === 'network_replay');
|
|
37
|
+
if (replaySteps.length === 0)
|
|
38
|
+
return false; // No point in HTTP mode without replay steps
|
|
39
|
+
for (const step of replaySteps) {
|
|
40
|
+
const snapshot = snapshots.snapshots[step.id];
|
|
41
|
+
if (!snapshot)
|
|
42
|
+
return false;
|
|
43
|
+
if (isSnapshotStale(snapshot))
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
// HTTP replay
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
/**
|
|
52
|
+
* Make a direct HTTP request using snapshot data + applied overrides.
|
|
53
|
+
* Uses Node's native `fetch()` with an AbortController timeout.
|
|
54
|
+
*/
|
|
55
|
+
export async function replayFromSnapshot(snapshot, inputs, vars, options) {
|
|
56
|
+
const timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
57
|
+
const { url, method, headers, body } = applyOverrides(snapshot, inputs, vars, options?.secrets);
|
|
58
|
+
// Remove content-length — the snapshot captures the original request's
|
|
59
|
+
// content-length, but overrides may change the body size. Node's fetch()
|
|
60
|
+
// sets the correct content-length automatically from the actual body.
|
|
61
|
+
delete headers['content-length'];
|
|
62
|
+
delete headers['Content-Length'];
|
|
63
|
+
const controller = new AbortController();
|
|
64
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
65
|
+
const fetchOptions = {
|
|
66
|
+
method,
|
|
67
|
+
headers,
|
|
68
|
+
signal: controller.signal,
|
|
69
|
+
};
|
|
70
|
+
if (body && method !== 'GET' && method !== 'HEAD') {
|
|
71
|
+
fetchOptions.body = body;
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
const response = await fetch(url, fetchOptions);
|
|
75
|
+
const responseBody = await response.text();
|
|
76
|
+
const contentType = response.headers.get('content-type') ?? undefined;
|
|
77
|
+
return {
|
|
78
|
+
status: response.status,
|
|
79
|
+
contentType,
|
|
80
|
+
body: responseBody,
|
|
81
|
+
bodySize: Buffer.byteLength(responseBody, 'utf8'),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
if (err instanceof Error && err.name === 'AbortError') {
|
|
86
|
+
throw new Error(`HTTP replay timed out after ${timeoutMs}ms for ${method} ${url}`);
|
|
87
|
+
}
|
|
88
|
+
throw err;
|
|
89
|
+
}
|
|
90
|
+
finally {
|
|
91
|
+
clearTimeout(timer);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Replay a snapshot and validate the response.
|
|
96
|
+
* Returns the result along with validation info.
|
|
97
|
+
*/
|
|
98
|
+
export async function replayAndValidate(snapshot, inputs, vars, options) {
|
|
99
|
+
const result = await replayFromSnapshot(snapshot, inputs, vars, options);
|
|
100
|
+
const validation = validateResponse(snapshot, result);
|
|
101
|
+
return { result, validation };
|
|
102
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -10,6 +10,9 @@ export * from './browserLauncher.js';
|
|
|
10
10
|
export * from './browserPersistence.js';
|
|
11
11
|
export * from './packUtils.js';
|
|
12
12
|
export * from './packVersioning.js';
|
|
13
|
+
export * from './config.js';
|
|
14
|
+
export * from './requestSnapshot.js';
|
|
15
|
+
export * from './httpReplay.js';
|
|
13
16
|
export * from './dsl/types.js';
|
|
14
17
|
export * from './dsl/builders.js';
|
|
15
18
|
export * from './dsl/interpreter.js';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,qBAAqB,CAAC;AACpC,cAAc,YAAY,CAAC;AAC3B,cAAc,aAAa,CAAC;AAC5B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,cAAc,CAAC;AAC7B,cAAc,aAAa,CAAC;AAC5B,cAAc,wBAAwB,CAAC;AACvC,cAAc,qBAAqB,CAAC;AACpC,cAAc,sBAAsB,CAAC;AACrC,cAAc,yBAAyB,CAAC;AACxC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,qBAAqB,CAAC;AACpC,cAAc,YAAY,CAAC;AAC3B,cAAc,aAAa,CAAC;AAC5B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,cAAc,CAAC;AAC7B,cAAc,aAAa,CAAC;AAC5B,cAAc,wBAAwB,CAAC;AACvC,cAAc,qBAAqB,CAAC;AACpC,cAAc,sBAAsB,CAAC;AACrC,cAAc,yBAAyB,CAAC;AACxC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,qBAAqB,CAAC;AACpC,cAAc,aAAa,CAAC;AAC5B,cAAc,sBAAsB,CAAC;AACrC,cAAc,iBAAiB,CAAC;AAGhC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,mBAAmB,CAAC;AAClC,cAAc,sBAAsB,CAAC;AACrC,cAAc,qBAAqB,CAAC;AACpC,cAAc,qBAAqB,CAAC;AACpC,cAAc,iBAAiB,CAAC;AAChC,cAAc,qBAAqB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -10,6 +10,9 @@ export * from './browserLauncher.js';
|
|
|
10
10
|
export * from './browserPersistence.js';
|
|
11
11
|
export * from './packUtils.js';
|
|
12
12
|
export * from './packVersioning.js';
|
|
13
|
+
export * from './config.js';
|
|
14
|
+
export * from './requestSnapshot.js';
|
|
15
|
+
export * from './httpReplay.js';
|
|
13
16
|
// DSL exports
|
|
14
17
|
export * from './dsl/types.js';
|
|
15
18
|
export * from './dsl/builders.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"jsonPackValidator.d.ts","sourceRoot":"","sources":["../src/jsonPackValidator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAClE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"jsonPackValidator.d.ts","sourceRoot":"","sources":["../src/jsonPackValidator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAClE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAS9C;;GAEG;AACH,wBAAgB,6BAA6B,CAC3C,YAAY,EAAE,qBAAqB,EAAE,EACrC,IAAI,EAAE,OAAO,EAAE,GACd,IAAI,CA0BN;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,QAAQ,GAAG,IAAI,CAsCzD"}
|
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
import { validateFlow, ValidationError } from './dsl/validation.js';
|
|
2
|
+
/** Step types that produce collectible output via an "out" parameter */
|
|
3
|
+
const STEPS_WITH_OUT = new Set([
|
|
4
|
+
'extract_title', 'extract_text', 'extract_attribute',
|
|
5
|
+
'network_replay', 'network_extract',
|
|
6
|
+
]);
|
|
2
7
|
/**
|
|
3
8
|
* Validates that collectibles referenced in flow steps exist
|
|
4
9
|
*/
|
|
5
10
|
export function validateCollectiblesMatchFlow(collectibles, flow) {
|
|
6
11
|
const collectibleNames = new Set(collectibles.map((c) => c.name));
|
|
7
12
|
const referencedOuts = new Set();
|
|
8
|
-
// Extract all 'out' parameters from
|
|
13
|
+
// Extract all 'out' parameters from steps that write to collectibles
|
|
9
14
|
for (const step of flow) {
|
|
10
|
-
if (
|
|
15
|
+
if (STEPS_WITH_OUT.has(step.type)) {
|
|
11
16
|
const out = step.params?.out;
|
|
12
17
|
if (out && typeof out === 'string') {
|
|
13
18
|
referencedOuts.add(out);
|
|
@@ -15,11 +20,15 @@ export function validateCollectiblesMatchFlow(collectibles, flow) {
|
|
|
15
20
|
}
|
|
16
21
|
}
|
|
17
22
|
// Check that all referenced outs exist in collectibles
|
|
23
|
+
const mismatches = [];
|
|
18
24
|
for (const out of referencedOuts) {
|
|
19
25
|
if (!collectibleNames.has(out)) {
|
|
20
|
-
|
|
26
|
+
mismatches.push(out);
|
|
21
27
|
}
|
|
22
28
|
}
|
|
29
|
+
if (mismatches.length > 0) {
|
|
30
|
+
throw new ValidationError(`Flow step(s) write to undeclared collectible(s): [${mismatches.join(', ')}]. Only declared collectibles are returned in the output. Declared: [${[...collectibleNames].join(', ')}]`);
|
|
31
|
+
}
|
|
23
32
|
}
|
|
24
33
|
/**
|
|
25
34
|
* Validates a JSON Task Pack structure
|
package/dist/loader.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../src/loader.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,gBAAgB,EAAsC,gBAAgB,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../src/loader.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,gBAAgB,EAAsC,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAInH;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,CAAC,CAAC;IACX,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACjC;AAED;;;;;;GAMG;AACH,qBAAa,cAAc;IACzB;;OAEG;IACH,MAAM,CAAC,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,gBAAgB;IA4BvD;;OAEG;WACU,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC;IA4C9D;;;OAGG;IACH,MAAM,CAAC,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;IAwB5D;;OAEG;IACH,MAAM,CAAC,oBAAoB,CAAC,QAAQ,EAAE,MAAM,GAAG,gBAAgB,EAAE;CAQlE"}
|
package/dist/loader.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readFileSync, existsSync } from 'fs';
|
|
2
2
|
import { join } from 'path';
|
|
3
|
+
import { loadSnapshots } from './requestSnapshot.js';
|
|
3
4
|
/**
|
|
4
5
|
* Loads a Task Pack from a directory path
|
|
5
6
|
*
|
|
@@ -54,6 +55,8 @@ export class TaskPackLoader {
|
|
|
54
55
|
if (!flowData.flow || !Array.isArray(flowData.flow)) {
|
|
55
56
|
throw new Error('flow.json must have a "flow" array');
|
|
56
57
|
}
|
|
58
|
+
// Optionally load snapshots.json (not an error if missing)
|
|
59
|
+
const snapshots = loadSnapshots(packPath);
|
|
57
60
|
return {
|
|
58
61
|
metadata: {
|
|
59
62
|
id: manifest.id,
|
|
@@ -66,6 +69,7 @@ export class TaskPackLoader {
|
|
|
66
69
|
flow: flowData.flow,
|
|
67
70
|
auth: manifest.auth,
|
|
68
71
|
browser: manifest.browser,
|
|
72
|
+
...(snapshots ? { snapshots } : {}),
|
|
69
73
|
};
|
|
70
74
|
}
|
|
71
75
|
/**
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request Snapshot types and utilities.
|
|
3
|
+
*
|
|
4
|
+
* Snapshots record the HTTP request/response details of `network_replay` steps
|
|
5
|
+
* so they can be replayed at runtime via direct HTTP calls — no browser needed.
|
|
6
|
+
*/
|
|
7
|
+
export interface RequestSnapshot {
|
|
8
|
+
stepId: string;
|
|
9
|
+
capturedAt: number;
|
|
10
|
+
/** TTL in milliseconds. null = indefinite (never expires by default). */
|
|
11
|
+
ttl: number | null;
|
|
12
|
+
request: {
|
|
13
|
+
method: string;
|
|
14
|
+
url: string;
|
|
15
|
+
headers: Record<string, string>;
|
|
16
|
+
body: string | null;
|
|
17
|
+
};
|
|
18
|
+
overrides?: {
|
|
19
|
+
/** Direct URL override (replaces the snapshot's request URL entirely). */
|
|
20
|
+
url?: string;
|
|
21
|
+
/** Direct body override (replaces the snapshot's request body entirely). */
|
|
22
|
+
body?: string;
|
|
23
|
+
setQuery?: Record<string, string>;
|
|
24
|
+
setHeaders?: Record<string, string>;
|
|
25
|
+
urlReplace?: Array<{
|
|
26
|
+
find: string;
|
|
27
|
+
replace: string;
|
|
28
|
+
}>;
|
|
29
|
+
bodyReplace?: Array<{
|
|
30
|
+
find: string;
|
|
31
|
+
replace: string;
|
|
32
|
+
}>;
|
|
33
|
+
};
|
|
34
|
+
responseValidation: {
|
|
35
|
+
expectedStatus: number;
|
|
36
|
+
expectedContentType: string;
|
|
37
|
+
/** Top-level JSON keys expected in the response body. */
|
|
38
|
+
expectedKeys: string[];
|
|
39
|
+
};
|
|
40
|
+
/** Header names that contain auth data (e.g. authorization, cookie). */
|
|
41
|
+
sensitiveHeaders: string[];
|
|
42
|
+
}
|
|
43
|
+
export interface SnapshotFile {
|
|
44
|
+
version: 1;
|
|
45
|
+
snapshots: Record<string, RequestSnapshot>;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Check if a snapshot is stale based on its TTL.
|
|
49
|
+
* Returns false when `ttl` is null (indefinite).
|
|
50
|
+
*/
|
|
51
|
+
export declare function isSnapshotStale(snapshot: RequestSnapshot): boolean;
|
|
52
|
+
export interface ValidationResult {
|
|
53
|
+
valid: boolean;
|
|
54
|
+
reason?: string;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Validate an HTTP response against a snapshot's expected shape.
|
|
58
|
+
*/
|
|
59
|
+
export declare function validateResponse(snapshot: RequestSnapshot, response: {
|
|
60
|
+
status: number;
|
|
61
|
+
contentType?: string;
|
|
62
|
+
body: string;
|
|
63
|
+
}): ValidationResult;
|
|
64
|
+
/**
|
|
65
|
+
* Apply overrides from the snapshot to produce the final request parameters.
|
|
66
|
+
* Template expressions ({{inputs.x}}, {{vars.y}}, {{secret.z}}) are resolved
|
|
67
|
+
* using Nunjucks — the same engine as the DSL interpreter, including filters.
|
|
68
|
+
*/
|
|
69
|
+
export declare function applyOverrides(snapshot: RequestSnapshot, inputs: Record<string, unknown>, vars: Record<string, unknown>, secrets?: Record<string, string>): {
|
|
70
|
+
url: string;
|
|
71
|
+
method: string;
|
|
72
|
+
headers: Record<string, string>;
|
|
73
|
+
body: string | null;
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* Load snapshots.json from a pack directory. Returns null if not found.
|
|
77
|
+
*/
|
|
78
|
+
export declare function loadSnapshots(packPath: string): SnapshotFile | null;
|
|
79
|
+
/**
|
|
80
|
+
* Write snapshots.json to a pack directory.
|
|
81
|
+
*/
|
|
82
|
+
export declare function writeSnapshots(packPath: string, snapshots: SnapshotFile): void;
|
|
83
|
+
/**
|
|
84
|
+
* Extract top-level keys from a JSON string. Returns empty array on parse failure.
|
|
85
|
+
*/
|
|
86
|
+
export declare function extractTopLevelKeys(jsonText: string | undefined | null): string[];
|
|
87
|
+
/**
|
|
88
|
+
* Detect which headers in a record are sensitive (contain auth data).
|
|
89
|
+
*/
|
|
90
|
+
export declare function detectSensitiveHeaders(headers: Record<string, string>): string[];
|
|
91
|
+
//# sourceMappingURL=requestSnapshot.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"requestSnapshot.d.ts","sourceRoot":"","sources":["../src/requestSnapshot.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAWH,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,yEAAyE;IACzE,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,OAAO,EAAE;QACP,MAAM,EAAE,MAAM,CAAC;QACf,GAAG,EAAE,MAAM,CAAC;QACZ,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QAChC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;KACrB,CAAC;IACF,SAAS,CAAC,EAAE;QACV,0EAA0E;QAC1E,GAAG,CAAC,EAAE,MAAM,CAAC;QACb,4EAA4E;QAC5E,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QAClC,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QACpC,UAAU,CAAC,EAAE,KAAK,CAAC;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,OAAO,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC;QACtD,WAAW,CAAC,EAAE,KAAK,CAAC;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,OAAO,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC;KACxD,CAAC;IACF,kBAAkB,EAAE;QAClB,cAAc,EAAE,MAAM,CAAC;QACvB,mBAAmB,EAAE,MAAM,CAAC;QAC5B,yDAAyD;QACzD,YAAY,EAAE,MAAM,EAAE,CAAC;KACxB,CAAC;IACF,wEAAwE;IACxE,gBAAgB,EAAE,MAAM,EAAE,CAAC;CAC5B;AAED,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,CAAC,CAAC;IACX,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;CAC5C;AAMD;;;GAGG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,eAAe,GAAG,OAAO,CAGlE;AAMD,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,eAAe,EACzB,QAAQ,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC/D,gBAAgB,CA4ClB;AAiBD;;;;GAIG;AACH,wBAAgB,cAAc,CAC5B,QAAQ,EAAE,eAAe,EACzB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC/B;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAoEvF;AAQD;;GAEG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI,CAiBnE;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,YAAY,GAAG,IAAI,CAG9E;AAQD;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,GAAG,MAAM,EAAE,CAWjF;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,EAAE,CAEhF"}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request Snapshot types and utilities.
|
|
3
|
+
*
|
|
4
|
+
* Snapshots record the HTTP request/response details of `network_replay` steps
|
|
5
|
+
* so they can be replayed at runtime via direct HTTP calls — no browser needed.
|
|
6
|
+
*/
|
|
7
|
+
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
|
8
|
+
import { join } from 'path';
|
|
9
|
+
import { resolveTemplate } from './dsl/templating.js';
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Staleness
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
/**
|
|
14
|
+
* Check if a snapshot is stale based on its TTL.
|
|
15
|
+
* Returns false when `ttl` is null (indefinite).
|
|
16
|
+
*/
|
|
17
|
+
export function isSnapshotStale(snapshot) {
|
|
18
|
+
if (snapshot.ttl === null)
|
|
19
|
+
return false;
|
|
20
|
+
return Date.now() - snapshot.capturedAt > snapshot.ttl;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Validate an HTTP response against a snapshot's expected shape.
|
|
24
|
+
*/
|
|
25
|
+
export function validateResponse(snapshot, response) {
|
|
26
|
+
const v = snapshot.responseValidation;
|
|
27
|
+
if (response.status !== v.expectedStatus) {
|
|
28
|
+
return {
|
|
29
|
+
valid: false,
|
|
30
|
+
reason: `Expected status ${v.expectedStatus}, got ${response.status}`,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
if (v.expectedContentType &&
|
|
34
|
+
response.contentType &&
|
|
35
|
+
!response.contentType.toLowerCase().startsWith(v.expectedContentType.toLowerCase())) {
|
|
36
|
+
return {
|
|
37
|
+
valid: false,
|
|
38
|
+
reason: `Expected content-type starting with "${v.expectedContentType}", got "${response.contentType}"`,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
if (v.expectedKeys.length > 0) {
|
|
42
|
+
try {
|
|
43
|
+
const parsed = JSON.parse(response.body);
|
|
44
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
45
|
+
const bodyKeys = Object.keys(parsed);
|
|
46
|
+
const missing = v.expectedKeys.filter((k) => !bodyKeys.includes(k));
|
|
47
|
+
if (missing.length > 0) {
|
|
48
|
+
return {
|
|
49
|
+
valid: false,
|
|
50
|
+
reason: `Missing expected keys: ${missing.join(', ')}`,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
// If we expected JSON keys but can't parse, that's a validation failure
|
|
57
|
+
return {
|
|
58
|
+
valid: false,
|
|
59
|
+
reason: 'Response body is not valid JSON but expectedKeys were specified',
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return { valid: true };
|
|
64
|
+
}
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
// Override application
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
/**
|
|
69
|
+
* Resolve a template string using Nunjucks (same engine as the DSL interpreter).
|
|
70
|
+
* Supports {{inputs.x}}, {{vars.x}}, {{secret.x}} and filters like `| urlencode`.
|
|
71
|
+
*/
|
|
72
|
+
function resolveTemplateValue(template, ctx) {
|
|
73
|
+
return resolveTemplate(template, ctx);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Apply overrides from the snapshot to produce the final request parameters.
|
|
77
|
+
* Template expressions ({{inputs.x}}, {{vars.y}}, {{secret.z}}) are resolved
|
|
78
|
+
* using Nunjucks — the same engine as the DSL interpreter, including filters.
|
|
79
|
+
*/
|
|
80
|
+
export function applyOverrides(snapshot, inputs, vars, secrets) {
|
|
81
|
+
const ov = snapshot.overrides;
|
|
82
|
+
let url = snapshot.request.url;
|
|
83
|
+
let body = snapshot.request.body;
|
|
84
|
+
const method = snapshot.request.method;
|
|
85
|
+
const headers = { ...snapshot.request.headers };
|
|
86
|
+
if (!ov) {
|
|
87
|
+
return { url, method, headers, body };
|
|
88
|
+
}
|
|
89
|
+
const ctx = { inputs, vars, secrets: secrets ?? {} };
|
|
90
|
+
// urlReplace
|
|
91
|
+
if (ov.urlReplace) {
|
|
92
|
+
for (const r of ov.urlReplace) {
|
|
93
|
+
const find = resolveTemplateValue(r.find, ctx);
|
|
94
|
+
const replace = resolveTemplateValue(r.replace, ctx);
|
|
95
|
+
try {
|
|
96
|
+
url = url.replace(new RegExp(find, 'g'), replace);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
// If regex is invalid, try literal replace
|
|
100
|
+
url = url.split(find).join(replace);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// setQuery
|
|
105
|
+
if (ov.setQuery) {
|
|
106
|
+
const u = new URL(url);
|
|
107
|
+
for (const [k, v] of Object.entries(ov.setQuery)) {
|
|
108
|
+
const resolved = resolveTemplateValue(v, ctx);
|
|
109
|
+
u.searchParams.set(k, resolved);
|
|
110
|
+
}
|
|
111
|
+
url = u.toString();
|
|
112
|
+
}
|
|
113
|
+
// setHeaders
|
|
114
|
+
if (ov.setHeaders) {
|
|
115
|
+
for (const [k, v] of Object.entries(ov.setHeaders)) {
|
|
116
|
+
headers[k] = resolveTemplateValue(v, ctx);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// Direct URL override (applied after urlReplace, matching networkCapture.ts)
|
|
120
|
+
if (ov.url != null) {
|
|
121
|
+
url = resolveTemplateValue(ov.url, ctx);
|
|
122
|
+
}
|
|
123
|
+
// bodyReplace
|
|
124
|
+
if (body && ov.bodyReplace) {
|
|
125
|
+
for (const r of ov.bodyReplace) {
|
|
126
|
+
const find = resolveTemplateValue(r.find, ctx);
|
|
127
|
+
const replace = resolveTemplateValue(r.replace, ctx);
|
|
128
|
+
try {
|
|
129
|
+
body = body.replace(new RegExp(find, 'g'), replace);
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
body = body.split(find).join(replace);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
// Direct body override (applied after bodyReplace, matching networkCapture.ts)
|
|
137
|
+
if (ov.body != null) {
|
|
138
|
+
body = resolveTemplateValue(ov.body, ctx);
|
|
139
|
+
}
|
|
140
|
+
return { url, method, headers, body };
|
|
141
|
+
}
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
// File I/O
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
const SNAPSHOTS_FILENAME = 'snapshots.json';
|
|
146
|
+
/**
|
|
147
|
+
* Load snapshots.json from a pack directory. Returns null if not found.
|
|
148
|
+
*/
|
|
149
|
+
export function loadSnapshots(packPath) {
|
|
150
|
+
const filePath = join(packPath, SNAPSHOTS_FILENAME);
|
|
151
|
+
if (!existsSync(filePath))
|
|
152
|
+
return null;
|
|
153
|
+
try {
|
|
154
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
155
|
+
const data = JSON.parse(content);
|
|
156
|
+
if (data.version !== 1) {
|
|
157
|
+
console.warn(`[requestSnapshot] Unsupported snapshot version: ${data.version}`);
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
return data;
|
|
161
|
+
}
|
|
162
|
+
catch (err) {
|
|
163
|
+
console.warn(`[requestSnapshot] Failed to load snapshots.json: ${err instanceof Error ? err.message : String(err)}`);
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Write snapshots.json to a pack directory.
|
|
169
|
+
*/
|
|
170
|
+
export function writeSnapshots(packPath, snapshots) {
|
|
171
|
+
const filePath = join(packPath, SNAPSHOTS_FILENAME);
|
|
172
|
+
writeFileSync(filePath, JSON.stringify(snapshots, null, 2), 'utf-8');
|
|
173
|
+
}
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
// Capture helpers
|
|
176
|
+
// ---------------------------------------------------------------------------
|
|
177
|
+
const SENSITIVE_HEADER_NAMES = ['authorization', 'cookie', 'set-cookie', 'x-api-key', 'proxy-authorization'];
|
|
178
|
+
/**
|
|
179
|
+
* Extract top-level keys from a JSON string. Returns empty array on parse failure.
|
|
180
|
+
*/
|
|
181
|
+
export function extractTopLevelKeys(jsonText) {
|
|
182
|
+
if (!jsonText)
|
|
183
|
+
return [];
|
|
184
|
+
try {
|
|
185
|
+
const parsed = JSON.parse(jsonText);
|
|
186
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
187
|
+
return Object.keys(parsed);
|
|
188
|
+
}
|
|
189
|
+
return [];
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
return [];
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Detect which headers in a record are sensitive (contain auth data).
|
|
197
|
+
*/
|
|
198
|
+
export function detectSensitiveHeaders(headers) {
|
|
199
|
+
return Object.keys(headers).filter((h) => SENSITIVE_HEADER_NAMES.includes(h.toLowerCase()));
|
|
200
|
+
}
|
package/dist/runner.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../src/runner.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAc,MAAM,YAAY,CAAC;AAElE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../src/runner.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAc,MAAM,YAAY,CAAC;AAElE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAWzC;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC;;OAEG;IACH,MAAM,EAAE,MAAM,CAAC;IACf;;OAEG;IACH,MAAM,EAAE,MAAM,CAAC;IACf;;OAEG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;OAEG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;OAEG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;OAEG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;OAEG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClC;AAED;;GAEG;AACH,MAAM,WAAW,iBAAkB,SAAQ,SAAS;IAClD;;OAEG;IACH,MAAM,EAAE,MAAM,CAAC;IACf;;OAEG;IACH,UAAU,EAAE,MAAM,CAAC;IACnB;;OAEG;IACH,YAAY,EAAE,MAAM,CAAC;CACtB;AAED;;;GAGG;AACH,wBAAsB,WAAW,CAC/B,QAAQ,EAAE,QAAQ,EAClB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,OAAO,EAAE,kBAAkB,GAC1B,OAAO,CAAC,iBAAiB,CAAC,CA0O5B"}
|