executable-stories-showcase 0.0.0-stage → 0.0.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/LICENSE +202 -0
- package/README.md +17 -2
- package/dist/adapters/cookies.d.ts +11 -0
- package/dist/adapters/cookies.js +27 -0
- package/dist/adapters/cookies.js.map +1 -0
- package/dist/adapters/redux.d.ts +23 -0
- package/dist/adapters/redux.js +28 -0
- package/dist/adapters/redux.js.map +1 -0
- package/dist/adapters/tanstack-query.d.ts +17 -0
- package/dist/adapters/tanstack-query.js +43 -0
- package/dist/adapters/tanstack-query.js.map +1 -0
- package/dist/adapters/xstate.d.ts +33 -0
- package/dist/adapters/xstate.js +36 -0
- package/dist/adapters/xstate.js.map +1 -0
- package/dist/adapters/zustand.d.ts +17 -0
- package/dist/adapters/zustand.js +16 -0
- package/dist/adapters/zustand.js.map +1 -0
- package/dist/capture-GCESXDQD.js +10 -0
- package/dist/capture-GCESXDQD.js.map +1 -0
- package/dist/capture.d.ts +26 -0
- package/dist/catalog.d.ts +27 -0
- package/dist/chunk-7VEOUHJP.js +40 -0
- package/dist/chunk-7VEOUHJP.js.map +1 -0
- package/dist/chunk-BK4EEQXV.js +98 -0
- package/dist/chunk-BK4EEQXV.js.map +1 -0
- package/dist/chunk-MCC3CAWQ.js +208 -0
- package/dist/chunk-MCC3CAWQ.js.map +1 -0
- package/dist/chunk-PKGBYTBX.js +46 -0
- package/dist/chunk-PKGBYTBX.js.map +1 -0
- package/dist/chunk-QDLRQPTA.js +125 -0
- package/dist/chunk-QDLRQPTA.js.map +1 -0
- package/dist/chunk-XLNYICOI.js +234 -0
- package/dist/chunk-XLNYICOI.js.map +1 -0
- package/dist/chunk-Z664X6AQ.js +148 -0
- package/dist/chunk-Z664X6AQ.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +531 -0
- package/dist/index.js.map +1 -0
- package/dist/jwt.d.ts +7 -0
- package/dist/link.d.ts +32 -0
- package/dist/network-4UFPO74C.js +7 -0
- package/dist/network-4UFPO74C.js.map +1 -0
- package/dist/network.d.ts +3 -0
- package/dist/next.d.ts +21 -0
- package/dist/next.js +30 -0
- package/dist/next.js.map +1 -0
- package/dist/no-msw-browser.d.ts +2 -0
- package/dist/no-msw-browser.js +8 -0
- package/dist/no-msw-browser.js.map +1 -0
- package/dist/panel.d.ts +30 -0
- package/dist/panel.js +608 -0
- package/dist/panel.js.map +1 -0
- package/dist/playwright.d.ts +31 -0
- package/dist/playwright.js +92 -0
- package/dist/playwright.js.map +1 -0
- package/dist/ready-4MENAMNI.js +13 -0
- package/dist/ready-4MENAMNI.js.map +1 -0
- package/dist/ready.d.ts +39 -0
- package/dist/record.d.ts +49 -0
- package/dist/redact.d.ts +19 -0
- package/dist/runtime.d.ts +191 -0
- package/dist/save.d.ts +27 -0
- package/dist/schema.d.ts +161 -0
- package/dist/vite.d.ts +11 -0
- package/dist/vite.js +26 -0
- package/dist/vite.js.map +1 -0
- package/package.json +118 -3
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// src/schema.ts
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
var JsonSchema = z.json();
|
|
4
|
+
var parseJson = (text) => JsonSchema.parse(JSON.parse(text));
|
|
5
|
+
var MethodSchema = z.enum(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]);
|
|
6
|
+
var ResponseSchema = z.object({
|
|
7
|
+
status: z.number().int().default(200),
|
|
8
|
+
/** Sent as JSON. */
|
|
9
|
+
body: JsonSchema.optional(),
|
|
10
|
+
/** Sent as text, e.g. an HTML error page. */
|
|
11
|
+
text: z.string().optional(),
|
|
12
|
+
headers: z.record(z.string(), z.string()).optional(),
|
|
13
|
+
/** Milliseconds, or "infinite" to hold the request open (loading states). */
|
|
14
|
+
delay: z.union([z.number().nonnegative(), z.literal("infinite")]).optional()
|
|
15
|
+
}).refine((r) => r.body === void 0 || r.text === void 0, {
|
|
16
|
+
message: 'Provide "body" (JSON) or "text", not both'
|
|
17
|
+
});
|
|
18
|
+
var NetworkEntrySchema = z.object({
|
|
19
|
+
method: MethodSchema.default("GET"),
|
|
20
|
+
/** MSW path syntax: "/api/users/:id", absolute URLs and wildcards work. */
|
|
21
|
+
path: z.string().min(1),
|
|
22
|
+
/** Only match requests whose query string contains these values. */
|
|
23
|
+
query: z.record(z.string(), z.string()).optional(),
|
|
24
|
+
response: ResponseSchema.optional(),
|
|
25
|
+
/** Successive responses for successive calls; the last one repeats. */
|
|
26
|
+
sequence: z.array(ResponseSchema).min(1).optional(),
|
|
27
|
+
/** Drop the inherited entry with the same method, path and query. */
|
|
28
|
+
remove: z.literal(true).optional()
|
|
29
|
+
}).refine((e) => [e.response, e.sequence, e.remove].filter(Boolean).length === 1, {
|
|
30
|
+
message: 'Provide exactly one of "response", "sequence" or "remove"'
|
|
31
|
+
});
|
|
32
|
+
var ScenarioSchema = z.object({
|
|
33
|
+
$schema: z.string().optional(),
|
|
34
|
+
// Kebab-case; a combined scenario is named by its parts, e.g. "signed-in,server-error".
|
|
35
|
+
name: z.string().regex(/^[a-z0-9][a-z0-9-]*(,[a-z0-9][a-z0-9-]*)*$/, 'Use kebab-case, e.g. "empty-search"'),
|
|
36
|
+
/** Not inherited. */
|
|
37
|
+
description: z.string().optional(),
|
|
38
|
+
/** Not inherited. */
|
|
39
|
+
tags: z.array(z.string()).optional(),
|
|
40
|
+
/** Scenario name(s) to build on. Later parents override earlier ones. */
|
|
41
|
+
extends: z.union([z.string(), z.array(z.string())]).optional(),
|
|
42
|
+
/** App route the scenario opens at, e.g. "/orders/42". Used by links, the panel, `shoot` and `openInlineScenario`. */
|
|
43
|
+
path: z.string().startsWith("/").optional(),
|
|
44
|
+
/** Query params the app should start with. null removes an inherited param. */
|
|
45
|
+
url: z.record(z.string(), z.string().nullable()).optional(),
|
|
46
|
+
/**
|
|
47
|
+
* A child entry replaces the inherited entry with the same method, path and query.
|
|
48
|
+
* Otherwise earlier entries win, and a scenario's own entries come before its parents'.
|
|
49
|
+
*/
|
|
50
|
+
network: z.array(NetworkEntrySchema).optional(),
|
|
51
|
+
/**
|
|
52
|
+
* Names of MSW handlers passed to `startScenarios({ handlers })`, e.g. ones your unit tests
|
|
53
|
+
* already use. They answer after this scenario's `network` entries. Inherited and combined.
|
|
54
|
+
*/
|
|
55
|
+
handlers: z.array(z.string().min(1)).optional(),
|
|
56
|
+
/** Per-adapter state, keyed by adapter key. Objects deep-merge; null removes a key. */
|
|
57
|
+
state: z.record(z.string(), JsonSchema).optional(),
|
|
58
|
+
/**
|
|
59
|
+
* Playwright selector that is visible once the UI shows this state,
|
|
60
|
+
* e.g. "text=Loading countries…". Not inherited: each state looks different.
|
|
61
|
+
*/
|
|
62
|
+
ready: z.string().optional(),
|
|
63
|
+
/** Browser size `shoot` and `record` use for this scenario, e.g. a phone layout. Inherited. */
|
|
64
|
+
viewport: z.object({ width: z.number().int().positive(), height: z.number().int().positive() }).optional()
|
|
65
|
+
});
|
|
66
|
+
function scenarioJsonSchema({ state = {} } = {}) {
|
|
67
|
+
return z.toJSONSchema(
|
|
68
|
+
ScenarioSchema.extend({ state: z.object(state).partial().catchall(JsonSchema).optional() }),
|
|
69
|
+
{
|
|
70
|
+
io: "input",
|
|
71
|
+
// Refinements aren't representable in JSON Schema; spell the exclusivity rules out.
|
|
72
|
+
override: ({ zodSchema, jsonSchema }) => {
|
|
73
|
+
if (zodSchema === NetworkEntrySchema) {
|
|
74
|
+
jsonSchema.oneOf = ["response", "sequence", "remove"].map((key) => ({ required: [key] }));
|
|
75
|
+
}
|
|
76
|
+
if (zodSchema === ResponseSchema) {
|
|
77
|
+
jsonSchema.not = { required: ["body", "text"] };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
function parseScenario(data, source = "scenario") {
|
|
84
|
+
const result = ScenarioSchema.safeParse(data);
|
|
85
|
+
if (result.success) return result.data;
|
|
86
|
+
throw new Error(`Invalid scenario "${data.name || source}":
|
|
87
|
+
${z.prettifyError(result.error)}`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export {
|
|
91
|
+
JsonSchema,
|
|
92
|
+
parseJson,
|
|
93
|
+
MethodSchema,
|
|
94
|
+
ScenarioSchema,
|
|
95
|
+
scenarioJsonSchema,
|
|
96
|
+
parseScenario
|
|
97
|
+
};
|
|
98
|
+
//# sourceMappingURL=chunk-BK4EEQXV.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/schema.ts"],"sourcesContent":["import { z } from 'zod';\n\n/** Scenarios live in JSON files and URLs, so everything they carry is JSON. */\nexport const JsonSchema = z.json();\n\nexport type Json = z.output<typeof JsonSchema>;\n\nexport type JsonObject = { [key: string]: Json; };\n\n/** Anything an app can hand over to be stored as JSON. */\nexport type Serializable = object | string | number | boolean | null;\n\n/**\n * Decode JSON text. Pair with `JSON.stringify` to copy app state into the format scenarios\n * are stored in: functions and undefined drop out, and the result is checked to be JSON.\n */\nexport const parseJson = (text: string): Json => JsonSchema.parse(JSON.parse(text));\n\nexport const MethodSchema = z.enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS']);\n\nconst ResponseSchema = z\n .object({\n status: z.number().int().default(200),\n /** Sent as JSON. */\n body: JsonSchema.optional(),\n /** Sent as text, e.g. an HTML error page. */\n text: z.string().optional(),\n headers: z.record(z.string(), z.string()).optional(),\n /** Milliseconds, or \"infinite\" to hold the request open (loading states). */\n delay: z.union([z.number().nonnegative(), z.literal('infinite')]).optional(),\n })\n .refine((r) => r.body === undefined || r.text === undefined, {\n message: 'Provide \"body\" (JSON) or \"text\", not both',\n });\n\nconst NetworkEntrySchema = z\n .object({\n method: MethodSchema.default('GET'),\n /** MSW path syntax: \"/api/users/:id\", absolute URLs and wildcards work. */\n path: z.string().min(1),\n /** Only match requests whose query string contains these values. */\n query: z.record(z.string(), z.string()).optional(),\n response: ResponseSchema.optional(),\n /** Successive responses for successive calls; the last one repeats. */\n sequence: z.array(ResponseSchema).min(1).optional(),\n /** Drop the inherited entry with the same method, path and query. */\n remove: z.literal(true).optional(),\n })\n .refine((e) => [e.response, e.sequence, e.remove].filter(Boolean).length === 1, {\n message: 'Provide exactly one of \"response\", \"sequence\" or \"remove\"',\n });\n\nexport const ScenarioSchema = z.object({\n $schema: z.string().optional(),\n // Kebab-case; a combined scenario is named by its parts, e.g. \"signed-in,server-error\".\n name: z.string().regex(/^[a-z0-9][a-z0-9-]*(,[a-z0-9][a-z0-9-]*)*$/, 'Use kebab-case, e.g. \"empty-search\"'),\n /** Not inherited. */\n description: z.string().optional(),\n /** Not inherited. */\n tags: z.array(z.string()).optional(),\n /** Scenario name(s) to build on. Later parents override earlier ones. */\n extends: z.union([z.string(), z.array(z.string())]).optional(),\n /** App route the scenario opens at, e.g. \"/orders/42\". Used by links, the panel, `shoot` and `openInlineScenario`. */\n path: z.string().startsWith('/').optional(),\n /** Query params the app should start with. null removes an inherited param. */\n url: z.record(z.string(), z.string().nullable()).optional(),\n /**\n * A child entry replaces the inherited entry with the same method, path and query.\n * Otherwise earlier entries win, and a scenario's own entries come before its parents'.\n */\n network: z.array(NetworkEntrySchema).optional(),\n /**\n * Names of MSW handlers passed to `startScenarios({ handlers })`, e.g. ones your unit tests\n * already use. They answer after this scenario's `network` entries. Inherited and combined.\n */\n handlers: z.array(z.string().min(1)).optional(),\n /** Per-adapter state, keyed by adapter key. Objects deep-merge; null removes a key. */\n state: z.record(z.string(), JsonSchema).optional(),\n /**\n * Playwright selector that is visible once the UI shows this state,\n * e.g. \"text=Loading countries…\". Not inherited: each state looks different.\n */\n ready: z.string().optional(),\n /** Browser size `shoot` and `record` use for this scenario, e.g. a phone layout. Inherited. */\n viewport: z.object({ width: z.number().int().positive(), height: z.number().int().positive() }).optional(),\n});\n\nexport type Scenario = z.output<typeof ScenarioSchema>;\n\nexport type ScenarioInput = z.input<typeof ScenarioSchema>;\n\nexport type NetworkEntry = z.output<typeof NetworkEntrySchema>;\n\nexport type ScenarioResponse = z.output<typeof ResponseSchema>;\n\n/**\n * JSON Schema for editors, validators and AI generators.\n * Pass adapter state schemas so `state` is described too.\n */\nexport function scenarioJsonSchema({ state = {} }: { state?: Record<string, z.ZodType>; } = {}) {\n return z.toJSONSchema(\n ScenarioSchema.extend({ state: z.object(state).partial().catchall(JsonSchema).optional() }),\n {\n io: 'input',\n // Refinements aren't representable in JSON Schema; spell the exclusivity rules out.\n override: ({ zodSchema, jsonSchema }) => {\n if (zodSchema === NetworkEntrySchema) {\n jsonSchema.oneOf = ['response', 'sequence', 'remove'].map((key) => ({ required: [key] }));\n }\n\n if (zodSchema === ResponseSchema) {\n jsonSchema.not = { required: ['body', 'text'] };\n }\n },\n },\n );\n}\n\n/** Validate scenario input (from a file, URL or editor) into a Scenario, with readable errors. */\nexport function parseScenario(data: ScenarioInput, source = 'scenario'): Scenario {\n const result = ScenarioSchema.safeParse(data);\n\n if (result.success) return result.data;\n throw new Error(`Invalid scenario \"${data.name || source}\":\\n${z.prettifyError(result.error)}`);\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import {
|
|
2
|
+
parseScenario
|
|
3
|
+
} from "./chunk-BK4EEQXV.js";
|
|
4
|
+
|
|
5
|
+
// src/catalog.ts
|
|
6
|
+
var defineScenarioRules = (rules) => rules;
|
|
7
|
+
function createCatalog(raw, { rules = [] } = {}) {
|
|
8
|
+
const byName = /* @__PURE__ */ new Map();
|
|
9
|
+
for (const [i, data] of raw.entries()) {
|
|
10
|
+
const s = parseScenario(data, `#${i}`);
|
|
11
|
+
if (s.name.includes(",")) throw new Error(`Scenario name "${s.name}" can't contain a comma`);
|
|
12
|
+
if (byName.has(s.name)) throw new Error(`Duplicate scenario name "${s.name}"`);
|
|
13
|
+
byName.set(s.name, s);
|
|
14
|
+
}
|
|
15
|
+
const flatten = (s, seen) => {
|
|
16
|
+
if (seen.includes(s.name)) {
|
|
17
|
+
throw new Error(`Circular extends: ${[...seen, s.name].join(" -> ")}`);
|
|
18
|
+
}
|
|
19
|
+
const parents = [s.extends ?? []].flat().map((name) => {
|
|
20
|
+
const parent = byName.get(name);
|
|
21
|
+
if (!parent) throw new Error(`Scenario "${s.name}" extends unknown "${name}"`);
|
|
22
|
+
return flatten(parent, [...seen, s.name]);
|
|
23
|
+
});
|
|
24
|
+
const { extends: _extends, $schema: _schema, ...own } = s;
|
|
25
|
+
return [...parents, own].reduce(merge, { name: own.name });
|
|
26
|
+
};
|
|
27
|
+
const resolve = (s) => {
|
|
28
|
+
const flat = flatten(s, []);
|
|
29
|
+
const violations = rules.flatMap((rule) => rule(flat) ?? []);
|
|
30
|
+
if (violations.length) {
|
|
31
|
+
throw new Error(`Scenario "${s.name}" breaks rules:
|
|
32
|
+
${violations.map((m) => `- ${m}`).join("\n")}`);
|
|
33
|
+
}
|
|
34
|
+
return flat;
|
|
35
|
+
};
|
|
36
|
+
return {
|
|
37
|
+
list: [...byName.values()],
|
|
38
|
+
get: (name) => byName.get(name),
|
|
39
|
+
resolve,
|
|
40
|
+
check: () => [...byName.values()].flatMap((s) => {
|
|
41
|
+
try {
|
|
42
|
+
resolve(s);
|
|
43
|
+
return [];
|
|
44
|
+
} catch (err) {
|
|
45
|
+
return [err instanceof Error ? err.message : String(err)];
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
var byKey = ([a], [b]) => a.localeCompare(b);
|
|
51
|
+
var routeKey = (e) => `${e.method} ${e.path} ${JSON.stringify(Object.entries(e.query ?? {}).toSorted(byKey))}`;
|
|
52
|
+
function merge(base, over) {
|
|
53
|
+
const overKeys = new Set((over.network ?? []).map(routeKey));
|
|
54
|
+
return {
|
|
55
|
+
...base,
|
|
56
|
+
...over,
|
|
57
|
+
// Own-only fields: a child describes and finishes rendering differently.
|
|
58
|
+
description: over.description,
|
|
59
|
+
tags: over.tags,
|
|
60
|
+
ready: over.ready,
|
|
61
|
+
// A null param removes the inherited one.
|
|
62
|
+
url: Object.fromEntries(Object.entries({ ...base.url, ...over.url }).filter(([, v]) => v !== null)),
|
|
63
|
+
network: [
|
|
64
|
+
...(over.network ?? []).filter((e) => !e.remove),
|
|
65
|
+
...(base.network ?? []).filter((e) => !overKeys.has(routeKey(e)))
|
|
66
|
+
],
|
|
67
|
+
handlers: union(over.handlers, base.handlers),
|
|
68
|
+
state: mergeState(base.state ?? {}, over.state ?? {})
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function mergeState(base, over) {
|
|
72
|
+
const out = mergeObjects(base, over);
|
|
73
|
+
for (const [k, v] of Object.entries(over)) {
|
|
74
|
+
if (v === null) out[k] = null;
|
|
75
|
+
}
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
var union = (a = [], b = []) => {
|
|
79
|
+
const all = [.../* @__PURE__ */ new Set([...a, ...b])];
|
|
80
|
+
return all.length ? all : void 0;
|
|
81
|
+
};
|
|
82
|
+
var isJsonObject = (value) => value instanceof Object && !Array.isArray(value);
|
|
83
|
+
function mergeObjects(base, over) {
|
|
84
|
+
const out = { ...base };
|
|
85
|
+
for (const [k, v] of Object.entries(over)) {
|
|
86
|
+
if (v === null) delete out[k];
|
|
87
|
+
else out[k] = deepMerge(base[k], v);
|
|
88
|
+
}
|
|
89
|
+
return out;
|
|
90
|
+
}
|
|
91
|
+
function deepMerge(base, over) {
|
|
92
|
+
return isJsonObject(base) && isJsonObject(over) ? mergeObjects(base, over) : over;
|
|
93
|
+
}
|
|
94
|
+
function diffScenario(captured, base) {
|
|
95
|
+
if (!base) {
|
|
96
|
+
return compactScenario({
|
|
97
|
+
name: captured.name,
|
|
98
|
+
description: captured.description,
|
|
99
|
+
tags: captured.tags,
|
|
100
|
+
path: captured.path,
|
|
101
|
+
url: captured.url,
|
|
102
|
+
network: captured.network,
|
|
103
|
+
handlers: captured.handlers,
|
|
104
|
+
state: captured.state,
|
|
105
|
+
ready: captured.ready,
|
|
106
|
+
viewport: captured.viewport
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
const url = diffUrl(base.url, captured.url);
|
|
110
|
+
const network = diffNetwork(base.network ?? [], captured.network ?? []);
|
|
111
|
+
const state = diffObjects(
|
|
112
|
+
Object.fromEntries(Object.entries(base.state ?? {}).filter(([k]) => k in (captured.state ?? {}))),
|
|
113
|
+
captured.state ?? {}
|
|
114
|
+
);
|
|
115
|
+
const handlers = diffHandlers(base.handlers ?? [], captured.handlers ?? []);
|
|
116
|
+
return compactScenario({
|
|
117
|
+
name: captured.name,
|
|
118
|
+
extends: base.name,
|
|
119
|
+
description: captured.description,
|
|
120
|
+
tags: captured.tags,
|
|
121
|
+
path: captured.path !== base.path ? captured.path : void 0,
|
|
122
|
+
url,
|
|
123
|
+
network: network.length ? network : void 0,
|
|
124
|
+
handlers,
|
|
125
|
+
state,
|
|
126
|
+
ready: captured.ready,
|
|
127
|
+
viewport: jsonEqual(captured.viewport ?? null, base.viewport ?? null) ? void 0 : captured.viewport
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
function diffHandlers(base, captured) {
|
|
131
|
+
for (let k = 0; k <= captured.length; k++) {
|
|
132
|
+
const own = captured.slice(0, k);
|
|
133
|
+
if (JSON.stringify(union(own, base)) === JSON.stringify(captured)) return own.length ? own : void 0;
|
|
134
|
+
}
|
|
135
|
+
return captured.length ? captured : void 0;
|
|
136
|
+
}
|
|
137
|
+
function diffUrl(base, captured) {
|
|
138
|
+
const baseUrl = base ?? {};
|
|
139
|
+
const capturedUrl = captured ?? {};
|
|
140
|
+
const out = {};
|
|
141
|
+
for (const [k, v] of Object.entries(capturedUrl)) {
|
|
142
|
+
if (v === null) continue;
|
|
143
|
+
if (baseUrl[k] !== v) out[k] = v;
|
|
144
|
+
}
|
|
145
|
+
for (const [k, v] of Object.entries(baseUrl)) {
|
|
146
|
+
if (v === null) continue;
|
|
147
|
+
if (!(k in capturedUrl) || capturedUrl[k] === null) out[k] = null;
|
|
148
|
+
}
|
|
149
|
+
return Object.keys(out).length ? out : void 0;
|
|
150
|
+
}
|
|
151
|
+
function diffNetwork(base, captured) {
|
|
152
|
+
const baseByKey = new Map(base.map((e) => [routeKey(e), e]));
|
|
153
|
+
const capByKey = /* @__PURE__ */ new Map();
|
|
154
|
+
for (const e of captured) if (!capByKey.has(routeKey(e))) capByKey.set(routeKey(e), e);
|
|
155
|
+
const out = [];
|
|
156
|
+
for (const [key, e] of capByKey) {
|
|
157
|
+
const prior = baseByKey.get(key);
|
|
158
|
+
if (!prior || !jsonEqual(prior, e)) out.push(e);
|
|
159
|
+
}
|
|
160
|
+
for (const [key, e] of baseByKey) {
|
|
161
|
+
if (!capByKey.has(key)) {
|
|
162
|
+
const removed = { method: e.method, path: e.path, remove: true };
|
|
163
|
+
if (e.query) removed.query = e.query;
|
|
164
|
+
out.push(removed);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return out;
|
|
168
|
+
}
|
|
169
|
+
function diffObjects(base, captured) {
|
|
170
|
+
const out = {};
|
|
171
|
+
for (const [k, v] of Object.entries(captured)) {
|
|
172
|
+
if (!(k in base)) out[k] = v;
|
|
173
|
+
else if (isJsonObject(base[k]) && isJsonObject(v)) {
|
|
174
|
+
const nested = diffObjects(base[k], v);
|
|
175
|
+
if (nested) out[k] = nested;
|
|
176
|
+
} else if (!jsonEqual(base[k], v)) out[k] = v;
|
|
177
|
+
}
|
|
178
|
+
for (const k of Object.keys(base)) {
|
|
179
|
+
if (!(k in captured)) out[k] = null;
|
|
180
|
+
}
|
|
181
|
+
return Object.keys(out).length ? out : void 0;
|
|
182
|
+
}
|
|
183
|
+
function jsonEqual(a, b) {
|
|
184
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
185
|
+
}
|
|
186
|
+
function compactScenario(s) {
|
|
187
|
+
const out = { name: s.name };
|
|
188
|
+
if (s.$schema !== void 0) out.$schema = s.$schema;
|
|
189
|
+
if (s.description !== void 0) out.description = s.description;
|
|
190
|
+
if (s.tags !== void 0) out.tags = s.tags;
|
|
191
|
+
if (s.extends !== void 0) out.extends = s.extends;
|
|
192
|
+
if (s.path !== void 0) out.path = s.path;
|
|
193
|
+
if (s.url !== void 0 && Object.keys(s.url).length) out.url = s.url;
|
|
194
|
+
if (s.network !== void 0 && s.network.length) out.network = s.network;
|
|
195
|
+
if (s.handlers !== void 0 && s.handlers.length) out.handlers = s.handlers;
|
|
196
|
+
if (s.state !== void 0 && Object.keys(s.state).length) out.state = s.state;
|
|
197
|
+
if (s.ready !== void 0) out.ready = s.ready;
|
|
198
|
+
if (s.viewport !== void 0) out.viewport = s.viewport;
|
|
199
|
+
return out;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export {
|
|
203
|
+
defineScenarioRules,
|
|
204
|
+
createCatalog,
|
|
205
|
+
deepMerge,
|
|
206
|
+
diffScenario
|
|
207
|
+
};
|
|
208
|
+
//# sourceMappingURL=chunk-MCC3CAWQ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/catalog.ts"],"sourcesContent":["import {\n type Json,\n type JsonObject,\n type NetworkEntry,\n parseScenario,\n type Scenario,\n type ScenarioInput,\n} from './schema.js';\n\n/**\n * A business rule a resolved scenario must satisfy, beyond schema shape.\n * Return a message (or several) for each violation.\n */\nexport type ScenarioRule = (scenario: Scenario) => string | string[] | void | undefined;\n\n/** Identity helper for typing rule lists. */\nexport const defineScenarioRules = (rules: ScenarioRule[]) => rules;\n\nexport interface Catalog {\n list: Scenario[];\n get(name: string): Scenario | undefined;\n /** Flatten `extends` into one self-contained scenario, then check the rules. */\n resolve(scenario: Scenario): Scenario;\n /** Resolve every scenario; returns one message per problem. */\n check(): string[];\n}\n\nexport function createCatalog(\n raw: ScenarioInput[],\n { rules = [] }: { rules?: ScenarioRule[]; } = {},\n): Catalog {\n const byName = new Map<string, Scenario>();\n\n for (const [i, data] of raw.entries()) {\n const s = parseScenario(data, `#${i}`);\n\n // Commas join names in ?scenario=a,b, so a catalog name can't contain one.\n if (s.name.includes(',')) throw new Error(`Scenario name \"${s.name}\" can't contain a comma`);\n\n if (byName.has(s.name)) throw new Error(`Duplicate scenario name \"${s.name}\"`);\n byName.set(s.name, s);\n }\n\n const flatten = (s: Scenario, seen: string[]): Scenario => {\n if (seen.includes(s.name)) {\n throw new Error(`Circular extends: ${[...seen, s.name].join(' -> ')}`);\n }\n\n const parents = [s.extends ?? []].flat().map((name) => {\n const parent = byName.get(name);\n\n if (!parent) throw new Error(`Scenario \"${s.name}\" extends unknown \"${name}\"`);\n\n return flatten(parent, [...seen, s.name]);\n });\n\n const { extends: _extends, $schema: _schema, ...own } = s;\n\n // Start from an empty base so standalone scenarios are normalised too (remove entries, nulls).\n return [...parents, own].reduce(merge, { name: own.name });\n };\n\n const resolve = (s: Scenario) => {\n const flat = flatten(s, []);\n const violations = rules.flatMap((rule) => rule(flat) ?? []);\n\n if (violations.length) {\n throw new Error(`Scenario \"${s.name}\" breaks rules:\\n${violations.map((m) => `- ${m}`).join('\\n')}`);\n }\n\n return flat;\n };\n\n return {\n list: [...byName.values()],\n get: (name) => byName.get(name),\n resolve,\n check: () =>\n [...byName.values()].flatMap((s) => {\n try {\n resolve(s);\n\n return [];\n } catch (err) {\n return [err instanceof Error ? err.message : String(err)];\n }\n }),\n };\n}\n\nconst byKey = ([a]: [string, unknown], [b]: [string, unknown]) => a.localeCompare(b);\n\nconst routeKey = (e: NetworkEntry) =>\n `${e.method} ${e.path} ${JSON.stringify(Object.entries(e.query ?? {}).toSorted(byKey))}`;\n\nfunction merge(base: Scenario, over: Scenario): Scenario {\n const overKeys = new Set((over.network ?? []).map(routeKey));\n\n return {\n ...base,\n ...over,\n // Own-only fields: a child describes and finishes rendering differently.\n description: over.description,\n tags: over.tags,\n ready: over.ready,\n // A null param removes the inherited one.\n url: Object.fromEntries(Object.entries({ ...base.url, ...over.url }).filter(([, v]) => v !== null)),\n network: [\n ...(over.network ?? []).filter((e) => !e.remove),\n ...(base.network ?? []).filter((e) => !overKeys.has(routeKey(e))),\n ],\n handlers: union(over.handlers, base.handlers),\n state: mergeState(base.state ?? {}, over.state ?? {}),\n };\n}\n\n/**\n * Like `mergeObjects`, except a top-level `null` sets that slice to null (a signed-out `user`)\n * rather than removing it, so capture can record one and replay restores it.\n */\nfunction mergeState(base: JsonObject, over: JsonObject): JsonObject {\n const out = mergeObjects(base, over);\n\n for (const [k, v] of Object.entries(over)) {\n if (v === null) out[k] = null;\n }\n\n return out;\n}\n\nconst union = (a: string[] = [], b: string[] = []) => {\n const all = [...new Set([...a, ...b])];\n\n return all.length ? all : undefined;\n};\n\nconst isJsonObject = (value: Json | undefined): value is JsonObject =>\n value instanceof Object && !Array.isArray(value);\n\nfunction mergeObjects(base: JsonObject, over: JsonObject): JsonObject {\n const out = { ...base };\n\n for (const [k, v] of Object.entries(over)) {\n if (v === null) delete out[k];\n else out[k] = deepMerge(base[k], v);\n }\n\n return out;\n}\n\n/** Objects merge recursively, `null` deletes a key, anything else replaces. */\nexport function deepMerge(base: Json | undefined, over: Json): Json {\n return isJsonObject(base) && isJsonObject(over) ? mergeObjects(base, over) : over;\n}\n\n/**\n * Inverse of `extends` merge: overrides that turn a resolved `base` into `captured`.\n * Pass the flattened base (from `catalog.resolve`). Copy-link keeps `captured` as-is;\n * Download JSON uses this so the file can `extends` the base instead of repeating it.\n */\nexport function diffScenario(captured: Scenario, base?: Scenario): ScenarioInput {\n if (!base) {\n return compactScenario({\n name: captured.name,\n description: captured.description,\n tags: captured.tags,\n path: captured.path,\n url: captured.url,\n network: captured.network,\n handlers: captured.handlers,\n state: captured.state,\n ready: captured.ready,\n viewport: captured.viewport,\n });\n }\n\n const url = diffUrl(base.url, captured.url);\n const network = diffNetwork(base.network ?? [], captured.network ?? []);\n // A slice missing from the capture had no adapter connected: inherit it, since a top-level null\n // would set it to null.\n const state = diffObjects(\n Object.fromEntries(Object.entries(base.state ?? {}).filter(([k]) => k in (captured.state ?? {}))),\n captured.state ?? {},\n );\n const handlers = diffHandlers(base.handlers ?? [], captured.handlers ?? []);\n\n return compactScenario({\n name: captured.name,\n extends: base.name,\n description: captured.description,\n tags: captured.tags,\n path: captured.path !== base.path ? captured.path : undefined,\n url,\n network: network.length ? network : undefined,\n handlers,\n state,\n ready: captured.ready,\n viewport: jsonEqual(captured.viewport ?? null, base.viewport ?? null) ? undefined : captured.viewport,\n });\n}\n\n/**\n * The shortest own list that `extends` merges back into `captured`, order included: merge puts\n * the child's handlers first, then the base's it didn't name. Handler order is precedence.\n */\nfunction diffHandlers(base: string[], captured: string[]): string[] | undefined {\n for (let k = 0; k <= captured.length; k++) {\n const own = captured.slice(0, k);\n\n if (JSON.stringify(union(own, base)) === JSON.stringify(captured)) return own.length ? own : undefined;\n }\n\n // An inherited handler can't be removed: keep the captured order and let the base's extras follow.\n return captured.length ? captured : undefined;\n}\n\nfunction diffUrl(\n base: Scenario['url'],\n captured: Scenario['url'],\n): ScenarioInput['url'] {\n const baseUrl = base ?? {};\n const capturedUrl = captured ?? {};\n const out: Record<string, string | null> = {};\n\n for (const [k, v] of Object.entries(capturedUrl)) {\n if (v === null) continue;\n\n if (baseUrl[k] !== v) out[k] = v;\n }\n\n for (const [k, v] of Object.entries(baseUrl)) {\n if (v === null) continue;\n\n if (!(k in capturedUrl) || capturedUrl[k] === null) out[k] = null;\n }\n\n return Object.keys(out).length ? out : undefined;\n}\n\nfunction diffNetwork(base: NetworkEntry[], captured: NetworkEntry[]): NetworkEntry[] {\n const baseByKey = new Map(base.map((e) => [routeKey(e), e]));\n const capByKey = new Map<string, NetworkEntry>();\n\n // First match wins in MSW, so a later duplicate route is unreachable; keep the first.\n for (const e of captured) if (!capByKey.has(routeKey(e))) capByKey.set(routeKey(e), e);\n\n const out: NetworkEntry[] = [];\n\n for (const [key, e] of capByKey) {\n const prior = baseByKey.get(key);\n\n if (!prior || !jsonEqual(prior, e)) out.push(e);\n }\n\n for (const [key, e] of baseByKey) {\n if (!capByKey.has(key)) {\n const removed: NetworkEntry = { method: e.method, path: e.path, remove: true };\n\n if (e.query) removed.query = e.query;\n\n out.push(removed);\n }\n }\n\n return out;\n}\n\nfunction diffObjects(base: JsonObject, captured: JsonObject): JsonObject | undefined {\n const out: JsonObject = {};\n\n for (const [k, v] of Object.entries(captured)) {\n if (!(k in base)) out[k] = v;\n else if (isJsonObject(base[k]) && isJsonObject(v)) {\n const nested = diffObjects(base[k], v);\n\n if (nested) out[k] = nested;\n } else if (!jsonEqual(base[k], v)) out[k] = v;\n }\n\n for (const k of Object.keys(base)) {\n if (!(k in captured)) out[k] = null;\n }\n\n return Object.keys(out).length ? out : undefined;\n}\n\nfunction jsonEqual(a: Json | NetworkEntry, b: Json | NetworkEntry): boolean {\n return JSON.stringify(a) === JSON.stringify(b);\n}\n\nfunction compactScenario(s: ScenarioInput): ScenarioInput {\n const out: ScenarioInput = { name: s.name };\n\n if (s.$schema !== undefined) out.$schema = s.$schema;\n\n if (s.description !== undefined) out.description = s.description;\n\n if (s.tags !== undefined) out.tags = s.tags;\n\n if (s.extends !== undefined) out.extends = s.extends;\n\n if (s.path !== undefined) out.path = s.path;\n\n if (s.url !== undefined && Object.keys(s.url).length) out.url = s.url;\n\n if (s.network !== undefined && s.network.length) out.network = s.network;\n\n if (s.handlers !== undefined && s.handlers.length) out.handlers = s.handlers;\n\n if (s.state !== undefined && Object.keys(s.state).length) out.state = s.state;\n\n if (s.ready !== undefined) out.ready = s.ready;\n\n if (s.viewport !== undefined) out.viewport = s.viewport;\n\n return out;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// src/link.ts
|
|
2
|
+
var PARAM = "scenario";
|
|
3
|
+
var STRICT_PARAM = "scenario-strict";
|
|
4
|
+
var RECORD_PARAM = "scenario-record";
|
|
5
|
+
var SAVE_ENDPOINT = "/__executable-stories-showcase/save";
|
|
6
|
+
var LOOPBACK = /* @__PURE__ */ new Set(["127.0.0.1", "localhost", "[::1]"]);
|
|
7
|
+
var isLoopbackSaveUrl = (value) => {
|
|
8
|
+
try {
|
|
9
|
+
const url = new URL(value);
|
|
10
|
+
return url.protocol === "http:" && LOOPBACK.has(url.hostname) && url.pathname === SAVE_ENDPOINT;
|
|
11
|
+
} catch {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
var DATA_KEY = "scenario-data";
|
|
16
|
+
function scenarioHref(resolved, { base = location.href, inline = false, strict = false } = {}) {
|
|
17
|
+
const url = new URL(base);
|
|
18
|
+
url.search = "";
|
|
19
|
+
url.hash = "";
|
|
20
|
+
if (resolved.path) url.pathname = resolved.path;
|
|
21
|
+
for (const [k, v] of Object.entries(resolved.url ?? {})) {
|
|
22
|
+
if (v !== null) url.searchParams.set(k, v);
|
|
23
|
+
}
|
|
24
|
+
if (inline) {
|
|
25
|
+
const { $schema: _schema, extends: _extends, ...snapshot } = resolved;
|
|
26
|
+
url.hash = new URLSearchParams({ [DATA_KEY]: JSON.stringify(snapshot) }).toString();
|
|
27
|
+
} else {
|
|
28
|
+
url.searchParams.set(PARAM, resolved.name);
|
|
29
|
+
}
|
|
30
|
+
if (strict) url.searchParams.set(STRICT_PARAM, "");
|
|
31
|
+
return url.toString();
|
|
32
|
+
}
|
|
33
|
+
var readInline = (hash) => new URLSearchParams(hash.slice(1)).get(DATA_KEY);
|
|
34
|
+
|
|
35
|
+
export {
|
|
36
|
+
PARAM,
|
|
37
|
+
STRICT_PARAM,
|
|
38
|
+
RECORD_PARAM,
|
|
39
|
+
SAVE_ENDPOINT,
|
|
40
|
+
LOOPBACK,
|
|
41
|
+
isLoopbackSaveUrl,
|
|
42
|
+
DATA_KEY,
|
|
43
|
+
scenarioHref,
|
|
44
|
+
readInline
|
|
45
|
+
};
|
|
46
|
+
//# sourceMappingURL=chunk-PKGBYTBX.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/link.ts"],"sourcesContent":["import type { Scenario } from './schema.js';\n\ndeclare global {\n /** Requests the active scenario didn't cover, for tests and agents (see `unhandledRequests`). */\n var executableStoriesShowcaseUnhandled: string[] | undefined;\n}\n\nexport const PARAM = 'scenario';\n\n/** Fail unmatched requests instead of letting them reach the network. */\nexport const STRICT_PARAM = 'scenario-strict';\n\n/** Record real responses so they can be saved as a scenario. */\nexport const RECORD_PARAM = 'scenario-record';\n\n/** Dev-server route the `executable-stories-showcase/vite` plugin serves for the panel's \"Save to project\". */\nexport const SAVE_ENDPOINT = '/__executable-stories-showcase/save';\n\nexport const LOOPBACK = new Set(['127.0.0.1', 'localhost', '[::1]']);\n\n/**\n * Whether a save URL the app's save route hands the panel is a loopback save server, such as\n * the one the Next.js route starts on 127.0.0.1. The panel posts captured state there, so a\n * URL on any other host is refused.\n */\nexport const isLoopbackSaveUrl = (value: string): boolean => {\n try {\n const url = new URL(value);\n\n return url.protocol === 'http:' && LOOPBACK.has(url.hostname) && url.pathname === SAVE_ENDPOINT;\n } catch {\n return false;\n }\n};\n\n/** Inline scenario JSON, kept in the URL hash so it never reaches a server. */\nexport const DATA_KEY = 'scenario-data';\n\n/**\n * A shareable URL that opens the app in a resolved scenario.\n * `inline` embeds the flattened scenario, so the link works without the catalog.\n */\nexport function scenarioHref(\n resolved: Scenario,\n { base = location.href, inline = false, strict = false } = {},\n): string {\n const url = new URL(base);\n url.search = '';\n url.hash = '';\n\n if (resolved.path) url.pathname = resolved.path;\n\n for (const [k, v] of Object.entries(resolved.url ?? {})) {\n if (v !== null) url.searchParams.set(k, v);\n }\n\n if (inline) {\n const { $schema: _schema, extends: _extends, ...snapshot } = resolved;\n url.hash = new URLSearchParams({ [DATA_KEY]: JSON.stringify(snapshot) }).toString();\n } else {\n url.searchParams.set(PARAM, resolved.name);\n }\n\n if (strict) url.searchParams.set(STRICT_PARAM, '');\n\n return url.toString();\n}\n\n/** Inline scenario JSON from a URL hash, if present. */\nexport const readInline = (hash: string) => new URLSearchParams(hash.slice(1)).get(DATA_KEY);\n"],"mappings":";AAOO,IAAM,QAAQ;AAGd,IAAM,eAAe;AAGrB,IAAM,eAAe;AAGrB,IAAM,gBAAgB;AAEtB,IAAM,WAAW,oBAAI,IAAI,CAAC,aAAa,aAAa,OAAO,CAAC;AAO5D,IAAM,oBAAoB,CAAC,UAA2B;AAC3D,MAAI;AACF,UAAM,MAAM,IAAI,IAAI,KAAK;AAEzB,WAAO,IAAI,aAAa,WAAW,SAAS,IAAI,IAAI,QAAQ,KAAK,IAAI,aAAa;AAAA,EACpF,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAGO,IAAM,WAAW;AAMjB,SAAS,aACd,UACA,EAAE,OAAO,SAAS,MAAM,SAAS,OAAO,SAAS,MAAM,IAAI,CAAC,GACpD;AACR,QAAM,MAAM,IAAI,IAAI,IAAI;AACxB,MAAI,SAAS;AACb,MAAI,OAAO;AAEX,MAAI,SAAS,KAAM,KAAI,WAAW,SAAS;AAE3C,aAAW,CAAC,GAAG,CAAC,KAAK,OAAO,QAAQ,SAAS,OAAO,CAAC,CAAC,GAAG;AACvD,QAAI,MAAM,KAAM,KAAI,aAAa,IAAI,GAAG,CAAC;AAAA,EAC3C;AAEA,MAAI,QAAQ;AACV,UAAM,EAAE,SAAS,SAAS,SAAS,UAAU,GAAG,SAAS,IAAI;AAC7D,QAAI,OAAO,IAAI,gBAAgB,EAAE,CAAC,QAAQ,GAAG,KAAK,UAAU,QAAQ,EAAE,CAAC,EAAE,SAAS;AAAA,EACpF,OAAO;AACL,QAAI,aAAa,IAAI,OAAO,SAAS,IAAI;AAAA,EAC3C;AAEA,MAAI,OAAQ,KAAI,aAAa,IAAI,cAAc,EAAE;AAEjD,SAAO,IAAI,SAAS;AACtB;AAGO,IAAM,aAAa,CAAC,SAAiB,IAAI,gBAAgB,KAAK,MAAM,CAAC,CAAC,EAAE,IAAI,QAAQ;","names":[]}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import {
|
|
2
|
+
PARAM,
|
|
3
|
+
RECORD_PARAM,
|
|
4
|
+
STRICT_PARAM
|
|
5
|
+
} from "./chunk-PKGBYTBX.js";
|
|
6
|
+
import {
|
|
7
|
+
diffScenario
|
|
8
|
+
} from "./chunk-MCC3CAWQ.js";
|
|
9
|
+
import {
|
|
10
|
+
parseScenario
|
|
11
|
+
} from "./chunk-BK4EEQXV.js";
|
|
12
|
+
|
|
13
|
+
// src/redact.ts
|
|
14
|
+
import { z } from "zod";
|
|
15
|
+
|
|
16
|
+
// src/jwt.ts
|
|
17
|
+
var encode = (value) => btoa(Array.from(new TextEncoder().encode(JSON.stringify(value)), (b) => String.fromCharCode(b)).join("")).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
18
|
+
var unsignedJwt = (claims) => `${encode({ alg: "none", typ: "JWT" })}.${encode(claims)}.`;
|
|
19
|
+
|
|
20
|
+
// src/redact.ts
|
|
21
|
+
var SECRET_KEY = /pass(word|phrase)?$|secret|api[-_]?key|private[-_]?key|access[-_]?key|credential|^otp$|card[-_]?number|^cvc$|^cvv$/i;
|
|
22
|
+
var TOKEN_KEY = /token|session[-_]?id|^sid$|authorization|cookie/i;
|
|
23
|
+
var NUMERIC_ID_KEY = /session[-_]?id$|^sid$/i;
|
|
24
|
+
var JWT = /^[\w-]+\.[\w-]+\.[\w-]*$/;
|
|
25
|
+
var SECRET_VALUE = /^(Bearer|Basic) \S|^[sr]k_(live|test)_\w|^gh[pousr]_\w{20}|^github_pat_\w|^AKIA[0-9A-Z]{16}$|^xox[abprs]-/;
|
|
26
|
+
var SECRET_PARAM = /^(code|state|nonce)$/i;
|
|
27
|
+
var PLACEHOLDER = "[redacted]";
|
|
28
|
+
var decode = (part) => {
|
|
29
|
+
try {
|
|
30
|
+
const bytes = Uint8Array.from(atob(part.replace(/-/g, "+").replace(/_/g, "/")), (c) => c.charCodeAt(0));
|
|
31
|
+
const value = JSON.parse(new TextDecoder().decode(bytes));
|
|
32
|
+
return value instanceof Object && !Array.isArray(value) ? value : void 0;
|
|
33
|
+
} catch {
|
|
34
|
+
return void 0;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
var unsign = (token) => {
|
|
38
|
+
if (!JWT.test(token)) return void 0;
|
|
39
|
+
const [header = "", payload = ""] = token.split(".");
|
|
40
|
+
if (!decode(header)) return void 0;
|
|
41
|
+
const claims = decode(payload);
|
|
42
|
+
return claims && unsignedJwt(claims);
|
|
43
|
+
};
|
|
44
|
+
function redactSecrets(value) {
|
|
45
|
+
const redacted = [];
|
|
46
|
+
const walk = (node, path, key) => {
|
|
47
|
+
const text = z.string().safeParse(node);
|
|
48
|
+
const jwt = text.success ? unsign(text.data) : void 0;
|
|
49
|
+
if (jwt !== void 0) {
|
|
50
|
+
if (jwt !== text.data) redacted.push(path);
|
|
51
|
+
return jwt;
|
|
52
|
+
}
|
|
53
|
+
const secret = SECRET_KEY.test(key) || TOKEN_KEY.test(key);
|
|
54
|
+
const number = z.number().safeParse(node);
|
|
55
|
+
if ((SECRET_KEY.test(key) || NUMERIC_ID_KEY.test(key)) && number.success) {
|
|
56
|
+
redacted.push(path);
|
|
57
|
+
return 0;
|
|
58
|
+
}
|
|
59
|
+
if (text.success && (secret && text.data || SECRET_VALUE.test(text.data))) {
|
|
60
|
+
redacted.push(path);
|
|
61
|
+
return PLACEHOLDER;
|
|
62
|
+
}
|
|
63
|
+
if (text.success || number.success) return node;
|
|
64
|
+
if (Array.isArray(node)) return node.map((item, i) => walk(item, `${path}[${i}]`, key));
|
|
65
|
+
if (node instanceof Object) {
|
|
66
|
+
return Object.fromEntries(
|
|
67
|
+
Object.entries(node).map(([k, v]) => [k, walk(v, path ? `${path}.${k}` : k, k)])
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
return node;
|
|
71
|
+
};
|
|
72
|
+
return { value: walk(value, "", ""), redacted };
|
|
73
|
+
}
|
|
74
|
+
function withoutSecretParams(params, where) {
|
|
75
|
+
const redacted = z.record(z.string(), z.unknown()).parse(redactSecrets(params).value);
|
|
76
|
+
const dropped = Object.keys(params).filter((key) => SECRET_PARAM.test(key) || redacted[key] !== params[key]);
|
|
77
|
+
if (dropped.length) console.info(`[executable-stories-showcase] left ${dropped.join(", ")} out of ${where}`);
|
|
78
|
+
return Object.fromEntries(Object.entries(params).filter(([key]) => !dropped.includes(key)));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// src/capture.ts
|
|
82
|
+
var SCENARIO_PARAMS = /* @__PURE__ */ new Set([PARAM, STRICT_PARAM, RECORD_PARAM]);
|
|
83
|
+
function captureScenario(session, options) {
|
|
84
|
+
const params = {};
|
|
85
|
+
new URLSearchParams(location.search).forEach((v, k) => {
|
|
86
|
+
if (!SCENARIO_PARAMS.has(k)) params[k] = v;
|
|
87
|
+
});
|
|
88
|
+
const url = withoutSecretParams(params, "the captured URL");
|
|
89
|
+
const state = Object.fromEntries(
|
|
90
|
+
session.keys().flatMap((key) => {
|
|
91
|
+
const live = session.read(key);
|
|
92
|
+
if (live === void 0) return [];
|
|
93
|
+
const { value, redacted } = redactSecrets(live);
|
|
94
|
+
if (redacted.length) {
|
|
95
|
+
console.info(`[executable-stories-showcase] redacted ${redacted.join(", ")} in state["${key}"]`);
|
|
96
|
+
}
|
|
97
|
+
return [[key, value]];
|
|
98
|
+
})
|
|
99
|
+
);
|
|
100
|
+
const network = [...session.resolved?.network ?? [], ...session.recorded()];
|
|
101
|
+
const draft = parseScenario({
|
|
102
|
+
name: options.name,
|
|
103
|
+
description: options.description || void 0,
|
|
104
|
+
path: location.pathname,
|
|
105
|
+
url,
|
|
106
|
+
network: network.length ? network : void 0,
|
|
107
|
+
handlers: session.resolved?.handlers,
|
|
108
|
+
viewport: session.resolved?.viewport,
|
|
109
|
+
state,
|
|
110
|
+
ready: options.ready || void 0
|
|
111
|
+
});
|
|
112
|
+
const flat = session.catalog.resolve(draft);
|
|
113
|
+
const flatBase = options.base ? session.catalog.resolve(options.base) : void 0;
|
|
114
|
+
const forCatalog = diffScenario(flat, flatBase);
|
|
115
|
+
if (options.schemaPath) forCatalog.$schema = options.schemaPath;
|
|
116
|
+
return { flat, forCatalog };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export {
|
|
120
|
+
unsignedJwt,
|
|
121
|
+
redactSecrets,
|
|
122
|
+
withoutSecretParams,
|
|
123
|
+
captureScenario
|
|
124
|
+
};
|
|
125
|
+
//# sourceMappingURL=chunk-QDLRQPTA.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/redact.ts","../src/jwt.ts","../src/capture.ts"],"sourcesContent":["import { z } from 'zod';\nimport { unsignedJwt } from './jwt.js';\nimport type { Json, JsonObject } from './schema.js';\n\n/** Keys whose values are secrets whatever they hold. */\nconst SECRET_KEY =\n /pass(word|phrase)?$|secret|api[-_]?key|private[-_]?key|access[-_]?key|credential|^otp$|card[-_]?number|^cvc$|^cvv$/i;\n\n/** Keys holding bearer values: a JWT keeps its claims, anything else is replaced. */\nconst TOKEN_KEY = /token|session[-_]?id|^sid$|authorization|cookie/i;\n\n/** Numeric bearer values: session ids. */\nconst NUMERIC_ID_KEY = /session[-_]?id$|^sid$/i;\n\nconst JWT = /^[\\w-]+\\.[\\w-]+\\.[\\w-]*$/;\n\n/**\n * Values that are credentials whatever key holds them: an auth header, and the key formats of\n * Stripe, GitHub, AWS and Slack.\n */\nconst SECRET_VALUE = /^(Bearer|Basic) \\S|^[sr]k_(live|test)_\\w|^gh[pousr]_\\w{20}|^github_pat_\\w|^AKIA[0-9A-Z]{16}$|^xox[abprs]-/;\n\n/** URL params that carry a one-time credential, such as an OAuth authorization `code`. */\nconst SECRET_PARAM = /^(code|state|nonce)$/i;\n\nconst PLACEHOLDER = '[redacted]';\n\nconst decode = (part: string): JsonObject | undefined => {\n try {\n const bytes = Uint8Array.from(atob(part.replace(/-/g, '+').replace(/_/g, '/')), (c) => c.charCodeAt(0));\n const value: Json = JSON.parse(new TextDecoder().decode(bytes));\n\n return value instanceof Object && !Array.isArray(value) ? value : undefined;\n } catch {\n return undefined;\n }\n};\n\n/** A JWT re-issued unsigned with the same claims, so the app still decodes it but no server accepts it. */\nconst unsign = (token: string) => {\n if (!JWT.test(token)) return undefined;\n const [header = '', payload = ''] = token.split('.');\n\n if (!decode(header)) return undefined;\n const claims = decode(payload);\n\n return claims && unsignedJwt(claims);\n};\n\n/**\n * Remove secrets from a recorded response body: strings under secret-like keys become\n * \"[redacted]\" and numbers become 0, and signed JWTs anywhere lose their signature but keep\n * their claims.\n * Returns the paths it changed, e.g. `user.accessToken`.\n */\nexport interface Redacted {\n value: Json;\n /** Paths that changed, e.g. `user.accessToken`. */\n redacted: string[];\n}\n\nexport function redactSecrets(value: Json): Redacted {\n const redacted: string[] = [];\n\n const walk = (node: Json, path: string, key: string): Json => {\n const text = z.string().safeParse(node);\n // A JWT keeps its claims, whatever key holds it: the app may decode it.\n const jwt = text.success ? unsign(text.data) : undefined;\n\n if (jwt !== undefined) {\n if (jwt !== text.data) redacted.push(path);\n\n return jwt;\n }\n\n // Checked before the value's type: a card number or one-time code is often a number.\n // Token-like keys only for strings: `tokens: 3` is usually a count, not a credential.\n const secret = SECRET_KEY.test(key) || TOKEN_KEY.test(key);\n const number = z.number().safeParse(node);\n\n if ((SECRET_KEY.test(key) || NUMERIC_ID_KEY.test(key)) && number.success) {\n redacted.push(path);\n\n return 0; // still a number, so the app's parsing and formatting keep working\n }\n\n if (text.success && ((secret && text.data) || SECRET_VALUE.test(text.data))) {\n redacted.push(path);\n\n return PLACEHOLDER;\n }\n\n if (text.success || number.success) return node;\n\n if (Array.isArray(node)) return node.map((item, i) => walk(item, `${path}[${i}]`, key));\n\n if (node instanceof Object) {\n return Object.fromEntries(\n Object.entries(node).map(([k, v]) => [k, walk(v, path ? `${path}.${k}` : k, k)]),\n );\n }\n\n return node;\n };\n\n return { value: walk(value, '', ''), redacted };\n}\n\n/**\n * Drops URL params whose value redaction would change (`?api_key=…`, `?token=…`), and OAuth's\n * one-time `code`, `state` and `nonce`. Saved params are matched or replayed as written, so `\"[redacted]\"`\n * would be wrong; without the param, a recorded request matches whatever value the app sends.\n */\nexport function withoutSecretParams(params: Record<string, string>, where: string): Record<string, string> {\n const redacted = z.record(z.string(), z.unknown()).parse(redactSecrets(params).value);\n const dropped = Object.keys(params).filter((key) => SECRET_PARAM.test(key) || redacted[key] !== params[key]);\n\n if (dropped.length) console.info(`[executable-stories-showcase] left ${dropped.join(', ')} out of ${where}`);\n\n return Object.fromEntries(Object.entries(params).filter(([key]) => !dropped.includes(key)));\n}\n","import type { JsonObject } from './schema.js';\n\n/** Base64url of UTF-8, so claims in any script (\"José\", \"李雷\", \"Zoë\") survive decoding. */\nconst encode = (value: JsonObject) =>\n btoa(Array.from(new TextEncoder().encode(JSON.stringify(value)), (b) => String.fromCharCode(b)).join(''))\n .replace(/\\+/g, '-')\n .replace(/\\//g, '_')\n .replace(/=+$/, '');\n\n/**\n * An unsigned JWT (`alg: \"none\"`) carrying `claims`, for mocked responses in \"signed-in\" scenarios\n * when the app only decodes the token it gets back. Anything that verifies signatures rejects it,\n * which keeps it out of real backends. Set `exp` in the future if the app checks expiry.\n */\nexport const unsignedJwt = (claims: JsonObject): string =>\n `${encode({ alg: 'none', typ: 'JWT' })}.${encode(claims)}.`;\n","import { diffScenario } from './catalog.js';\nimport { PARAM, RECORD_PARAM, STRICT_PARAM } from './link.js';\nimport { redactSecrets, withoutSecretParams } from './redact.js';\nimport type { ScenarioSession } from './runtime.js';\nimport { parseScenario, type Scenario, type ScenarioInput } from './schema.js';\n\nconst SCENARIO_PARAMS = new Set([PARAM, STRICT_PARAM, RECORD_PARAM]);\n\nexport interface CaptureOptions {\n name: string;\n description?: string;\n /** Playwright selector that marks this UI state ready. */\n ready?: string;\n /**\n * Catalog scenario to `extends`. Diff uses its resolved (flattened) form.\n * Omit for a full self-contained snapshot.\n */\n base?: Scenario;\n /** Relative `$schema` path stamped on the catalog file only, e.g. `../scenario.schema.json`. */\n schemaPath?: string;\n}\n\nexport interface CapturedScenario {\n /** Flattened snapshot for inline share links. */\n flat: Scenario;\n /** File to commit: diffs against `base` when set, otherwise the flat snapshot as input. */\n forCatalog: ScenarioInput;\n}\n\n/**\n * Snapshot what the app shows now: current route and URL params, the active network\n * fixtures (plus recorded responses), and every connected adapter's live state.\n */\nexport function captureScenario(session: ScenarioSession, options: CaptureOptions): CapturedScenario {\n const params: Record<string, string> = {};\n new URLSearchParams(location.search).forEach((v, k) => {\n if (!SCENARIO_PARAMS.has(k)) params[k] = v;\n });\n // A page URL can carry a token or a magic-link code; it is saved like recorded traffic.\n const url = withoutSecretParams(params, 'the captured URL');\n\n const state = Object.fromEntries(\n session.keys().flatMap((key) => {\n const live = session.read(key);\n if (live === undefined) return [];\n\n // A store or cache can hold a session token; captured scenarios get committed and shared,\n // so they get the same redaction as recorded responses.\n const { value, redacted } = redactSecrets(live);\n\n if (redacted.length) {\n console.info(`[executable-stories-showcase] redacted ${redacted.join(', ')} in state[\"${key}\"]`);\n }\n\n return [[key, value] as const];\n }),\n );\n\n // Recorded responses go after the scenario's own: they answered requests it didn't mock.\n const network = [...(session.resolved?.network ?? []), ...session.recorded()];\n\n const draft = parseScenario({\n name: options.name,\n description: options.description || undefined,\n path: location.pathname,\n url,\n network: network.length ? network : undefined,\n handlers: session.resolved?.handlers,\n viewport: session.resolved?.viewport,\n state,\n ready: options.ready || undefined,\n });\n\n const flat = session.catalog.resolve(draft);\n const flatBase = options.base ? session.catalog.resolve(options.base) : undefined;\n const forCatalog = diffScenario(flat, flatBase);\n\n if (options.schemaPath) forCatalog.$schema = options.schemaPath;\n\n return { flat, forCatalog };\n}\n"],"mappings":"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","names":[]}
|