@napplet/conformance-cli 0.3.4 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -0
- package/dist/cli.js +46 -9
- package/dist/cli.js.map +1 -1
- package/dist/host-bundle.global.js +52 -16
- package/dist/ui/assets/index-CQX23owf.js +5450 -0
- package/dist/ui/index.html +1 -1
- package/package.json +5 -3
- package/dist/ui/assets/index-B0s98LMQ.js +0 -171
package/README.md
CHANGED
|
@@ -72,3 +72,13 @@ The CLI also provides the browser runner for `napplet screenshot`. Run `npx @nap
|
|
|
72
72
|
Controller artifacts use the engine’s GAMEPAD envelope validation and injected cached API. The reference mock reports unavailable controllers; validate live focus isolation in a compatible runtime. See the proposed [NAP-GAMEPAD](https://github.com/napplet/naps/pull/108).
|
|
73
73
|
|
|
74
74
|
Conformance CLI 0.3.4 requires the published Paja 0.21.x line for GAMEPAD screenshots and compatibility with core/NAP 0.34.0. Paja 0.20.x is excluded because its peer range does not accept those core/NAP versions. Publish this package after its core/NAP dependencies; the standalone CLI runner update follows conformance CLI publication.
|
|
75
|
+
|
|
76
|
+
## Signed manifests and runtime bindings
|
|
77
|
+
|
|
78
|
+
Headless runs accept `--manifest ./signed-event.json` alongside a local directory or `--url`. Supply the signed Nostr event itself, not an unsigned build sidecar. The CLI checks its structure, signature, event id and exact raw HTML SHA-256 before starting the browser. It boots the same loaded snapshot in both passes, so a changed response cannot replace the verified artifact. Missing or mismatched manifest inputs exit with code 2; omitting the option leaves manifest checks skipped. UI mode rejects this headless-only option; use a NIP-5D pointer in the web runtime instead.
|
|
79
|
+
|
|
80
|
+
```sh
|
|
81
|
+
napplet-conformance ./dist --manifest ./signed-event.json
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
The CLI and web runtime inject the maintained `@napplet/shim` bindings before subject code. Installation is observed separately from app errors, and private harness observations are excluded from the envelope log. The normal sandbox remains `allow-scripts`; `--allow-same-origin` is a debug setting and correctly fails the sandbox conformance check. Neither domain presence nor manifest declarations grant production permissions. This is implementation guidance; the living [NIP-5D proposal](https://github.com/nostr-protocol/nips/pull/2303) and [NAPs](https://github.com/napplet/naps) define the protocol.
|
package/dist/cli.js
CHANGED
|
@@ -54,7 +54,7 @@ async function sendFile(res, absPath) {
|
|
|
54
54
|
var HOST_HTML_ROUTE = "/__conformance__/host.html";
|
|
55
55
|
var HOST_BUNDLE_ROUTE = "/__conformance__/host-bundle.js";
|
|
56
56
|
function renderHostPage(boot) {
|
|
57
|
-
const cfg = JSON.stringify(boot);
|
|
57
|
+
const cfg = JSON.stringify(boot).replace(/</g, "\\u003c");
|
|
58
58
|
return `<!doctype html>
|
|
59
59
|
<html lang="en">
|
|
60
60
|
<head><meta charset="utf-8"><title>napplet conformance host</title></head>
|
|
@@ -452,6 +452,28 @@ function integer(value, flag, min, max) {
|
|
|
452
452
|
return number;
|
|
453
453
|
}
|
|
454
454
|
|
|
455
|
+
// src/manifest.ts
|
|
456
|
+
import { createHash } from "crypto";
|
|
457
|
+
import { validateEvent, verifyEvent } from "nostr-tools/pure";
|
|
458
|
+
import { validateManifestEvent } from "@napplet/conformance";
|
|
459
|
+
function verifyManifestArtifact(input, artifact) {
|
|
460
|
+
if (!validateEvent(input)) throw new Error("Manifest is not a Nostr event");
|
|
461
|
+
const { id, pubkey, created_at, kind, tags, content, sig } = input;
|
|
462
|
+
const event = { id, pubkey, created_at, kind, tags, content, sig };
|
|
463
|
+
let validSignature = false;
|
|
464
|
+
try {
|
|
465
|
+
validSignature = verifyEvent(event);
|
|
466
|
+
} catch {
|
|
467
|
+
}
|
|
468
|
+
if (!validSignature) throw new Error("Manifest signature or event id is invalid");
|
|
469
|
+
const verdict = validateManifestEvent(event);
|
|
470
|
+
if (!verdict.ok) throw new Error(`Invalid NIP-5D manifest: ${verdict.errors.map((error) => error.message).join("; ")}`);
|
|
471
|
+
const expected = event.tags.find((tag) => tag[0] === "x")?.[1];
|
|
472
|
+
const actual = createHash("sha256").update(artifact).digest("hex");
|
|
473
|
+
if (actual !== expected) throw new Error(`Artifact SHA-256 mismatch: expected ${expected}, got ${actual}`);
|
|
474
|
+
return event;
|
|
475
|
+
}
|
|
476
|
+
|
|
455
477
|
// src/cli.ts
|
|
456
478
|
var HELP = `napplet-conformance \u2014 run NAP protocol conformance against a napplet
|
|
457
479
|
|
|
@@ -466,6 +488,7 @@ Arguments:
|
|
|
466
488
|
|
|
467
489
|
Options:
|
|
468
490
|
--url <url> Test a remotely-served napplet instead of a local dir (headless)
|
|
491
|
+
--manifest <file> Signed NIP-5D event JSON; verify its signature and raw artifact hash before boot (headless)
|
|
469
492
|
--reporter <fmt> pretty | json | junit (default: pretty)
|
|
470
493
|
--out <file> Write the report to a file instead of stdout
|
|
471
494
|
--ready-timeout <ms> Boot timeout waiting for iframe load (default: 5000)
|
|
@@ -509,6 +532,9 @@ function parseArgs(argv) {
|
|
|
509
532
|
case "--url":
|
|
510
533
|
opts.url = value();
|
|
511
534
|
break;
|
|
535
|
+
case "--manifest":
|
|
536
|
+
opts.manifest = value();
|
|
537
|
+
break;
|
|
512
538
|
case "--reporter": {
|
|
513
539
|
const r = value();
|
|
514
540
|
if (r !== "pretty" && r !== "json" && r !== "junit") throw new Error(`Invalid reporter: ${r}`);
|
|
@@ -550,7 +576,7 @@ function parseArgs(argv) {
|
|
|
550
576
|
}
|
|
551
577
|
return opts;
|
|
552
578
|
}
|
|
553
|
-
async function collectBoot(hostUrl,
|
|
579
|
+
async function collectBoot(hostUrl, totalTimeoutMs) {
|
|
554
580
|
const browser = await chromium2.launch({ args: ["--no-sandbox"] });
|
|
555
581
|
try {
|
|
556
582
|
const page = await browser.newPage();
|
|
@@ -566,7 +592,6 @@ async function collectBoot(hostUrl, allowSameOrigin, totalTimeoutMs) {
|
|
|
566
592
|
if (error) throw new Error(`Host harness failed: ${error}`);
|
|
567
593
|
const boot = await page.evaluate(() => window.__conformanceBoot__);
|
|
568
594
|
if (!boot) throw new Error("Host harness produced no result");
|
|
569
|
-
void allowSameOrigin;
|
|
570
595
|
return boot;
|
|
571
596
|
} finally {
|
|
572
597
|
await browser.close();
|
|
@@ -588,6 +613,10 @@ function openBrowser(url) {
|
|
|
588
613
|
}
|
|
589
614
|
}
|
|
590
615
|
async function runUiMode(opts) {
|
|
616
|
+
if (opts.manifest) {
|
|
617
|
+
process.stderr.write("--manifest is supported in headless mode; use a NIP-5D pointer in the web runtime for signed manifests.\n");
|
|
618
|
+
return 2;
|
|
619
|
+
}
|
|
591
620
|
if (opts.url) {
|
|
592
621
|
process.stderr.write("--ui serves a local napplet directory; --url is not supported in UI mode.\n");
|
|
593
622
|
return 2;
|
|
@@ -648,12 +677,14 @@ ${HELP}`);
|
|
|
648
677
|
return runUiMode(opts);
|
|
649
678
|
}
|
|
650
679
|
let nappletDir = null;
|
|
651
|
-
let
|
|
680
|
+
let artifact;
|
|
652
681
|
let forbiddenGlobals = [];
|
|
653
682
|
let bootUrl;
|
|
654
683
|
if (opts.url) {
|
|
655
684
|
bootUrl = opts.url;
|
|
656
|
-
|
|
685
|
+
const response = await fetch(opts.url);
|
|
686
|
+
if (!response.ok) throw new Error(`Failed to fetch napplet HTML: ${response.status} ${response.statusText}`);
|
|
687
|
+
artifact = new Uint8Array(await response.arrayBuffer());
|
|
657
688
|
} else {
|
|
658
689
|
if (!opts.target) {
|
|
659
690
|
process.stderr.write(`Provide a napplet directory or --url.
|
|
@@ -663,30 +694,36 @@ ${HELP}`);
|
|
|
663
694
|
}
|
|
664
695
|
const resolved = await resolveNappletDir(opts.target);
|
|
665
696
|
nappletDir = resolved.dir;
|
|
666
|
-
|
|
697
|
+
artifact = await readFile5(resolved.indexHtml);
|
|
667
698
|
forbiddenGlobals = await scanForbiddenGlobals(resolved.dir);
|
|
668
699
|
bootUrl = "/";
|
|
669
700
|
}
|
|
701
|
+
const manifestEvent = opts.manifest ? verifyManifestArtifact(JSON.parse(await readFile5(opts.manifest, "utf8")), artifact) : null;
|
|
702
|
+
const manifestHtml = new TextDecoder().decode(artifact);
|
|
670
703
|
const here = dirname3(fileURLToPath(import.meta.url));
|
|
671
704
|
const hostBundle = await readFile5(join6(here, "host-bundle.global.js"), "utf8");
|
|
705
|
+
const runtimePreludeSource = await readFile5(fileURLToPath(import.meta.resolve("@napplet/shim/prelude.global")), "utf8");
|
|
672
706
|
const server = await startHarnessServer({
|
|
673
707
|
nappletDir,
|
|
674
708
|
hostBundle,
|
|
675
709
|
bootConfig: {
|
|
676
710
|
url: bootUrl,
|
|
711
|
+
html: manifestHtml,
|
|
677
712
|
readyTimeoutMs: opts.readyTimeoutMs,
|
|
678
713
|
settleMs: opts.settleMs,
|
|
679
|
-
runDegraded: opts.runDegraded
|
|
714
|
+
runDegraded: opts.runDegraded,
|
|
715
|
+
runtimePreludeSource,
|
|
716
|
+
allowSameOrigin: opts.allowSameOrigin
|
|
680
717
|
}
|
|
681
718
|
});
|
|
682
719
|
let boot;
|
|
683
720
|
try {
|
|
684
721
|
const totalTimeout = (opts.readyTimeoutMs + opts.settleMs) * 2 + 3e4;
|
|
685
|
-
boot = await collectBoot(server.hostUrl,
|
|
722
|
+
boot = await collectBoot(server.hostUrl, totalTimeout);
|
|
686
723
|
} finally {
|
|
687
724
|
await server.close();
|
|
688
725
|
}
|
|
689
|
-
const context = buildContext({ manifestHtml, boot, forbiddenGlobals });
|
|
726
|
+
const context = buildContext({ manifestHtml, manifestEvent, boot, forbiddenGlobals, sandbox: { allowScripts: true, allowSameOrigin: opts.allowSameOrigin } });
|
|
690
727
|
const run2 = runConformance(context);
|
|
691
728
|
const output = report(run2, opts.reporter);
|
|
692
729
|
if (opts.out) {
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/cli.ts","../src/server.ts","../src/static.ts","../src/ui-server.ts","../src/scan.ts","../src/resolve-napplet.ts","../src/screenshot.ts","../src/screenshot-preview.ts"],"sourcesContent":["/**\n * @napplet/conformance-cli -- `napplet-conformance` entry point.\n *\n * Runs the @napplet/conformance engine against a napplet in real headless Chromium:\n *\n * 1. Resolve the napplet's built directory (or a remote `--url`).\n * 2. Serve it on loopback alongside a host harness page + the engine bundle.\n * 3. Drive Playwright Chromium to `bootAndCollect` inside the host page.\n * 4. Assemble a ConformanceContext on the node side (manifest HTML + static scan),\n * run the check catalog, print the chosen report, and set the exit code.\n *\n * Exit codes: `0` conformant, `1` non-conformant, `2` usage/runtime error.\n *\n * @packageDocumentation\n */\n\nimport { readFile, writeFile, stat } from 'node:fs/promises';\nimport { join, dirname, resolve } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { spawn } from 'node:child_process';\nimport { chromium } from 'playwright';\nimport {\n buildContext,\n runConformance,\n report,\n type BootCollection,\n type ReporterFormat,\n} from '@napplet/conformance';\nimport { startHarnessServer } from './server.js';\nimport { startUiServer } from './ui-server.js';\nimport { scanForbiddenGlobals } from './scan.js';\nimport { resolveNappletDir } from './resolve-napplet.js';\nimport { captureScreenshot, parseScreenshotArgs } from './screenshot.js';\n\ninterface CliOptions {\n target?: string;\n url?: string;\n reporter: ReporterFormat;\n out?: string;\n readyTimeoutMs: number;\n settleMs: number;\n runDegraded: boolean;\n allowSameOrigin: boolean;\n ui: boolean;\n port?: number;\n open: boolean;\n exec?: string;\n help: boolean;\n}\n\nconst HELP = `napplet-conformance — run NAP protocol conformance against a napplet\n\nUsage:\n napplet-conformance [dir] [options] # headless, one-shot, CI exit code\n napplet-conformance --ui [dir] [options] # open the live web runtime (like vitest --ui)\n napplet-conformance --url https://my.napplet/ [options]\n napplet-conformance screenshot [directory|preview-url] [--output preview.png]\n\nArguments:\n dir Built napplet directory (prefers ./dist/index.html, falls back to ./index.html)\n\nOptions:\n --url <url> Test a remotely-served napplet instead of a local dir (headless)\n --reporter <fmt> pretty | json | junit (default: pretty)\n --out <file> Write the report to a file instead of stdout\n --ready-timeout <ms> Boot timeout waiting for iframe load (default: 5000)\n --settle <ms> Envelope-collection window after boot (default: 600)\n --no-degraded Skip the no-capability graceful-degradation pass\n --allow-same-origin Debug: also grant allow-same-origin (a conformant napplet must not need it)\n\nUI / watch mode (--ui):\n --port <n> Port for the UI server (default: random)\n --no-open Do not open a browser automatically\n --exec <cmd> Run a command (e.g. \"vite build --watch\") so edits rebuild the served dir;\n the watcher then re-runs conformance live on every change\n -h, --help Show this help\n\nExit codes (headless): 0 conformant, 1 non-conformant, 2 usage/runtime error.\n`;\n\nfunction parseArgs(argv: string[]): CliOptions {\n const opts: CliOptions = {\n reporter: 'pretty',\n readyTimeoutMs: 5000,\n settleMs: 600,\n runDegraded: true,\n allowSameOrigin: false,\n ui: false,\n open: true,\n help: false,\n };\n for (let i = 0; i < argv.length; i += 1) {\n const arg = argv[i];\n const value = (): string => {\n const v = argv[i + 1];\n if (v === undefined || v.startsWith('--')) throw new Error(`Missing value for ${arg}`);\n i += 1;\n return v;\n };\n switch (arg) {\n case '-h':\n case '--help':\n opts.help = true;\n break;\n case '--url':\n opts.url = value();\n break;\n case '--reporter': {\n const r = value();\n if (r !== 'pretty' && r !== 'json' && r !== 'junit') throw new Error(`Invalid reporter: ${r}`);\n opts.reporter = r;\n break;\n }\n case '--out':\n opts.out = value();\n break;\n case '--ready-timeout':\n opts.readyTimeoutMs = Number(value());\n break;\n case '--settle':\n opts.settleMs = Number(value());\n break;\n case '--no-degraded':\n opts.runDegraded = false;\n break;\n case '--allow-same-origin':\n opts.allowSameOrigin = true;\n break;\n case '--ui':\n opts.ui = true;\n break;\n case '--port':\n opts.port = Number(value());\n break;\n case '--no-open':\n opts.open = false;\n break;\n case '--exec':\n opts.exec = value();\n break;\n default:\n if (arg.startsWith('--')) throw new Error(`Unknown option: ${arg}`);\n if (opts.target) throw new Error(`Unexpected extra argument: ${arg}`);\n opts.target = arg;\n }\n }\n return opts;\n}\n\nasync function collectBoot(hostUrl: string, allowSameOrigin: boolean, totalTimeoutMs: number): Promise<BootCollection> {\n const browser = await chromium.launch({ args: ['--no-sandbox'] });\n try {\n const page = await browser.newPage();\n page.on('pageerror', (e) => process.stderr.write(`[host page error] ${e.message}\\n`));\n await page.goto(hostUrl, { waitUntil: 'load' });\n await page.waitForFunction(\n () => window.__conformanceBoot__ !== undefined || window.__conformanceError__ !== undefined,\n undefined,\n { timeout: totalTimeoutMs },\n );\n const error = await page.evaluate(() => window.__conformanceError__);\n if (error) throw new Error(`Host harness failed: ${error}`);\n const boot = await page.evaluate(() => window.__conformanceBoot__);\n if (!boot) throw new Error('Host harness produced no result');\n void allowSameOrigin;\n return boot;\n } finally {\n await browser.close();\n }\n}\n\nasync function isDir(path: string): Promise<boolean> {\n return (await stat(path).catch(() => null))?.isDirectory() ?? false;\n}\n\n/** Best-effort: open a URL in the default browser. */\nfunction openBrowser(url: string): void {\n try {\n if (process.platform === 'darwin') {\n spawn('open', [url], { stdio: 'ignore', detached: true }).unref();\n } else if (process.platform === 'win32') {\n spawn('cmd', ['/c', 'start', '', url], { stdio: 'ignore', detached: true }).unref();\n } else {\n spawn('xdg-open', [url], { stdio: 'ignore', detached: true }).unref();\n }\n } catch {\n /* best-effort — the URL is printed regardless */\n }\n}\n\n/** `--ui`: serve the live web runtime + the napplet, watch, and re-run on change. */\nasync function runUiMode(opts: CliOptions): Promise<number> {\n if (opts.url) {\n process.stderr.write('--ui serves a local napplet directory; --url is not supported in UI mode.\\n');\n return 2;\n }\n if (!opts.target) {\n process.stderr.write(`Provide a napplet directory.\\n\\n${HELP}`);\n return 2;\n }\n const resolved = await resolveNappletDir(opts.target);\n const here = dirname(fileURLToPath(import.meta.url));\n const appDir = join(here, 'ui');\n if (!(await isDir(appDir))) {\n process.stderr.write(`Conformance UI assets are missing at ${appDir}. Rebuild @napplet/conformance-cli.\\n`);\n return 2;\n }\n\n const server = await startUiServer({ appDir, nappletDir: resolved.dir, port: opts.port });\n\n const child = opts.exec ? spawn(opts.exec, { cwd: resolve(opts.target), shell: true, stdio: 'inherit' }) : null;\n\n process.stdout.write(\n `\\n napplet conformance UI → ${server.appUrl}\\n` +\n ` watching ${resolved.dir}${opts.exec ? ` (running: ${opts.exec})` : ''}\\n` +\n ` Ctrl-C to stop\\n\\n`,\n );\n if (opts.open) openBrowser(server.appUrl);\n\n await new Promise<void>((resolveWait) => {\n let closing = false;\n const shutdown = (): void => {\n if (closing) return;\n closing = true;\n child?.kill();\n void server.close().finally(resolveWait);\n };\n process.on('SIGINT', shutdown);\n process.on('SIGTERM', shutdown);\n });\n return 0;\n}\n\nasync function main(argv: string[]): Promise<number> {\n let opts: CliOptions;\n try {\n opts = parseArgs(argv);\n } catch (err) {\n process.stderr.write(`${err instanceof Error ? err.message : String(err)}\\n\\n${HELP}`);\n return 2;\n }\n if (opts.help) {\n process.stdout.write(HELP);\n return 0;\n }\n\n if (opts.ui) {\n return runUiMode(opts);\n }\n\n // Resolve the subject: local dir or remote URL.\n let nappletDir: string | null = null;\n let manifestHtml: string;\n let forbiddenGlobals: string[] = [];\n let bootUrl: string;\n\n if (opts.url) {\n bootUrl = opts.url;\n manifestHtml = await (await fetch(opts.url)).text();\n } else {\n if (!opts.target) {\n process.stderr.write(`Provide a napplet directory or --url.\\n\\n${HELP}`);\n return 2;\n }\n const resolved = await resolveNappletDir(opts.target);\n nappletDir = resolved.dir;\n manifestHtml = await readFile(resolved.indexHtml, 'utf8');\n forbiddenGlobals = await scanForbiddenGlobals(resolved.dir);\n bootUrl = '/';\n }\n\n const here = dirname(fileURLToPath(import.meta.url));\n const hostBundle = await readFile(join(here, 'host-bundle.global.js'), 'utf8');\n\n const server = await startHarnessServer({\n nappletDir,\n hostBundle,\n bootConfig: {\n url: bootUrl,\n readyTimeoutMs: opts.readyTimeoutMs,\n settleMs: opts.settleMs,\n runDegraded: opts.runDegraded,\n },\n });\n\n let boot: BootCollection;\n try {\n // Generous overall budget: two boots (primary + degraded), each up to\n // readyTimeout + settle, plus browser startup.\n const totalTimeout = (opts.readyTimeoutMs + opts.settleMs) * 2 + 30000;\n boot = await collectBoot(server.hostUrl, opts.allowSameOrigin, totalTimeout);\n } finally {\n await server.close();\n }\n\n const context = buildContext({ manifestHtml, boot, forbiddenGlobals });\n const run = runConformance(context);\n const output = report(run, opts.reporter);\n\n if (opts.out) {\n await writeFile(opts.out, `${output}\\n`, 'utf8');\n process.stdout.write(`Report written to ${opts.out}\\n`);\n } else {\n process.stdout.write(`${output}\\n`);\n }\n\n return run.ok ? 0 : 1;\n}\n\nasync function run(args: string[]): Promise<number> {\n if (args[0] !== 'screenshot') return main(args);\n if (args.includes('--help') || args.includes('-h')) {\n console.log('Usage: napplet screenshot [directory|preview-url] [--output preview.png] [--selector iframe] [--ready-selector <css>] [--width 1200] [--height 750] [--delay 1500]');\n return 0;\n }\n const options = parseScreenshotArgs(args.slice(1));\n await captureScreenshot(options);\n console.log(`Screenshot saved to ${options.output}`);\n return 0;\n}\n\nrun(process.argv.slice(2))\n .then((code) => {\n process.exitCode = code;\n })\n .catch((err: unknown) => {\n process.stderr.write(`napplet-conformance: ${err instanceof Error ? err.stack ?? err.message : String(err)}\\n`);\n process.exitCode = 2;\n });\n","/**\n * @napplet/conformance-cli -- Loopback harness server.\n *\n * Serves the napplet's files plus two reserved harness routes (the host page and\n * the browser bundle). Every response carries `Access-Control-Allow-Origin: *`:\n * the napplet iframe runs at an opaque origin (sandbox without `allow-same-origin`),\n * so its `<script type=\"module\">` subresources are fetched in CORS mode and would\n * otherwise be blocked.\n *\n * @packageDocumentation\n */\n\nimport { createServer, type Server, type ServerResponse } from 'node:http';\nimport { readFile, stat } from 'node:fs/promises';\nimport { join, normalize, extname, resolve } from 'node:path';\nimport type { AddressInfo } from 'node:net';\nimport { MIME } from './static.js';\n\n/** The napplet URL + timing knobs the host page passes to `bootAndCollect`. */\nexport interface BootConfig {\n url: string;\n readyTimeoutMs: number;\n settleMs: number;\n runDegraded: boolean;\n}\n\n/** Options for {@link startHarnessServer}. */\nexport interface HarnessServerOptions {\n /** Directory of napplet files to serve, or `null` in remote-URL mode. */\n nappletDir: string | null;\n /** JavaScript source of the host browser bundle. */\n hostBundle: string;\n /** Boot configuration injected into the host page. */\n bootConfig: BootConfig;\n}\n\n/** A running harness server. */\nexport interface HarnessServer {\n /** Base origin, e.g. `http://127.0.0.1:54213`. */\n origin: string;\n /** URL of the host harness page. */\n hostUrl: string;\n /** Shut the server down. */\n close(): Promise<void>;\n}\n\nconst HOST_HTML_ROUTE = '/__conformance__/host.html';\nconst HOST_BUNDLE_ROUTE = '/__conformance__/host-bundle.js';\n\nfunction renderHostPage(boot: BootConfig): string {\n // The inline script lives on the harness page (full privilege, our origin), not\n // in the sandboxed napplet — so it is not subject to the napplet CSP.\n const cfg = JSON.stringify(boot);\n return `<!doctype html>\n<html lang=\"en\">\n<head><meta charset=\"utf-8\"><title>napplet conformance host</title></head>\n<body>\n<script src=\"${HOST_BUNDLE_ROUTE}\"></script>\n<script>\n (function () {\n var cfg = ${cfg};\n window.NappletConformanceHost.run(cfg).then(function (boot) {\n window.__conformanceBoot__ = boot;\n }).catch(function (err) {\n window.__conformanceError__ = String((err && err.stack) || err);\n });\n })();\n</script>\n</body>\n</html>`;\n}\n\n/** Start the loopback harness server on a random port. */\nexport async function startHarnessServer(options: HarnessServerOptions): Promise<HarnessServer> {\n const dir = options.nappletDir ? resolve(options.nappletDir) : null;\n const hostHtml = renderHostPage(options.bootConfig);\n\n const server: Server = createServer((req, res) => {\n void serve(req.url ?? '/', res);\n });\n\n async function serve(rawUrl: string, res: ServerResponse): Promise<void> {\n res.setHeader('Access-Control-Allow-Origin', '*');\n const path = decodeURIComponent(rawUrl.split('?')[0]);\n\n if (path === HOST_HTML_ROUTE) {\n res.writeHead(200, { 'Content-Type': MIME['.html'] });\n res.end(hostHtml);\n return;\n }\n if (path === HOST_BUNDLE_ROUTE) {\n res.writeHead(200, { 'Content-Type': MIME['.js'] });\n res.end(options.hostBundle);\n return;\n }\n\n if (!dir) {\n res.writeHead(404).end('not found');\n return;\n }\n\n // Resolve a safe path under the napplet dir.\n const relative = normalize(path).replace(/^(\\.\\.[/\\\\])+/, '').replace(/^[/\\\\]+/, '');\n let filePath = join(dir, relative);\n if (!filePath.startsWith(dir)) {\n res.writeHead(403).end('forbidden');\n return;\n }\n try {\n let info = await stat(filePath).catch(() => null);\n if (info?.isDirectory()) {\n filePath = join(filePath, 'index.html');\n info = await stat(filePath).catch(() => null);\n }\n if (path === '/' && !info) {\n filePath = join(dir, 'index.html');\n info = await stat(filePath).catch(() => null);\n }\n if (!info?.isFile()) {\n res.writeHead(404).end('not found');\n return;\n }\n const body = await readFile(filePath);\n res.writeHead(200, { 'Content-Type': MIME[extname(filePath).toLowerCase()] ?? 'application/octet-stream' });\n res.end(body);\n } catch {\n res.writeHead(500).end('server error');\n }\n }\n\n await new Promise<void>((resolveListen) => server.listen(0, '127.0.0.1', resolveListen));\n const address = server.address() as AddressInfo;\n const origin = `http://127.0.0.1:${address.port}`;\n\n return {\n origin,\n hostUrl: `${origin}${HOST_HTML_ROUTE}`,\n close: () => new Promise<void>((res, rej) => server.close((err) => (err ? rej(err) : res()))),\n };\n}\n","/**\n * @napplet/conformance-cli -- Shared static-serving helpers used by both the\n * one-shot harness server and the `--ui` watch server.\n *\n * @packageDocumentation\n */\n\nimport { readFile, stat } from 'node:fs/promises';\nimport { extname } from 'node:path';\nimport type { ServerResponse } from 'node:http';\n\n/** Content types by extension. */\nexport const MIME: Record<string, string> = {\n '.html': 'text/html; charset=utf-8',\n '.js': 'text/javascript; charset=utf-8',\n '.mjs': 'text/javascript; charset=utf-8',\n '.css': 'text/css; charset=utf-8',\n '.json': 'application/json; charset=utf-8',\n '.svg': 'image/svg+xml',\n '.png': 'image/png',\n '.jpg': 'image/jpeg',\n '.jpeg': 'image/jpeg',\n '.gif': 'image/gif',\n '.webp': 'image/webp',\n '.ico': 'image/x-icon',\n '.wasm': 'application/wasm',\n '.woff': 'font/woff',\n '.woff2': 'font/woff2',\n '.map': 'application/json; charset=utf-8',\n '.txt': 'text/plain; charset=utf-8',\n};\n\n/** The napplet iframe runs at an opaque origin; allow its module subresources. */\nexport function setCors(res: ServerResponse): void {\n res.setHeader('Access-Control-Allow-Origin', '*');\n}\n\n/** Send a file if it exists; return false (without writing) when it does not. */\nexport async function sendFile(res: ServerResponse, absPath: string): Promise<boolean> {\n const info = await stat(absPath).catch(() => null);\n if (!info?.isFile()) return false;\n res.writeHead(200, { 'Content-Type': MIME[extname(absPath).toLowerCase()] ?? 'application/octet-stream' });\n res.end(await readFile(absPath));\n return true;\n}\n","/**\n * @napplet/conformance-cli -- The `--ui` watch server.\n *\n * Serves the standalone conformance web app (bundled with the CLI), the napplet\n * under test (same origin, with permissive CORS for the sandboxed iframe), and a\n * Server-Sent-Events stream. It watches the napplet's served directory and pushes\n * a `rerun` event on every change, so the open app re-runs conformance live — the\n * `vitest --ui` model: edit, save, see the verdict update.\n *\n * @packageDocumentation\n */\n\nimport { createServer, type Server, type ServerResponse } from 'node:http';\nimport { watch, type FSWatcher } from 'node:fs';\nimport { join, normalize, resolve } from 'node:path';\nimport type { AddressInfo } from 'node:net';\nimport { setCors, sendFile } from './static.js';\n\n/** Path the app under `?live=1` is served the napplet from. */\nconst NAPPLET_PREFIX = '/__napplet__/';\n/** SSE endpoint the live app subscribes to. */\nconst EVENTS_ROUTE = '/__conformance__/events';\n\n/** Options for {@link startUiServer}. */\nexport interface UiServerOptions {\n /** Directory of the built conformance web app (served at `/`). */\n appDir: string;\n /** Directory of the built napplet under test (served at `/__napplet__/`). */\n nappletDir: string;\n /** Preferred port (0 = random). */\n port?: number;\n /** Debounce window (ms) for coalescing file-change bursts. Default 120. */\n debounceMs?: number;\n}\n\n/** A running `--ui` server. */\nexport interface UiServer {\n /** Base origin, e.g. `http://127.0.0.1:5219`. */\n origin: string;\n /** The app URL to open (already pointed at the napplet, live mode on). */\n appUrl: string;\n /** Force a `rerun` broadcast (used for the manual/initial trigger). */\n notify(): void;\n /** Shut the server + watcher down. */\n close(): Promise<void>;\n}\n\nfunction safeJoin(dir: string, urlPath: string): string | null {\n const relative = normalize(urlPath).replace(/^(\\.\\.[/\\\\])+/, '').replace(/^[/\\\\]+/, '');\n const full = join(dir, relative);\n return full.startsWith(dir) ? full : null;\n}\n\n/** Register an SSE subscriber and keep the connection open. */\nfunction handleSse(res: ServerResponse, clients: Set<ServerResponse>): void {\n res.writeHead(200, {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache, no-transform',\n Connection: 'keep-alive',\n });\n res.write('retry: 1000\\n\\n');\n clients.add(res);\n res.on('close', () => clients.delete(res));\n}\n\n/** Serve a file under `/__napplet__/`. */\nasync function handleNapplet(res: ServerResponse, nappletDir: string, path: string): Promise<void> {\n const rel = path.slice(NAPPLET_PREFIX.length) || 'index.html';\n const abs = safeJoin(nappletDir, rel);\n if (abs && (await sendFile(res, abs))) return;\n res.writeHead(404).end('napplet asset not found');\n}\n\n/** Serve the conformance web app, with an SPA fallback to its index. */\nasync function handleApp(res: ServerResponse, appDir: string, path: string): Promise<void> {\n const abs = safeJoin(appDir, path === '/' ? 'index.html' : path);\n if (abs && (await sendFile(res, abs))) return;\n if (await sendFile(res, join(appDir, 'index.html'))) return;\n res.writeHead(404).end('not found');\n}\n\n/** Watch a directory (recursively when supported) and call `fire` debounced. */\nfunction startWatcher(dir: string, debounceMs: number, fire: () => void): { close(): void } {\n let timer: ReturnType<typeof setTimeout> | null = null;\n const onEvent = (): void => {\n if (timer) clearTimeout(timer);\n timer = setTimeout(fire, debounceMs);\n };\n let watcher: FSWatcher;\n try {\n watcher = watch(dir, { recursive: true }, onEvent);\n } catch {\n // Recursive watch may be unsupported on some platforms.\n watcher = watch(dir, onEvent);\n }\n return {\n close() {\n if (timer) clearTimeout(timer);\n watcher.close();\n },\n };\n}\n\n/** Start the `--ui` watch server. */\nexport async function startUiServer(options: UiServerOptions): Promise<UiServer> {\n const appDir = resolve(options.appDir);\n const nappletDir = resolve(options.nappletDir);\n const clients = new Set<ServerResponse>();\n const broadcast = (): void => {\n for (const res of clients) res.write(`event: rerun\\ndata: ${Date.now()}\\n\\n`);\n };\n\n const server: Server = createServer((req, res) => {\n setCors(res);\n res.setHeader('Cache-Control', 'no-store'); // never serve stale napplet code\n const path = decodeURIComponent((req.url ?? '/').split('?')[0]);\n if (path === EVENTS_ROUTE) {\n handleSse(res, clients);\n } else if (path === NAPPLET_PREFIX.slice(0, -1) || path.startsWith(NAPPLET_PREFIX)) {\n void handleNapplet(res, nappletDir, path);\n } else {\n void handleApp(res, appDir, path);\n }\n });\n\n await new Promise<void>((r) => server.listen(options.port ?? 0, '127.0.0.1', r));\n const { port } = server.address() as AddressInfo;\n const origin = `http://127.0.0.1:${port}`;\n const appUrl = `${origin}/?url=${encodeURIComponent(NAPPLET_PREFIX)}&live=1`;\n const watcher = startWatcher(nappletDir, options.debounceMs ?? 120, broadcast);\n\n return {\n origin,\n appUrl,\n notify: broadcast,\n close: () =>\n new Promise<void>((res, rej) => {\n watcher.close();\n for (const c of clients) c.end();\n clients.clear();\n server.close((err) => (err ? rej(err) : res()));\n }),\n };\n}\n","/**\n * @napplet/conformance-cli -- Static scan for forbidden napplet authority.\n *\n * A napplet runs in a `sandbox=\"allow-scripts\"` iframe at an opaque origin, so the\n * host cannot observe every forbidden browser surface at runtime. Instead we scan\n * served source for references to authority the shell owns: signer globals,\n * browser storage, direct network primitives, and external network-loaded assets.\n * This is a heuristic, but a true positive is meaningful: NIP-5D and the NAPs\n * route these capabilities through `window.napplet` domains.\n *\n * @packageDocumentation\n */\n\nimport { readdir, readFile, stat } from 'node:fs/promises';\nimport { join, extname } from 'node:path';\n\n/** Patterns considered forbidden, keyed by the label reported in the verdict. */\nconst FORBIDDEN: Array<{ label: string; re: RegExp }> = [\n { label: 'window.nostr', re: /\\bwindow\\s*\\.\\s*nostr\\b/ },\n { label: 'globalThis.nostr', re: /\\bglobalThis\\s*\\.\\s*nostr\\b/ },\n { label: 'fetch', re: /\\b(?:window\\s*\\.\\s*|globalThis\\s*\\.\\s*)?fetch\\s*\\(/ },\n { label: 'XMLHttpRequest', re: /\\b(?:new\\s+)?XMLHttpRequest\\s*\\(/ },\n { label: 'WebSocket', re: /\\b(?:new\\s+)?WebSocket\\s*\\(/ },\n { label: 'localStorage', re: /\\b(?:window\\s*\\.\\s*)?localStorage\\b/ },\n { label: 'sessionStorage', re: /\\b(?:window\\s*\\.\\s*)?sessionStorage\\b/ },\n { label: 'indexedDB', re: /\\b(?:window\\s*\\.\\s*)?indexedDB\\b/ },\n { label: 'document.cookie', re: /\\bdocument\\s*\\.\\s*cookie\\b/ },\n { label: 'external img src', re: /<img\\b[^>]*\\bsrc\\s*=\\s*[\"']?https?:/i },\n { label: 'external script src', re: /<script\\b[^>]*\\bsrc\\s*=\\s*[\"']?https?:/i },\n { label: 'external stylesheet href', re: /<link\\b[^>]*\\brel\\s*=\\s*[\"']?stylesheet[\"']?[^>]*\\bhref\\s*=\\s*[\"']?https?:/i },\n { label: 'external stylesheet href', re: /<link\\b[^>]*\\bhref\\s*=\\s*[\"']?https?:[^>]*\\brel\\s*=\\s*[\"']?stylesheet[\"']?/i },\n];\n\nconst SCANNABLE = new Set(['.js', '.mjs', '.cjs', '.html', '.htm']);\nconst SKIP_DIRS = new Set(['node_modules', '.git', '.turbo', 'coverage']);\n\nasync function collectFiles(dir: string, acc: string[]): Promise<void> {\n for (const entry of await readdir(dir, { withFileTypes: true })) {\n if (entry.isDirectory()) {\n if (SKIP_DIRS.has(entry.name)) continue;\n await collectFiles(join(dir, entry.name), acc);\n } else if (entry.isFile() && SCANNABLE.has(extname(entry.name).toLowerCase())) {\n acc.push(join(dir, entry.name));\n }\n }\n}\n\n/**\n * Scan a napplet directory for forbidden browser authority references.\n *\n * @param dir - The directory of served napplet files.\n * @returns The distinct forbidden labels found (e.g. `['window.nostr']`).\n */\nexport async function scanForbiddenGlobals(dir: string): Promise<string[]> {\n const info = await stat(dir).catch(() => null);\n if (!info?.isDirectory()) return [];\n const files: string[] = [];\n await collectFiles(dir, files);\n const found = new Set<string>();\n for (const file of files) {\n const text = await readFile(file, 'utf8').catch(() => '');\n for (const { label, re } of FORBIDDEN) {\n if (re.test(text)) found.add(label);\n }\n }\n return [...found];\n}\n","/**\n * @napplet/conformance-cli -- Resolve which local artifact to conformance-test.\n *\n * Kept in its own module (not `cli.ts`, whose top level runs `main()`) so tests\n * can import the resolver without launching the CLI.\n */\n\nimport { stat } from 'node:fs/promises';\nimport { join, resolve } from 'node:path';\n\nasync function isFile(path: string): Promise<boolean> {\n return (await stat(path).catch(() => null))?.isFile() ?? false;\n}\n\n/**\n * Resolve the directory + index.html for a local napplet target.\n *\n * A built napplet is the artifact we test, so `dist/index.html` is preferred over\n * a sibling `index.html`. When `target` is a project root, its source\n * `index.html` (a Vite entry referencing `/src/*.ts` that cannot run in the\n * opaque-origin `srcdoc` sandbox) would otherwise shadow the built single-file\n * output and make conformance test the wrong, never-booting artifact.\n */\nexport async function resolveNappletDir(target: string): Promise<{ dir: string; indexHtml: string }> {\n const base = resolve(target);\n const dist = join(base, 'dist');\n if (await isFile(join(dist, 'index.html'))) return { dir: dist, indexHtml: join(dist, 'index.html') };\n if (await isFile(join(base, 'index.html'))) return { dir: base, indexHtml: join(base, 'index.html') };\n throw new Error(`No index.html found in ${join(dist, 'index.html')} or ${join(base, 'index.html')}. Build the napplet first.`);\n}\n","/** Capture a local build or an existing shell preview through its iframe. */\nimport { access, writeFile } from 'node:fs/promises';\nimport { execFile } from 'node:child_process';\nimport { createRequire } from 'node:module';\nimport { dirname, join } from 'node:path';\nimport { promisify } from 'node:util';\nimport { chromium } from 'playwright';\nimport { startScreenshotPreview } from './screenshot-preview.js';\nimport { resolveNappletDir } from './resolve-napplet.js';\n\n/** Options for capturing a shell-hosted napplet. */\nexport interface ScreenshotOptions {\n url?: string;\n directory?: string;\n artifact?: string;\n output: string;\n selector: string;\n readySelector?: string;\n width: number;\n height: number;\n delay: number;\n}\n\n/**\n * Parse screenshot arguments without invoking a browser.\n * @param args CLI arguments after the screenshot command.\n * @returns Validated capture options.\n * @example parseScreenshotArgs(['http://localhost:5173', '--output', 'preview.png'])\n */\nexport function parseScreenshotArgs(args: readonly string[]): ScreenshotOptions {\n const hasTarget = args[0] && !args[0].startsWith('--');\n const target = hasTarget ? args[0] : '.';\n const rest = hasTarget ? args.slice(1) : args;\n const remote = /^https?:\\/\\//.test(target);\n if (remote) new URL(target);\n else if (/^[a-z][a-z0-9+.-]*:/i.test(target) && !/^[a-z]:[\\\\/]/i.test(target)) throw new Error('Preview URLs must use HTTP(S)');\n const options: ScreenshotOptions = { ...(remote ? { url: target } : { directory: target }), output: 'preview.png', selector: 'iframe', width: 1200, height: 750, delay: 1500 };\n for (let index = 0; index < rest.length; index += 2) {\n const flag = rest[index];\n const value = rest[index + 1];\n if (!value || value.startsWith('--')) throw new Error(`Missing value for ${flag}`);\n switch (flag) {\n case '--output': options.output = value; break;\n case '--artifact': options.artifact = value; break;\n case '--selector': options.selector = value; break;\n case '--ready-selector': options.readySelector = value; break;\n case '--width': options.width = integer(value, flag, 1, 4096); break;\n case '--height': options.height = integer(value, flag, 1, 4096); break;\n case '--delay': options.delay = integer(value, flag, 0, 30000); break;\n default: throw new Error(`Unknown screenshot option: ${flag}`);\n }\n }\n if (!options.output.toLowerCase().endsWith('.png')) throw new Error('--output must end in .png');\n if (options.artifact && hasTarget) throw new Error('Use a target or --artifact, not both');\n return options;\n}\n\n/**\n * Save a PNG of a napplet iframe, starting a local preview when needed.\n * @param options Local build or URL, output, dimensions and readiness settings.\n * @returns Resolves after capture and cleanup of Chromium and any local preview.\n * @example await captureScreenshot(parseScreenshotArgs(['http://localhost:5173']))\n */\nexport async function captureScreenshot(options: ScreenshotOptions): Promise<void> {\n await ensureChromium();\n const preview = options.url ? undefined : await startScreenshotPreview(\n options.artifact ?? (await resolveNappletDir(options.directory ?? '.')).indexHtml,\n );\n let browser;\n try {\n browser = await chromium.launch({ channel: 'chromium' });\n const page = await browser.newPage({ viewport: { width: options.width, height: options.height } });\n page.setDefaultTimeout(30000);\n await page.goto(options.url ?? preview!.url, { waitUntil: 'domcontentloaded' });\n if (preview) {\n await page.waitForFunction(() => {\n const iframe = document.querySelector<HTMLIFrameElement>('#napplet-frame');\n return Boolean(iframe?.srcdoc);\n });\n }\n const frame = page.locator(options.selector);\n await frame.waitFor({ state: 'visible' });\n await frame.evaluate((element, size) => {\n if (!(element instanceof HTMLIFrameElement)) throw new Error('Screenshot selector must select one iframe');\n Object.assign(element.style, { width: `${size.width}px`, height: `${size.height}px`, maxWidth: 'none', maxHeight: 'none', boxSizing: 'border-box' });\n }, { width: options.width, height: options.height });\n const content = frame.contentFrame();\n await content.locator('body').waitFor({ state: 'visible' });\n if (options.readySelector) await content.locator(options.readySelector).waitFor({ state: 'visible' });\n await content.locator('body').evaluate(async () => { await document.fonts.ready; });\n await page.waitForTimeout(options.delay);\n const png = await frame.screenshot({ type: 'png', timeout: 30000 });\n await writeFile(options.output, png, { flag: 'wx' });\n } finally {\n try { await browser?.close(); } finally { await preview?.close(); }\n }\n}\n\nasync function ensureChromium(): Promise<void> {\n try { await access(chromium.executablePath()); return; } catch { /* Install the matching browser on first use. */ }\n console.error('Installing Chromium for screenshot capture...');\n const require = createRequire(import.meta.url);\n const cli = join(dirname(require.resolve('playwright/package.json')), 'cli.js');\n await promisify(execFile)(process.execPath, [cli, 'install', 'chromium'], { timeout: 180000 });\n}\n\nfunction integer(value: string, flag: string, min: number, max: number): number {\n const number = Number(value);\n if (!Number.isInteger(number) || number < min || number > max) throw new Error(`${flag} must be an integer from ${min} to ${max}`);\n return number;\n}\n","/** Disposable Kehto Paja preview for a local build. */\nimport { createServer } from 'node:http';\nimport type { AddressInfo } from 'node:net';\nimport { readFile, realpath } from 'node:fs/promises';\nimport { dirname, resolve, sep } from 'node:path';\nimport { startPajaServer } from '@kehto/paja';\nimport { sendFile, setCors } from './static.js';\n\n/**\n * Host a built artifact in Paja on ephemeral loopback ports.\n * @param artifact Exact index.html selected by deployment.\n * @returns Shell URL and an async cleanup handle for both servers.\n * @example const preview = await startScreenshotPreview('/project/dist/index.html');\n */\nexport async function startScreenshotPreview(artifact: string): Promise<{ url: string; close(): Promise<void> }> {\n const index = await realpath(artifact);\n const root = dirname(index);\n const html = await readFile(index);\n const server = createServer((request, response) => {\n setCors(response);\n void (async () => {\n const pathname = decodeURIComponent(new URL(request.url ?? '/', 'http://localhost').pathname);\n if (pathname === '/' || pathname === '/index.html') {\n response.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });\n response.end(html);\n return;\n }\n const file = await realpath(resolve(root, `.${pathname}`));\n if (!file.startsWith(`${root}${sep}`)) { response.writeHead(403).end(); return; }\n if (!await sendFile(response, file)) response.writeHead(404).end();\n })().catch(() => { if (!response.headersSent) response.writeHead(404); response.end(); });\n });\n await new Promise<void>((done, reject) => {\n server.once('error', reject);\n server.listen(0, '127.0.0.1', () => { server.off('error', reject); done(); });\n });\n const closeTarget = () => new Promise<void>((done, reject) => {\n server.closeAllConnections();\n server.close((error) => error ? reject(error) : done());\n });\n try {\n const targetUrl = `http://127.0.0.1:${(server.address() as AddressInfo).port}`;\n const paja = await startPajaServer({ options: {\n host: '127.0.0.1', port: 0, targetUrl,\n } });\n return { url: paja.url, close: async () => { try { await paja.close(); } finally { await closeTarget(); } } };\n } catch (error) {\n await closeTarget();\n throw error;\n }\n}\n"],"mappings":"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","names":["readFile","writeFile","stat","join","dirname","resolve","chromium","readFile","stat","extname","stat","readFile","extname","createServer","join","normalize","resolve","normalize","join","resolve","createServer","readFile","stat","join","extname","stat","join","resolve","dirname","join","createServer","readFile","resolve","readFile","createServer","resolve","require","join","dirname","chromium","stat","dirname","join","resolve","readFile","run","writeFile"]}
|
|
1
|
+
{"version":3,"sources":["../src/cli.ts","../src/server.ts","../src/static.ts","../src/ui-server.ts","../src/scan.ts","../src/resolve-napplet.ts","../src/screenshot.ts","../src/screenshot-preview.ts","../src/manifest.ts"],"sourcesContent":["/**\n * @napplet/conformance-cli -- `napplet-conformance` entry point.\n *\n * Runs the @napplet/conformance engine against a napplet in real headless Chromium:\n *\n * 1. Resolve the napplet's built directory (or a remote `--url`).\n * 2. Serve it on loopback alongside a host harness page + the engine bundle.\n * 3. Drive Playwright Chromium to `bootAndCollect` inside the host page.\n * 4. Assemble a ConformanceContext on the node side (manifest HTML + static scan),\n * run the check catalog, print the chosen report, and set the exit code.\n *\n * Exit codes: `0` conformant, `1` non-conformant, `2` usage/runtime error.\n *\n * @packageDocumentation\n */\n\nimport { readFile, writeFile, stat } from 'node:fs/promises';\nimport { join, dirname, resolve } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { spawn } from 'node:child_process';\nimport { chromium } from 'playwright';\nimport {\n buildContext,\n runConformance,\n report,\n type BootCollection,\n type ReporterFormat,\n} from '@napplet/conformance';\nimport { startHarnessServer } from './server.js';\nimport { startUiServer } from './ui-server.js';\nimport { scanForbiddenGlobals } from './scan.js';\nimport { resolveNappletDir } from './resolve-napplet.js';\nimport { captureScreenshot, parseScreenshotArgs } from './screenshot.js';\nimport { verifyManifestArtifact } from './manifest.js';\n\ninterface CliOptions {\n target?: string;\n url?: string;\n manifest?: string;\n reporter: ReporterFormat;\n out?: string;\n readyTimeoutMs: number;\n settleMs: number;\n runDegraded: boolean;\n allowSameOrigin: boolean;\n ui: boolean;\n port?: number;\n open: boolean;\n exec?: string;\n help: boolean;\n}\n\nconst HELP = `napplet-conformance — run NAP protocol conformance against a napplet\n\nUsage:\n napplet-conformance [dir] [options] # headless, one-shot, CI exit code\n napplet-conformance --ui [dir] [options] # open the live web runtime (like vitest --ui)\n napplet-conformance --url https://my.napplet/ [options]\n napplet-conformance screenshot [directory|preview-url] [--output preview.png]\n\nArguments:\n dir Built napplet directory (prefers ./dist/index.html, falls back to ./index.html)\n\nOptions:\n --url <url> Test a remotely-served napplet instead of a local dir (headless)\n --manifest <file> Signed NIP-5D event JSON; verify its signature and raw artifact hash before boot (headless)\n --reporter <fmt> pretty | json | junit (default: pretty)\n --out <file> Write the report to a file instead of stdout\n --ready-timeout <ms> Boot timeout waiting for iframe load (default: 5000)\n --settle <ms> Envelope-collection window after boot (default: 600)\n --no-degraded Skip the no-capability graceful-degradation pass\n --allow-same-origin Debug: also grant allow-same-origin (a conformant napplet must not need it)\n\nUI / watch mode (--ui):\n --port <n> Port for the UI server (default: random)\n --no-open Do not open a browser automatically\n --exec <cmd> Run a command (e.g. \"vite build --watch\") so edits rebuild the served dir;\n the watcher then re-runs conformance live on every change\n -h, --help Show this help\n\nExit codes (headless): 0 conformant, 1 non-conformant, 2 usage/runtime error.\n`;\n\nfunction parseArgs(argv: string[]): CliOptions {\n const opts: CliOptions = {\n reporter: 'pretty',\n readyTimeoutMs: 5000,\n settleMs: 600,\n runDegraded: true,\n allowSameOrigin: false,\n ui: false,\n open: true,\n help: false,\n };\n for (let i = 0; i < argv.length; i += 1) {\n const arg = argv[i];\n const value = (): string => {\n const v = argv[i + 1];\n if (v === undefined || v.startsWith('--')) throw new Error(`Missing value for ${arg}`);\n i += 1;\n return v;\n };\n switch (arg) {\n case '-h':\n case '--help':\n opts.help = true;\n break;\n case '--url':\n opts.url = value();\n break;\n case '--manifest':\n opts.manifest = value();\n break;\n case '--reporter': {\n const r = value();\n if (r !== 'pretty' && r !== 'json' && r !== 'junit') throw new Error(`Invalid reporter: ${r}`);\n opts.reporter = r;\n break;\n }\n case '--out':\n opts.out = value();\n break;\n case '--ready-timeout':\n opts.readyTimeoutMs = Number(value());\n break;\n case '--settle':\n opts.settleMs = Number(value());\n break;\n case '--no-degraded':\n opts.runDegraded = false;\n break;\n case '--allow-same-origin':\n opts.allowSameOrigin = true;\n break;\n case '--ui':\n opts.ui = true;\n break;\n case '--port':\n opts.port = Number(value());\n break;\n case '--no-open':\n opts.open = false;\n break;\n case '--exec':\n opts.exec = value();\n break;\n default:\n if (arg.startsWith('--')) throw new Error(`Unknown option: ${arg}`);\n if (opts.target) throw new Error(`Unexpected extra argument: ${arg}`);\n opts.target = arg;\n }\n }\n return opts;\n}\n\nasync function collectBoot(hostUrl: string, totalTimeoutMs: number): Promise<BootCollection> {\n const browser = await chromium.launch({ args: ['--no-sandbox'] });\n try {\n const page = await browser.newPage();\n page.on('pageerror', (e) => process.stderr.write(`[host page error] ${e.message}\\n`));\n await page.goto(hostUrl, { waitUntil: 'load' });\n await page.waitForFunction(\n () => window.__conformanceBoot__ !== undefined || window.__conformanceError__ !== undefined,\n undefined,\n { timeout: totalTimeoutMs },\n );\n const error = await page.evaluate(() => window.__conformanceError__);\n if (error) throw new Error(`Host harness failed: ${error}`);\n const boot = await page.evaluate(() => window.__conformanceBoot__);\n if (!boot) throw new Error('Host harness produced no result');\n return boot;\n } finally {\n await browser.close();\n }\n}\n\nasync function isDir(path: string): Promise<boolean> {\n return (await stat(path).catch(() => null))?.isDirectory() ?? false;\n}\n\n/** Best-effort: open a URL in the default browser. */\nfunction openBrowser(url: string): void {\n try {\n if (process.platform === 'darwin') {\n spawn('open', [url], { stdio: 'ignore', detached: true }).unref();\n } else if (process.platform === 'win32') {\n spawn('cmd', ['/c', 'start', '', url], { stdio: 'ignore', detached: true }).unref();\n } else {\n spawn('xdg-open', [url], { stdio: 'ignore', detached: true }).unref();\n }\n } catch {\n /* best-effort — the URL is printed regardless */\n }\n}\n\n/** `--ui`: serve the live web runtime + the napplet, watch, and re-run on change. */\nasync function runUiMode(opts: CliOptions): Promise<number> {\n if (opts.manifest) {\n process.stderr.write('--manifest is supported in headless mode; use a NIP-5D pointer in the web runtime for signed manifests.\\n');\n return 2;\n }\n if (opts.url) {\n process.stderr.write('--ui serves a local napplet directory; --url is not supported in UI mode.\\n');\n return 2;\n }\n if (!opts.target) {\n process.stderr.write(`Provide a napplet directory.\\n\\n${HELP}`);\n return 2;\n }\n const resolved = await resolveNappletDir(opts.target);\n const here = dirname(fileURLToPath(import.meta.url));\n const appDir = join(here, 'ui');\n if (!(await isDir(appDir))) {\n process.stderr.write(`Conformance UI assets are missing at ${appDir}. Rebuild @napplet/conformance-cli.\\n`);\n return 2;\n }\n\n const server = await startUiServer({ appDir, nappletDir: resolved.dir, port: opts.port });\n\n const child = opts.exec ? spawn(opts.exec, { cwd: resolve(opts.target), shell: true, stdio: 'inherit' }) : null;\n\n process.stdout.write(\n `\\n napplet conformance UI → ${server.appUrl}\\n` +\n ` watching ${resolved.dir}${opts.exec ? ` (running: ${opts.exec})` : ''}\\n` +\n ` Ctrl-C to stop\\n\\n`,\n );\n if (opts.open) openBrowser(server.appUrl);\n\n await new Promise<void>((resolveWait) => {\n let closing = false;\n const shutdown = (): void => {\n if (closing) return;\n closing = true;\n child?.kill();\n void server.close().finally(resolveWait);\n };\n process.on('SIGINT', shutdown);\n process.on('SIGTERM', shutdown);\n });\n return 0;\n}\n\nasync function main(argv: string[]): Promise<number> {\n let opts: CliOptions;\n try {\n opts = parseArgs(argv);\n } catch (err) {\n process.stderr.write(`${err instanceof Error ? err.message : String(err)}\\n\\n${HELP}`);\n return 2;\n }\n if (opts.help) {\n process.stdout.write(HELP);\n return 0;\n }\n\n if (opts.ui) {\n return runUiMode(opts);\n }\n\n // Resolve the subject: local dir or remote URL.\n let nappletDir: string | null = null;\n let artifact: Uint8Array;\n let forbiddenGlobals: string[] = [];\n let bootUrl: string;\n\n if (opts.url) {\n bootUrl = opts.url;\n const response = await fetch(opts.url);\n if (!response.ok) throw new Error(`Failed to fetch napplet HTML: ${response.status} ${response.statusText}`);\n artifact = new Uint8Array(await response.arrayBuffer());\n } else {\n if (!opts.target) {\n process.stderr.write(`Provide a napplet directory or --url.\\n\\n${HELP}`);\n return 2;\n }\n const resolved = await resolveNappletDir(opts.target);\n nappletDir = resolved.dir;\n artifact = await readFile(resolved.indexHtml);\n forbiddenGlobals = await scanForbiddenGlobals(resolved.dir);\n bootUrl = '/';\n }\n\n const manifestEvent = opts.manifest\n ? verifyManifestArtifact(JSON.parse(await readFile(opts.manifest, 'utf8')) as unknown, artifact)\n : null;\n const manifestHtml = new TextDecoder().decode(artifact);\n\n const here = dirname(fileURLToPath(import.meta.url));\n const hostBundle = await readFile(join(here, 'host-bundle.global.js'), 'utf8');\n const runtimePreludeSource = await readFile(fileURLToPath(import.meta.resolve('@napplet/shim/prelude.global')), 'utf8');\n\n const server = await startHarnessServer({\n nappletDir,\n hostBundle,\n bootConfig: {\n url: bootUrl,\n html: manifestHtml,\n readyTimeoutMs: opts.readyTimeoutMs,\n settleMs: opts.settleMs,\n runDegraded: opts.runDegraded,\n runtimePreludeSource,\n allowSameOrigin: opts.allowSameOrigin,\n },\n });\n\n let boot: BootCollection;\n try {\n // Generous overall budget: two boots (primary + degraded), each up to\n // readyTimeout + settle, plus browser startup.\n const totalTimeout = (opts.readyTimeoutMs + opts.settleMs) * 2 + 30000;\n boot = await collectBoot(server.hostUrl, totalTimeout);\n } finally {\n await server.close();\n }\n\n const context = buildContext({ manifestHtml, manifestEvent, boot, forbiddenGlobals, sandbox: { allowScripts: true, allowSameOrigin: opts.allowSameOrigin } });\n const run = runConformance(context);\n const output = report(run, opts.reporter);\n\n if (opts.out) {\n await writeFile(opts.out, `${output}\\n`, 'utf8');\n process.stdout.write(`Report written to ${opts.out}\\n`);\n } else {\n process.stdout.write(`${output}\\n`);\n }\n\n return run.ok ? 0 : 1;\n}\n\nasync function run(args: string[]): Promise<number> {\n if (args[0] !== 'screenshot') return main(args);\n if (args.includes('--help') || args.includes('-h')) {\n console.log('Usage: napplet screenshot [directory|preview-url] [--output preview.png] [--selector iframe] [--ready-selector <css>] [--width 1200] [--height 750] [--delay 1500]');\n return 0;\n }\n const options = parseScreenshotArgs(args.slice(1));\n await captureScreenshot(options);\n console.log(`Screenshot saved to ${options.output}`);\n return 0;\n}\n\nrun(process.argv.slice(2))\n .then((code) => {\n process.exitCode = code;\n })\n .catch((err: unknown) => {\n process.stderr.write(`napplet-conformance: ${err instanceof Error ? err.stack ?? err.message : String(err)}\\n`);\n process.exitCode = 2;\n });\n","/**\n * @napplet/conformance-cli -- Loopback harness server.\n *\n * Serves the napplet's files plus two reserved harness routes (the host page and\n * the browser bundle). Every response carries `Access-Control-Allow-Origin: *`:\n * the napplet iframe runs at an opaque origin (sandbox without `allow-same-origin`),\n * so its `<script type=\"module\">` subresources are fetched in CORS mode and would\n * otherwise be blocked.\n *\n * @packageDocumentation\n */\n\nimport { createServer, type Server, type ServerResponse } from 'node:http';\nimport { readFile, stat } from 'node:fs/promises';\nimport { join, normalize, extname, resolve } from 'node:path';\nimport type { AddressInfo } from 'node:net';\nimport { MIME } from './static.js';\n\n/** The napplet URL + timing knobs the host page passes to `bootAndCollect`. */\nexport interface BootConfig {\n url: string;\n html?: string;\n readyTimeoutMs: number;\n settleMs: number;\n runDegraded: boolean;\n runtimePreludeSource?: string;\n allowSameOrigin?: boolean;\n}\n\n/** Options for {@link startHarnessServer}. */\nexport interface HarnessServerOptions {\n /** Directory of napplet files to serve, or `null` in remote-URL mode. */\n nappletDir: string | null;\n /** JavaScript source of the host browser bundle. */\n hostBundle: string;\n /** Boot configuration injected into the host page. */\n bootConfig: BootConfig;\n}\n\n/** A running harness server. */\nexport interface HarnessServer {\n /** Base origin, e.g. `http://127.0.0.1:54213`. */\n origin: string;\n /** URL of the host harness page. */\n hostUrl: string;\n /** Shut the server down. */\n close(): Promise<void>;\n}\n\nconst HOST_HTML_ROUTE = '/__conformance__/host.html';\nconst HOST_BUNDLE_ROUTE = '/__conformance__/host-bundle.js';\n\nfunction renderHostPage(boot: BootConfig): string {\n // The inline script lives on the harness page (full privilege, our origin), not\n // in the sandboxed napplet — so it is not subject to the napplet CSP.\n const cfg = JSON.stringify(boot).replace(/</g, '\\\\u003c');\n return `<!doctype html>\n<html lang=\"en\">\n<head><meta charset=\"utf-8\"><title>napplet conformance host</title></head>\n<body>\n<script src=\"${HOST_BUNDLE_ROUTE}\"></script>\n<script>\n (function () {\n var cfg = ${cfg};\n window.NappletConformanceHost.run(cfg).then(function (boot) {\n window.__conformanceBoot__ = boot;\n }).catch(function (err) {\n window.__conformanceError__ = String((err && err.stack) || err);\n });\n })();\n</script>\n</body>\n</html>`;\n}\n\n/** Start the loopback harness server on a random port. */\nexport async function startHarnessServer(options: HarnessServerOptions): Promise<HarnessServer> {\n const dir = options.nappletDir ? resolve(options.nappletDir) : null;\n const hostHtml = renderHostPage(options.bootConfig);\n\n const server: Server = createServer((req, res) => {\n void serve(req.url ?? '/', res);\n });\n\n async function serve(rawUrl: string, res: ServerResponse): Promise<void> {\n res.setHeader('Access-Control-Allow-Origin', '*');\n const path = decodeURIComponent(rawUrl.split('?')[0]);\n\n if (path === HOST_HTML_ROUTE) {\n res.writeHead(200, { 'Content-Type': MIME['.html'] });\n res.end(hostHtml);\n return;\n }\n if (path === HOST_BUNDLE_ROUTE) {\n res.writeHead(200, { 'Content-Type': MIME['.js'] });\n res.end(options.hostBundle);\n return;\n }\n\n if (!dir) {\n res.writeHead(404).end('not found');\n return;\n }\n\n // Resolve a safe path under the napplet dir.\n const relative = normalize(path).replace(/^(\\.\\.[/\\\\])+/, '').replace(/^[/\\\\]+/, '');\n let filePath = join(dir, relative);\n if (!filePath.startsWith(dir)) {\n res.writeHead(403).end('forbidden');\n return;\n }\n try {\n let info = await stat(filePath).catch(() => null);\n if (info?.isDirectory()) {\n filePath = join(filePath, 'index.html');\n info = await stat(filePath).catch(() => null);\n }\n if (path === '/' && !info) {\n filePath = join(dir, 'index.html');\n info = await stat(filePath).catch(() => null);\n }\n if (!info?.isFile()) {\n res.writeHead(404).end('not found');\n return;\n }\n const body = await readFile(filePath);\n res.writeHead(200, { 'Content-Type': MIME[extname(filePath).toLowerCase()] ?? 'application/octet-stream' });\n res.end(body);\n } catch {\n res.writeHead(500).end('server error');\n }\n }\n\n await new Promise<void>((resolveListen) => server.listen(0, '127.0.0.1', resolveListen));\n const address = server.address() as AddressInfo;\n const origin = `http://127.0.0.1:${address.port}`;\n\n return {\n origin,\n hostUrl: `${origin}${HOST_HTML_ROUTE}`,\n close: () => new Promise<void>((res, rej) => server.close((err) => (err ? rej(err) : res()))),\n };\n}\n","/**\n * @napplet/conformance-cli -- Shared static-serving helpers used by both the\n * one-shot harness server and the `--ui` watch server.\n *\n * @packageDocumentation\n */\n\nimport { readFile, stat } from 'node:fs/promises';\nimport { extname } from 'node:path';\nimport type { ServerResponse } from 'node:http';\n\n/** Content types by extension. */\nexport const MIME: Record<string, string> = {\n '.html': 'text/html; charset=utf-8',\n '.js': 'text/javascript; charset=utf-8',\n '.mjs': 'text/javascript; charset=utf-8',\n '.css': 'text/css; charset=utf-8',\n '.json': 'application/json; charset=utf-8',\n '.svg': 'image/svg+xml',\n '.png': 'image/png',\n '.jpg': 'image/jpeg',\n '.jpeg': 'image/jpeg',\n '.gif': 'image/gif',\n '.webp': 'image/webp',\n '.ico': 'image/x-icon',\n '.wasm': 'application/wasm',\n '.woff': 'font/woff',\n '.woff2': 'font/woff2',\n '.map': 'application/json; charset=utf-8',\n '.txt': 'text/plain; charset=utf-8',\n};\n\n/** The napplet iframe runs at an opaque origin; allow its module subresources. */\nexport function setCors(res: ServerResponse): void {\n res.setHeader('Access-Control-Allow-Origin', '*');\n}\n\n/** Send a file if it exists; return false (without writing) when it does not. */\nexport async function sendFile(res: ServerResponse, absPath: string): Promise<boolean> {\n const info = await stat(absPath).catch(() => null);\n if (!info?.isFile()) return false;\n res.writeHead(200, { 'Content-Type': MIME[extname(absPath).toLowerCase()] ?? 'application/octet-stream' });\n res.end(await readFile(absPath));\n return true;\n}\n","/**\n * @napplet/conformance-cli -- The `--ui` watch server.\n *\n * Serves the standalone conformance web app (bundled with the CLI), the napplet\n * under test (same origin, with permissive CORS for the sandboxed iframe), and a\n * Server-Sent-Events stream. It watches the napplet's served directory and pushes\n * a `rerun` event on every change, so the open app re-runs conformance live — the\n * `vitest --ui` model: edit, save, see the verdict update.\n *\n * @packageDocumentation\n */\n\nimport { createServer, type Server, type ServerResponse } from 'node:http';\nimport { watch, type FSWatcher } from 'node:fs';\nimport { join, normalize, resolve } from 'node:path';\nimport type { AddressInfo } from 'node:net';\nimport { setCors, sendFile } from './static.js';\n\n/** Path the app under `?live=1` is served the napplet from. */\nconst NAPPLET_PREFIX = '/__napplet__/';\n/** SSE endpoint the live app subscribes to. */\nconst EVENTS_ROUTE = '/__conformance__/events';\n\n/** Options for {@link startUiServer}. */\nexport interface UiServerOptions {\n /** Directory of the built conformance web app (served at `/`). */\n appDir: string;\n /** Directory of the built napplet under test (served at `/__napplet__/`). */\n nappletDir: string;\n /** Preferred port (0 = random). */\n port?: number;\n /** Debounce window (ms) for coalescing file-change bursts. Default 120. */\n debounceMs?: number;\n}\n\n/** A running `--ui` server. */\nexport interface UiServer {\n /** Base origin, e.g. `http://127.0.0.1:5219`. */\n origin: string;\n /** The app URL to open (already pointed at the napplet, live mode on). */\n appUrl: string;\n /** Force a `rerun` broadcast (used for the manual/initial trigger). */\n notify(): void;\n /** Shut the server + watcher down. */\n close(): Promise<void>;\n}\n\nfunction safeJoin(dir: string, urlPath: string): string | null {\n const relative = normalize(urlPath).replace(/^(\\.\\.[/\\\\])+/, '').replace(/^[/\\\\]+/, '');\n const full = join(dir, relative);\n return full.startsWith(dir) ? full : null;\n}\n\n/** Register an SSE subscriber and keep the connection open. */\nfunction handleSse(res: ServerResponse, clients: Set<ServerResponse>): void {\n res.writeHead(200, {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache, no-transform',\n Connection: 'keep-alive',\n });\n res.write('retry: 1000\\n\\n');\n clients.add(res);\n res.on('close', () => clients.delete(res));\n}\n\n/** Serve a file under `/__napplet__/`. */\nasync function handleNapplet(res: ServerResponse, nappletDir: string, path: string): Promise<void> {\n const rel = path.slice(NAPPLET_PREFIX.length) || 'index.html';\n const abs = safeJoin(nappletDir, rel);\n if (abs && (await sendFile(res, abs))) return;\n res.writeHead(404).end('napplet asset not found');\n}\n\n/** Serve the conformance web app, with an SPA fallback to its index. */\nasync function handleApp(res: ServerResponse, appDir: string, path: string): Promise<void> {\n const abs = safeJoin(appDir, path === '/' ? 'index.html' : path);\n if (abs && (await sendFile(res, abs))) return;\n if (await sendFile(res, join(appDir, 'index.html'))) return;\n res.writeHead(404).end('not found');\n}\n\n/** Watch a directory (recursively when supported) and call `fire` debounced. */\nfunction startWatcher(dir: string, debounceMs: number, fire: () => void): { close(): void } {\n let timer: ReturnType<typeof setTimeout> | null = null;\n const onEvent = (): void => {\n if (timer) clearTimeout(timer);\n timer = setTimeout(fire, debounceMs);\n };\n let watcher: FSWatcher;\n try {\n watcher = watch(dir, { recursive: true }, onEvent);\n } catch {\n // Recursive watch may be unsupported on some platforms.\n watcher = watch(dir, onEvent);\n }\n return {\n close() {\n if (timer) clearTimeout(timer);\n watcher.close();\n },\n };\n}\n\n/** Start the `--ui` watch server. */\nexport async function startUiServer(options: UiServerOptions): Promise<UiServer> {\n const appDir = resolve(options.appDir);\n const nappletDir = resolve(options.nappletDir);\n const clients = new Set<ServerResponse>();\n const broadcast = (): void => {\n for (const res of clients) res.write(`event: rerun\\ndata: ${Date.now()}\\n\\n`);\n };\n\n const server: Server = createServer((req, res) => {\n setCors(res);\n res.setHeader('Cache-Control', 'no-store'); // never serve stale napplet code\n const path = decodeURIComponent((req.url ?? '/').split('?')[0]);\n if (path === EVENTS_ROUTE) {\n handleSse(res, clients);\n } else if (path === NAPPLET_PREFIX.slice(0, -1) || path.startsWith(NAPPLET_PREFIX)) {\n void handleNapplet(res, nappletDir, path);\n } else {\n void handleApp(res, appDir, path);\n }\n });\n\n await new Promise<void>((r) => server.listen(options.port ?? 0, '127.0.0.1', r));\n const { port } = server.address() as AddressInfo;\n const origin = `http://127.0.0.1:${port}`;\n const appUrl = `${origin}/?url=${encodeURIComponent(NAPPLET_PREFIX)}&live=1`;\n const watcher = startWatcher(nappletDir, options.debounceMs ?? 120, broadcast);\n\n return {\n origin,\n appUrl,\n notify: broadcast,\n close: () =>\n new Promise<void>((res, rej) => {\n watcher.close();\n for (const c of clients) c.end();\n clients.clear();\n server.close((err) => (err ? rej(err) : res()));\n }),\n };\n}\n","/**\n * @napplet/conformance-cli -- Static scan for forbidden napplet authority.\n *\n * A napplet runs in a `sandbox=\"allow-scripts\"` iframe at an opaque origin, so the\n * host cannot observe every forbidden browser surface at runtime. Instead we scan\n * served source for references to authority the shell owns: signer globals,\n * browser storage, direct network primitives, and external network-loaded assets.\n * This is a heuristic, but a true positive is meaningful: NIP-5D and the NAPs\n * route these capabilities through `window.napplet` domains.\n *\n * @packageDocumentation\n */\n\nimport { readdir, readFile, stat } from 'node:fs/promises';\nimport { join, extname } from 'node:path';\n\n/** Patterns considered forbidden, keyed by the label reported in the verdict. */\nconst FORBIDDEN: Array<{ label: string; re: RegExp }> = [\n { label: 'window.nostr', re: /\\bwindow\\s*\\.\\s*nostr\\b/ },\n { label: 'globalThis.nostr', re: /\\bglobalThis\\s*\\.\\s*nostr\\b/ },\n { label: 'fetch', re: /\\b(?:window\\s*\\.\\s*|globalThis\\s*\\.\\s*)?fetch\\s*\\(/ },\n { label: 'XMLHttpRequest', re: /\\b(?:new\\s+)?XMLHttpRequest\\s*\\(/ },\n { label: 'WebSocket', re: /\\b(?:new\\s+)?WebSocket\\s*\\(/ },\n { label: 'localStorage', re: /\\b(?:window\\s*\\.\\s*)?localStorage\\b/ },\n { label: 'sessionStorage', re: /\\b(?:window\\s*\\.\\s*)?sessionStorage\\b/ },\n { label: 'indexedDB', re: /\\b(?:window\\s*\\.\\s*)?indexedDB\\b/ },\n { label: 'document.cookie', re: /\\bdocument\\s*\\.\\s*cookie\\b/ },\n { label: 'external img src', re: /<img\\b[^>]*\\bsrc\\s*=\\s*[\"']?https?:/i },\n { label: 'external script src', re: /<script\\b[^>]*\\bsrc\\s*=\\s*[\"']?https?:/i },\n { label: 'external stylesheet href', re: /<link\\b[^>]*\\brel\\s*=\\s*[\"']?stylesheet[\"']?[^>]*\\bhref\\s*=\\s*[\"']?https?:/i },\n { label: 'external stylesheet href', re: /<link\\b[^>]*\\bhref\\s*=\\s*[\"']?https?:[^>]*\\brel\\s*=\\s*[\"']?stylesheet[\"']?/i },\n];\n\nconst SCANNABLE = new Set(['.js', '.mjs', '.cjs', '.html', '.htm']);\nconst SKIP_DIRS = new Set(['node_modules', '.git', '.turbo', 'coverage']);\n\nasync function collectFiles(dir: string, acc: string[]): Promise<void> {\n for (const entry of await readdir(dir, { withFileTypes: true })) {\n if (entry.isDirectory()) {\n if (SKIP_DIRS.has(entry.name)) continue;\n await collectFiles(join(dir, entry.name), acc);\n } else if (entry.isFile() && SCANNABLE.has(extname(entry.name).toLowerCase())) {\n acc.push(join(dir, entry.name));\n }\n }\n}\n\n/**\n * Scan a napplet directory for forbidden browser authority references.\n *\n * @param dir - The directory of served napplet files.\n * @returns The distinct forbidden labels found (e.g. `['window.nostr']`).\n */\nexport async function scanForbiddenGlobals(dir: string): Promise<string[]> {\n const info = await stat(dir).catch(() => null);\n if (!info?.isDirectory()) return [];\n const files: string[] = [];\n await collectFiles(dir, files);\n const found = new Set<string>();\n for (const file of files) {\n const text = await readFile(file, 'utf8').catch(() => '');\n for (const { label, re } of FORBIDDEN) {\n if (re.test(text)) found.add(label);\n }\n }\n return [...found];\n}\n","/**\n * @napplet/conformance-cli -- Resolve which local artifact to conformance-test.\n *\n * Kept in its own module (not `cli.ts`, whose top level runs `main()`) so tests\n * can import the resolver without launching the CLI.\n */\n\nimport { stat } from 'node:fs/promises';\nimport { join, resolve } from 'node:path';\n\nasync function isFile(path: string): Promise<boolean> {\n return (await stat(path).catch(() => null))?.isFile() ?? false;\n}\n\n/**\n * Resolve the directory + index.html for a local napplet target.\n *\n * A built napplet is the artifact we test, so `dist/index.html` is preferred over\n * a sibling `index.html`. When `target` is a project root, its source\n * `index.html` (a Vite entry referencing `/src/*.ts` that cannot run in the\n * opaque-origin `srcdoc` sandbox) would otherwise shadow the built single-file\n * output and make conformance test the wrong, never-booting artifact.\n */\nexport async function resolveNappletDir(target: string): Promise<{ dir: string; indexHtml: string }> {\n const base = resolve(target);\n const dist = join(base, 'dist');\n if (await isFile(join(dist, 'index.html'))) return { dir: dist, indexHtml: join(dist, 'index.html') };\n if (await isFile(join(base, 'index.html'))) return { dir: base, indexHtml: join(base, 'index.html') };\n throw new Error(`No index.html found in ${join(dist, 'index.html')} or ${join(base, 'index.html')}. Build the napplet first.`);\n}\n","/** Capture a local build or an existing shell preview through its iframe. */\nimport { access, writeFile } from 'node:fs/promises';\nimport { execFile } from 'node:child_process';\nimport { createRequire } from 'node:module';\nimport { dirname, join } from 'node:path';\nimport { promisify } from 'node:util';\nimport { chromium } from 'playwright';\nimport { startScreenshotPreview } from './screenshot-preview.js';\nimport { resolveNappletDir } from './resolve-napplet.js';\n\n/** Options for capturing a shell-hosted napplet. */\nexport interface ScreenshotOptions {\n url?: string;\n directory?: string;\n artifact?: string;\n output: string;\n selector: string;\n readySelector?: string;\n width: number;\n height: number;\n delay: number;\n}\n\n/**\n * Parse screenshot arguments without invoking a browser.\n * @param args CLI arguments after the screenshot command.\n * @returns Validated capture options.\n * @example parseScreenshotArgs(['http://localhost:5173', '--output', 'preview.png'])\n */\nexport function parseScreenshotArgs(args: readonly string[]): ScreenshotOptions {\n const hasTarget = args[0] && !args[0].startsWith('--');\n const target = hasTarget ? args[0] : '.';\n const rest = hasTarget ? args.slice(1) : args;\n const remote = /^https?:\\/\\//.test(target);\n if (remote) new URL(target);\n else if (/^[a-z][a-z0-9+.-]*:/i.test(target) && !/^[a-z]:[\\\\/]/i.test(target)) throw new Error('Preview URLs must use HTTP(S)');\n const options: ScreenshotOptions = { ...(remote ? { url: target } : { directory: target }), output: 'preview.png', selector: 'iframe', width: 1200, height: 750, delay: 1500 };\n for (let index = 0; index < rest.length; index += 2) {\n const flag = rest[index];\n const value = rest[index + 1];\n if (!value || value.startsWith('--')) throw new Error(`Missing value for ${flag}`);\n switch (flag) {\n case '--output': options.output = value; break;\n case '--artifact': options.artifact = value; break;\n case '--selector': options.selector = value; break;\n case '--ready-selector': options.readySelector = value; break;\n case '--width': options.width = integer(value, flag, 1, 4096); break;\n case '--height': options.height = integer(value, flag, 1, 4096); break;\n case '--delay': options.delay = integer(value, flag, 0, 30000); break;\n default: throw new Error(`Unknown screenshot option: ${flag}`);\n }\n }\n if (!options.output.toLowerCase().endsWith('.png')) throw new Error('--output must end in .png');\n if (options.artifact && hasTarget) throw new Error('Use a target or --artifact, not both');\n return options;\n}\n\n/**\n * Save a PNG of a napplet iframe, starting a local preview when needed.\n * @param options Local build or URL, output, dimensions and readiness settings.\n * @returns Resolves after capture and cleanup of Chromium and any local preview.\n * @example await captureScreenshot(parseScreenshotArgs(['http://localhost:5173']))\n */\nexport async function captureScreenshot(options: ScreenshotOptions): Promise<void> {\n await ensureChromium();\n const preview = options.url ? undefined : await startScreenshotPreview(\n options.artifact ?? (await resolveNappletDir(options.directory ?? '.')).indexHtml,\n );\n let browser;\n try {\n browser = await chromium.launch({ channel: 'chromium' });\n const page = await browser.newPage({ viewport: { width: options.width, height: options.height } });\n page.setDefaultTimeout(30000);\n await page.goto(options.url ?? preview!.url, { waitUntil: 'domcontentloaded' });\n if (preview) {\n await page.waitForFunction(() => {\n const iframe = document.querySelector<HTMLIFrameElement>('#napplet-frame');\n return Boolean(iframe?.srcdoc);\n });\n }\n const frame = page.locator(options.selector);\n await frame.waitFor({ state: 'visible' });\n await frame.evaluate((element, size) => {\n if (!(element instanceof HTMLIFrameElement)) throw new Error('Screenshot selector must select one iframe');\n Object.assign(element.style, { width: `${size.width}px`, height: `${size.height}px`, maxWidth: 'none', maxHeight: 'none', boxSizing: 'border-box' });\n }, { width: options.width, height: options.height });\n const content = frame.contentFrame();\n await content.locator('body').waitFor({ state: 'visible' });\n if (options.readySelector) await content.locator(options.readySelector).waitFor({ state: 'visible' });\n await content.locator('body').evaluate(async () => { await document.fonts.ready; });\n await page.waitForTimeout(options.delay);\n const png = await frame.screenshot({ type: 'png', timeout: 30000 });\n await writeFile(options.output, png, { flag: 'wx' });\n } finally {\n try { await browser?.close(); } finally { await preview?.close(); }\n }\n}\n\nasync function ensureChromium(): Promise<void> {\n try { await access(chromium.executablePath()); return; } catch { /* Install the matching browser on first use. */ }\n console.error('Installing Chromium for screenshot capture...');\n const require = createRequire(import.meta.url);\n const cli = join(dirname(require.resolve('playwright/package.json')), 'cli.js');\n await promisify(execFile)(process.execPath, [cli, 'install', 'chromium'], { timeout: 180000 });\n}\n\nfunction integer(value: string, flag: string, min: number, max: number): number {\n const number = Number(value);\n if (!Number.isInteger(number) || number < min || number > max) throw new Error(`${flag} must be an integer from ${min} to ${max}`);\n return number;\n}\n","/** Disposable Kehto Paja preview for a local build. */\nimport { createServer } from 'node:http';\nimport type { AddressInfo } from 'node:net';\nimport { readFile, realpath } from 'node:fs/promises';\nimport { dirname, resolve, sep } from 'node:path';\nimport { startPajaServer } from '@kehto/paja';\nimport { sendFile, setCors } from './static.js';\n\n/**\n * Host a built artifact in Paja on ephemeral loopback ports.\n * @param artifact Exact index.html selected by deployment.\n * @returns Shell URL and an async cleanup handle for both servers.\n * @example const preview = await startScreenshotPreview('/project/dist/index.html');\n */\nexport async function startScreenshotPreview(artifact: string): Promise<{ url: string; close(): Promise<void> }> {\n const index = await realpath(artifact);\n const root = dirname(index);\n const html = await readFile(index);\n const server = createServer((request, response) => {\n setCors(response);\n void (async () => {\n const pathname = decodeURIComponent(new URL(request.url ?? '/', 'http://localhost').pathname);\n if (pathname === '/' || pathname === '/index.html') {\n response.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });\n response.end(html);\n return;\n }\n const file = await realpath(resolve(root, `.${pathname}`));\n if (!file.startsWith(`${root}${sep}`)) { response.writeHead(403).end(); return; }\n if (!await sendFile(response, file)) response.writeHead(404).end();\n })().catch(() => { if (!response.headersSent) response.writeHead(404); response.end(); });\n });\n await new Promise<void>((done, reject) => {\n server.once('error', reject);\n server.listen(0, '127.0.0.1', () => { server.off('error', reject); done(); });\n });\n const closeTarget = () => new Promise<void>((done, reject) => {\n server.closeAllConnections();\n server.close((error) => error ? reject(error) : done());\n });\n try {\n const targetUrl = `http://127.0.0.1:${(server.address() as AddressInfo).port}`;\n const paja = await startPajaServer({ options: {\n host: '127.0.0.1', port: 0, targetUrl,\n } });\n return { url: paja.url, close: async () => { try { await paja.close(); } finally { await closeTarget(); } } };\n } catch (error) {\n await closeTarget();\n throw error;\n }\n}\n","import { createHash } from 'node:crypto';\nimport { validateEvent, verifyEvent, type Event } from 'nostr-tools/pure';\nimport { validateManifestEvent } from '@napplet/conformance';\n\n/** Verify the signed event and exact raw artifact before the browser executes it (NIP-5D §Security Considerations). */\nexport function verifyManifestArtifact(input: unknown, artifact: Uint8Array): Event {\n if (!validateEvent(input)) throw new Error('Manifest is not a Nostr event');\n // Verify a fresh object: nostr-tools caches verification on a symbol which a\n // caller could otherwise copy from a previously signed event and then mutate.\n const { id, pubkey, created_at, kind, tags, content, sig } = input as Event;\n const event: Event = { id, pubkey, created_at, kind, tags, content, sig };\n let validSignature = false;\n try {\n validSignature = verifyEvent(event);\n } catch {\n // Malformed signature fields are rejected along with cryptographic failures.\n }\n if (!validSignature) throw new Error('Manifest signature or event id is invalid');\n const verdict = validateManifestEvent(event);\n if (!verdict.ok) throw new Error(`Invalid NIP-5D manifest: ${verdict.errors.map((error) => error.message).join('; ')}`);\n const expected = event.tags.find((tag) => tag[0] === 'x')?.[1];\n const actual = createHash('sha256').update(artifact).digest('hex');\n if (actual !== expected) throw new Error(`Artifact SHA-256 mismatch: expected ${expected}, got ${actual}`);\n return event;\n}\n"],"mappings":"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","names":["readFile","writeFile","stat","join","dirname","resolve","chromium","readFile","stat","extname","stat","readFile","extname","createServer","join","normalize","resolve","normalize","join","resolve","createServer","readFile","stat","join","extname","stat","join","resolve","dirname","join","createServer","readFile","resolve","readFile","createServer","resolve","require","join","dirname","chromium","stat","dirname","join","resolve","readFile","run","writeFile"]}
|
|
@@ -569,6 +569,11 @@ var NappletConformanceHostBundle = (() => {
|
|
|
569
569
|
errors.push({ code: "invalid-metadata", message: `Invalid ${name} tag cardinality for kind ${event.kind}` });
|
|
570
570
|
}
|
|
571
571
|
}
|
|
572
|
+
for (const tag of event.tags.filter((tag2) => tag2[0] === "source")) {
|
|
573
|
+
if (tag.length !== 2 || !isSourceLocation(tag[1])) {
|
|
574
|
+
errors.push({ code: "invalid-metadata", message: "source must contain one HTTP(S), SSH, Git, or SCP repository location" });
|
|
575
|
+
}
|
|
576
|
+
}
|
|
572
577
|
for (const tag of event.tags.filter((tag2) => tag2[0] === "i")) {
|
|
573
578
|
if (!tag[1]?.trim() || tag[1].includes("?")) {
|
|
574
579
|
errors.push({ code: "invalid-metadata", message: "Intent advertisements must be non-empty queryless identities" });
|
|
@@ -579,6 +584,16 @@ var NappletConformanceHostBundle = (() => {
|
|
|
579
584
|
warnings.push({ code: "invalid-icon", message: "Ignore invalid icon metadata and use generic artwork (NIP-5D \xA7Icon)" });
|
|
580
585
|
}
|
|
581
586
|
}
|
|
587
|
+
function isSourceLocation(value) {
|
|
588
|
+
if (!value || /^[a-z]:[\\/]/i.test(value)) return false;
|
|
589
|
+
try {
|
|
590
|
+
const { protocol } = new URL(value);
|
|
591
|
+
if (["http:", "https:", "ssh:", "git:"].includes(protocol)) return true;
|
|
592
|
+
if (protocol === "file:") return false;
|
|
593
|
+
} catch {
|
|
594
|
+
}
|
|
595
|
+
return !value.includes("://") && /^(?:[^@\s/:]+@)?[^:\s/]+:[^\s]+$/.test(value);
|
|
596
|
+
}
|
|
582
597
|
var REFERENCE_PUBKEY = "f".repeat(64);
|
|
583
598
|
var REFERENCE_ENDPOINT = { id: "reference-source-endpoint", catalogId: "reference-source" };
|
|
584
599
|
var REFERENCE_BLOB_URL = "https://reference.invalid/blob";
|
|
@@ -979,6 +994,7 @@ var NappletConformanceHostBundle = (() => {
|
|
|
979
994
|
};
|
|
980
995
|
}
|
|
981
996
|
var INTERNAL_BOOT_ERROR = "__nappletConformance.error";
|
|
997
|
+
var INTERNAL_INSTALLED = "__nappletConformance.installed";
|
|
982
998
|
function sleep(ms) {
|
|
983
999
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
984
1000
|
}
|
|
@@ -1013,12 +1029,30 @@ var NappletConformanceHostBundle = (() => {
|
|
|
1013
1029
|
};
|
|
1014
1030
|
return { isGamepadState, copyGamepadState };
|
|
1015
1031
|
})()`;
|
|
1016
|
-
function
|
|
1032
|
+
function errorObserver() {
|
|
1033
|
+
return `(() => {
|
|
1034
|
+
const report = (reason) => {
|
|
1035
|
+
try { window.parent.postMessage({ type: '${INTERNAL_BOOT_ERROR}', reason: String(reason && reason.message || reason) }, '*'); }
|
|
1036
|
+
catch {}
|
|
1037
|
+
};
|
|
1038
|
+
window.addEventListener('error', (event) => report(event.error || event.message));
|
|
1039
|
+
window.addEventListener('unhandledrejection', (event) => report(event.reason));
|
|
1040
|
+
})();`;
|
|
1041
|
+
}
|
|
1042
|
+
function runtimePrelude(domains, runtimePreludeSource) {
|
|
1043
|
+
const installed = `window.parent.postMessage({ type: '${INTERNAL_INSTALLED}', installed: !!window.napplet }, '*');`;
|
|
1044
|
+
if (runtimePreludeSource !== void 0) {
|
|
1045
|
+
return `${runtimePreludeSource}
|
|
1046
|
+
;globalThis.NappletShimPrelude.install(${JSON.stringify({ domains })});
|
|
1047
|
+
${installed}`;
|
|
1048
|
+
}
|
|
1017
1049
|
return `
|
|
1018
1050
|
(() => {
|
|
1019
1051
|
const domains = ${JSON.stringify(domains)};
|
|
1020
1052
|
const napplet = {};
|
|
1021
|
-
for (const domain of domains)
|
|
1053
|
+
for (const domain of domains) {
|
|
1054
|
+
if (domain === 'resource' || domain === 'gamepad') napplet[domain] = {};
|
|
1055
|
+
}
|
|
1022
1056
|
const gamepadTools = ${GAMEPAD_STATE_TOOLS_SOURCE};
|
|
1023
1057
|
let gamepadState = { available: true, focused: false, pads: [] };
|
|
1024
1058
|
const gamepadHandlers = new Set();
|
|
@@ -1119,19 +1153,14 @@ var NappletConformanceHostBundle = (() => {
|
|
|
1119
1153
|
};
|
|
1120
1154
|
}
|
|
1121
1155
|
window.napplet = napplet;
|
|
1122
|
-
|
|
1123
|
-
try {
|
|
1124
|
-
window.parent.postMessage({ type: '${INTERNAL_BOOT_ERROR}', reason: String(reason && reason.message || reason) }, '*');
|
|
1125
|
-
} catch {}
|
|
1126
|
-
};
|
|
1127
|
-
window.addEventListener('error', (event) => report(event.error || event.message));
|
|
1128
|
-
window.addEventListener('unhandledrejection', (event) => report(event.reason));
|
|
1156
|
+
${installed}
|
|
1129
1157
|
})();
|
|
1130
1158
|
`;
|
|
1131
1159
|
}
|
|
1132
|
-
function injectRuntime(html, url, domains) {
|
|
1160
|
+
function injectRuntime(html, url, domains, runtimePreludeSource) {
|
|
1133
1161
|
const base = `<base href="${escapeHtmlAttr(url)}">`;
|
|
1134
|
-
const
|
|
1162
|
+
const source = runtimePrelude(domains, runtimePreludeSource).replace(/<\/script/gi, "<\\/script");
|
|
1163
|
+
const script = `<script>${errorObserver()}<\/script><script>${source}<\/script>`;
|
|
1135
1164
|
const headOpen = /<head(?:\s[^>]*)?>/i.exec(html);
|
|
1136
1165
|
if (headOpen) {
|
|
1137
1166
|
const at = headOpen.index + headOpen[0].length;
|
|
@@ -1167,9 +1196,14 @@ var NappletConformanceHostBundle = (() => {
|
|
|
1167
1196
|
iframe.style.height = "1px";
|
|
1168
1197
|
iframe.style.left = "-9999px";
|
|
1169
1198
|
let bootError = null;
|
|
1199
|
+
let installedGlobal = false;
|
|
1170
1200
|
const listener = (event) => {
|
|
1171
1201
|
if (event.source !== iframe.contentWindow) return;
|
|
1172
1202
|
const data = event.data;
|
|
1203
|
+
if (data && typeof data === "object" && data.type === INTERNAL_INSTALLED) {
|
|
1204
|
+
installedGlobal = data.installed === true;
|
|
1205
|
+
return;
|
|
1206
|
+
}
|
|
1173
1207
|
if (data && typeof data === "object" && data.type === INTERNAL_BOOT_ERROR) {
|
|
1174
1208
|
bootError = `napplet boot error: ${String(data.reason ?? "unknown error")}`;
|
|
1175
1209
|
return;
|
|
@@ -1186,10 +1220,10 @@ var NappletConformanceHostBundle = (() => {
|
|
|
1186
1220
|
}, { once: true });
|
|
1187
1221
|
});
|
|
1188
1222
|
win.addEventListener("message", listener);
|
|
1189
|
-
(doc.body ?? doc.documentElement).appendChild(iframe);
|
|
1190
1223
|
try {
|
|
1191
|
-
const html = await loadHtml(opts.url);
|
|
1192
|
-
iframe.srcdoc = injectRuntime(html, opts.url, domains);
|
|
1224
|
+
const html = opts.html ?? await loadHtml(opts.url);
|
|
1225
|
+
iframe.srcdoc = injectRuntime(html, opts.url, domains, opts.runtimePreludeSource);
|
|
1226
|
+
(doc.body ?? doc.documentElement).appendChild(iframe);
|
|
1193
1227
|
} catch (error) {
|
|
1194
1228
|
bootError = error instanceof Error ? error.message : String(error);
|
|
1195
1229
|
}
|
|
@@ -1207,16 +1241,18 @@ var NappletConformanceHostBundle = (() => {
|
|
|
1207
1241
|
iframe.remove();
|
|
1208
1242
|
} catch {
|
|
1209
1243
|
}
|
|
1210
|
-
return { installedGlobal
|
|
1244
|
+
return { installedGlobal, bootError, emitted: [...shell.records] };
|
|
1211
1245
|
}
|
|
1212
1246
|
async function bootAndCollect(options) {
|
|
1213
1247
|
const doc = options.document ?? document;
|
|
1214
1248
|
const win = options.window ?? window;
|
|
1215
1249
|
const resolved = {
|
|
1216
1250
|
url: options.url,
|
|
1251
|
+
html: options.html,
|
|
1217
1252
|
readyTimeoutMs: options.readyTimeoutMs ?? 5e3,
|
|
1218
1253
|
settleMs: options.settleMs ?? 600,
|
|
1219
|
-
allowSameOrigin: options.allowSameOrigin ?? false
|
|
1254
|
+
allowSameOrigin: options.allowSameOrigin ?? false,
|
|
1255
|
+
runtimePreludeSource: options.runtimePreludeSource
|
|
1220
1256
|
};
|
|
1221
1257
|
const primary = await bootOnce(NAP_DOMAINS, resolved, doc, win);
|
|
1222
1258
|
let degraded = null;
|