@warlock.js/core 5.33.0 → 5.34.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/CHANGELOG.md +26 -46
- package/esm/cli/commands/check/check-command.action.mjs +2 -1
- package/esm/cli/commands/check/check-command.action.mjs.map +1 -1
- package/esm/cli/commands/check.command.mjs +2 -2
- package/esm/cli/commands/check.command.mjs.map +1 -1
- package/esm/cli/commands/e2e.command.mjs +63 -0
- package/esm/cli/commands/e2e.command.mjs.map +1 -0
- package/esm/cli/commands/load-e2e-spec.mjs +47 -0
- package/esm/cli/commands/load-e2e-spec.mjs.map +1 -0
- package/esm/cli/framework-cli-commands.mjs +2 -0
- package/esm/cli/framework-cli-commands.mjs.map +1 -1
- package/esm/dev-server/health-checker/checkers/oxlint-health-checker.mjs +1 -1
- package/esm/dev-server/health-checker/default-health-checkers.mjs +1 -6
- package/esm/dev-server/health-checker/default-health-checkers.mjs.map +1 -1
- package/esm/dev-server/health-checker/file-health-checker.contract.d.mts +0 -2
- package/esm/dev-server/health-checker/file-health-checker.contract.d.mts.map +1 -1
- package/esm/dev-server/health-checker/file-health-result.d.mts +1 -1
- package/esm/dev-server/health-checker/file-health-result.mjs.map +1 -1
- package/esm/e2e/browser.d.mts +4 -0
- package/esm/e2e/browser.d.mts.map +1 -0
- package/esm/e2e/browser.mjs +88 -0
- package/esm/e2e/browser.mjs.map +1 -0
- package/esm/e2e/cdp.d.mts +152 -0
- package/esm/e2e/cdp.d.mts.map +1 -0
- package/esm/e2e/cdp.mjs +106 -0
- package/esm/e2e/cdp.mjs.map +1 -0
- package/esm/e2e/fixture.mjs +195 -0
- package/esm/e2e/fixture.mjs.map +1 -0
- package/esm/e2e/index.d.mts +5 -0
- package/esm/e2e/index.mjs +6 -0
- package/esm/e2e/owned-process.mjs +193 -0
- package/esm/e2e/owned-process.mjs.map +1 -0
- package/esm/e2e/page.d.mts +64 -0
- package/esm/e2e/page.d.mts.map +1 -0
- package/esm/e2e/page.mjs +376 -0
- package/esm/e2e/page.mjs.map +1 -0
- package/esm/e2e/run-e2e.d.mts +17 -0
- package/esm/e2e/run-e2e.d.mts.map +1 -0
- package/esm/e2e/run-e2e.mjs +143 -0
- package/esm/e2e/run-e2e.mjs.map +1 -0
- package/esm/e2e/types.d.mts +123 -0
- package/esm/e2e/types.d.mts.map +1 -0
- package/esm/e2e/types.mjs +24 -0
- package/esm/e2e/types.mjs.map +1 -0
- package/esm/secrets/errors.d.mts +1 -1
- package/esm/secrets/errors.d.mts.map +1 -1
- package/esm/secrets/errors.mjs +2 -0
- package/esm/secrets/errors.mjs.map +1 -1
- package/esm/secrets/resolve-driver.mjs +14 -0
- package/esm/secrets/resolve-driver.mjs.map +1 -0
- package/esm/secrets/secret-store.d.mts.map +1 -1
- package/esm/secrets/secret-store.mjs +4 -1
- package/esm/secrets/secret-store.mjs.map +1 -1
- package/esm/secrets/secrets.d.mts.map +1 -1
- package/esm/secrets/secrets.mjs +10 -1
- package/esm/secrets/secrets.mjs.map +1 -1
- package/esm/secrets/types.d.mts +4 -0
- package/esm/secrets/types.d.mts.map +1 -1
- package/esm/tooling/check/lint-step.d.mts +3 -3
- package/esm/tooling/check/lint-step.d.mts.map +1 -1
- package/esm/tooling/check/lint-step.mjs +3 -47
- package/esm/tooling/check/lint-step.mjs.map +1 -1
- package/esm/tooling/check/run-check.d.mts +2 -2
- package/esm/tooling/check/run-check.mjs +4 -2
- package/esm/tooling/check/run-check.mjs.map +1 -1
- package/esm/tooling/problem.d.mts +1 -1
- package/esm/tooling/problem.d.mts.map +1 -1
- package/esm/tooling/problem.mjs.map +1 -1
- package/esm/updater/package-manager.mjs +5 -1
- package/esm/updater/package-manager.mjs.map +1 -1
- package/esm/upgrade/codemods/index.mjs +2 -1
- package/esm/upgrade/codemods/index.mjs.map +1 -1
- package/esm/upgrade/codemods/oxc-toolchain.mjs +59 -10
- package/esm/upgrade/codemods/oxc-toolchain.mjs.map +1 -1
- package/esm/upgrade/codemods/oxlint-rule-schemas.mjs +6 -0
- package/esm/upgrade/codemods/oxlint-rule-schemas.mjs.map +1 -0
- package/esm/upgrade/codemods/port-eslint-rules.mjs +343 -0
- package/esm/upgrade/codemods/port-eslint-rules.mjs.map +1 -0
- package/esm/upgrade/codemods/toolchain-templates.mjs +1 -1
- package/esm/upgrade/codemods/toolchain-templates.mjs.map +1 -1
- package/esm/upgrade/codemods/validate-oxlint-rule.mjs +60 -0
- package/esm/upgrade/codemods/validate-oxlint-rule.mjs.map +1 -0
- package/esm/upgrade/notes.mjs +14 -7
- package/esm/upgrade/notes.mjs.map +1 -1
- package/esm/warlock-config/types.d.mts +0 -1
- package/esm/warlock-config/types.d.mts.map +1 -1
- package/llms-full.txt +243 -41
- package/llms.txt +2 -1
- package/package.json +19 -20
- package/skills/create-module/SKILL.md +2 -2
- package/skills/run-app/SKILL.md +3 -3
- package/skills/store-secrets/SKILL.md +40 -1
- package/skills/test-browser/SKILL.md +119 -0
- package/skills/upgrade-from-5-29/SKILL.md +70 -29
- package/skills/warlock-check/SKILL.md +3 -3
- package/skills/warlock-doctor/SKILL.md +2 -3
- package/esm/dev-server/health-checker/checkers/eslint-health-checker.mjs +0 -152
- package/esm/dev-server/health-checker/checkers/eslint-health-checker.mjs.map +0 -1
- package/esm/dev-server/health-checker/eslint-support.mjs +0 -27
- package/esm/dev-server/health-checker/eslint-support.mjs.map +0 -1
- package/esm/dev-server/health-checker/workers/eslint-health.worker.d.mts +0 -1
- package/esm/dev-server/health-checker/workers/eslint-health.worker.mjs +0 -183
- package/esm/dev-server/health-checker/workers/eslint-health.worker.mjs.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"page.mjs","names":["delay"],"sources":["../../../../../../../../packages/core/src/e2e/page.ts"],"sourcesContent":["import { writeFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { setTimeout as delay } from \"node:timers/promises\";\nimport { CdpConnection } from \"./cdp\";\nimport {\n defaultViewports,\n errorMessage,\n matches,\n type E2EEvidence,\n type E2ESpec,\n type Viewport,\n} from \"./types\";\n\ntype WaitOptions = { selector?: string; text?: string; url?: string; timeoutMs?: number };\ntype ClickBox = { x: number; y: number; width: number; height: number; reason: string };\ntype ViewportReport = {\n requestedWidth: number;\n innerWidth: number;\n documentWidth: number;\n bodyWidth: number;\n offenders: {\n tag: string;\n id: string;\n className: string;\n width: number;\n scrollWidth: number;\n right: number;\n left: number;\n }[];\n};\n\n/** Runs inside the browser. Two paints plus cross-probe box comparison detect\n * scroll/animation/layout races. The hit test also accepts target descendants.\n */\nfunction clickProbe(selector: string): Promise<ClickBox> {\n return (async () => {\n const empty = (reason: string): ClickBox => ({ x: 0, y: 0, width: 0, height: 0, reason });\n const element = document.querySelector(selector);\n if (!(element instanceof HTMLElement || element instanceof SVGElement))\n return empty(\"target is missing or is not an HTML/SVG element\");\n element.scrollIntoView({ block: \"center\", inline: \"center\", behavior: \"instant\" });\n await new Promise<void>((resolve) =>\n requestAnimationFrame(() => requestAnimationFrame(() => resolve())),\n );\n if (!element.isConnected || document.querySelector(selector) !== element)\n return empty(\"target was replaced during layout\");\n const box = element.getBoundingClientRect();\n const x = box.x + box.width / 2;\n const y = box.y + box.height / 2;\n const style = getComputedStyle(element);\n let reason = \"\";\n if (\n box.width <= 0 ||\n box.height <= 0 ||\n style.visibility !== \"visible\" ||\n style.display === \"none\" ||\n Number(style.opacity) === 0\n )\n reason = \"target is hidden or has an empty box\";\n else if (element.matches(\":disabled\") || element.closest('[inert], [aria-disabled=\"true\"]'))\n reason = \"target is disabled or inert\";\n else if (x < 0 || y < 0 || x >= innerWidth || y >= innerHeight)\n reason = \"target center is outside the viewport\";\n else {\n const top = document.elementFromPoint(x, y);\n if (!top || !element.contains(top))\n reason = `target is covered by ${top?.tagName ?? \"no element\"}${top?.id ? \"#\" + top.id : \"\"}`;\n }\n return { x, y, width: box.width, height: box.height, reason };\n })();\n}\n\nexport function boxesStable(first: ClickBox, second: ClickBox): boolean {\n return (\n !first.reason &&\n !second.reason &&\n ([\"x\", \"y\", \"width\", \"height\"] as const).every(\n (key) => Math.abs(first[key] - second[key]) < 0.25,\n )\n );\n}\n\nexport class BrowserPage {\n private viewport: Viewport = { ...defaultViewports[0] };\n private loads = 0;\n private pendingRequests = new Map<string, string>();\n private requestUrls = new Map<string, string>();\n private lastNetworkActivity = Date.now();\n\n constructor(\n private cdp: CdpConnection,\n private origin: string,\n private outputDirectory: string,\n private evidence: E2EEvidence,\n private spec: E2ESpec,\n ) {\n cdp.on(\"Page.loadEventFired\", () => {\n this.loads++;\n });\n cdp.on(\"Runtime.consoleAPICalled\", ({ type, args }) =>\n evidence.console.push({\n at: new Date().toISOString(),\n level: type === \"assert\" ? \"error\" : type,\n text: args.map((arg) => String(arg.value ?? arg.description ?? \"\")).join(\" \"),\n }),\n );\n cdp.on(\"Runtime.exceptionThrown\", ({ exceptionDetails }) =>\n evidence.console.push({\n at: new Date().toISOString(),\n level: \"error\",\n text: exceptionDetails.exception?.description ?? exceptionDetails.text,\n }),\n );\n cdp.on(\"Log.entryAdded\", ({ entry }) =>\n evidence.console.push({ at: new Date().toISOString(), level: entry.level, text: entry.text }),\n );\n cdp.on(\"Network.requestWillBeSent\", ({ requestId, request }) => {\n this.pendingRequests.set(requestId, request.url);\n this.requestUrls.set(requestId, request.url);\n this.lastNetworkActivity = Date.now();\n });\n cdp.on(\"Network.responseReceived\", ({ requestId, response }) => {\n this.requestUrls.set(requestId, response.url);\n if (response.status >= 400)\n evidence.networkFailures.push({\n at: new Date().toISOString(),\n url: response.url,\n status: response.status,\n });\n });\n cdp.on(\"Network.loadingFailed\", ({ requestId, errorText, canceled }) => {\n evidence.networkFailures.push({\n at: new Date().toISOString(),\n url: this.requestUrls.get(requestId) ?? `unknown request ${requestId}`,\n errorText,\n canceled,\n });\n this.completeRequest(requestId);\n });\n cdp.on(\"Network.loadingFinished\", ({ requestId }) => this.completeRequest(requestId));\n }\n\n private completeRequest(requestId: string): void {\n this.pendingRequests.delete(requestId);\n this.requestUrls.delete(requestId);\n this.lastNetworkActivity = Date.now();\n }\n\n async initialize(): Promise<void> {\n await this.cdp.command(\"Page.enable\", {});\n await this.cdp.command(\"Runtime.enable\", {});\n await this.cdp.command(\"Network.enable\", {});\n await this.cdp.command(\"Log.enable\", {});\n await this.setViewport(this.spec.viewports?.[0] ?? defaultViewports[0]);\n }\n\n async step<T>(name: string, action: () => Promise<T>): Promise<T> {\n const start = performance.now();\n const entry = {\n name,\n startedAt: new Date().toISOString(),\n durationMs: 0,\n status: \"passed\" as \"passed\" | \"failed\",\n error: undefined as string | undefined,\n };\n this.evidence.steps.push(entry);\n try {\n return await action();\n } catch (error) {\n entry.status = \"failed\";\n entry.error = errorMessage(error);\n throw error;\n } finally {\n entry.durationMs = Math.round(performance.now() - start);\n }\n }\n\n async evaluate<T = unknown>(expression: string, timeoutMs = 10_000): Promise<T> {\n const response = await this.cdp.command(\n \"Runtime.evaluate\",\n { expression, returnByValue: true, awaitPromise: true, silent: true },\n timeoutMs,\n );\n if (response.exceptionDetails)\n throw new Error(\n `Browser evaluation failed: ${response.exceptionDetails.exception?.description ?? response.exceptionDetails.text}`,\n );\n return response.result.value as T;\n }\n\n private async poll(\n description: string,\n check: (remainingMs: number) => Promise<boolean>,\n timeoutMs: number,\n ): Promise<void> {\n const deadline = Date.now() + timeoutMs;\n let reason = \"condition stayed false\";\n while (Date.now() < deadline) {\n try {\n if (await check(Math.max(1, deadline - Date.now()))) return;\n } catch (error) {\n reason = errorMessage(error);\n }\n await delay(Math.min(50, Math.max(0, deadline - Date.now())));\n }\n throw new Error(`${description} timed out after ${timeoutMs}ms: ${reason}`);\n }\n\n async goto(path: string, timeoutMs = 10_000): Promise<void> {\n await this.step(`goto ${path}`, async () => {\n const url = new URL(path, this.origin).href;\n const before = this.loads;\n const navigation = await this.cdp.command(\"Page.navigate\", { url }, timeoutMs);\n if (navigation.errorText) throw new Error(`Navigation failed: ${navigation.errorText}`);\n await this.poll(\n `Navigation to ${url}`,\n (remaining) =>\n this.evaluate<boolean>(\n `document.readyState === 'complete' && ${navigation.loaderId ? this.loads > before : \"true\"}`,\n remaining,\n ),\n timeoutMs,\n );\n });\n }\n\n async waitFor(options: WaitOptions): Promise<void> {\n if (options.selector === undefined && options.text === undefined && options.url === undefined)\n throw new Error(\"waitFor requires a selector, text, or URL\");\n await this.step(`waitFor ${JSON.stringify(options)}`, () =>\n this.poll(\n \"waitFor \" + JSON.stringify(options),\n (remaining) =>\n this.evaluate<boolean>(\n `(() => {\n const options = ${JSON.stringify(options)};\n const element = options.selector ? document.querySelector(options.selector) : document.body;\n if (!element) return false;\n if (options.selector) { const box=element.getBoundingClientRect(); const css=getComputedStyle(element); if (!box.width || !box.height || css.visibility !== 'visible') return false; }\n return (options.text === undefined || (element.innerText ?? element.textContent ?? '').includes(options.text)) &&\n (options.url === undefined || location.href === new URL(options.url, ${JSON.stringify(this.origin)}).href);\n })()`,\n remaining,\n ),\n options.timeoutMs ?? 10_000,\n ),\n );\n }\n\n /** Arms the document/URL comparison BEFORE invoking the navigation action,\n * covering full loads, reloads, hash changes and client-side routing.\n */\n async waitForNavigation(\n action: () => Promise<unknown>,\n options: { url?: string; timeoutMs?: number } = {},\n ): Promise<void> {\n await this.step(\"waitForNavigation\", async () => {\n const before = await this.evaluate<{ href: string; timeOrigin: number }>(\n \"({href:location.href,timeOrigin:performance.timeOrigin})\",\n );\n await action();\n await this.poll(\n \"Navigation\",\n (remaining) =>\n this.evaluate<boolean>(\n `document.readyState === 'complete' && (location.href !== ${JSON.stringify(before.href)} || performance.timeOrigin !== ${before.timeOrigin}) && ${options.url ? `location.href === ${JSON.stringify(new URL(options.url, this.origin).href)}` : \"true\"}`,\n remaining,\n ),\n options.timeoutMs ?? 10_000,\n );\n });\n }\n\n async click(selector: string, timeoutMs = 10_000): Promise<void> {\n await this.step(`click ${selector}`, async () => {\n const deadline = Date.now() + timeoutMs;\n let previous: ClickBox | undefined;\n let reason = \"layout has not settled\";\n while (Date.now() < deadline) {\n const remaining = () => Math.max(1, deadline - Date.now());\n let box: ClickBox;\n try {\n box = await this.evaluate<ClickBox>(\n `(${clickProbe.toString()})(${JSON.stringify(selector)})`,\n remaining(),\n );\n } catch (error) {\n reason = errorMessage(error);\n previous = undefined;\n await delay(25);\n continue;\n }\n reason = box.reason || \"target box is moving\";\n if (previous && boxesStable(previous, box)) {\n // Hover can move menus and overlays. Re-measure and hit-test after it.\n let hovered: ClickBox;\n try {\n await this.cdp.command(\n \"Input.dispatchMouseEvent\",\n { type: \"mouseMoved\", x: box.x, y: box.y },\n remaining(),\n );\n hovered = await this.evaluate<ClickBox>(\n `(${clickProbe.toString()})(${JSON.stringify(selector)})`,\n remaining(),\n );\n } catch (error) {\n reason = errorMessage(error);\n previous = undefined;\n await delay(Math.min(25, remaining()));\n continue;\n }\n if (Date.now() < deadline && boxesStable(box, hovered)) {\n await this.cdp.command(\n \"Input.dispatchMouseEvent\",\n {\n type: \"mousePressed\",\n x: hovered.x,\n y: hovered.y,\n button: \"left\",\n buttons: 1,\n clickCount: 1,\n },\n remaining(),\n );\n await this.cdp.command(\n \"Input.dispatchMouseEvent\",\n {\n type: \"mouseReleased\",\n x: hovered.x,\n y: hovered.y,\n button: \"left\",\n buttons: 0,\n clickCount: 1,\n },\n remaining(),\n );\n return;\n }\n reason = hovered.reason || \"hover changed the target box\";\n previous = undefined;\n } else previous = box;\n await delay(Math.min(50, remaining()));\n }\n throw new Error(`Cannot click ${JSON.stringify(selector)} within ${timeoutMs}ms: ${reason}`);\n });\n }\n\n async fill(selector: string, value: string): Promise<void> {\n await this.step(`fill ${selector}`, async () => {\n await this.click(selector);\n // The native value setter works with controlled inputs; input/change are\n // emitted after clearing. CDP insertText then performs actual text input.\n await this.evaluate(`(() => {\n const element=document.querySelector(${JSON.stringify(selector)});\n if (element !== document.activeElement) throw new Error('Fill target lost focus');\n if ((element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) && !element.readOnly && !element.disabled) {\n const prototype=element instanceof HTMLInputElement ? HTMLInputElement.prototype : HTMLTextAreaElement.prototype;\n const setter=Object.getOwnPropertyDescriptor(prototype,'value')?.set;\n if (!setter) throw new Error('Fill target has no native value setter');\n setter.call(element,'');\n } else if (element instanceof HTMLElement && element.isContentEditable) element.textContent='';\n else throw new Error('Fill target must be an editable input, textarea or contenteditable');\n element.dispatchEvent(new Event('input',{bubbles:true}));\n })()`);\n await this.evaluate(\n `document.activeElement === document.querySelector(${JSON.stringify(selector)}) || (() => { throw new Error(\"Fill target lost focus after input\"); })()`,\n );\n if (value) await this.cdp.command(\"Input.insertText\", { text: value });\n await this.evaluate(\n \"document.activeElement?.dispatchEvent(new Event('change',{bubbles:true}))\",\n );\n });\n }\n\n /** Appends Unicode text through the browser's text-input path. */\n async type(selector: string, text: string): Promise<void> {\n await this.step(`type ${selector}`, async () => {\n await this.click(selector);\n const needsEndKey = await this.evaluate<boolean>(\n `(() => { const element=document.querySelector(${JSON.stringify(selector)}); if (element !== document.activeElement || !((element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) && !element.readOnly && !element.disabled || element instanceof HTMLElement && element.isContentEditable)) throw new Error('Type target is not focused and editable'); if(element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) { if (typeof element.selectionStart !== \"number\") return true; element.setSelectionRange(element.value.length,element.value.length); } else { const range=document.createRange(); range.selectNodeContents(element); range.collapse(false); const selection=getSelection(); selection?.removeAllRanges(); selection?.addRange(range); } return false; })()`,\n );\n // Email/number inputs do not expose selectionRange; use their native\n // keyboard editing path instead of throwing on setSelectionRange.\n if (needsEndKey) {\n await this.cdp.command(\"Input.dispatchKeyEvent\", {\n type: \"keyDown\",\n key: \"End\",\n code: \"End\",\n windowsVirtualKeyCode: 35,\n });\n await this.cdp.command(\"Input.dispatchKeyEvent\", {\n type: \"keyUp\",\n key: \"End\",\n code: \"End\",\n windowsVirtualKeyCode: 35,\n });\n }\n await this.cdp.command(\"Input.insertText\", { text });\n });\n }\n\n async setViewport(viewport: Viewport): Promise<void> {\n if (\n !/^[a-z0-9_-]+$/i.test(viewport.name) ||\n !Number.isInteger(viewport.width) ||\n !Number.isInteger(viewport.height) ||\n viewport.width <= 0 ||\n viewport.height <= 0\n )\n throw new Error(\"Viewport requires a safe name and positive integer width/height\");\n await this.step(`viewport ${viewport.name} ${viewport.width}x${viewport.height}`, async () => {\n await this.cdp.command(\"Emulation.setDeviceMetricsOverride\", {\n width: viewport.width,\n height: viewport.height,\n deviceScaleFactor: 1,\n mobile: viewport.mobile ?? false,\n });\n this.viewport = { ...viewport };\n await this.evaluate(\n \"new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(() => resolve(true))))\",\n );\n });\n }\n\n async assertViewport(): Promise<ViewportReport> {\n return this.step(`assertViewport ${this.viewport.name}`, async () => {\n const report = await this.evaluate<ViewportReport>(`({\n requestedWidth:${this.viewport.width},innerWidth,documentWidth:document.documentElement.scrollWidth,bodyWidth:document.body?.scrollWidth ?? 0,\n offenders:[...document.querySelectorAll('*')].map(element=>{const box=element.getBoundingClientRect();return {tag:element.tagName,id:element.id,className:element.getAttribute('class')??'',width:box.width,scrollWidth:element.scrollWidth,right:box.right,left:box.left}})\n .filter(element=>element.right>${this.viewport.width} || element.left<0 || element.width>${this.viewport.width} || element.scrollWidth>${this.viewport.width})\n .sort((a,b)=>Math.max(b.width,b.scrollWidth,b.right)-Math.max(a.width,a.scrollWidth,a.right)).slice(0,10)\n })`);\n if (\n report.innerWidth !== report.requestedWidth ||\n report.documentWidth > report.requestedWidth ||\n report.bodyWidth > report.requestedWidth\n )\n throw new Error(\n `Horizontal overflow or expanded viewport at ${this.viewport.name}: ${JSON.stringify(report)}`,\n );\n return report;\n });\n }\n\n async screenshot(name: string): Promise<string> {\n if (!/^[a-z0-9_-]+$/i.test(name))\n throw new Error(\"Screenshot names must contain only letters, digits, '_' or '-'\");\n return this.step(`screenshot ${name}`, async () => {\n const file = `${String(this.evidence.screenshots.length + 1).padStart(3, \"0\")}-${name}-${this.viewport.name}.png`;\n const { data } = await this.cdp.command(\"Page.captureScreenshot\", { format: \"png\" });\n await writeFile(join(this.outputDirectory, file), Buffer.from(data, \"base64\"));\n this.evidence.screenshots.push({ name, file, viewport: { ...this.viewport } });\n return join(this.outputDirectory, file);\n });\n }\n\n async captureViewports(\n name: string,\n viewports = this.spec.viewports ?? defaultViewports,\n ): Promise<void> {\n const failures: unknown[] = [];\n const original = this.viewport;\n for (const viewport of viewports) {\n try {\n await this.setViewport(viewport);\n try {\n await this.assertViewport();\n } finally {\n await this.screenshot(name);\n }\n } catch (error) {\n failures.push(error);\n }\n }\n try {\n await this.setViewport(original);\n } catch (error) {\n failures.push(error);\n }\n if (failures.length) throw new AggregateError(failures, failures.map(errorMessage).join(\"; \"));\n }\n\n async waitForNetworkIdle(timeoutMs = 10_000): Promise<void> {\n await this.step(\"waitForNetworkIdle\", () =>\n this.poll(\n \"Network idle\",\n async () => {\n const pending = [...this.pendingRequests.values()].filter(\n (url) => !this.spec.allowPendingRequests?.some((pattern) => matches(pattern, url)),\n );\n if (pending.length) throw new Error(`Pending requests: ${pending.join(\", \")}`);\n return Date.now() - this.lastNetworkActivity >= 300;\n },\n timeoutMs,\n ),\n );\n }\n\n assertHealthy(): void {\n for (const entry of this.evidence.console)\n if (entry.level === \"error\")\n entry.allowed =\n this.spec.allowConsoleErrors?.some((pattern) => matches(pattern, entry.text)) ?? false;\n for (const entry of this.evidence.networkFailures)\n entry.allowed =\n this.spec.allowNetworkFailures?.some(\n (rule) =>\n matches(rule.url, entry.url) &&\n (rule.status === undefined || rule.status === entry.status) &&\n (rule.errorText === undefined || matches(rule.errorText, entry.errorText ?? \"\")),\n ) ?? false;\n const consoleErrors = this.evidence.console.filter(\n (entry) => entry.level === \"error\" && !entry.allowed,\n );\n const networkErrors = this.evidence.networkFailures.filter((entry) => !entry.allowed);\n if (consoleErrors.length || networkErrors.length)\n throw new Error(\n `Browser health failed: ${consoleErrors.length} console errors, ${networkErrors.length} failed HTTP/network requests\\n${JSON.stringify({ console: consoleErrors, network: networkErrors })}`,\n );\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { E2EEvidence, E2ESpec, RunE2EOptions } from "./types.mjs";
|
|
2
|
+
//#region ../../packages/core/src/e2e/run-e2e.d.ts
|
|
3
|
+
export type E2EResult = {
|
|
4
|
+
outputDirectory: string;
|
|
5
|
+
evidenceFile: string;
|
|
6
|
+
evidence: E2EEvidence;
|
|
7
|
+
};
|
|
8
|
+
export declare class E2ERunError extends Error {
|
|
9
|
+
readonly result: E2EResult;
|
|
10
|
+
constructor(result: E2EResult);
|
|
11
|
+
}
|
|
12
|
+
/** Each invocation owns a fresh app copy, data root, process groups, profile,
|
|
13
|
+
* screenshots and evidence file. Failures still publish evidence after cleanup.
|
|
14
|
+
*/
|
|
15
|
+
export declare function runE2E(spec: E2ESpec, options?: RunE2EOptions): Promise<E2EResult>;
|
|
16
|
+
//#endregion
|
|
17
|
+
//# sourceMappingURL=run-e2e.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run-e2e.d.mts","names":[],"sources":["../../../../../../../../packages/core/src/e2e/run-e2e.ts"],"mappings":";;YAOY;EAAc;EAAyB;EAAsB,UAAU;;qBACtE,oBAAoB;WACV,QAAQ;EAA7B,YAAqB,QAAQ;;;;;wBAWT,OAAO,MAAM,SAAS,UAAS,gBAAqB,QAAQ"}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { AppFixture } from "./fixture.mjs";
|
|
2
|
+
import { OwnedBrowser } from "./browser.mjs";
|
|
3
|
+
import { errorMessage } from "./types.mjs";
|
|
4
|
+
import { BrowserPage } from "./page.mjs";
|
|
5
|
+
import { join, resolve } from "node:path";
|
|
6
|
+
import { mkdir, mkdtemp, writeFile } from "node:fs/promises";
|
|
7
|
+
|
|
8
|
+
//#region ../../packages/core/src/e2e/run-e2e.ts
|
|
9
|
+
var E2ERunError = class extends Error {
|
|
10
|
+
constructor(result) {
|
|
11
|
+
super(`Browser e2e failed: ${result.evidence.errors.join("; ")}\nEvidence: ${result.evidenceFile}`);
|
|
12
|
+
this.result = result;
|
|
13
|
+
this.name = "E2ERunError";
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
/** Each invocation owns a fresh app copy, data root, process groups, profile,
|
|
17
|
+
* screenshots and evidence file. Failures still publish evidence after cleanup.
|
|
18
|
+
*/
|
|
19
|
+
async function runE2E(spec, options = {}) {
|
|
20
|
+
if (!spec || !spec.app || typeof spec.run !== "function") throw new Error("An e2e spec must default-export defineE2E({ app, run })");
|
|
21
|
+
const base = resolve(options.outputDirectory ?? ".warlock/e2e");
|
|
22
|
+
await mkdir(base, { recursive: true });
|
|
23
|
+
const outputDirectory = await mkdtemp(join(base, "run-"));
|
|
24
|
+
const started = performance.now();
|
|
25
|
+
const evidence = {
|
|
26
|
+
schemaVersion: 1,
|
|
27
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
28
|
+
durationMs: 0,
|
|
29
|
+
status: "running",
|
|
30
|
+
steps: [],
|
|
31
|
+
console: [],
|
|
32
|
+
networkFailures: [],
|
|
33
|
+
screenshots: [],
|
|
34
|
+
processes: [],
|
|
35
|
+
errors: []
|
|
36
|
+
};
|
|
37
|
+
const result = {
|
|
38
|
+
outputDirectory,
|
|
39
|
+
evidenceFile: join(outputDirectory, "evidence.json"),
|
|
40
|
+
evidence
|
|
41
|
+
};
|
|
42
|
+
let fixture;
|
|
43
|
+
let browser;
|
|
44
|
+
let page;
|
|
45
|
+
const abort = new AbortController();
|
|
46
|
+
const forwardAbort = () => abort.abort(options.signal?.reason ?? /* @__PURE__ */ new Error("Browser e2e aborted"));
|
|
47
|
+
options.signal?.addEventListener("abort", forwardAbort, { once: true });
|
|
48
|
+
if (options.signal?.aborted) forwardAbort();
|
|
49
|
+
const onInterrupt = () => abort.abort(/* @__PURE__ */ new Error("Browser e2e interrupted (SIGINT)"));
|
|
50
|
+
const onTerminate = () => abort.abort(/* @__PURE__ */ new Error("Browser e2e interrupted (SIGTERM)"));
|
|
51
|
+
process.on("SIGINT", onInterrupt);
|
|
52
|
+
process.on("SIGTERM", onTerminate);
|
|
53
|
+
const timeout = setTimeout(() => abort.abort(/* @__PURE__ */ new Error(`Browser e2e timed out after ${spec.timeoutMs ?? 12e4}ms`)), spec.timeoutMs ?? 12e4);
|
|
54
|
+
const aborted = new Promise((_resolve, reject) => {
|
|
55
|
+
if (abort.signal.aborted) reject(abort.signal.reason);
|
|
56
|
+
else abort.signal.addEventListener("abort", () => reject(abort.signal.reason), { once: true });
|
|
57
|
+
});
|
|
58
|
+
aborted.catch(() => {});
|
|
59
|
+
try {
|
|
60
|
+
abort.signal.throwIfAborted();
|
|
61
|
+
fixture = await AppFixture.create(evidence);
|
|
62
|
+
const context = await fixture.start(spec.app, {
|
|
63
|
+
...options,
|
|
64
|
+
signal: abort.signal
|
|
65
|
+
});
|
|
66
|
+
abort.signal.throwIfAborted();
|
|
67
|
+
browser = new OwnedBrowser(fixture.root, evidence);
|
|
68
|
+
const connection = await browser.start({
|
|
69
|
+
...spec.browser,
|
|
70
|
+
binary: options.browser ?? spec.browser?.binary
|
|
71
|
+
}, abort.signal);
|
|
72
|
+
abort.signal.throwIfAborted();
|
|
73
|
+
page = new BrowserPage(connection, context.url, outputDirectory, evidence, spec);
|
|
74
|
+
await page.initialize();
|
|
75
|
+
const activePage = page;
|
|
76
|
+
await Promise.race([aborted, spec.run({
|
|
77
|
+
...context,
|
|
78
|
+
page: activePage,
|
|
79
|
+
evidence,
|
|
80
|
+
outputDirectory,
|
|
81
|
+
signal: abort.signal,
|
|
82
|
+
step: (name, action) => activePage.step(name, action)
|
|
83
|
+
})]);
|
|
84
|
+
await page.waitForNetworkIdle();
|
|
85
|
+
} catch (error) {
|
|
86
|
+
evidence.errors.push(errorMessage(error));
|
|
87
|
+
} finally {
|
|
88
|
+
clearTimeout(timeout);
|
|
89
|
+
if (page) {
|
|
90
|
+
try {
|
|
91
|
+
await page.captureViewports("final");
|
|
92
|
+
} catch (error) {
|
|
93
|
+
evidence.errors.push(errorMessage(error));
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
await page.waitForNetworkIdle();
|
|
97
|
+
} catch (error) {
|
|
98
|
+
evidence.errors.push(errorMessage(error));
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
page.assertHealthy();
|
|
102
|
+
} catch (error) {
|
|
103
|
+
evidence.errors.push(errorMessage(error));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (fixture) for (const owned of fixture.processes) try {
|
|
107
|
+
await writeFile(join(outputDirectory, `${owned.record.role}-${owned.record.pid}.log`), owned.logs());
|
|
108
|
+
} catch (error) {
|
|
109
|
+
evidence.errors.push(`Writing process log: ${errorMessage(error)}`);
|
|
110
|
+
}
|
|
111
|
+
if (browser?.process) try {
|
|
112
|
+
await writeFile(join(outputDirectory, `browser-${browser.process.record.pid}.log`), browser.process.logs());
|
|
113
|
+
} catch (error) {
|
|
114
|
+
evidence.errors.push(`Writing browser log: ${errorMessage(error)}`);
|
|
115
|
+
}
|
|
116
|
+
try {
|
|
117
|
+
await browser?.stop();
|
|
118
|
+
} catch (error) {
|
|
119
|
+
evidence.errors.push(`Browser teardown: ${errorMessage(error)}`);
|
|
120
|
+
}
|
|
121
|
+
if (browser?.process && !browser.process.record.stopped) {
|
|
122
|
+
evidence.errors.push(`Fixture files retained at ${fixture?.root}: browser could not be safely stopped`);
|
|
123
|
+
const stopped = await Promise.allSettled(fixture?.processes.map((owned) => owned.stop()) ?? []);
|
|
124
|
+
for (const entry of stopped) if (entry.status === "rejected") evidence.errors.push(errorMessage(entry.reason));
|
|
125
|
+
} else try {
|
|
126
|
+
await fixture?.stop();
|
|
127
|
+
} catch (error) {
|
|
128
|
+
evidence.errors.push(`App teardown: ${errorMessage(error)}`);
|
|
129
|
+
}
|
|
130
|
+
options.signal?.removeEventListener("abort", forwardAbort);
|
|
131
|
+
process.removeListener("SIGINT", onInterrupt);
|
|
132
|
+
process.removeListener("SIGTERM", onTerminate);
|
|
133
|
+
evidence.durationMs = Math.round(performance.now() - started);
|
|
134
|
+
evidence.status = evidence.errors.length ? "failed" : "passed";
|
|
135
|
+
await writeFile(result.evidenceFile, JSON.stringify(evidence, null, 2) + "\n");
|
|
136
|
+
}
|
|
137
|
+
if (evidence.status === "failed") throw new E2ERunError(result);
|
|
138
|
+
return result;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
//#endregion
|
|
142
|
+
export { E2ERunError, runE2E };
|
|
143
|
+
//# sourceMappingURL=run-e2e.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run-e2e.mjs","names":[],"sources":["../../../../../../../../packages/core/src/e2e/run-e2e.ts"],"sourcesContent":["import { mkdir, mkdtemp, writeFile } from \"node:fs/promises\";\nimport { join, resolve } from \"node:path\";\nimport { OwnedBrowser } from \"./browser\";\nimport { AppFixture } from \"./fixture\";\nimport { BrowserPage } from \"./page\";\nimport { errorMessage, type E2EEvidence, type E2ESpec, type RunE2EOptions } from \"./types\";\n\nexport type E2EResult = { outputDirectory: string; evidenceFile: string; evidence: E2EEvidence };\nexport class E2ERunError extends Error {\n constructor(readonly result: E2EResult) {\n super(\n `Browser e2e failed: ${result.evidence.errors.join(\"; \")}\\nEvidence: ${result.evidenceFile}`,\n );\n this.name = \"E2ERunError\";\n }\n}\n\n/** Each invocation owns a fresh app copy, data root, process groups, profile,\n * screenshots and evidence file. Failures still publish evidence after cleanup.\n */\nexport async function runE2E(spec: E2ESpec, options: RunE2EOptions = {}): Promise<E2EResult> {\n if (!spec || !spec.app || typeof spec.run !== \"function\")\n throw new Error(\"An e2e spec must default-export defineE2E({ app, run })\");\n const base = resolve(options.outputDirectory ?? \".warlock/e2e\");\n await mkdir(base, { recursive: true });\n const outputDirectory = await mkdtemp(join(base, \"run-\"));\n const started = performance.now();\n const evidence: E2EEvidence = {\n schemaVersion: 1,\n startedAt: new Date().toISOString(),\n durationMs: 0,\n status: \"running\",\n steps: [],\n console: [],\n networkFailures: [],\n screenshots: [],\n processes: [],\n errors: [],\n };\n const result: E2EResult = {\n outputDirectory,\n evidenceFile: join(outputDirectory, \"evidence.json\"),\n evidence,\n };\n let fixture: AppFixture | undefined;\n let browser: OwnedBrowser | undefined;\n let page: BrowserPage | undefined;\n const abort = new AbortController();\n const forwardAbort = () =>\n abort.abort(options.signal?.reason ?? new Error(\"Browser e2e aborted\"));\n options.signal?.addEventListener(\"abort\", forwardAbort, { once: true });\n if (options.signal?.aborted) forwardAbort();\n const onInterrupt = () => abort.abort(new Error(\"Browser e2e interrupted (SIGINT)\"));\n const onTerminate = () => abort.abort(new Error(\"Browser e2e interrupted (SIGTERM)\"));\n process.on(\"SIGINT\", onInterrupt);\n process.on(\"SIGTERM\", onTerminate);\n const timeout = setTimeout(\n () => abort.abort(new Error(`Browser e2e timed out after ${spec.timeoutMs ?? 120_000}ms`)),\n spec.timeoutMs ?? 120_000,\n );\n const aborted = new Promise<never>((_resolve, reject) => {\n if (abort.signal.aborted) reject(abort.signal.reason);\n else abort.signal.addEventListener(\"abort\", () => reject(abort.signal.reason), { once: true });\n });\n // Attach immediately: fixture startup may take time before the race is awaited.\n void aborted.catch(() => {});\n try {\n abort.signal.throwIfAborted();\n fixture = await AppFixture.create(evidence);\n const context = await fixture.start(spec.app, { ...options, signal: abort.signal });\n abort.signal.throwIfAborted();\n browser = new OwnedBrowser(fixture.root, evidence);\n const connection = await browser.start(\n { ...spec.browser, binary: options.browser ?? spec.browser?.binary },\n abort.signal,\n );\n abort.signal.throwIfAborted();\n page = new BrowserPage(connection, context.url, outputDirectory, evidence, spec);\n await page.initialize();\n const activePage = page;\n await Promise.race([\n aborted,\n spec.run({\n ...context,\n page: activePage,\n evidence,\n outputDirectory,\n signal: abort.signal,\n step: (name, action) => activePage.step(name, action),\n }),\n ]);\n await page.waitForNetworkIdle();\n } catch (error) {\n evidence.errors.push(errorMessage(error));\n } finally {\n clearTimeout(timeout);\n // Capture both widths even when the spec failed. An overflow failure still\n // captures the offending layout and remains a run failure.\n if (page) {\n try {\n await page.captureViewports(\"final\");\n } catch (error) {\n evidence.errors.push(errorMessage(error));\n }\n try {\n await page.waitForNetworkIdle();\n } catch (error) {\n evidence.errors.push(errorMessage(error));\n }\n try {\n page.assertHealthy();\n } catch (error) {\n evidence.errors.push(errorMessage(error));\n }\n }\n if (fixture) {\n for (const owned of fixture.processes) {\n try {\n await writeFile(\n join(outputDirectory, `${owned.record.role}-${owned.record.pid}.log`),\n owned.logs(),\n );\n } catch (error) {\n evidence.errors.push(`Writing process log: ${errorMessage(error)}`);\n }\n }\n }\n if (browser?.process) {\n try {\n await writeFile(\n join(outputDirectory, `browser-${browser.process.record.pid}.log`),\n browser.process.logs(),\n );\n } catch (error) {\n evidence.errors.push(`Writing browser log: ${errorMessage(error)}`);\n }\n }\n try {\n await browser?.stop();\n } catch (error) {\n evidence.errors.push(`Browser teardown: ${errorMessage(error)}`);\n }\n // Never remove a live browser profile after an identity verification failure.\n if (browser?.process && !browser.process.record.stopped) {\n evidence.errors.push(\n `Fixture files retained at ${fixture?.root}: browser could not be safely stopped`,\n );\n const stopped = await Promise.allSettled(\n fixture?.processes.map((owned) => owned.stop()) ?? [],\n );\n for (const entry of stopped)\n if (entry.status === \"rejected\") evidence.errors.push(errorMessage(entry.reason));\n } else {\n try {\n await fixture?.stop();\n } catch (error) {\n evidence.errors.push(`App teardown: ${errorMessage(error)}`);\n }\n }\n options.signal?.removeEventListener(\"abort\", forwardAbort);\n process.removeListener(\"SIGINT\", onInterrupt);\n process.removeListener(\"SIGTERM\", onTerminate);\n evidence.durationMs = Math.round(performance.now() - started);\n evidence.status = evidence.errors.length ? \"failed\" : \"passed\";\n await writeFile(result.evidenceFile, JSON.stringify(evidence, null, 2) + \"\\n\");\n }\n if (evidence.status === \"failed\") throw new E2ERunError(result);\n return result;\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { BrowserPage } from "./page.mjs";
|
|
2
|
+
//#region ../../packages/core/src/e2e/types.d.ts
|
|
3
|
+
export type Command = readonly [executable: string, ...args: string[]];
|
|
4
|
+
export type Pattern = string | RegExp;
|
|
5
|
+
export type Viewport = {
|
|
6
|
+
name: string;
|
|
7
|
+
width: number;
|
|
8
|
+
height: number;
|
|
9
|
+
mobile?: boolean;
|
|
10
|
+
};
|
|
11
|
+
export declare const defaultViewports: readonly Viewport[];
|
|
12
|
+
export type FixtureContext = {
|
|
13
|
+
/** Disposable copy of the app. All commands run here. */
|
|
14
|
+
directory: string;
|
|
15
|
+
dataRoot: string;
|
|
16
|
+
/** OS-selected ephemeral port, reserved until immediately before app startup. */
|
|
17
|
+
port: number;
|
|
18
|
+
url: string;
|
|
19
|
+
/** Write an actual port here when the app binds port 0 itself. */
|
|
20
|
+
portFile: string;
|
|
21
|
+
};
|
|
22
|
+
export type AppFixtureOptions = {
|
|
23
|
+
directory?: string;
|
|
24
|
+
/** Optional build, executed inside the disposable copy before start. */
|
|
25
|
+
build?: Command | ((fixture: FixtureContext) => Command);
|
|
26
|
+
/** Defaults to [process.execPath, "dist/app.js"]. Must stay in the foreground. */
|
|
27
|
+
start?: Command | ((fixture: FixtureContext) => Command);
|
|
28
|
+
/** Only these app variables are added to the isolated environment. */
|
|
29
|
+
env?: Record<string, string> | ((fixture: FixtureContext) => Record<string, string>);
|
|
30
|
+
/** Directories prepended to PATH, for fixture-owned fake engines/binaries. */
|
|
31
|
+
path?: string[];
|
|
32
|
+
/** Prepare data/fake binaries in the disposable fixture before build/start. */
|
|
33
|
+
prepare?: (fixture: FixtureContext) => void | Promise<void>;
|
|
34
|
+
/** Additional relative directories to omit when copying app files. */
|
|
35
|
+
exclude?: string[];
|
|
36
|
+
readyPath?: string;
|
|
37
|
+
/** If true, await portFile instead of using the preallocated port. */
|
|
38
|
+
portFromFile?: boolean;
|
|
39
|
+
startupTimeoutMs?: number;
|
|
40
|
+
};
|
|
41
|
+
export type BrowserOptions = {
|
|
42
|
+
binary?: string;
|
|
43
|
+
/** Additional Chrome flags, e.g. --no-sandbox in a controlled root container. */
|
|
44
|
+
args?: string[];
|
|
45
|
+
};
|
|
46
|
+
export type NetworkFailure = {
|
|
47
|
+
at: string;
|
|
48
|
+
url: string;
|
|
49
|
+
status?: number;
|
|
50
|
+
errorText?: string;
|
|
51
|
+
canceled?: boolean;
|
|
52
|
+
allowed?: boolean;
|
|
53
|
+
};
|
|
54
|
+
export type ConsoleEntry = {
|
|
55
|
+
at: string;
|
|
56
|
+
level: string;
|
|
57
|
+
text: string;
|
|
58
|
+
allowed?: boolean;
|
|
59
|
+
};
|
|
60
|
+
export type StepEvidence = {
|
|
61
|
+
name: string;
|
|
62
|
+
startedAt: string;
|
|
63
|
+
durationMs: number;
|
|
64
|
+
status: "passed" | "failed";
|
|
65
|
+
error?: string;
|
|
66
|
+
};
|
|
67
|
+
export type ProcessEvidence = {
|
|
68
|
+
role: string;
|
|
69
|
+
pid: number;
|
|
70
|
+
childPid?: number;
|
|
71
|
+
command: string[];
|
|
72
|
+
ownerFile: string;
|
|
73
|
+
stopped?: boolean;
|
|
74
|
+
};
|
|
75
|
+
export type E2EEvidence = {
|
|
76
|
+
schemaVersion: 1;
|
|
77
|
+
startedAt: string;
|
|
78
|
+
durationMs: number;
|
|
79
|
+
status: "running" | "passed" | "failed";
|
|
80
|
+
url?: string;
|
|
81
|
+
steps: StepEvidence[];
|
|
82
|
+
console: ConsoleEntry[];
|
|
83
|
+
networkFailures: NetworkFailure[];
|
|
84
|
+
screenshots: {
|
|
85
|
+
name: string;
|
|
86
|
+
file: string;
|
|
87
|
+
viewport: Viewport;
|
|
88
|
+
}[];
|
|
89
|
+
processes: ProcessEvidence[];
|
|
90
|
+
errors: string[];
|
|
91
|
+
};
|
|
92
|
+
export type NetworkAllowance = {
|
|
93
|
+
url: Pattern;
|
|
94
|
+
status?: number;
|
|
95
|
+
errorText?: Pattern;
|
|
96
|
+
};
|
|
97
|
+
export type E2EContext = FixtureContext & {
|
|
98
|
+
page: BrowserPage;
|
|
99
|
+
evidence: E2EEvidence;
|
|
100
|
+
outputDirectory: string;
|
|
101
|
+
signal: AbortSignal;
|
|
102
|
+
step: <T>(name: string, action: () => Promise<T>) => Promise<T>;
|
|
103
|
+
};
|
|
104
|
+
export type E2ESpec = {
|
|
105
|
+
app: AppFixtureOptions;
|
|
106
|
+
browser?: BrowserOptions;
|
|
107
|
+
viewports?: readonly Viewport[];
|
|
108
|
+
allowConsoleErrors?: Pattern[];
|
|
109
|
+
allowNetworkFailures?: NetworkAllowance[];
|
|
110
|
+
/** Long-lived HTTP requests excluded from the final network-idle barrier. */
|
|
111
|
+
allowPendingRequests?: Pattern[];
|
|
112
|
+
timeoutMs?: number;
|
|
113
|
+
run: (context: E2EContext) => Promise<void>;
|
|
114
|
+
};
|
|
115
|
+
export type RunE2EOptions = {
|
|
116
|
+
browser?: string;
|
|
117
|
+
path?: string[];
|
|
118
|
+
outputDirectory?: string;
|
|
119
|
+
signal?: AbortSignal;
|
|
120
|
+
};
|
|
121
|
+
export declare function defineE2E(spec: E2ESpec): E2ESpec;
|
|
122
|
+
//#endregion
|
|
123
|
+
//# sourceMappingURL=types.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.mts","names":[],"sources":["../../../../../../../../packages/core/src/e2e/types.ts"],"mappings":";;YAEY,oBAAoB,uBAAuB;YAC3C,mBAAmB;YACnB;EAAa;EAAc;EAAe;EAAgB;;qBACzD,2BAA2B;YAK5B;;EAEV;EACA;;EAEA;EACA;;EAEA;;YAEU;EACV;;EAEA,QAAQ,YAAY,SAAS,mBAAmB;;EAEhD,QAAQ,YAAY,SAAS,mBAAmB;;EAEhD,MAAM,2BAA2B,SAAS,mBAAmB;;EAE7D;;EAEA,WAAW,SAAS,0BAA0B;;EAE9C;EACA;;EAEA;EACA;;YAEU;EACV;;EAEA;;YAEU;EACV;EACA;EACA;EACA;EACA;EACA;;YAEU;EAAiB;EAAY;EAAe;EAAc;;YAC1D;EACV;EACA;EACA;EACA;EACA;;YAEU;EACV;EACA;EACA;EACA;EACA;EACA;;YAEU;EACV;EACA;EACA;EACA;EACA;EACA,OAAO;EACP,SAAS;EACT,iBAAiB;EACjB;IAAe;IAAc;IAAc,UAAU;;EACrD,WAAW;EACX;;YAEU;EAAqB,KAAK;EAAS;EAAiB,YAAY;;YAChE,aAAa;EACvB,MAAM;EACN,UAAU;EACV;EACA,QAAQ;EACR,OAAO,GAAG,cAAc,cAAc,QAAQ,OAAO,QAAQ;;YAEnD;EACV,KAAK;EACL,UAAU;EACV,qBAAqB;EACrB,qBAAqB;EACrB,uBAAuB;;EAEvB,uBAAuB;EACvB;EACA,MAAM,SAAS,eAAe;;YAEpB;EACV;EACA;EACA;EACA,SAAS;;wBAGK,UAAU,MAAM,UAAU"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
//#region ../../packages/core/src/e2e/types.ts
|
|
2
|
+
const defaultViewports = [{
|
|
3
|
+
name: "desktop",
|
|
4
|
+
width: 1280,
|
|
5
|
+
height: 800
|
|
6
|
+
}, {
|
|
7
|
+
name: "phone",
|
|
8
|
+
width: 390,
|
|
9
|
+
height: 844,
|
|
10
|
+
mobile: true
|
|
11
|
+
}];
|
|
12
|
+
function defineE2E(spec) {
|
|
13
|
+
return spec;
|
|
14
|
+
}
|
|
15
|
+
function errorMessage(error) {
|
|
16
|
+
return error instanceof Error ? error.message : String(error);
|
|
17
|
+
}
|
|
18
|
+
function matches(pattern, value) {
|
|
19
|
+
return typeof pattern === "string" ? value.includes(pattern) : new RegExp(pattern.source, pattern.flags).test(value);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
//#endregion
|
|
23
|
+
export { defaultViewports, defineE2E, errorMessage, matches };
|
|
24
|
+
//# sourceMappingURL=types.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.mjs","names":[],"sources":["../../../../../../../../packages/core/src/e2e/types.ts"],"sourcesContent":["import type { BrowserPage } from \"./page\";\n\nexport type Command = readonly [executable: string, ...args: string[]];\nexport type Pattern = string | RegExp;\nexport type Viewport = { name: string; width: number; height: number; mobile?: boolean };\nexport const defaultViewports: readonly Viewport[] = [\n { name: \"desktop\", width: 1280, height: 800 },\n { name: \"phone\", width: 390, height: 844, mobile: true },\n];\n\nexport type FixtureContext = {\n /** Disposable copy of the app. All commands run here. */\n directory: string;\n dataRoot: string;\n /** OS-selected ephemeral port, reserved until immediately before app startup. */\n port: number;\n url: string;\n /** Write an actual port here when the app binds port 0 itself. */\n portFile: string;\n};\nexport type AppFixtureOptions = {\n directory?: string;\n /** Optional build, executed inside the disposable copy before start. */\n build?: Command | ((fixture: FixtureContext) => Command);\n /** Defaults to [process.execPath, \"dist/app.js\"]. Must stay in the foreground. */\n start?: Command | ((fixture: FixtureContext) => Command);\n /** Only these app variables are added to the isolated environment. */\n env?: Record<string, string> | ((fixture: FixtureContext) => Record<string, string>);\n /** Directories prepended to PATH, for fixture-owned fake engines/binaries. */\n path?: string[];\n /** Prepare data/fake binaries in the disposable fixture before build/start. */\n prepare?: (fixture: FixtureContext) => void | Promise<void>;\n /** Additional relative directories to omit when copying app files. */\n exclude?: string[];\n readyPath?: string;\n /** If true, await portFile instead of using the preallocated port. */\n portFromFile?: boolean;\n startupTimeoutMs?: number;\n};\nexport type BrowserOptions = {\n binary?: string;\n /** Additional Chrome flags, e.g. --no-sandbox in a controlled root container. */\n args?: string[];\n};\nexport type NetworkFailure = {\n at: string;\n url: string;\n status?: number;\n errorText?: string;\n canceled?: boolean;\n allowed?: boolean;\n};\nexport type ConsoleEntry = { at: string; level: string; text: string; allowed?: boolean };\nexport type StepEvidence = {\n name: string;\n startedAt: string;\n durationMs: number;\n status: \"passed\" | \"failed\";\n error?: string;\n};\nexport type ProcessEvidence = {\n role: string;\n pid: number;\n childPid?: number;\n command: string[];\n ownerFile: string;\n stopped?: boolean;\n};\nexport type E2EEvidence = {\n schemaVersion: 1;\n startedAt: string;\n durationMs: number;\n status: \"running\" | \"passed\" | \"failed\";\n url?: string;\n steps: StepEvidence[];\n console: ConsoleEntry[];\n networkFailures: NetworkFailure[];\n screenshots: { name: string; file: string; viewport: Viewport }[];\n processes: ProcessEvidence[];\n errors: string[];\n};\nexport type NetworkAllowance = { url: Pattern; status?: number; errorText?: Pattern };\nexport type E2EContext = FixtureContext & {\n page: BrowserPage;\n evidence: E2EEvidence;\n outputDirectory: string;\n signal: AbortSignal;\n step: <T>(name: string, action: () => Promise<T>) => Promise<T>;\n};\nexport type E2ESpec = {\n app: AppFixtureOptions;\n browser?: BrowserOptions;\n viewports?: readonly Viewport[];\n allowConsoleErrors?: Pattern[];\n allowNetworkFailures?: NetworkAllowance[];\n /** Long-lived HTTP requests excluded from the final network-idle barrier. */\n allowPendingRequests?: Pattern[];\n timeoutMs?: number;\n run: (context: E2EContext) => Promise<void>;\n};\nexport type RunE2EOptions = {\n browser?: string;\n path?: string[];\n outputDirectory?: string;\n signal?: AbortSignal;\n};\n\nexport function defineE2E(spec: E2ESpec): E2ESpec {\n return spec;\n}\nexport function errorMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\nexport function matches(pattern: Pattern, value: string): boolean {\n // A fresh regexp avoids stateful /g and /y allowlists changing their verdict.\n return typeof pattern === \"string\"\n ? value.includes(pattern)\n : new RegExp(pattern.source, pattern.flags).test(value);\n}\n"],"mappings":";AAKA,MAAa,mBAAwC,CACnD;CAAE,MAAM;CAAW,OAAO;CAAM,QAAQ;AAAI,GAC5C;CAAE,MAAM;CAAS,OAAO;CAAK,QAAQ;CAAK,QAAQ;AAAK,CACzD;AAmGA,SAAgB,UAAU,MAAwB;CAChD,OAAO;AACT;AACA,SAAgB,aAAa,OAAwB;CACnD,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAC9D;AACA,SAAgB,QAAQ,SAAkB,OAAwB;CAEhE,OAAO,OAAO,YAAY,WACtB,MAAM,SAAS,OAAO,IACtB,IAAI,OAAO,QAAQ,QAAQ,QAAQ,KAAK,CAAC,CAAC,KAAK,KAAK;AAC1D"}
|
package/esm/secrets/errors.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
//#region ../../packages/core/src/secrets/errors.d.ts
|
|
2
|
-
export type SecretStoreErrorCode = "INVALID_NAME" | "INVALID_VALUE" | "INVALID_NAMESPACE" | "READ_ONLY" | "KEY_REQUIRED" | "UNAVAILABLE" | "UNSAFE_FILE" | "CORRUPT_FILE";
|
|
2
|
+
export type SecretStoreErrorCode = "INVALID_DRIVER" | "MEMORY_IN_PRODUCTION" | "INVALID_NAME" | "INVALID_VALUE" | "INVALID_NAMESPACE" | "READ_ONLY" | "KEY_REQUIRED" | "UNAVAILABLE" | "UNSAFE_FILE" | "CORRUPT_FILE";
|
|
3
3
|
/** Safe to log: never carries names, values, paths, or vendor error causes. */
|
|
4
4
|
export declare class SecretStoreError extends Error {
|
|
5
5
|
readonly code: SecretStoreErrorCode;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.d.mts","names":[],"sources":["../../../../../../../../packages/core/src/secrets/errors.ts"],"mappings":";YAAY;;
|
|
1
|
+
{"version":3,"file":"errors.d.mts","names":[],"sources":["../../../../../../../../packages/core/src/secrets/errors.ts"],"mappings":";YAAY;;qBAaC,yBAAyB;WACD,MAAM;EAAzC,YAAmC,MAAM"}
|
package/esm/secrets/errors.mjs
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
var SecretStoreError = class extends Error {
|
|
4
4
|
constructor(code) {
|
|
5
5
|
super({
|
|
6
|
+
INVALID_DRIVER: "secrets.driver / SECRETS_DRIVER must be auto or memory.",
|
|
7
|
+
MEMORY_IN_PRODUCTION: "The memory secrets driver requires secrets.allowMemoryInProduction: true when NODE_ENV=production.",
|
|
6
8
|
INVALID_NAME: "Secret names must be valid Unicode strings of 1 to 256 UTF-16 code units.",
|
|
7
9
|
INVALID_VALUE: "Secret values must be strings.",
|
|
8
10
|
INVALID_NAMESPACE: "A nonempty secret store namespace is required.",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.mjs","names":[],"sources":["../../../../../../../../packages/core/src/secrets/errors.ts"],"sourcesContent":["export type SecretStoreErrorCode =\n | \"INVALID_NAME\"\n | \"INVALID_VALUE\"\n | \"INVALID_NAMESPACE\"\n | \"READ_ONLY\"\n | \"KEY_REQUIRED\"\n | \"UNAVAILABLE\"\n | \"UNSAFE_FILE\"\n | \"CORRUPT_FILE\";\n\n/** Safe to log: never carries names, values, paths, or vendor error causes. */\nexport class SecretStoreError extends Error {\n public constructor(public readonly code: SecretStoreErrorCode) {\n super(\n {\n INVALID_NAME: \"Secret names must be valid Unicode strings of 1 to 256 UTF-16 code units.\",\n INVALID_VALUE: \"Secret values must be strings.\",\n INVALID_NAMESPACE: \"A nonempty secret store namespace is required.\",\n READ_ONLY: \"This secret is supplied by a read-only environment variable.\",\n KEY_REQUIRED: \"File secrets require encryption.key: exactly 64 hexadecimal characters.\",\n UNAVAILABLE: \"The selected secret backend is unavailable. No unsafe write was attempted.\",\n UNSAFE_FILE: \"Secret storage must use an owned, private directory and regular files.\",\n CORRUPT_FILE:\n \"A secret file could not be authenticated. Check the app key and restore a backup.\",\n }[code],\n );\n this.name = \"SecretStoreError\";\n }\n}\n"],"mappings":";;
|
|
1
|
+
{"version":3,"file":"errors.mjs","names":[],"sources":["../../../../../../../../packages/core/src/secrets/errors.ts"],"sourcesContent":["export type SecretStoreErrorCode =\n | \"INVALID_DRIVER\"\n | \"MEMORY_IN_PRODUCTION\"\n | \"INVALID_NAME\"\n | \"INVALID_VALUE\"\n | \"INVALID_NAMESPACE\"\n | \"READ_ONLY\"\n | \"KEY_REQUIRED\"\n | \"UNAVAILABLE\"\n | \"UNSAFE_FILE\"\n | \"CORRUPT_FILE\";\n\n/** Safe to log: never carries names, values, paths, or vendor error causes. */\nexport class SecretStoreError extends Error {\n public constructor(public readonly code: SecretStoreErrorCode) {\n super(\n {\n INVALID_DRIVER: \"secrets.driver / SECRETS_DRIVER must be auto or memory.\",\n MEMORY_IN_PRODUCTION:\n \"The memory secrets driver requires secrets.allowMemoryInProduction: true when NODE_ENV=production.\",\n INVALID_NAME: \"Secret names must be valid Unicode strings of 1 to 256 UTF-16 code units.\",\n INVALID_VALUE: \"Secret values must be strings.\",\n INVALID_NAMESPACE: \"A nonempty secret store namespace is required.\",\n READ_ONLY: \"This secret is supplied by a read-only environment variable.\",\n KEY_REQUIRED: \"File secrets require encryption.key: exactly 64 hexadecimal characters.\",\n UNAVAILABLE: \"The selected secret backend is unavailable. No unsafe write was attempted.\",\n UNSAFE_FILE: \"Secret storage must use an owned, private directory and regular files.\",\n CORRUPT_FILE:\n \"A secret file could not be authenticated. Check the app key and restore a backup.\",\n }[code],\n );\n this.name = \"SecretStoreError\";\n }\n}\n"],"mappings":";;AAaA,IAAa,mBAAb,cAAsC,MAAM;CAC1C,AAAO,YAAY,AAAgB,MAA4B;EAC7D,MACE;GACE,gBAAgB;GAChB,sBACE;GACF,cAAc;GACd,eAAe;GACf,mBAAmB;GACnB,WAAW;GACX,cAAc;GACd,aAAa;GACb,aAAa;GACb,cACE;EACJ,EAAE,KACJ;EAhBiC;EAiBjC,KAAK,OAAO;CACd;AACF"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { SecretStoreError } from "./errors.mjs";
|
|
2
|
+
|
|
3
|
+
//#region ../../packages/core/src/secrets/resolve-driver.ts
|
|
4
|
+
/** Resolve and enforce policy before looking up even an already cached store. */
|
|
5
|
+
function resolveSecretsDriver(options) {
|
|
6
|
+
const driver = options.driver ?? process.env.SECRETS_DRIVER ?? "auto";
|
|
7
|
+
if (driver !== "auto" && driver !== "memory") throw new SecretStoreError("INVALID_DRIVER");
|
|
8
|
+
if (driver === "memory" && process.env.NODE_ENV === "production" && options.allowMemoryInProduction !== true) throw new SecretStoreError("MEMORY_IN_PRODUCTION");
|
|
9
|
+
return driver;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
//#endregion
|
|
13
|
+
export { resolveSecretsDriver };
|
|
14
|
+
//# sourceMappingURL=resolve-driver.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolve-driver.mjs","names":[],"sources":["../../../../../../../../packages/core/src/secrets/resolve-driver.ts"],"sourcesContent":["import { SecretStoreError } from \"./errors\";\nimport type { SecretsConfigurations } from \"./types\";\n\n/** Resolve and enforce policy before looking up even an already cached store. */\nexport function resolveSecretsDriver(options: SecretsConfigurations): \"auto\" | \"memory\" {\n const driver = options.driver ?? process.env.SECRETS_DRIVER ?? \"auto\";\n if (driver !== \"auto\" && driver !== \"memory\") throw new SecretStoreError(\"INVALID_DRIVER\");\n if (\n driver === \"memory\" &&\n process.env.NODE_ENV === \"production\" &&\n options.allowMemoryInProduction !== true\n ) {\n throw new SecretStoreError(\"MEMORY_IN_PRODUCTION\");\n }\n return driver;\n}\n"],"mappings":";;;;AAIA,SAAgB,qBAAqB,SAAmD;CACtF,MAAM,SAAS,QAAQ,UAAU,QAAQ,IAAI,kBAAkB;CAC/D,IAAI,WAAW,UAAU,WAAW,UAAU,MAAM,IAAI,iBAAiB,gBAAgB;CACzF,IACE,WAAW,YACX,QAAQ,IAAI,aAAa,gBACzB,QAAQ,4BAA4B,MAEpC,MAAM,IAAI,iBAAiB,sBAAsB;CAEnD,OAAO;AACT"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"secret-store.d.mts","names":[],"sources":["../../../../../../../../packages/core/src/secrets/secret-store.ts"],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"secret-store.d.mts","names":[],"sources":["../../../../../../../../packages/core/src/secrets/secret-store.ts"],"mappings":";;;qBAWa;UACH;mBACS;mBACA;WACD;EAEhB,YAAmB,SAAS;UAoBpB;;EAYD,gBAAgB;UAMf;UAKA;UAIA;UAIA;UAQM;EAUD,IAAI,eAAe;EAkBnB,IAAI,eAAe;EAInB,IAAI,cAAc,gBAAgB;;EA2BlC,OAAO,eAAe"}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { config } from "../config/config-getter.mjs";
|
|
2
2
|
import { storagePath } from "../utils/paths.mjs";
|
|
3
3
|
import { SecretStoreError } from "./errors.mjs";
|
|
4
|
+
import { MemorySecretBackend } from "./memory-backend.mjs";
|
|
4
5
|
import { FileSecretBackend } from "./file-backend.mjs";
|
|
6
|
+
import { resolveSecretsDriver } from "./resolve-driver.mjs";
|
|
5
7
|
import { KeyringSecretBackend } from "./keyring-backend.mjs";
|
|
6
8
|
import { registerLogSecret } from "@warlock.js/logger";
|
|
7
9
|
|
|
@@ -11,7 +13,8 @@ var SecretStore = class {
|
|
|
11
13
|
constructor(options) {
|
|
12
14
|
if (typeof options.namespace !== "string" || !options.namespace.trim() || Buffer.from(options.namespace).toString("utf8") !== options.namespace) throw new SecretStoreError("INVALID_NAMESPACE");
|
|
13
15
|
this.namespace = options.namespace;
|
|
14
|
-
|
|
16
|
+
const driver = options.backend ? "auto" : resolveSecretsDriver(options);
|
|
17
|
+
this.backend = options.backend ?? (driver === "memory" ? new MemorySecretBackend() : void 0);
|
|
15
18
|
const encryptionKey = options.encryptionKey;
|
|
16
19
|
this.file = new FileSecretBackend(options.directory ?? storagePath("secrets"), typeof encryptionKey === "function" ? encryptionKey : () => encryptionKey ?? config.key("encryption.key"));
|
|
17
20
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"secret-store.mjs","names":[],"sources":["../../../../../../../../packages/core/src/secrets/secret-store.ts"],"sourcesContent":["import { registerLogSecret } from \"@warlock.js/logger\";\nimport { config } from \"../config/config-getter\";\nimport { storagePath } from \"../utils/paths\";\nimport { SecretStoreError } from \"./errors\";\nimport { FileSecretBackend } from \"./file-backend\";\nimport { KeyringSecretBackend } from \"./keyring-backend\";\nimport type { SecretBackend, SecretStoreOptions } from \"./types\";\n\n/** Lazily prepared, application-scoped credentials. All operations are asynchronous. */\nexport class SecretStore {\n private primary: Promise<KeyringSecretBackend | undefined> | undefined;\n private readonly file: FileSecretBackend;\n private readonly backend: SecretBackend | undefined;\n public readonly namespace: string;\n\n public constructor(options: SecretStoreOptions) {\n if (\n typeof options.namespace !== \"string\" ||\n !options.namespace.trim() ||\n Buffer.from(options.namespace).toString(\"utf8\") !== options.namespace\n ) {\n throw new SecretStoreError(\"INVALID_NAMESPACE\");\n }\n this.namespace = options.namespace;\n this.backend = options.backend;\n const encryptionKey = options.encryptionKey;\n this.file = new FileSecretBackend(\n options.directory ?? storagePath(\"secrets\"),\n typeof encryptionKey === \"function\"\n ? encryptionKey\n : () => encryptionKey ?? config.key<string | undefined>(\"encryption.key\"),\n );\n }\n\n private validate(name: string): void {\n if (\n typeof name !== \"string\" ||\n !name.length ||\n name.length > 256 ||\n Buffer.from(name).toString(\"utf8\") !== name\n ) {\n throw new SecretStoreError(\"INVALID_NAME\");\n }\n }\n\n /** Exact, collision-free environment variable name (UTF-8 encoded as uppercase hex). */\n public environmentName(name: string): string {\n this.validate(name);\n const hex = (value: string) => Buffer.from(value).toString(\"hex\").toUpperCase();\n return `WARLOCK_SECRET_${hex(this.namespace)}_${hex(name)}`;\n }\n\n private keyring(): Promise<KeyringSecretBackend | undefined> {\n this.primary ??= KeyringSecretBackend.load(this.namespace);\n return this.primary;\n }\n\n private register(name: string, value?: string): void {\n registerLogSecret(value ?? \"\", [name, this.environmentName(name)]);\n }\n\n private environment(name: string): string | undefined {\n return process.env[this.environmentName(name)];\n }\n\n private assertWritable(name: string): void {\n const value = this.environment(name);\n if (value !== undefined) {\n this.register(name, value);\n throw new SecretStoreError(\"READ_ONLY\");\n }\n }\n\n private async safe<T>(operation: () => Promise<T>): Promise<T> {\n try {\n return await operation();\n } catch (error) {\n if (error instanceof SecretStoreError) throw error;\n // Custom backends may throw messages containing values. Do not propagate them.\n throw new SecretStoreError(\"UNAVAILABLE\");\n }\n }\n\n public async get(name: string): Promise<string | undefined> {\n this.validate(name);\n this.register(name);\n return this.safe(async () => {\n let value: string | undefined;\n if (this.backend) {\n value = await this.backend.get(this.namespace, name);\n } else {\n const primary = await this.keyring();\n value = await primary?.get(this.namespace, name);\n value ??= this.environment(name);\n value ??= await this.file.get(this.namespace, name);\n }\n if (value !== undefined) this.register(name, value);\n return value;\n });\n }\n\n public async has(name: string): Promise<boolean> {\n return (await this.get(name)) !== undefined;\n }\n\n public async set(name: string, value: string): Promise<void> {\n this.validate(name);\n if (typeof value !== \"string\") throw new SecretStoreError(\"INVALID_VALUE\");\n // Register before any backend call, including failed writes.\n this.register(name, value);\n return this.safe(async () => {\n if (this.backend) {\n const previous = await this.backend.get(this.namespace, name);\n if (previous !== undefined) this.register(name, previous);\n return this.backend.set(this.namespace, name, value);\n }\n this.assertWritable(name);\n const primary = await this.keyring();\n const backend = primary ?? this.file;\n if (primary) {\n const fallback = await this.file.get(this.namespace, name);\n if (fallback !== undefined) this.register(name, fallback);\n }\n // Retain masking for the old value after replacement as well.\n const previous = await backend.get(this.namespace, name);\n if (previous !== undefined) this.register(name, previous);\n await backend.set(this.namespace, name, value);\n if (primary) await this.file.remove(this.namespace, name);\n });\n }\n\n /** Remove every writable layer so a fallback value cannot reappear. */\n public async remove(name: string): Promise<boolean> {\n this.validate(name);\n this.register(name);\n return this.safe(async () => {\n if (this.backend) {\n const previous = await this.backend.get(this.namespace, name);\n if (previous !== undefined) this.register(name, previous);\n return this.backend.remove(this.namespace, name);\n }\n this.assertWritable(name);\n const primary = await this.keyring();\n const previous = await primary?.get(this.namespace, name);\n if (previous !== undefined) this.register(name, previous);\n const fromFile = await this.file.get(this.namespace, name);\n if (fromFile !== undefined) this.register(name, fromFile);\n const removed = (await primary?.remove(this.namespace, name)) ?? false;\n return (await this.file.remove(this.namespace, name)) || removed;\n });\n }\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"secret-store.mjs","names":[],"sources":["../../../../../../../../packages/core/src/secrets/secret-store.ts"],"sourcesContent":["import { registerLogSecret } from \"@warlock.js/logger\";\nimport { config } from \"../config/config-getter\";\nimport { storagePath } from \"../utils/paths\";\nimport { SecretStoreError } from \"./errors\";\nimport { FileSecretBackend } from \"./file-backend\";\nimport { MemorySecretBackend } from \"./memory-backend\";\nimport { resolveSecretsDriver } from \"./resolve-driver\";\nimport { KeyringSecretBackend } from \"./keyring-backend\";\nimport type { SecretBackend, SecretStoreOptions } from \"./types\";\n\n/** Lazily prepared, application-scoped credentials. All operations are asynchronous. */\nexport class SecretStore {\n private primary: Promise<KeyringSecretBackend | undefined> | undefined;\n private readonly file: FileSecretBackend;\n private readonly backend: SecretBackend | undefined;\n public readonly namespace: string;\n\n public constructor(options: SecretStoreOptions) {\n if (\n typeof options.namespace !== \"string\" ||\n !options.namespace.trim() ||\n Buffer.from(options.namespace).toString(\"utf8\") !== options.namespace\n ) {\n throw new SecretStoreError(\"INVALID_NAMESPACE\");\n }\n this.namespace = options.namespace;\n const driver = options.backend ? \"auto\" : resolveSecretsDriver(options);\n this.backend = options.backend ?? (driver === \"memory\" ? new MemorySecretBackend() : undefined);\n const encryptionKey = options.encryptionKey;\n this.file = new FileSecretBackend(\n options.directory ?? storagePath(\"secrets\"),\n typeof encryptionKey === \"function\"\n ? encryptionKey\n : () => encryptionKey ?? config.key<string | undefined>(\"encryption.key\"),\n );\n }\n\n private validate(name: string): void {\n if (\n typeof name !== \"string\" ||\n !name.length ||\n name.length > 256 ||\n Buffer.from(name).toString(\"utf8\") !== name\n ) {\n throw new SecretStoreError(\"INVALID_NAME\");\n }\n }\n\n /** Exact, collision-free environment variable name (UTF-8 encoded as uppercase hex). */\n public environmentName(name: string): string {\n this.validate(name);\n const hex = (value: string) => Buffer.from(value).toString(\"hex\").toUpperCase();\n return `WARLOCK_SECRET_${hex(this.namespace)}_${hex(name)}`;\n }\n\n private keyring(): Promise<KeyringSecretBackend | undefined> {\n this.primary ??= KeyringSecretBackend.load(this.namespace);\n return this.primary;\n }\n\n private register(name: string, value?: string): void {\n registerLogSecret(value ?? \"\", [name, this.environmentName(name)]);\n }\n\n private environment(name: string): string | undefined {\n return process.env[this.environmentName(name)];\n }\n\n private assertWritable(name: string): void {\n const value = this.environment(name);\n if (value !== undefined) {\n this.register(name, value);\n throw new SecretStoreError(\"READ_ONLY\");\n }\n }\n\n private async safe<T>(operation: () => Promise<T>): Promise<T> {\n try {\n return await operation();\n } catch (error) {\n if (error instanceof SecretStoreError) throw error;\n // Custom backends may throw messages containing values. Do not propagate them.\n throw new SecretStoreError(\"UNAVAILABLE\");\n }\n }\n\n public async get(name: string): Promise<string | undefined> {\n this.validate(name);\n this.register(name);\n return this.safe(async () => {\n let value: string | undefined;\n if (this.backend) {\n value = await this.backend.get(this.namespace, name);\n } else {\n const primary = await this.keyring();\n value = await primary?.get(this.namespace, name);\n value ??= this.environment(name);\n value ??= await this.file.get(this.namespace, name);\n }\n if (value !== undefined) this.register(name, value);\n return value;\n });\n }\n\n public async has(name: string): Promise<boolean> {\n return (await this.get(name)) !== undefined;\n }\n\n public async set(name: string, value: string): Promise<void> {\n this.validate(name);\n if (typeof value !== \"string\") throw new SecretStoreError(\"INVALID_VALUE\");\n // Register before any backend call, including failed writes.\n this.register(name, value);\n return this.safe(async () => {\n if (this.backend) {\n const previous = await this.backend.get(this.namespace, name);\n if (previous !== undefined) this.register(name, previous);\n return this.backend.set(this.namespace, name, value);\n }\n this.assertWritable(name);\n const primary = await this.keyring();\n const backend = primary ?? this.file;\n if (primary) {\n const fallback = await this.file.get(this.namespace, name);\n if (fallback !== undefined) this.register(name, fallback);\n }\n // Retain masking for the old value after replacement as well.\n const previous = await backend.get(this.namespace, name);\n if (previous !== undefined) this.register(name, previous);\n await backend.set(this.namespace, name, value);\n if (primary) await this.file.remove(this.namespace, name);\n });\n }\n\n /** Remove every writable layer so a fallback value cannot reappear. */\n public async remove(name: string): Promise<boolean> {\n this.validate(name);\n this.register(name);\n return this.safe(async () => {\n if (this.backend) {\n const previous = await this.backend.get(this.namespace, name);\n if (previous !== undefined) this.register(name, previous);\n return this.backend.remove(this.namespace, name);\n }\n this.assertWritable(name);\n const primary = await this.keyring();\n const previous = await primary?.get(this.namespace, name);\n if (previous !== undefined) this.register(name, previous);\n const fromFile = await this.file.get(this.namespace, name);\n if (fromFile !== undefined) this.register(name, fromFile);\n const removed = (await primary?.remove(this.namespace, name)) ?? false;\n return (await this.file.remove(this.namespace, name)) || removed;\n });\n }\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"secrets.d.mts","names":[],"sources":["../../../../../../../../packages/core/src/secrets/secrets.ts"],"mappings":";;
|
|
1
|
+
{"version":3,"file":"secrets.d.mts","names":[],"sources":["../../../../../../../../packages/core/src/secrets/secrets.ts"],"mappings":";;qBA6Ca;EACL,IAAG,eAAgB;EAGnB,IAAG,cAAa,gBAAkB;EAGlC,OAAM,eAAgB;EAGtB,IAAG,eAAgB;EAGzB,gBAAe"}
|
package/esm/secrets/secrets.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { config } from "../config/config-getter.mjs";
|
|
2
2
|
import { rootPath, storagePath } from "../utils/paths.mjs";
|
|
3
|
+
import { resolveSecretsDriver } from "./resolve-driver.mjs";
|
|
3
4
|
import { SecretStore } from "./secret-store.mjs";
|
|
4
5
|
import { createHash } from "node:crypto";
|
|
5
6
|
import path from "node:path";
|
|
@@ -23,14 +24,22 @@ function current() {
|
|
|
23
24
|
const appName = config.key("app.appName") || path.basename(root);
|
|
24
25
|
const namespace = config.key("secrets.namespace") ?? `${appName}:${createHash("sha256").update(root).digest("hex")}`;
|
|
25
26
|
const directory = config.key("secrets.directory") ?? storagePath("secrets");
|
|
27
|
+
const options = {
|
|
28
|
+
driver: config.key("secrets.driver"),
|
|
29
|
+
allowMemoryInProduction: config.key("secrets.allowMemoryInProduction")
|
|
30
|
+
};
|
|
31
|
+
const driver = testBackend ? "auto" : resolveSecretsDriver(options);
|
|
26
32
|
const identity = JSON.stringify([
|
|
27
33
|
root,
|
|
28
34
|
namespace,
|
|
29
|
-
directory
|
|
35
|
+
directory,
|
|
36
|
+
driver
|
|
30
37
|
]);
|
|
31
38
|
let store = stores.get(identity);
|
|
32
39
|
if (!store) {
|
|
33
40
|
store = new SecretStore({
|
|
41
|
+
...options,
|
|
42
|
+
driver,
|
|
34
43
|
namespace,
|
|
35
44
|
directory,
|
|
36
45
|
backend: testBackend
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"secrets.mjs","names":[],"sources":["../../../../../../../../packages/core/src/secrets/secrets.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport path from \"node:path\";\nimport { config } from \"../config/config-getter\";\nimport { rootPath, storagePath } from \"../utils/paths\";\nimport { SecretStore } from \"./secret-store\";\nimport type { SecretBackend } from \"./types\";\n\nlet testBackend: SecretBackend | undefined;\nlet stores = new Map<string, SecretStore>();\n\n/** @internal The test kit swaps only the facade, never explicitly constructed stores. */\nexport function replaceSecretsBackend(backend: SecretBackend): () => void {\n const previous = testBackend;\n const previousStores = stores;\n testBackend = backend;\n stores = new Map();\n return () => {\n testBackend = previous;\n stores = previousStores;\n };\n}\n\nfunction current(): SecretStore {\n const root = rootPath();\n const appName = config.key<string | undefined>(\"app.appName\") || path.basename(root);\n const namespace =\n config.key<string | undefined>(\"secrets.namespace\") ??\n `${appName}:${createHash(\"sha256\").update(root).digest(\"hex\")}`;\n const directory = config.key<string | undefined>(\"secrets.directory\") ?? storagePath(\"secrets\");\n const identity = JSON.stringify([root, namespace, directory]);\n let store = stores.get(identity);\n if (!store) {\n store = new SecretStore({ namespace, directory, backend: testBackend });\n stores.set(identity, store);\n }\n return store;\n}\n\n/** App identity and configuration are resolved on first use, after config has loaded. */\nexport const secrets = {\n async get(name: string): Promise<string | undefined> {\n return current().get(name);\n },\n async set(name: string, value: string): Promise<void> {\n return current().set(name, value);\n },\n async remove(name: string): Promise<boolean> {\n return current().remove(name);\n },\n async has(name: string): Promise<boolean> {\n return current().has(name);\n },\n environmentName(name: string): string {\n return current().environmentName(name);\n },\n};\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"secrets.mjs","names":[],"sources":["../../../../../../../../packages/core/src/secrets/secrets.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport path from \"node:path\";\nimport { config } from \"../config/config-getter\";\nimport { rootPath, storagePath } from \"../utils/paths\";\nimport { resolveSecretsDriver } from \"./resolve-driver\";\nimport { SecretStore } from \"./secret-store\";\nimport type { SecretBackend, SecretsConfigurations } from \"./types\";\n\nlet testBackend: SecretBackend | undefined;\nlet stores = new Map<string, SecretStore>();\n\n/** @internal The test kit swaps only the facade, never explicitly constructed stores. */\nexport function replaceSecretsBackend(backend: SecretBackend): () => void {\n const previous = testBackend;\n const previousStores = stores;\n testBackend = backend;\n stores = new Map();\n return () => {\n testBackend = previous;\n stores = previousStores;\n };\n}\n\nfunction current(): SecretStore {\n const root = rootPath();\n const appName = config.key<string | undefined>(\"app.appName\") || path.basename(root);\n const namespace =\n config.key<string | undefined>(\"secrets.namespace\") ??\n `${appName}:${createHash(\"sha256\").update(root).digest(\"hex\")}`;\n const directory = config.key<string | undefined>(\"secrets.directory\") ?? storagePath(\"secrets\");\n const options: SecretsConfigurations = {\n driver: config.key<SecretsConfigurations[\"driver\"]>(\"secrets.driver\"),\n allowMemoryInProduction: config.key<boolean | undefined>(\"secrets.allowMemoryInProduction\"),\n };\n const driver = testBackend ? \"auto\" : resolveSecretsDriver(options);\n const identity = JSON.stringify([root, namespace, directory, driver]);\n let store = stores.get(identity);\n if (!store) {\n store = new SecretStore({ ...options, driver, namespace, directory, backend: testBackend });\n stores.set(identity, store);\n }\n return store;\n}\n\n/** App identity and configuration are resolved on first use, after config has loaded. */\nexport const secrets = {\n async get(name: string): Promise<string | undefined> {\n return current().get(name);\n },\n async set(name: string, value: string): Promise<void> {\n return current().set(name, value);\n },\n async remove(name: string): Promise<boolean> {\n return current().remove(name);\n },\n async has(name: string): Promise<boolean> {\n return current().has(name);\n },\n environmentName(name: string): string {\n return current().environmentName(name);\n },\n};\n"],"mappings":";;;;;;;;AAQA,IAAI;AACJ,IAAI,yBAAS,IAAI,IAAyB;;AAG1C,SAAgB,sBAAsB,SAAoC;CACxE,MAAM,WAAW;CACjB,MAAM,iBAAiB;CACvB,cAAc;CACd,yBAAS,IAAI,IAAI;CACjB,aAAa;EACX,cAAc;EACd,SAAS;CACX;AACF;AAEA,SAAS,UAAuB;CAC9B,MAAM,OAAO,SAAS;CACtB,MAAM,UAAU,OAAO,IAAwB,aAAa,KAAK,KAAK,SAAS,IAAI;CACnF,MAAM,YACJ,OAAO,IAAwB,mBAAmB,KAClD,GAAG,QAAQ,GAAG,WAAW,QAAQ,CAAC,CAAC,OAAO,IAAI,CAAC,CAAC,OAAO,KAAK;CAC9D,MAAM,YAAY,OAAO,IAAwB,mBAAmB,KAAK,YAAY,SAAS;CAC9F,MAAM,UAAiC;EACrC,QAAQ,OAAO,IAAqC,gBAAgB;EACpE,yBAAyB,OAAO,IAAyB,iCAAiC;CAC5F;CACA,MAAM,SAAS,cAAc,SAAS,qBAAqB,OAAO;CAClE,MAAM,WAAW,KAAK,UAAU;EAAC;EAAM;EAAW;EAAW;CAAM,CAAC;CACpE,IAAI,QAAQ,OAAO,IAAI,QAAQ;CAC/B,IAAI,CAAC,OAAO;EACV,QAAQ,IAAI,YAAY;GAAE,GAAG;GAAS;GAAQ;GAAW;GAAW,SAAS;EAAY,CAAC;EAC1F,OAAO,IAAI,UAAU,KAAK;CAC5B;CACA,OAAO;AACT;;AAGA,MAAa,UAAU;CACrB,MAAM,IAAI,MAA2C;EACnD,OAAO,QAAQ,CAAC,CAAC,IAAI,IAAI;CAC3B;CACA,MAAM,IAAI,MAAc,OAA8B;EACpD,OAAO,QAAQ,CAAC,CAAC,IAAI,MAAM,KAAK;CAClC;CACA,MAAM,OAAO,MAAgC;EAC3C,OAAO,QAAQ,CAAC,CAAC,OAAO,IAAI;CAC9B;CACA,MAAM,IAAI,MAAgC;EACxC,OAAO,QAAQ,CAAC,CAAC,IAAI,IAAI;CAC3B;CACA,gBAAgB,MAAsB;EACpC,OAAO,QAAQ,CAAC,CAAC,gBAAgB,IAAI;CACvC;AACF"}
|