@napplet/conformance-cli 0.2.9 → 0.2.11

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 CHANGED
@@ -61,7 +61,7 @@ unchanged — `--ui` is purely additive.)
61
61
  --url <url> Test a remotely-served napplet instead of a local dir
62
62
  --reporter <fmt> pretty | json | junit (default: pretty)
63
63
  --out <file> Write the report to a file instead of stdout
64
- --ready-timeout <ms> Boot timeout waiting for shell.ready (default: 5000)
64
+ --ready-timeout <ms> Boot timeout waiting for iframe load (default: 5000)
65
65
  --settle <ms> Envelope-collection window after boot (default: 600)
66
66
  --no-degraded Skip the graceful-degradation pass
67
67
  --allow-same-origin Debug only (a conformant napplet must not need it)
package/dist/cli.js CHANGED
@@ -288,7 +288,7 @@ Options:
288
288
  --url <url> Test a remotely-served napplet instead of a local dir (headless)
289
289
  --reporter <fmt> pretty | json | junit (default: pretty)
290
290
  --out <file> Write the report to a file instead of stdout
291
- --ready-timeout <ms> Boot timeout waiting for shell.ready (default: 5000)
291
+ --ready-timeout <ms> Boot timeout waiting for iframe load (default: 5000)
292
292
  --settle <ms> Envelope-collection window after boot (default: 600)
293
293
  --no-degraded Skip the no-capability graceful-degradation pass
294
294
  --allow-same-origin Debug: also grant allow-same-origin (a conformant napplet must not need it)
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"],"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';\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\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 shell.ready (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\nmain(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 globals.\n *\n * A napplet runs in a `sandbox=\"allow-scripts\"` iframe at an opaque origin, so the\n * host cannot observe `window.nostr` access at runtime. Instead we scan the served\n * source for references to globals a shell never provides. This is a heuristic, but\n * a true positive is meaningful: shells removed `window.nostr` from napplets in\n * v0.24.0, so any reference is dead or broken code.\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];\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 global 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"],"mappings":"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","names":["readFile","stat","join","resolve","readFile","stat","extname","stat","readFile","extname","createServer","join","normalize","resolve","normalize","join","resolve","createServer","readFile","stat","join","extname","stat","join","resolve","stat","join","resolve","readFile"]}
1
+ {"version":3,"sources":["../src/cli.ts","../src/server.ts","../src/static.ts","../src/ui-server.ts","../src/scan.ts","../src/resolve-napplet.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';\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\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\nmain(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 globals.\n *\n * A napplet runs in a `sandbox=\"allow-scripts\"` iframe at an opaque origin, so the\n * host cannot observe `window.nostr` access at runtime. Instead we scan the served\n * source for references to globals a shell never provides. This is a heuristic, but\n * a true positive is meaningful: shells removed `window.nostr` from napplets in\n * v0.24.0, so any reference is dead or broken code.\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];\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 global 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"],"mappings":"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","names":["readFile","stat","join","resolve","readFile","stat","extname","stat","readFile","extname","createServer","join","normalize","resolve","normalize","join","resolve","createServer","readFile","stat","join","extname","stat","join","resolve","stat","join","resolve","readFile"]}
@@ -30,14 +30,8 @@ var NappletConformanceHostBundle = (() => {
30
30
  var getRegisteredDomains = _default.getRegisteredDomains;
31
31
 
32
32
  // ../conformance/dist/index.js
33
- var FOUNDATIONAL_DOMAINS = ["shell"];
34
33
  var ID = { id: "string" };
35
34
  var ENVELOPE_SPECS = {
36
- // ── shell (NAP-SHELL — foundational handshake) ─────────────────────────────
37
- // `shell.ready` is a bare liveness ping (no required fields); `shell.init` is
38
- // the runtime's environment reply (inbound — a napplet must not emit it).
39
- "shell.ready": { dir: "out" },
40
- "shell.init": { dir: "in" },
41
35
  // ── relay ────────────────────────────────────────────────────────────────
42
36
  "relay.subscribe": { dir: "out", fields: { ...ID, subId: "string", filters: "array" } },
43
37
  "relay.close": { dir: "out", fields: { ...ID, subId: "string" } },
@@ -139,9 +133,12 @@ var NappletConformanceHostBundle = (() => {
139
133
  "config.values": { dir: "in" },
140
134
  "config.schemaError": { dir: "in" },
141
135
  // ── resource ─────────────────────────────────────────────────────────────
136
+ "resource.info": { dir: "out", fields: { ...ID } },
142
137
  "resource.bytes": { dir: "out", fields: { ...ID, url: "string" } },
143
138
  "resource.bytesMany": { dir: "out", fields: { ...ID, urls: "array" } },
144
139
  "resource.cancel": { dir: "out", fields: { ...ID } },
140
+ "resource.info.result": { dir: "in" },
141
+ "resource.info.error": { dir: "in" },
145
142
  "resource.bytes.result": { dir: "in" },
146
143
  "resource.bytes.error": { dir: "in" },
147
144
  "resource.bytesMany.result": { dir: "in" },
@@ -155,11 +152,13 @@ var NappletConformanceHostBundle = (() => {
155
152
  "cvm.close.result": { dir: "in" },
156
153
  "cvm.event": { dir: "in" },
157
154
  // ── outbox ───────────────────────────────────────────────────────────────
155
+ "outbox.getEvent": { dir: "out", fields: { ...ID, eventId: "string" } },
158
156
  "outbox.query": { dir: "out", fields: { ...ID, filters: "present" } },
159
157
  "outbox.subscribe": { dir: "out", fields: { ...ID, subId: "string", filters: "present" } },
160
158
  "outbox.close": { dir: "out", fields: { ...ID, subId: "string" } },
161
159
  "outbox.publish": { dir: "out", fields: { ...ID, event: "object" } },
162
160
  "outbox.resolveRelays": { dir: "out", fields: { ...ID, target: "present" } },
161
+ "outbox.getEvent.result": { dir: "in" },
163
162
  "outbox.query.result": { dir: "in" },
164
163
  "outbox.event": { dir: "in" },
165
164
  "outbox.eose": { dir: "in" },
@@ -167,8 +166,10 @@ var NappletConformanceHostBundle = (() => {
167
166
  "outbox.publish.result": { dir: "in" },
168
167
  "outbox.resolveRelays.result": { dir: "in" },
169
168
  // ── upload ───────────────────────────────────────────────────────────────
169
+ "upload.info": { dir: "out", fields: { ...ID } },
170
170
  "upload.upload": { dir: "out", fields: { ...ID, request: "object" } },
171
171
  "upload.status": { dir: "out", fields: { ...ID, uploadId: "string" } },
172
+ "upload.info.result": { dir: "in" },
172
173
  "upload.upload.result": { dir: "in" },
173
174
  "upload.status.result": { dir: "in" },
174
175
  "upload.status.changed": { dir: "in" },
@@ -282,7 +283,7 @@ var NappletConformanceHostBundle = (() => {
282
283
  return { ok: false, type, errors: [{ code: "malformed-type", message: `Envelope type "${type}" is not in domain.action form` }] };
283
284
  }
284
285
  const domain = type.slice(0, dotIndex);
285
- const isKnownDomain = NAP_DOMAINS.includes(domain) || FOUNDATIONAL_DOMAINS.includes(domain);
286
+ const isKnownDomain = NAP_DOMAINS.includes(domain);
286
287
  if (!isKnownDomain) {
287
288
  return { ok: false, type, domain, errors: [{ code: "unknown-domain", message: `"${domain}" is not a known NAP domain` }] };
288
289
  }
@@ -460,18 +461,39 @@ var NappletConformanceHostBundle = (() => {
460
461
  mime: "application/octet-stream"
461
462
  })) : []
462
463
  }),
464
+ "resource.info": (e) => ok({
465
+ type: "resource.info.result",
466
+ id: e.id,
467
+ info: {
468
+ schemes: [
469
+ { scheme: "data", enabled: true },
470
+ { scheme: "https", enabled: true }
471
+ ]
472
+ }
473
+ }),
463
474
  "resource.cancel": none,
464
475
  // cvm
465
476
  "cvm.discover": (e) => ok({ type: "cvm.discover.result", id: e.id, servers: [] }),
466
477
  "cvm.request": (e) => ok({ type: "cvm.request.result", id: e.id, message: {} }),
467
478
  "cvm.close": (e) => ok({ type: "cvm.close.result", id: e.id }),
468
479
  // outbox
480
+ "outbox.getEvent": (e) => ok({ type: "outbox.getEvent.result", id: e.id, relays: [] }),
469
481
  "outbox.query": (e) => ok({ type: "outbox.query.result", id: e.id, events: [], relays: {} }),
470
482
  "outbox.subscribe": (e) => ok({ type: "outbox.eose", subId: e.subId }),
471
483
  "outbox.close": none,
472
484
  "outbox.publish": (e) => ok({ type: "outbox.publish.result", id: e.id, ok: true }),
473
485
  "outbox.resolveRelays": (e) => ok({ type: "outbox.resolveRelays.result", id: e.id, plan: {} }),
474
486
  // upload
487
+ "upload.info": (e) => ok({
488
+ type: "upload.info.result",
489
+ id: e.id,
490
+ info: {
491
+ rails: [
492
+ { rail: "nip96", enabled: true, returns: ["https"] },
493
+ { rail: "blossom", enabled: true, returns: ["https", "blossom"] }
494
+ ]
495
+ }
496
+ }),
475
497
  "upload.upload": (e) => ok({ type: "upload.upload.result", id: e.id, result: { url: REFERENCE_BLOB_URL } }),
476
498
  "upload.status": (e) => ok({ type: "upload.status.result", id: e.id, status: {} }),
477
499
  // intent
@@ -546,31 +568,15 @@ var NappletConformanceHostBundle = (() => {
546
568
  };
547
569
  function createReferenceShell(options = {}) {
548
570
  const now = options.now ?? (() => Date.now());
549
- const capabilities = {
550
- domains: options.capabilities?.domains ?? [...NAP_DOMAINS],
551
- protocols: options.capabilities?.protocols ?? {}
552
- };
553
- const services = options.services ?? [];
554
571
  const records = [];
555
572
  function handle(envelope) {
556
573
  const type = envelope && typeof envelope === "object" && typeof envelope.type === "string" ? envelope.type : void 0;
557
- if (type === "shell.ready") {
558
- return [
559
- {
560
- type: "shell.init",
561
- capabilities: { domains: capabilities.domains, protocols: capabilities.protocols },
562
- services
563
- }
564
- ];
565
- }
566
574
  records.push({ envelope, verdict: validateEnvelope(envelope), timestamp: now() });
567
575
  if (!type) return [];
568
576
  const responder = RESPONDERS[type];
569
577
  return responder ? responder(envelope) : [];
570
578
  }
571
579
  return {
572
- capabilities,
573
- services,
574
580
  get records() {
575
581
  return records;
576
582
  },
@@ -580,11 +586,59 @@ var NappletConformanceHostBundle = (() => {
580
586
  }
581
587
  };
582
588
  }
589
+ var INTERNAL_BOOT_ERROR = "__nappletConformance.error";
583
590
  function sleep(ms) {
584
591
  return new Promise((resolve) => setTimeout(resolve, ms));
585
592
  }
586
- async function bootOnce(caps, opts, doc, win) {
587
- const shell = createReferenceShell(caps ? { capabilities: caps } : void 0);
593
+ function escapeHtmlAttr(value) {
594
+ return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
595
+ }
596
+ function runtimePrelude(domains) {
597
+ return `
598
+ (() => {
599
+ const domains = ${JSON.stringify(domains)};
600
+ const napplet = {};
601
+ for (const domain of domains) napplet[domain] = {};
602
+ window.napplet = napplet;
603
+ const report = (reason) => {
604
+ try {
605
+ window.parent.postMessage({ type: '${INTERNAL_BOOT_ERROR}', reason: String(reason && reason.message || reason) }, '*');
606
+ } catch {}
607
+ };
608
+ window.addEventListener('error', (event) => report(event.error || event.message));
609
+ window.addEventListener('unhandledrejection', (event) => report(event.reason));
610
+ })();
611
+ `;
612
+ }
613
+ function injectRuntime(html, url, domains) {
614
+ const base = `<base href="${escapeHtmlAttr(url)}">`;
615
+ const script = `<script>${runtimePrelude(domains)}<\/script>`;
616
+ const headOpen = /<head(?:\s[^>]*)?>/i.exec(html);
617
+ if (headOpen) {
618
+ const at = headOpen.index + headOpen[0].length;
619
+ return `${html.slice(0, at)}${base}${script}${html.slice(at)}`;
620
+ }
621
+ return `${base}${script}${html}`;
622
+ }
623
+ function decodeDataUrl(url) {
624
+ if (!url.startsWith("data:")) return null;
625
+ const comma = url.indexOf(",");
626
+ if (comma < 0) return "";
627
+ const meta = url.slice(0, comma);
628
+ const body = url.slice(comma + 1);
629
+ if (meta.endsWith(";base64")) return atob(body);
630
+ return decodeURIComponent(body);
631
+ }
632
+ async function loadHtml(url) {
633
+ if (url === "about:blank") return "<!doctype html><html><head></head><body></body></html>";
634
+ const data = decodeDataUrl(url);
635
+ if (data !== null) return data;
636
+ const response = await fetch(url);
637
+ if (!response.ok) throw new Error(`Failed to fetch napplet HTML: ${response.status} ${response.statusText}`);
638
+ return response.text();
639
+ }
640
+ async function bootOnce(domains, opts, doc, win) {
641
+ const shell = createReferenceShell();
588
642
  const iframe = doc.createElement("iframe");
589
643
  iframe.setAttribute("sandbox", opts.allowSameOrigin ? "allow-scripts allow-same-origin" : "allow-scripts");
590
644
  iframe.setAttribute("aria-hidden", "true");
@@ -592,37 +646,48 @@ var NappletConformanceHostBundle = (() => {
592
646
  iframe.style.width = "1px";
593
647
  iframe.style.height = "1px";
594
648
  iframe.style.left = "-9999px";
595
- let resolveReady;
596
- const readyPromise = new Promise((resolve) => {
597
- resolveReady = resolve;
598
- });
649
+ let bootError = null;
599
650
  const listener = (event) => {
600
651
  if (event.source !== iframe.contentWindow) return;
652
+ const data = event.data;
653
+ if (data && typeof data === "object" && data.type === INTERNAL_BOOT_ERROR) {
654
+ bootError = `napplet boot error: ${String(data.reason ?? "unknown error")}`;
655
+ return;
656
+ }
601
657
  const responses = shell.handle(event.data);
602
658
  const source = event.source;
603
659
  for (const response of responses) source?.postMessage(response, "*");
604
- const data = event.data;
605
- if (data && typeof data === "object" && data.type === "shell.ready") resolveReady();
606
660
  };
661
+ let loaded = false;
662
+ const loadPromise = new Promise((resolve) => {
663
+ iframe.addEventListener("load", () => {
664
+ loaded = true;
665
+ resolve();
666
+ }, { once: true });
667
+ });
607
668
  win.addEventListener("message", listener);
608
669
  (doc.body ?? doc.documentElement).appendChild(iframe);
609
- iframe.setAttribute("src", opts.url);
610
- let installedGlobal = false;
611
- let bootError = null;
612
- const timedOut = /* @__PURE__ */ Symbol("timeout");
613
- const race = await Promise.race([readyPromise.then(() => "ready"), sleep(opts.readyTimeoutMs).then(() => timedOut)]);
614
- if (race === timedOut) {
615
- bootError = `napplet did not boot (no shell.ready within ${opts.readyTimeoutMs}ms; a conformant napplet must run under sandbox="allow-scripts" without allow-same-origin)`;
616
- } else {
617
- installedGlobal = true;
618
- await sleep(opts.settleMs);
670
+ try {
671
+ const html = await loadHtml(opts.url);
672
+ iframe.srcdoc = injectRuntime(html, opts.url, domains);
673
+ } catch (error) {
674
+ bootError = error instanceof Error ? error.message : String(error);
675
+ }
676
+ if (!bootError) {
677
+ const timedOut = /* @__PURE__ */ Symbol("timeout");
678
+ const race = await Promise.race([loadPromise.then(() => "loaded"), sleep(opts.readyTimeoutMs).then(() => timedOut)]);
679
+ if (race === timedOut || !loaded) {
680
+ bootError = `napplet did not boot (iframe did not load within ${opts.readyTimeoutMs}ms; a conformant napplet must run under sandbox="allow-scripts" without allow-same-origin)`;
681
+ } else {
682
+ await sleep(opts.settleMs);
683
+ }
619
684
  }
620
685
  win.removeEventListener("message", listener);
621
686
  try {
622
687
  iframe.remove();
623
688
  } catch {
624
689
  }
625
- return { installedGlobal, bootError, emitted: [...shell.records] };
690
+ return { installedGlobal: bootError === null, bootError, emitted: [...shell.records] };
626
691
  }
627
692
  async function bootAndCollect(options) {
628
693
  const doc = options.document ?? document;
@@ -633,10 +698,10 @@ var NappletConformanceHostBundle = (() => {
633
698
  settleMs: options.settleMs ?? 600,
634
699
  allowSameOrigin: options.allowSameOrigin ?? false
635
700
  };
636
- const primary = await bootOnce(void 0, resolved, doc, win);
701
+ const primary = await bootOnce(NAP_DOMAINS, resolved, doc, win);
637
702
  let degraded = null;
638
703
  if (options.runDegraded ?? true) {
639
- const d = await bootOnce({ domains: [], protocols: {} }, resolved, doc, win);
704
+ const d = await bootOnce([], resolved, doc, win);
640
705
  degraded = { bootError: d.bootError, emitted: d.emitted };
641
706
  }
642
707
  return {
@@ -690,18 +755,14 @@ var NappletConformanceHostBundle = (() => {
690
755
  id: "boot/installs-global",
691
756
  area: "boot",
692
757
  severity: "error",
693
- // NAP-SHELL: the observable boot signal across the opaque sandbox is the
694
- // shim posting `shell.ready`. Receiving it proves window.napplet installed.
695
- title: "Completes the NAP-SHELL handshake (posts shell.ready)",
696
- run: (ctx) => ctx.installedGlobal ? result.pass() : result.fail("window.napplet was not present after load (no NAP-SHELL shell.ready)")
758
+ title: "Runs after runtime-injected window.napplet is installed",
759
+ run: (ctx) => ctx.installedGlobal ? result.pass() : result.fail("runtime did not inject window.napplet before load")
697
760
  },
698
761
  {
699
762
  id: "boot/no-boot-error",
700
763
  area: "boot",
701
764
  severity: "error",
702
- // Boot is observed via the NAP-SHELL `shell.ready` signal; its absence within
703
- // the timeout (or a crash before it) surfaces here as a boot error.
704
- title: "Boots without an uncaught error (observed via NAP-SHELL shell.ready)",
765
+ title: "Boots without an uncaught error",
705
766
  run: (ctx) => ctx.bootError ? result.fail(ctx.bootError) : result.pass()
706
767
  },
707
768
  {
@@ -743,16 +804,13 @@ var NappletConformanceHostBundle = (() => {
743
804
  },
744
805
  // ── degradation ───────────────────────────────────────────────────────────────
745
806
  {
746
- id: "degrade/supports-false",
807
+ id: "degrade/domain-absence",
747
808
  area: "degradation",
748
809
  severity: "error",
749
- // NAP-SHELL: when the runtime advertises an empty environment (no domains/
750
- // protocols), every shell.supports() returns false; a conformant napplet must
751
- // degrade rather than crash.
752
- title: "Does not crash when the NAP-SHELL environment offers no capabilities (supports() false)",
810
+ title: "Does not crash when no NAP domains are injected",
753
811
  run: (ctx) => {
754
812
  if (!ctx.degraded) return result.skip("Host did not run the degraded (no-capabilities) pass");
755
- return ctx.degraded.bootError ? result.fail(`Crashed under a no-capability shell: ${ctx.degraded.bootError}`) : result.pass();
813
+ return ctx.degraded.bootError ? result.fail(`Crashed with no injected domains: ${ctx.degraded.bootError}`) : result.pass();
756
814
  }
757
815
  },
758
816
  // ── lifecycle ─────────────────────────────────────────────────────────────────
@@ -0,0 +1,46 @@
1
+ (function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const i of document.querySelectorAll('link[rel="modulepreload"]'))d(i);new MutationObserver(i=>{for(const o of i)if(o.type==="childList")for(const a of o.addedNodes)a.tagName==="LINK"&&a.rel==="modulepreload"&&d(a)}).observe(document,{childList:!0,subtree:!0});function s(i){const o={};return i.integrity&&(o.integrity=i.integrity),i.referrerPolicy&&(o.referrerPolicy=i.referrerPolicy),i.crossOrigin==="use-credentials"?o.credentials="include":i.crossOrigin==="anonymous"?o.credentials="omit":o.credentials="same-origin",o}function d(i){if(i.ep)return;i.ep=!0;const o=s(i);fetch(i.href,o)}})();var P=["relay","identity","storage","inc","theme","keys","media","notify","config","resource","cvm","outbox","upload","intent","ble","webrtc","link","lists","serial","common","dm"],r={id:"string"},F={"relay.subscribe":{dir:"out",fields:{...r,subId:"string",filters:"array"}},"relay.close":{dir:"out",fields:{...r,subId:"string"}},"relay.publish":{dir:"out",fields:{...r,event:"object"}},"relay.publishEncrypted":{dir:"out",fields:{...r,event:"object",recipient:"string"}},"relay.query":{dir:"out",fields:{...r,filters:"array"}},"relay.event":{dir:"in"},"relay.eose":{dir:"in"},"relay.closed":{dir:"in"},"relay.publish.result":{dir:"in"},"relay.publishEncrypted.result":{dir:"in"},"relay.query.result":{dir:"in"},"identity.getPublicKey":{dir:"out",fields:{...r}},"identity.getRelays":{dir:"out",fields:{...r}},"identity.getProfile":{dir:"out",fields:{...r}},"identity.getFollows":{dir:"out",fields:{...r}},"identity.getList":{dir:"out",fields:{...r,listType:"string"}},"identity.getZaps":{dir:"out",fields:{...r}},"identity.getMutes":{dir:"out",fields:{...r}},"identity.getBlocked":{dir:"out",fields:{...r}},"identity.getBadges":{dir:"out",fields:{...r}},"identity.changed":{dir:"in"},"identity.getPublicKey.result":{dir:"in"},"identity.getRelays.result":{dir:"in"},"identity.getProfile.result":{dir:"in"},"identity.getFollows.result":{dir:"in"},"identity.getList.result":{dir:"in"},"identity.getZaps.result":{dir:"in"},"identity.getMutes.result":{dir:"in"},"identity.getBlocked.result":{dir:"in"},"identity.getBadges.result":{dir:"in"},"storage.get":{dir:"out",fields:{...r,key:"string"}},"storage.set":{dir:"out",fields:{...r,key:"string",value:"string"}},"storage.remove":{dir:"out",fields:{...r,key:"string"}},"storage.keys":{dir:"out",fields:{...r}},"storage.get.result":{dir:"in"},"storage.set.result":{dir:"in"},"storage.remove.result":{dir:"in"},"storage.keys.result":{dir:"in"},"inc.emit":{dir:"out",fields:{topic:"string"}},"inc.subscribe":{dir:"out",fields:{...r,topic:"string"}},"inc.unsubscribe":{dir:"out",fields:{topic:"string"}},"inc.channel.open":{dir:"out",fields:{...r,target:"string"}},"inc.channel.emit":{dir:"out",fields:{channelId:"string"}},"inc.channel.broadcast":{dir:"out"},"inc.channel.list":{dir:"out",fields:{...r}},"inc.channel.close":{dir:"out",fields:{channelId:"string"}},"inc.subscribe.result":{dir:"in"},"inc.event":{dir:"in"},"inc.channel.open.result":{dir:"in"},"inc.channel.event":{dir:"in"},"inc.channel.list.result":{dir:"in"},"inc.channel.closed":{dir:"in"},"theme.get":{dir:"out",fields:{...r}},"theme.get.result":{dir:"in"},"theme.changed":{dir:"in"},"keys.forward":{dir:"out",fields:{key:"string",code:"string",ctrl:"boolean",alt:"boolean",shift:"boolean",meta:"boolean"}},"keys.registerAction":{dir:"out",fields:{...r,action:"object"}},"keys.unregisterAction":{dir:"out",fields:{actionId:"string"}},"keys.registerAction.result":{dir:"in"},"keys.bindings":{dir:"in"},"keys.action":{dir:"in"},"media.session.create":{dir:"out",fields:{...r,owner:"object"}},"media.session.update":{dir:"out",fields:{sessionId:"string",metadata:"object"}},"media.session.destroy":{dir:"out",fields:{sessionId:"string"}},"media.state":{dir:"out",fields:{sessionId:"string",status:"string"}},"media.capabilities":{dir:"out",fields:{sessionId:"string",actions:"array"}},"media.session.create.result":{dir:"in"},"media.command":{dir:"in"},"media.controls":{dir:"in"},"notify.send":{dir:"out",fields:{...r,title:"string"}},"notify.dismiss":{dir:"out",fields:{notificationId:"string"}},"notify.badge":{dir:"out",fields:{count:"number"}},"notify.channel.register":{dir:"out",fields:{channelId:"string",label:"string"}},"notify.permission.request":{dir:"out",fields:{...r}},"notify.send.result":{dir:"in"},"notify.permission.result":{dir:"in"},"notify.action":{dir:"in"},"notify.clicked":{dir:"in"},"notify.dismissed":{dir:"in"},"notify.controls":{dir:"in"},"config.registerSchema":{dir:"out",fields:{...r,schema:"object"}},"config.get":{dir:"out",fields:{...r}},"config.subscribe":{dir:"out"},"config.unsubscribe":{dir:"out"},"config.openSettings":{dir:"out"},"config.registerSchema.result":{dir:"in"},"config.values":{dir:"in"},"config.schemaError":{dir:"in"},"resource.info":{dir:"out",fields:{...r}},"resource.bytes":{dir:"out",fields:{...r,url:"string"}},"resource.bytesMany":{dir:"out",fields:{...r,urls:"array"}},"resource.cancel":{dir:"out",fields:{...r}},"resource.info.result":{dir:"in"},"resource.info.error":{dir:"in"},"resource.bytes.result":{dir:"in"},"resource.bytes.error":{dir:"in"},"resource.bytesMany.result":{dir:"in"},"resource.bytesMany.error":{dir:"in"},"cvm.discover":{dir:"out",fields:{...r}},"cvm.request":{dir:"out",fields:{...r,server:"object",message:"object"}},"cvm.close":{dir:"out",fields:{...r,server:"object"}},"cvm.discover.result":{dir:"in"},"cvm.request.result":{dir:"in"},"cvm.close.result":{dir:"in"},"cvm.event":{dir:"in"},"outbox.getEvent":{dir:"out",fields:{...r,eventId:"string"}},"outbox.query":{dir:"out",fields:{...r,filters:"present"}},"outbox.subscribe":{dir:"out",fields:{...r,subId:"string",filters:"present"}},"outbox.close":{dir:"out",fields:{...r,subId:"string"}},"outbox.publish":{dir:"out",fields:{...r,event:"object"}},"outbox.resolveRelays":{dir:"out",fields:{...r,target:"present"}},"outbox.getEvent.result":{dir:"in"},"outbox.query.result":{dir:"in"},"outbox.event":{dir:"in"},"outbox.eose":{dir:"in"},"outbox.closed":{dir:"in"},"outbox.publish.result":{dir:"in"},"outbox.resolveRelays.result":{dir:"in"},"upload.info":{dir:"out",fields:{...r}},"upload.upload":{dir:"out",fields:{...r,request:"object"}},"upload.status":{dir:"out",fields:{...r,uploadId:"string"}},"upload.info.result":{dir:"in"},"upload.upload.result":{dir:"in"},"upload.status.result":{dir:"in"},"upload.status.changed":{dir:"in"},"intent.invoke":{dir:"out",fields:{...r,request:"object"}},"intent.available":{dir:"out",fields:{...r,archetype:"string"}},"intent.handlers":{dir:"out",fields:{...r}},"intent.invoke.result":{dir:"in"},"intent.available.result":{dir:"in"},"intent.handlers.result":{dir:"in"},"intent.changed":{dir:"in"},"ble.open":{dir:"out",fields:{...r,request:"object"}},"ble.services":{dir:"out",fields:{...r,sessionId:"string"}},"ble.read":{dir:"out",fields:{...r,sessionId:"string",target:"object"}},"ble.write":{dir:"out",fields:{...r,sessionId:"string",target:"object",data:"array"}},"ble.subscribe":{dir:"out",fields:{...r,sessionId:"string",target:"object"}},"ble.unsubscribe":{dir:"out",fields:{...r,sessionId:"string",target:"object"}},"ble.close":{dir:"out",fields:{...r,sessionId:"string"}},"ble.open.result":{dir:"in"},"ble.services.result":{dir:"in"},"ble.read.result":{dir:"in"},"ble.write.result":{dir:"in"},"ble.subscribe.result":{dir:"in"},"ble.unsubscribe.result":{dir:"in"},"ble.close.result":{dir:"in"},"ble.event":{dir:"in"},"common.encodeNip19":{dir:"out",fields:{...r,input:"object"}},"common.decodeNip19":{dir:"out",fields:{...r,value:"string"}},"common.getProfile":{dir:"out",fields:{...r,target:"string"}},"common.follows":{dir:"out",fields:{...r}},"common.follow":{dir:"out",fields:{...r,pubkeys:"array"}},"common.unfollow":{dir:"out",fields:{...r,pubkeys:"array"}},"common.react":{dir:"out",fields:{...r,targetEventId:"string",reaction:"string"}},"common.report":{dir:"out",fields:{...r,target:"object",reason:"string",text:"string"}},"common.encodeNip19.result":{dir:"in"},"common.decodeNip19.result":{dir:"in"},"common.getProfile.result":{dir:"in"},"common.follows.result":{dir:"in"},"common.follow.result":{dir:"in"},"common.unfollow.result":{dir:"in"},"common.react.result":{dir:"in"},"common.report.result":{dir:"in"},"webrtc.open":{dir:"out",fields:{...r,request:"object"}},"webrtc.send":{dir:"out",fields:{...r,sessionId:"string",payload:"present"}},"webrtc.close":{dir:"out",fields:{...r,sessionId:"string"}},"webrtc.open.result":{dir:"in"},"webrtc.send.result":{dir:"in"},"webrtc.close.result":{dir:"in"},"webrtc.event":{dir:"in"},"link.open":{dir:"out",fields:{...r,url:"string"}},"link.open.result":{dir:"in"},"lists.supported":{dir:"out",fields:{...r}},"lists.add":{dir:"out",fields:{...r,list:"object",items:"array"}},"lists.remove":{dir:"out",fields:{...r,list:"object",items:"array"}},"lists.supported.result":{dir:"in"},"lists.add.result":{dir:"in"},"lists.remove.result":{dir:"in"},"serial.open":{dir:"out",fields:{...r,request:"object"}},"serial.write":{dir:"out",fields:{...r,sessionId:"string",data:"array"}},"serial.close":{dir:"out",fields:{...r,sessionId:"string"}},"serial.open.result":{dir:"in"},"serial.write.result":{dir:"in"},"serial.close.result":{dir:"in"},"serial.event":{dir:"in"},"dm.status":{dir:"out",fields:{...r}},"dm.conversations":{dir:"out",fields:{...r}},"dm.messages":{dir:"out",fields:{...r,conversationId:"string"}},"dm.send":{dir:"out",fields:{...r,recipients:"array",content:"string"}},"dm.subscribe":{dir:"out",fields:{...r}},"dm.unsubscribe":{dir:"out",fields:{...r,subscriptionId:"string"}},"dm.status.result":{dir:"in"},"dm.conversations.result":{dir:"in"},"dm.messages.result":{dir:"in"},"dm.send.result":{dir:"in"},"dm.subscribe.result":{dir:"in"},"dm.unsubscribe.result":{dir:"in"},"dm.message":{dir:"in"}};function L(e){if(e===void 0)return"undefined";if(e===null)return"null";if(Array.isArray(e))return"array";const t=typeof e;return t==="string"||t==="number"||t==="boolean"?t:t==="object"?"object":"present"}function G(e,t){return t==="present"?e!=null:L(e)===t}function K(e){const t=[];if(typeof e!="object"||e===null||Array.isArray(e))return{ok:!1,errors:[{code:"not-an-object",message:"Envelope must be a non-null object"}]};const s=e,d=s.type;if(typeof d!="string")return{ok:!1,errors:[{code:"missing-type",message:"Envelope is missing a string `type` field"}]};const i=d.indexOf(".");if(i<=0)return{ok:!1,type:d,errors:[{code:"malformed-type",message:`Envelope type "${d}" is not in domain.action form`}]};const o=d.slice(0,i);if(!P.includes(o))return{ok:!1,type:d,domain:o,errors:[{code:"unknown-domain",message:`"${o}" is not a known NAP domain`}]};const m=F[d];if(!m)return{ok:!1,type:d,domain:o,errors:[{code:"unknown-type",message:`"${d}" is not a known ${o} message type`}]};if(m.dir==="in")return{ok:!1,type:d,domain:o,direction:"in",errors:[{code:"inbound-type-emitted",message:`"${d}" is a shell→napplet message; a napplet must not emit it`}]};for(const[l,y]of Object.entries(m.fields??{})){if(!(l in s)||s[l]===void 0){t.push({code:"missing-field",message:`Required field "${l}" is missing`,field:l});continue}G(s[l],y)||t.push({code:"wrong-type",message:`Field "${l}" should be ${y} but is ${L(s[l])}`,field:l})}return{ok:t.length===0,type:d,domain:o,direction:"out",errors:t}}var x=/^[a-z0-9][a-z0-9._:-]*$/;function U(e){return e.replace(/&(#x?[0-9a-fA-F]+|[a-zA-Z]+);/g,(t,s)=>{if(s[0]==="#"){const i=s[1]==="x"||s[1]==="X"?parseInt(s.slice(2),16):parseInt(s.slice(1),10);return Number.isFinite(i)?String.fromCodePoint(i):t}return{amp:"&",lt:"<",gt:">",quot:'"',apos:"'"}[s]??t})}function S(e,t){const s=/<meta\b[^>]*>/gi;let d;for(;(d=s.exec(e))!==null;){const i=d[0],o=/\bname\s*=\s*(?:"([^"]*)"|'([^']*)')/i.exec(i);if((o?.[1]??o?.[2]??"").trim()!==t)continue;const m=/\bcontent\s*=\s*(?:"([^"]*)"|'([^']*)')/i.exec(i);return U((m?.[1]??m?.[2]??"").trim())}}function q(e){if(Array.isArray(e))return e.some(q);if(e&&typeof e=="object"){const t=e;return"pattern"in t?!0:Object.values(t).some(q)}return!1}function $(e,t={}){const s=[],d=[],i=S(e,"napplet-type");i?x.test(i)||s.push({code:"invalid-napplet-type",message:`napplet-type "${i}" must match ${x}`}):s.push({code:"missing-napplet-type",message:'No <meta name="napplet-type"> declared'});const o=S(e,"napplet-requires"),a=o?o.split(",").map(l=>l.trim()).filter(Boolean):[];for(const l of a){const y=l.startsWith("nap:")?l.slice(4):l;P.includes(y)||s.push({code:"unknown-required-nap",message:`napplet-requires lists "${l}" which is not a known NAP domain`})}const m=S(e,"napplet-config-schema");if(m){let l;try{l=JSON.parse(m)}catch{l=void 0,s.push({code:"invalid-config-schema",message:"napplet-config-schema is not valid JSON"})}l!==void 0&&(typeof l!="object"||l===null||Array.isArray(l)?s.push({code:"invalid-config-schema",message:"napplet-config-schema must be a JSON Schema object"}):q(l)&&s.push({code:"invalid-config-schema",message:"napplet-config-schema uses the `pattern` keyword, which the draft-07 core subset excludes (CVE-2025-69873)"}))}return{ok:s.length===0,nappletType:i,requires:a,errors:s,warnings:d}}var w="f".repeat(64),J="https://reference.invalid/blob",n=e=>[e],c=()=>[],W={"relay.subscribe":e=>n({type:"relay.eose",subId:e.subId}),"relay.close":c,"relay.publish":e=>n({type:"relay.publish.result",id:e.id,ok:!0,event:e.event}),"relay.publishEncrypted":e=>n({type:"relay.publishEncrypted.result",id:e.id,ok:!0,event:e.event}),"relay.query":e=>n({type:"relay.query.result",id:e.id,events:[]}),"identity.getPublicKey":e=>n({type:"identity.getPublicKey.result",id:e.id,pubkey:w}),"identity.getRelays":e=>n({type:"identity.getRelays.result",id:e.id,relays:{}}),"identity.getProfile":e=>n({type:"identity.getProfile.result",id:e.id,profile:null}),"identity.getFollows":e=>n({type:"identity.getFollows.result",id:e.id,pubkeys:[]}),"identity.getList":e=>n({type:"identity.getList.result",id:e.id,entries:[]}),"identity.getZaps":e=>n({type:"identity.getZaps.result",id:e.id,zaps:[]}),"identity.getMutes":e=>n({type:"identity.getMutes.result",id:e.id,pubkeys:[]}),"identity.getBlocked":e=>n({type:"identity.getBlocked.result",id:e.id,pubkeys:[]}),"identity.getBadges":e=>n({type:"identity.getBadges.result",id:e.id,badges:[]}),"storage.get":e=>n({type:"storage.get.result",id:e.id,value:null}),"storage.set":e=>n({type:"storage.set.result",id:e.id}),"storage.remove":e=>n({type:"storage.remove.result",id:e.id}),"storage.keys":e=>n({type:"storage.keys.result",id:e.id,keys:[]}),"inc.emit":c,"inc.subscribe":e=>n({type:"inc.subscribe.result",id:e.id}),"inc.unsubscribe":c,"inc.channel.open":e=>n({type:"inc.channel.open.result",id:e.id,channelId:`chan-${String(e.id)}`,peer:"reference-peer"}),"inc.channel.emit":c,"inc.channel.broadcast":c,"inc.channel.list":e=>n({type:"inc.channel.list.result",id:e.id,channels:[]}),"inc.channel.close":c,"theme.get":e=>n({type:"theme.get.result",id:e.id,theme:{colors:{},mode:"dark"}}),"keys.forward":c,"keys.registerAction":e=>n({type:"keys.registerAction.result",id:e.id,actionId:`action-${String(e.id)}`}),"keys.unregisterAction":c,"media.session.create":e=>n({type:"media.session.create.result",id:e.id,sessionId:`session-${String(e.id)}`,owner:e.owner}),"media.session.update":c,"media.session.destroy":c,"media.state":c,"media.capabilities":c,"notify.send":e=>n({type:"notify.send.result",id:e.id,notificationId:`notif-${String(e.id)}`}),"notify.dismiss":c,"notify.badge":c,"notify.channel.register":c,"notify.permission.request":e=>n({type:"notify.permission.result",id:e.id,granted:!0}),"config.registerSchema":e=>n({type:"config.registerSchema.result",id:e.id,ok:!0}),"config.get":e=>n({type:"config.values",id:e.id,values:{}}),"config.subscribe":()=>n({type:"config.values",values:{}}),"config.unsubscribe":c,"config.openSettings":c,"resource.bytes":e=>n({type:"resource.bytes.result",id:e.id,blob:new Blob([]),mime:"application/octet-stream"}),"resource.bytesMany":e=>n({type:"resource.bytesMany.result",id:e.id,items:Array.isArray(e.urls)?e.urls.map(t=>({url:t,ok:!0,blob:new Blob([]),mime:"application/octet-stream"})):[]}),"resource.info":e=>n({type:"resource.info.result",id:e.id,info:{schemes:[{scheme:"data",enabled:!0},{scheme:"https",enabled:!0}]}}),"resource.cancel":c,"cvm.discover":e=>n({type:"cvm.discover.result",id:e.id,servers:[]}),"cvm.request":e=>n({type:"cvm.request.result",id:e.id,message:{}}),"cvm.close":e=>n({type:"cvm.close.result",id:e.id}),"outbox.getEvent":e=>n({type:"outbox.getEvent.result",id:e.id,relays:[]}),"outbox.query":e=>n({type:"outbox.query.result",id:e.id,events:[],relays:{}}),"outbox.subscribe":e=>n({type:"outbox.eose",subId:e.subId}),"outbox.close":c,"outbox.publish":e=>n({type:"outbox.publish.result",id:e.id,ok:!0}),"outbox.resolveRelays":e=>n({type:"outbox.resolveRelays.result",id:e.id,plan:{}}),"upload.info":e=>n({type:"upload.info.result",id:e.id,info:{rails:[{rail:"nip96",enabled:!0,returns:["https"]},{rail:"blossom",enabled:!0,returns:["https","blossom"]}]}}),"upload.upload":e=>n({type:"upload.upload.result",id:e.id,result:{url:J}}),"upload.status":e=>n({type:"upload.status.result",id:e.id,status:{}}),"intent.invoke":e=>n({type:"intent.invoke.result",id:e.id,result:{ok:!0,archetype:"reference",action:"open",handled:!0}}),"intent.available":e=>n({type:"intent.available.result",id:e.id,availability:{archetype:e.archetype,available:!0,candidates:[{dTag:"reference-handler",actions:["open"],protocols:["NAP-4"],contracts:[{action:"open",protocol:"NAP-4",eventKinds:[1]}],isDefault:!0}],hasDefault:!0}}),"intent.handlers":e=>n({type:"intent.handlers.result",id:e.id,handlers:[]}),"ble.open":e=>n({type:"ble.open.result",id:e.id,session:{id:"ble-reference",state:"open",device:{id:"reference-device",name:"Reference BLE"}}}),"ble.services":e=>n({type:"ble.services.result",id:e.id,services:[]}),"ble.read":e=>n({type:"ble.read.result",id:e.id,data:[]}),"ble.write":e=>n({type:"ble.write.result",id:e.id}),"ble.subscribe":e=>n({type:"ble.subscribe.result",id:e.id}),"ble.unsubscribe":e=>n({type:"ble.unsubscribe.result",id:e.id}),"ble.close":e=>n({type:"ble.close.result",id:e.id}),"common.encodeNip19":e=>n({type:"common.encodeNip19.result",id:e.id,ok:!0,value:"npub1reference",nip19Type:"npub"}),"common.decodeNip19":e=>n({type:"common.decodeNip19.result",id:e.id,ok:!0,nip19Type:"npub",hex:w}),"common.getProfile":e=>n({type:"common.getProfile.result",id:e.id,ok:!0,pubkey:w,profile:null}),"common.follows":e=>n({type:"common.follows.result",id:e.id,ok:!0,pubkeys:[]}),"common.follow":e=>n({type:"common.follow.result",id:e.id,ok:!0}),"common.unfollow":e=>n({type:"common.unfollow.result",id:e.id,ok:!0}),"common.react":e=>n({type:"common.react.result",id:e.id,ok:!0,eventId:"0".repeat(64)}),"common.report":e=>n({type:"common.report.result",id:e.id,ok:!0,eventId:"1".repeat(64)}),"webrtc.open":e=>n({type:"webrtc.open.result",id:e.id,session:{id:"webrtc-reference",scope:{type:"direct",pubkey:w},channel:"default",state:"connecting"}}),"webrtc.send":e=>n({type:"webrtc.send.result",id:e.id}),"webrtc.close":e=>n({type:"webrtc.close.result",id:e.id}),"link.open":e=>n({type:"link.open.result",id:e.id,status:"opened"}),"lists.supported":e=>n({type:"lists.supported.result",id:e.id,lists:[]}),"lists.add":e=>n({type:"lists.add.result",id:e.id,ok:!0,added:0,skipped:0}),"lists.remove":e=>n({type:"lists.remove.result",id:e.id,ok:!0,removed:0,skipped:0}),"serial.open":e=>n({type:"serial.open.result",id:e.id,session:{id:`serial-${String(e.id)}`,state:"open"}}),"serial.write":e=>n({type:"serial.write.result",id:e.id}),"serial.close":e=>n({type:"serial.close.result",id:e.id})};function Z(e={}){const t=e.now??(()=>Date.now()),s=[];function d(i){const o=i&&typeof i=="object"&&typeof i.type=="string"?i.type:void 0;if(s.push({envelope:i,verdict:K(i),timestamp:t()}),!o)return[];const a=W[o];return a?a(i):[]}return{get records(){return s},handle:d,reset(){s.length=0}}}function z(e){return{manifestHtml:e.manifestHtml,sandbox:e.sandbox??{allowScripts:!0,allowSameOrigin:!1},installedGlobal:e.boot.installedGlobal,bootError:e.boot.bootError,emitted:e.boot.emitted,forbiddenGlobals:e.forbiddenGlobals??[],degraded:e.boot.degraded,lifecycle:e.lifecycle??null}}var M="__nappletConformance.error";function R(e){return new Promise(t=>setTimeout(t,e))}function V(e){return e.replace(/&/g,"&amp;").replace(/"/g,"&quot;").replace(/</g,"&lt;").replace(/>/g,"&gt;")}function Y(e){return`
2
+ (() => {
3
+ const domains = ${JSON.stringify(e)};
4
+ const napplet = {};
5
+ for (const domain of domains) napplet[domain] = {};
6
+ window.napplet = napplet;
7
+ const report = (reason) => {
8
+ try {
9
+ window.parent.postMessage({ type: '${M}', reason: String(reason && reason.message || reason) }, '*');
10
+ } catch {}
11
+ };
12
+ window.addEventListener('error', (event) => report(event.error || event.message));
13
+ window.addEventListener('unhandledrejection', (event) => report(event.reason));
14
+ })();
15
+ `}function X(e,t,s){const d=`<base href="${V(t)}">`,i=`<script>${Y(s)}<\/script>`,o=/<head(?:\s[^>]*)?>/i.exec(e);if(o){const a=o.index+o[0].length;return`${e.slice(0,a)}${d}${i}${e.slice(a)}`}return`${d}${i}${e}`}function Q(e){if(!e.startsWith("data:"))return null;const t=e.indexOf(",");if(t<0)return"";const s=e.slice(0,t),d=e.slice(t+1);return s.endsWith(";base64")?atob(d):decodeURIComponent(d)}async function ee(e){if(e==="about:blank")return"<!doctype html><html><head></head><body></body></html>";const t=Q(e);if(t!==null)return t;const s=await fetch(e);if(!s.ok)throw new Error(`Failed to fetch napplet HTML: ${s.status} ${s.statusText}`);return s.text()}async function O(e,t,s,d){const i=Z(),o=s.createElement("iframe");o.setAttribute("sandbox",t.allowSameOrigin?"allow-scripts allow-same-origin":"allow-scripts"),o.setAttribute("aria-hidden","true"),o.style.position="absolute",o.style.width="1px",o.style.height="1px",o.style.left="-9999px";let a=null;const m=p=>{if(p.source!==o.contentWindow)return;const h=p.data;if(h&&typeof h=="object"&&h.type===M){a=`napplet boot error: ${String(h.reason??"unknown error")}`;return}const D=i.handle(p.data),H=p.source;for(const _ of D)H?.postMessage(_,"*")};let l=!1;const y=new Promise(p=>{o.addEventListener("load",()=>{l=!0,p()},{once:!0})});d.addEventListener("message",m),(s.body??s.documentElement).appendChild(o);try{const p=await ee(t.url);o.srcdoc=X(p,t.url,e)}catch(p){a=p instanceof Error?p.message:String(p)}if(!a){const p=Symbol("timeout");await Promise.race([y.then(()=>"loaded"),R(t.readyTimeoutMs).then(()=>p)])===p||!l?a=`napplet did not boot (iframe did not load within ${t.readyTimeoutMs}ms; a conformant napplet must run under sandbox="allow-scripts" without allow-same-origin)`:await R(t.settleMs)}d.removeEventListener("message",m);try{o.remove()}catch{}return{installedGlobal:a===null,bootError:a,emitted:[...i.records]}}async function te(e){const t=e.document??document,s=e.window??window,d={url:e.url,readyTimeoutMs:e.readyTimeoutMs??5e3,settleMs:e.settleMs,allowSameOrigin:e.allowSameOrigin??!1},i=await O(P,d,t,s);let o=null;{const a=await O([],d,t,s);o={bootError:a.bootError,emitted:a.emitted}}return{installedGlobal:i.installedGlobal,bootError:i.bootError,emitted:i.emitted,degraded:o}}var ie=e=>({status:"pass",detail:e}),re=(e,t)=>({status:"fail",detail:e,diagnostics:t}),se=e=>({status:"skip",detail:e}),u={pass:ie,fail:re,skip:se};function ne(e,t){return $(e).errors.filter(s=>t.includes(s.code))}function B(e,t){return new RegExp(`name\\s*=\\s*["']${t}["']`,"i").test(e)}function I(e,t,s,d){return{id:e,area:"manifest",severity:"error",title:t,run:i=>{if(d&&!B(i.manifestHtml,d))return u.skip(`No ${d} declared`);const o=ne(i.manifestHtml,s);return o.length?u.fail(o[0].message,o):u.pass()}}}var oe=[I("manifest/napplet-type","Declares a valid napplet-type",["missing-napplet-type","invalid-napplet-type"]),I("manifest/declared-naps","napplet-requires lists only real NAP domains",["unknown-required-nap"],"napplet-requires"),I("manifest/config-schema","Config schema is a draft-07 core subset",["invalid-config-schema"],"napplet-config-schema"),{id:"boot/sandbox-allow-scripts",area:"boot",severity:"error",title:"Runs under allow-scripts without allow-same-origin",run:e=>e.sandbox.allowScripts?e.sandbox.allowSameOrigin?u.fail("Napplet relied on allow-same-origin, which a shell must not grant"):u.pass():u.fail("Napplet was not loaded with allow-scripts")},{id:"boot/installs-global",area:"boot",severity:"error",title:"Runs after runtime-injected window.napplet is installed",run:e=>e.installedGlobal?u.pass():u.fail("runtime did not inject window.napplet before load")},{id:"boot/no-boot-error",area:"boot",severity:"error",title:"Boots without an uncaught error",run:e=>e.bootError?u.fail(e.bootError):u.pass()},{id:"boot/no-forbidden-globals",area:"boot",severity:"error",title:"Does not access forbidden globals (e.g. window.nostr)",run:e=>e.forbiddenGlobals.length?u.fail(`Accessed forbidden global(s): ${e.forbiddenGlobals.join(", ")}`,e.forbiddenGlobals):u.pass()},{id:"wire/envelope-well-formed",area:"wire",severity:"error",title:"Every emitted envelope is well-formed",run:e=>{if(e.emitted.length===0)return u.skip("Napplet emitted no envelopes");const t=e.emitted.filter(d=>!d.verdict.ok);if(t.length===0)return u.pass(`${e.emitted.length} envelope(s) valid`);const s=t.map(d=>({envelope:d.envelope,errors:d.verdict.errors}));return u.fail(`${t.length}/${e.emitted.length} emitted envelope(s) invalid`,s)}},{id:"wire/declared-naps-only",area:"wire",severity:"warning",title:"Only emits NAP domains it declared in napplet-requires",run:e=>{if(e.emitted.length===0)return u.skip("Napplet emitted no envelopes");if(!B(e.manifestHtml,"napplet-requires"))return u.skip("No napplet-requires to compare against");const t=new Set($(e.manifestHtml).requires.map(i=>i.startsWith("nap:")?i.slice(4):i)),d=[...new Set(e.emitted.map(i=>i.verdict.domain).filter(i=>typeof i=="string"))].filter(i=>!t.has(i));return d.length?u.fail(`Emitted undeclared NAP domain(s): ${d.join(", ")}`,d):u.pass()}},{id:"degrade/domain-absence",area:"degradation",severity:"error",title:"Does not crash when no NAP domains are injected",run:e=>e.degraded?e.degraded.bootError?u.fail(`Crashed with no injected domains: ${e.degraded.bootError}`):u.pass():u.skip("Host did not run the degraded (no-capabilities) pass")},{id:"lifecycle/clean-teardown",area:"lifecycle",severity:"warning",title:"Tears down listeners on unload",run:e=>e.lifecycle?e.lifecycle.listenerLeak?u.fail("Message listeners remained after unload"):u.pass():u.skip("Lifecycle not measured")}];function de(e,t={}){const s=t.now??(()=>Date.now()),d=t.checks??oe,i=s(),o=d.map(l=>{const y=l.run(e);return{id:l.id,area:l.area,severity:l.severity,title:l.title,status:y.status,detail:y.detail,diagnostics:y.diagnostics}}),a={total:o.length,passed:o.filter(l=>l.status==="pass").length,failed:o.filter(l=>l.status==="fail").length,skipped:o.filter(l=>l.status==="skip").length,errors:o.filter(l=>l.status==="fail"&&l.severity==="error").length,warnings:o.filter(l=>l.status==="fail"&&l.severity==="warning").length};return{napplet:$(e.manifestHtml).nappletType,startedAt:i,finishedAt:s(),checks:o,summary:a,ok:a.errors===0}}const le=/\bwindow\s*\.\s*nostr\b|\bglobalThis\s*\.\s*nostr\b/,ae="/__conformance__/events",C=new URLSearchParams(location.search),T=C.get("live")==="1";function b(e){return String(e).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}const g=document.getElementById("app");function ue(){return g.innerHTML=`
16
+ <header class="masthead">
17
+ <h1>napplet conformance ${T?'<span class="live-badge" id="live-badge">● live</span>':""}</h1>
18
+ <p>Load a napplet by URL and run NAP protocol conformance live. Same engine as <code>napplet-conformance</code> CI.</p>
19
+ </header>
20
+ <form id="run-form" class="runbar">
21
+ <input id="url" type="url" placeholder="https://localhost:5173/ (a napplet served with permissive CORS)" autocomplete="off" />
22
+ <button id="run" type="submit">Run conformance</button>
23
+ <button id="rerun" type="button" class="secondary" hidden>Re-run</button>
24
+ </form>
25
+ <p id="status" class="status" role="status"></p>
26
+ <section id="output" class="output"></section>
27
+ `,{urlInput:g.querySelector("#url"),runButton:g.querySelector("#run"),rerunButton:g.querySelector("#rerun"),liveBadge:g.querySelector("#live-badge")??document.createElement("span"),status:g.querySelector("#status"),output:g.querySelector("#output")}}const f=ue();let v="",A=0;function k(e,t="info"){f.status.textContent=e,f.status.dataset.kind=t}function N(e){return String(e).padStart(2,"0")}function ce(e){const t=new Date(e);return`${N(t.getHours())}:${N(t.getMinutes())}:${N(t.getSeconds())}`}async function pe(e){try{const s=await(await fetch(e,{mode:"cors"})).text();return{html:s,forbidden:le.test(s)?["window.nostr"]:[],fetched:!0}}catch{return{html:"",forbidden:[],fetched:!1}}}function fe(e){const t=e.ok?"conformant":"non-conformant",d=[...new Set(e.checks.map(a=>a.area))].map(a=>{const m=e.checks.filter(l=>l.area===a).map(l=>{const y=l.severity==="warning"&&l.status==="fail"?' <span class="tag warn">warning</span>':"",p=l.detail?`<span class="detail">${b(l.detail)}</span>`:"";return`<li class="check ${l.status}"><span class="badge ${l.status}">${l.status}</span><span class="id">${b(l.id)}</span>${y}${p}</li>`}).join("");return`<div class="area"><h3>${b(a)}</h3><ul>${m}</ul></div>`}).join(""),i=e.summary,o=A>1?`<span class="run-stamp">run #${A} · ${ce(e.finishedAt)}</span>`:"";return`
28
+ <div class="verdict ${t}">
29
+ <span class="result">${e.ok?"CONFORMANT":"NON-CONFORMANT"}</span>
30
+ <span class="counts">${i.passed} passed · ${i.failed} failed · ${i.skipped} skipped${i.warnings?` · ${i.warnings} warning(s)`:""}</span>
31
+ ${o}
32
+ </div>
33
+ <div class="checks">${d}</div>
34
+ `}function me(e){return e.length===0?'<p class="empty">No envelopes were emitted.</p>':`<ul class="envlog">${e.map(s=>{const d=s.verdict.ok?"ok":"bad",i=s.verdict.ok?"":`<div class="errs">${s.verdict.errors.map(o=>b(`${o.code}: ${o.message}`)).join("<br>")}</div>`;return`<li class="env ${d}"><code>${b(JSON.stringify(s.envelope))}</code>${i}</li>`}).join("")}</ul>`}function ye(e,t){if(!t)return`<p class="empty">Couldn't fetch the napplet HTML (likely CORS). Serve it with <code>Access-Control-Allow-Origin: *</code>, or use the CLI for the authoritative manifest checks.</p>`;const s=$(e),d=(i,o)=>`<tr><th>${b(i)}</th><td>${o?b(String(o)):"<em>—</em>"}</td></tr>`;return`
35
+ <table class="manifest">
36
+ ${d("napplet-type",s.nappletType)}
37
+ ${d("requires",s.requires.join(", "))}
38
+ </table>
39
+ ${s.errors.length?`<div class="errs">${s.errors.map(i=>b(`${i.code}: ${i.message}`)).join("<br>")}</div>`:""}
40
+ `}function be(e,t,s,d){f.output.innerHTML=`
41
+ ${fe(e)}
42
+ <div class="panels">
43
+ <section class="panel"><h2>Emitted envelopes (${t.length})</h2>${me(t)}</section>
44
+ <section class="panel"><h2>Manifest</h2>${ye(s,d)}</section>
45
+ </div>
46
+ `}async function E(e){v=e,f.runButton.disabled=!0,f.rerunButton.disabled=!0;try{k(`Fetching manifest for ${e} …`);const{html:t,forbidden:s,fetched:d}=await pe(e);k("Booting napplet in a sandboxed iframe …");const i=await te({url:e,readyTimeoutMs:5e3,settleMs:800,runDegraded:!0}),o=z({manifestHtml:t,boot:i,forbiddenGlobals:s}),a=de(o);A+=1,k(a.ok?"Done — conformant.":"Done — non-conformant.",a.ok?"info":"error"),be(a,i.emitted,t,d),f.rerunButton.hidden=!1}catch(t){k(`Error: ${t instanceof Error?t.message:String(t)}`,"error")}finally{f.runButton.disabled=!1,f.rerunButton.disabled=!1}}function ge(){if(!T||typeof EventSource>"u")return;let e=!1;const t=new EventSource(ae);t.addEventListener("rerun",()=>{!v||e||(e=!0,setTimeout(()=>{e=!1,E(v)},50))}),t.addEventListener("open",()=>{f.liveBadge&&(f.liveBadge.dataset.state="connected")}),t.addEventListener("error",()=>{f.liveBadge&&(f.liveBadge.dataset.state="disconnected")})}f.runButton.parentElement.addEventListener("submit",e=>{e.preventDefault();const t=f.urlInput.value.trim();if(t){const s=new URL(location.href);s.searchParams.set("url",t),history.replaceState(null,"",s),E(t)}});f.rerunButton.addEventListener("click",()=>{v&&E(v)});ge();const j=C.get("url");j&&(f.urlInput.value=j,E(j));
@@ -5,7 +5,7 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>napplet conformance runtime</title>
7
7
  <meta name="description" content="Load a napplet and run NAP protocol conformance checks live in your browser." />
8
- <script type="module" crossorigin src="/assets/index-_DMnN297.js"></script>
8
+ <script type="module" crossorigin src="/assets/index-y56oSaYx.js"></script>
9
9
  <link rel="stylesheet" crossorigin href="/assets/index-D4WUxbBl.css">
10
10
  </head>
11
11
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@napplet/conformance-cli",
3
- "version": "0.2.9",
3
+ "version": "0.2.11",
4
4
  "description": "Headless CLI that runs @napplet/conformance against a napplet in real Chromium via Playwright — the `test:conformance` runner",
5
5
  "type": "module",
6
6
  "bin": {
@@ -15,13 +15,13 @@
15
15
  },
16
16
  "dependencies": {
17
17
  "playwright": "^1.59.1",
18
- "@napplet/conformance": "^0.9.0"
18
+ "@napplet/conformance": "^0.10.0"
19
19
  },
20
20
  "devDependencies": {
21
21
  "@types/node": "^22.15.0",
22
22
  "tsup": "^8.5.0",
23
23
  "typescript": "^5.9.3",
24
- "@napplet/conformance-web": "0.0.9"
24
+ "@napplet/conformance-web": "0.0.11"
25
25
  },
26
26
  "license": "MIT",
27
27
  "repository": {
@@ -1,32 +0,0 @@
1
- (function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const s of document.querySelectorAll('link[rel="modulepreload"]'))l(s);new MutationObserver(s=>{for(const o of s)if(o.type==="childList")for(const a of o.addedNodes)a.tagName==="LINK"&&a.rel==="modulepreload"&&l(a)}).observe(document,{childList:!0,subtree:!0});function r(s){const o={};return s.integrity&&(o.integrity=s.integrity),s.referrerPolicy&&(o.referrerPolicy=s.referrerPolicy),s.crossOrigin==="use-credentials"?o.credentials="include":s.crossOrigin==="anonymous"?o.credentials="omit":o.credentials="same-origin",o}function l(s){if(s.ep)return;s.ep=!0;const o=r(s);fetch(s.href,o)}})();var L=["relay","identity","storage","inc","theme","keys","media","notify","config","resource","cvm","outbox","upload","intent","ble","webrtc","link","lists","serial","common","dm"],G=["shell"],i={id:"string"},K={"shell.ready":{dir:"out"},"shell.init":{dir:"in"},"relay.subscribe":{dir:"out",fields:{...i,subId:"string",filters:"array"}},"relay.close":{dir:"out",fields:{...i,subId:"string"}},"relay.publish":{dir:"out",fields:{...i,event:"object"}},"relay.publishEncrypted":{dir:"out",fields:{...i,event:"object",recipient:"string"}},"relay.query":{dir:"out",fields:{...i,filters:"array"}},"relay.event":{dir:"in"},"relay.eose":{dir:"in"},"relay.closed":{dir:"in"},"relay.publish.result":{dir:"in"},"relay.publishEncrypted.result":{dir:"in"},"relay.query.result":{dir:"in"},"identity.getPublicKey":{dir:"out",fields:{...i}},"identity.getRelays":{dir:"out",fields:{...i}},"identity.getProfile":{dir:"out",fields:{...i}},"identity.getFollows":{dir:"out",fields:{...i}},"identity.getList":{dir:"out",fields:{...i,listType:"string"}},"identity.getZaps":{dir:"out",fields:{...i}},"identity.getMutes":{dir:"out",fields:{...i}},"identity.getBlocked":{dir:"out",fields:{...i}},"identity.getBadges":{dir:"out",fields:{...i}},"identity.changed":{dir:"in"},"identity.getPublicKey.result":{dir:"in"},"identity.getRelays.result":{dir:"in"},"identity.getProfile.result":{dir:"in"},"identity.getFollows.result":{dir:"in"},"identity.getList.result":{dir:"in"},"identity.getZaps.result":{dir:"in"},"identity.getMutes.result":{dir:"in"},"identity.getBlocked.result":{dir:"in"},"identity.getBadges.result":{dir:"in"},"storage.get":{dir:"out",fields:{...i,key:"string"}},"storage.set":{dir:"out",fields:{...i,key:"string",value:"string"}},"storage.remove":{dir:"out",fields:{...i,key:"string"}},"storage.keys":{dir:"out",fields:{...i}},"storage.get.result":{dir:"in"},"storage.set.result":{dir:"in"},"storage.remove.result":{dir:"in"},"storage.keys.result":{dir:"in"},"inc.emit":{dir:"out",fields:{topic:"string"}},"inc.subscribe":{dir:"out",fields:{...i,topic:"string"}},"inc.unsubscribe":{dir:"out",fields:{topic:"string"}},"inc.channel.open":{dir:"out",fields:{...i,target:"string"}},"inc.channel.emit":{dir:"out",fields:{channelId:"string"}},"inc.channel.broadcast":{dir:"out"},"inc.channel.list":{dir:"out",fields:{...i}},"inc.channel.close":{dir:"out",fields:{channelId:"string"}},"inc.subscribe.result":{dir:"in"},"inc.event":{dir:"in"},"inc.channel.open.result":{dir:"in"},"inc.channel.event":{dir:"in"},"inc.channel.list.result":{dir:"in"},"inc.channel.closed":{dir:"in"},"theme.get":{dir:"out",fields:{...i}},"theme.get.result":{dir:"in"},"theme.changed":{dir:"in"},"keys.forward":{dir:"out",fields:{key:"string",code:"string",ctrl:"boolean",alt:"boolean",shift:"boolean",meta:"boolean"}},"keys.registerAction":{dir:"out",fields:{...i,action:"object"}},"keys.unregisterAction":{dir:"out",fields:{actionId:"string"}},"keys.registerAction.result":{dir:"in"},"keys.bindings":{dir:"in"},"keys.action":{dir:"in"},"media.session.create":{dir:"out",fields:{...i,owner:"object"}},"media.session.update":{dir:"out",fields:{sessionId:"string",metadata:"object"}},"media.session.destroy":{dir:"out",fields:{sessionId:"string"}},"media.state":{dir:"out",fields:{sessionId:"string",status:"string"}},"media.capabilities":{dir:"out",fields:{sessionId:"string",actions:"array"}},"media.session.create.result":{dir:"in"},"media.command":{dir:"in"},"media.controls":{dir:"in"},"notify.send":{dir:"out",fields:{...i,title:"string"}},"notify.dismiss":{dir:"out",fields:{notificationId:"string"}},"notify.badge":{dir:"out",fields:{count:"number"}},"notify.channel.register":{dir:"out",fields:{channelId:"string",label:"string"}},"notify.permission.request":{dir:"out",fields:{...i}},"notify.send.result":{dir:"in"},"notify.permission.result":{dir:"in"},"notify.action":{dir:"in"},"notify.clicked":{dir:"in"},"notify.dismissed":{dir:"in"},"notify.controls":{dir:"in"},"config.registerSchema":{dir:"out",fields:{...i,schema:"object"}},"config.get":{dir:"out",fields:{...i}},"config.subscribe":{dir:"out"},"config.unsubscribe":{dir:"out"},"config.openSettings":{dir:"out"},"config.registerSchema.result":{dir:"in"},"config.values":{dir:"in"},"config.schemaError":{dir:"in"},"resource.bytes":{dir:"out",fields:{...i,url:"string"}},"resource.bytesMany":{dir:"out",fields:{...i,urls:"array"}},"resource.cancel":{dir:"out",fields:{...i}},"resource.bytes.result":{dir:"in"},"resource.bytes.error":{dir:"in"},"resource.bytesMany.result":{dir:"in"},"resource.bytesMany.error":{dir:"in"},"cvm.discover":{dir:"out",fields:{...i}},"cvm.request":{dir:"out",fields:{...i,server:"object",message:"object"}},"cvm.close":{dir:"out",fields:{...i,server:"object"}},"cvm.discover.result":{dir:"in"},"cvm.request.result":{dir:"in"},"cvm.close.result":{dir:"in"},"cvm.event":{dir:"in"},"outbox.query":{dir:"out",fields:{...i,filters:"present"}},"outbox.subscribe":{dir:"out",fields:{...i,subId:"string",filters:"present"}},"outbox.close":{dir:"out",fields:{...i,subId:"string"}},"outbox.publish":{dir:"out",fields:{...i,event:"object"}},"outbox.resolveRelays":{dir:"out",fields:{...i,target:"present"}},"outbox.query.result":{dir:"in"},"outbox.event":{dir:"in"},"outbox.eose":{dir:"in"},"outbox.closed":{dir:"in"},"outbox.publish.result":{dir:"in"},"outbox.resolveRelays.result":{dir:"in"},"upload.upload":{dir:"out",fields:{...i,request:"object"}},"upload.status":{dir:"out",fields:{...i,uploadId:"string"}},"upload.upload.result":{dir:"in"},"upload.status.result":{dir:"in"},"upload.status.changed":{dir:"in"},"intent.invoke":{dir:"out",fields:{...i,request:"object"}},"intent.available":{dir:"out",fields:{...i,archetype:"string"}},"intent.handlers":{dir:"out",fields:{...i}},"intent.invoke.result":{dir:"in"},"intent.available.result":{dir:"in"},"intent.handlers.result":{dir:"in"},"intent.changed":{dir:"in"},"ble.open":{dir:"out",fields:{...i,request:"object"}},"ble.services":{dir:"out",fields:{...i,sessionId:"string"}},"ble.read":{dir:"out",fields:{...i,sessionId:"string",target:"object"}},"ble.write":{dir:"out",fields:{...i,sessionId:"string",target:"object",data:"array"}},"ble.subscribe":{dir:"out",fields:{...i,sessionId:"string",target:"object"}},"ble.unsubscribe":{dir:"out",fields:{...i,sessionId:"string",target:"object"}},"ble.close":{dir:"out",fields:{...i,sessionId:"string"}},"ble.open.result":{dir:"in"},"ble.services.result":{dir:"in"},"ble.read.result":{dir:"in"},"ble.write.result":{dir:"in"},"ble.subscribe.result":{dir:"in"},"ble.unsubscribe.result":{dir:"in"},"ble.close.result":{dir:"in"},"ble.event":{dir:"in"},"common.encodeNip19":{dir:"out",fields:{...i,input:"object"}},"common.decodeNip19":{dir:"out",fields:{...i,value:"string"}},"common.getProfile":{dir:"out",fields:{...i,target:"string"}},"common.follows":{dir:"out",fields:{...i}},"common.follow":{dir:"out",fields:{...i,pubkeys:"array"}},"common.unfollow":{dir:"out",fields:{...i,pubkeys:"array"}},"common.react":{dir:"out",fields:{...i,targetEventId:"string",reaction:"string"}},"common.report":{dir:"out",fields:{...i,target:"object",reason:"string",text:"string"}},"common.encodeNip19.result":{dir:"in"},"common.decodeNip19.result":{dir:"in"},"common.getProfile.result":{dir:"in"},"common.follows.result":{dir:"in"},"common.follow.result":{dir:"in"},"common.unfollow.result":{dir:"in"},"common.react.result":{dir:"in"},"common.report.result":{dir:"in"},"webrtc.open":{dir:"out",fields:{...i,request:"object"}},"webrtc.send":{dir:"out",fields:{...i,sessionId:"string",payload:"present"}},"webrtc.close":{dir:"out",fields:{...i,sessionId:"string"}},"webrtc.open.result":{dir:"in"},"webrtc.send.result":{dir:"in"},"webrtc.close.result":{dir:"in"},"webrtc.event":{dir:"in"},"link.open":{dir:"out",fields:{...i,url:"string"}},"link.open.result":{dir:"in"},"lists.supported":{dir:"out",fields:{...i}},"lists.add":{dir:"out",fields:{...i,list:"object",items:"array"}},"lists.remove":{dir:"out",fields:{...i,list:"object",items:"array"}},"lists.supported.result":{dir:"in"},"lists.add.result":{dir:"in"},"lists.remove.result":{dir:"in"},"serial.open":{dir:"out",fields:{...i,request:"object"}},"serial.write":{dir:"out",fields:{...i,sessionId:"string",data:"array"}},"serial.close":{dir:"out",fields:{...i,sessionId:"string"}},"serial.open.result":{dir:"in"},"serial.write.result":{dir:"in"},"serial.close.result":{dir:"in"},"serial.event":{dir:"in"},"dm.status":{dir:"out",fields:{...i}},"dm.conversations":{dir:"out",fields:{...i}},"dm.messages":{dir:"out",fields:{...i,conversationId:"string"}},"dm.send":{dir:"out",fields:{...i,recipients:"array",content:"string"}},"dm.subscribe":{dir:"out",fields:{...i}},"dm.unsubscribe":{dir:"out",fields:{...i,subscriptionId:"string"}},"dm.status.result":{dir:"in"},"dm.conversations.result":{dir:"in"},"dm.messages.result":{dir:"in"},"dm.send.result":{dir:"in"},"dm.subscribe.result":{dir:"in"},"dm.unsubscribe.result":{dir:"in"},"dm.message":{dir:"in"}};function B(e){if(e===void 0)return"undefined";if(e===null)return"null";if(Array.isArray(e))return"array";const t=typeof e;return t==="string"||t==="number"||t==="boolean"?t:t==="object"?"object":"present"}function U(e,t){return t==="present"?e!=null:B(e)===t}function Z(e){const t=[];if(typeof e!="object"||e===null||Array.isArray(e))return{ok:!1,errors:[{code:"not-an-object",message:"Envelope must be a non-null object"}]};const r=e,l=r.type;if(typeof l!="string")return{ok:!1,errors:[{code:"missing-type",message:"Envelope is missing a string `type` field"}]};const s=l.indexOf(".");if(s<=0)return{ok:!1,type:l,errors:[{code:"malformed-type",message:`Envelope type "${l}" is not in domain.action form`}]};const o=l.slice(0,s);if(!(L.includes(o)||G.includes(o)))return{ok:!1,type:l,domain:o,errors:[{code:"unknown-domain",message:`"${o}" is not a known NAP domain`}]};const p=K[l];if(!p)return{ok:!1,type:l,domain:o,errors:[{code:"unknown-type",message:`"${l}" is not a known ${o} message type`}]};if(p.dir==="in")return{ok:!1,type:l,domain:o,direction:"in",errors:[{code:"inbound-type-emitted",message:`"${l}" is a shell→napplet message; a napplet must not emit it`}]};for(const[d,m]of Object.entries(p.fields??{})){if(!(d in r)||r[d]===void 0){t.push({code:"missing-field",message:`Required field "${d}" is missing`,field:d});continue}U(r[d],m)||t.push({code:"wrong-type",message:`Field "${d}" should be ${m} but is ${B(r[d])}`,field:d})}return{ok:t.length===0,type:l,domain:o,direction:"out",errors:t}}var M=/^[a-z0-9][a-z0-9._:-]*$/;function z(e){return e.replace(/&(#x?[0-9a-fA-F]+|[a-zA-Z]+);/g,(t,r)=>{if(r[0]==="#"){const s=r[1]==="x"||r[1]==="X"?parseInt(r.slice(2),16):parseInt(r.slice(1),10);return Number.isFinite(s)?String.fromCodePoint(s):t}return{amp:"&",lt:"<",gt:">",quot:'"',apos:"'"}[r]??t})}function N(e,t){const r=/<meta\b[^>]*>/gi;let l;for(;(l=r.exec(e))!==null;){const s=l[0],o=/\bname\s*=\s*(?:"([^"]*)"|'([^']*)')/i.exec(s);if((o?.[1]??o?.[2]??"").trim()!==t)continue;const p=/\bcontent\s*=\s*(?:"([^"]*)"|'([^']*)')/i.exec(s);return z((p?.[1]??p?.[2]??"").trim())}}function q(e){if(Array.isArray(e))return e.some(q);if(e&&typeof e=="object"){const t=e;return"pattern"in t?!0:Object.values(t).some(q)}return!1}function E(e,t={}){const r=[],l=[],s=N(e,"napplet-type");s?M.test(s)||r.push({code:"invalid-napplet-type",message:`napplet-type "${s}" must match ${M}`}):r.push({code:"missing-napplet-type",message:'No <meta name="napplet-type"> declared'});const o=N(e,"napplet-requires"),a=o?o.split(",").map(d=>d.trim()).filter(Boolean):[];for(const d of a){const m=d.startsWith("nap:")?d.slice(4):d;L.includes(m)||r.push({code:"unknown-required-nap",message:`napplet-requires lists "${d}" which is not a known NAP domain`})}const p=N(e,"napplet-config-schema");if(p){let d;try{d=JSON.parse(p)}catch{d=void 0,r.push({code:"invalid-config-schema",message:"napplet-config-schema is not valid JSON"})}d!==void 0&&(typeof d!="object"||d===null||Array.isArray(d)?r.push({code:"invalid-config-schema",message:"napplet-config-schema must be a JSON Schema object"}):q(d)&&r.push({code:"invalid-config-schema",message:"napplet-config-schema uses the `pattern` keyword, which the draft-07 core subset excludes (CVE-2025-69873)"}))}return{ok:r.length===0,nappletType:s,requires:a,errors:r,warnings:l}}var w="f".repeat(64),J="https://reference.invalid/blob",n=e=>[e],c=()=>[],V={"relay.subscribe":e=>n({type:"relay.eose",subId:e.subId}),"relay.close":c,"relay.publish":e=>n({type:"relay.publish.result",id:e.id,ok:!0,event:e.event}),"relay.publishEncrypted":e=>n({type:"relay.publishEncrypted.result",id:e.id,ok:!0,event:e.event}),"relay.query":e=>n({type:"relay.query.result",id:e.id,events:[]}),"identity.getPublicKey":e=>n({type:"identity.getPublicKey.result",id:e.id,pubkey:w}),"identity.getRelays":e=>n({type:"identity.getRelays.result",id:e.id,relays:{}}),"identity.getProfile":e=>n({type:"identity.getProfile.result",id:e.id,profile:null}),"identity.getFollows":e=>n({type:"identity.getFollows.result",id:e.id,pubkeys:[]}),"identity.getList":e=>n({type:"identity.getList.result",id:e.id,entries:[]}),"identity.getZaps":e=>n({type:"identity.getZaps.result",id:e.id,zaps:[]}),"identity.getMutes":e=>n({type:"identity.getMutes.result",id:e.id,pubkeys:[]}),"identity.getBlocked":e=>n({type:"identity.getBlocked.result",id:e.id,pubkeys:[]}),"identity.getBadges":e=>n({type:"identity.getBadges.result",id:e.id,badges:[]}),"storage.get":e=>n({type:"storage.get.result",id:e.id,value:null}),"storage.set":e=>n({type:"storage.set.result",id:e.id}),"storage.remove":e=>n({type:"storage.remove.result",id:e.id}),"storage.keys":e=>n({type:"storage.keys.result",id:e.id,keys:[]}),"inc.emit":c,"inc.subscribe":e=>n({type:"inc.subscribe.result",id:e.id}),"inc.unsubscribe":c,"inc.channel.open":e=>n({type:"inc.channel.open.result",id:e.id,channelId:`chan-${String(e.id)}`,peer:"reference-peer"}),"inc.channel.emit":c,"inc.channel.broadcast":c,"inc.channel.list":e=>n({type:"inc.channel.list.result",id:e.id,channels:[]}),"inc.channel.close":c,"theme.get":e=>n({type:"theme.get.result",id:e.id,theme:{colors:{},mode:"dark"}}),"keys.forward":c,"keys.registerAction":e=>n({type:"keys.registerAction.result",id:e.id,actionId:`action-${String(e.id)}`}),"keys.unregisterAction":c,"media.session.create":e=>n({type:"media.session.create.result",id:e.id,sessionId:`session-${String(e.id)}`,owner:e.owner}),"media.session.update":c,"media.session.destroy":c,"media.state":c,"media.capabilities":c,"notify.send":e=>n({type:"notify.send.result",id:e.id,notificationId:`notif-${String(e.id)}`}),"notify.dismiss":c,"notify.badge":c,"notify.channel.register":c,"notify.permission.request":e=>n({type:"notify.permission.result",id:e.id,granted:!0}),"config.registerSchema":e=>n({type:"config.registerSchema.result",id:e.id,ok:!0}),"config.get":e=>n({type:"config.values",id:e.id,values:{}}),"config.subscribe":()=>n({type:"config.values",values:{}}),"config.unsubscribe":c,"config.openSettings":c,"resource.bytes":e=>n({type:"resource.bytes.result",id:e.id,blob:new Blob([]),mime:"application/octet-stream"}),"resource.bytesMany":e=>n({type:"resource.bytesMany.result",id:e.id,items:Array.isArray(e.urls)?e.urls.map(t=>({url:t,ok:!0,blob:new Blob([]),mime:"application/octet-stream"})):[]}),"resource.cancel":c,"cvm.discover":e=>n({type:"cvm.discover.result",id:e.id,servers:[]}),"cvm.request":e=>n({type:"cvm.request.result",id:e.id,message:{}}),"cvm.close":e=>n({type:"cvm.close.result",id:e.id}),"outbox.query":e=>n({type:"outbox.query.result",id:e.id,events:[],relays:{}}),"outbox.subscribe":e=>n({type:"outbox.eose",subId:e.subId}),"outbox.close":c,"outbox.publish":e=>n({type:"outbox.publish.result",id:e.id,ok:!0}),"outbox.resolveRelays":e=>n({type:"outbox.resolveRelays.result",id:e.id,plan:{}}),"upload.upload":e=>n({type:"upload.upload.result",id:e.id,result:{url:J}}),"upload.status":e=>n({type:"upload.status.result",id:e.id,status:{}}),"intent.invoke":e=>n({type:"intent.invoke.result",id:e.id,result:{ok:!0,archetype:"reference",action:"open",handled:!0}}),"intent.available":e=>n({type:"intent.available.result",id:e.id,availability:{archetype:e.archetype,available:!0,candidates:[{dTag:"reference-handler",actions:["open"],protocols:["NAP-4"],contracts:[{action:"open",protocol:"NAP-4",eventKinds:[1]}],isDefault:!0}],hasDefault:!0}}),"intent.handlers":e=>n({type:"intent.handlers.result",id:e.id,handlers:[]}),"ble.open":e=>n({type:"ble.open.result",id:e.id,session:{id:"ble-reference",state:"open",device:{id:"reference-device",name:"Reference BLE"}}}),"ble.services":e=>n({type:"ble.services.result",id:e.id,services:[]}),"ble.read":e=>n({type:"ble.read.result",id:e.id,data:[]}),"ble.write":e=>n({type:"ble.write.result",id:e.id}),"ble.subscribe":e=>n({type:"ble.subscribe.result",id:e.id}),"ble.unsubscribe":e=>n({type:"ble.unsubscribe.result",id:e.id}),"ble.close":e=>n({type:"ble.close.result",id:e.id}),"common.encodeNip19":e=>n({type:"common.encodeNip19.result",id:e.id,ok:!0,value:"npub1reference",nip19Type:"npub"}),"common.decodeNip19":e=>n({type:"common.decodeNip19.result",id:e.id,ok:!0,nip19Type:"npub",hex:w}),"common.getProfile":e=>n({type:"common.getProfile.result",id:e.id,ok:!0,pubkey:w,profile:null}),"common.follows":e=>n({type:"common.follows.result",id:e.id,ok:!0,pubkeys:[]}),"common.follow":e=>n({type:"common.follow.result",id:e.id,ok:!0}),"common.unfollow":e=>n({type:"common.unfollow.result",id:e.id,ok:!0}),"common.react":e=>n({type:"common.react.result",id:e.id,ok:!0,eventId:"0".repeat(64)}),"common.report":e=>n({type:"common.report.result",id:e.id,ok:!0,eventId:"1".repeat(64)}),"webrtc.open":e=>n({type:"webrtc.open.result",id:e.id,session:{id:"webrtc-reference",scope:{type:"direct",pubkey:w},channel:"default",state:"connecting"}}),"webrtc.send":e=>n({type:"webrtc.send.result",id:e.id}),"webrtc.close":e=>n({type:"webrtc.close.result",id:e.id}),"link.open":e=>n({type:"link.open.result",id:e.id,status:"opened"}),"lists.supported":e=>n({type:"lists.supported.result",id:e.id,lists:[]}),"lists.add":e=>n({type:"lists.add.result",id:e.id,ok:!0,added:0,skipped:0}),"lists.remove":e=>n({type:"lists.remove.result",id:e.id,ok:!0,removed:0,skipped:0}),"serial.open":e=>n({type:"serial.open.result",id:e.id,session:{id:`serial-${String(e.id)}`,state:"open"}}),"serial.write":e=>n({type:"serial.write.result",id:e.id}),"serial.close":e=>n({type:"serial.close.result",id:e.id})};function W(e={}){const t=e.now??(()=>Date.now()),r={domains:e.capabilities?.domains??[...L],protocols:e.capabilities?.protocols??{}},l=e.services??[],s=[];function o(a){const p=a&&typeof a=="object"&&typeof a.type=="string"?a.type:void 0;if(p==="shell.ready")return[{type:"shell.init",capabilities:{domains:r.domains,protocols:r.protocols},services:l}];if(s.push({envelope:a,verdict:Z(a),timestamp:t()}),!p)return[];const d=V[p];return d?d(a):[]}return{capabilities:r,services:l,get records(){return s},handle:o,reset(){s.length=0}}}function Y(e){return{manifestHtml:e.manifestHtml,sandbox:e.sandbox??{allowScripts:!0,allowSameOrigin:!1},installedGlobal:e.boot.installedGlobal,bootError:e.boot.bootError,emitted:e.boot.emitted,forbiddenGlobals:e.forbiddenGlobals??[],degraded:e.boot.degraded,lifecycle:e.lifecycle??null}}function R(e){return new Promise(t=>setTimeout(t,e))}async function x(e,t,r,l){const s=W(e?{capabilities:e}:void 0),o=r.createElement("iframe");o.setAttribute("sandbox",t.allowSameOrigin?"allow-scripts allow-same-origin":"allow-scripts"),o.setAttribute("aria-hidden","true"),o.style.position="absolute",o.style.width="1px",o.style.height="1px",o.style.left="-9999px";let a;const p=new Promise(g=>{a=g}),d=g=>{if(g.source!==o.contentWindow)return;const H=s.handle(g.data),F=g.source;for(const _ of H)F?.postMessage(_,"*");const S=g.data;S&&typeof S=="object"&&S.type==="shell.ready"&&a()};l.addEventListener("message",d),(r.body??r.documentElement).appendChild(o),o.setAttribute("src",t.url);let m=!1,v=null;const O=Symbol("timeout");await Promise.race([p.then(()=>"ready"),R(t.readyTimeoutMs).then(()=>O)])===O?v=`napplet did not boot (no shell.ready within ${t.readyTimeoutMs}ms; a conformant napplet must run under sandbox="allow-scripts" without allow-same-origin)`:(m=!0,await R(t.settleMs)),l.removeEventListener("message",d);try{o.remove()}catch{}return{installedGlobal:m,bootError:v,emitted:[...s.records]}}async function X(e){const t=e.document??document,r=e.window??window,l={url:e.url,readyTimeoutMs:e.readyTimeoutMs??5e3,settleMs:e.settleMs,allowSameOrigin:e.allowSameOrigin??!1},s=await x(void 0,l,t,r);let o=null;{const a=await x({domains:[],protocols:{}},l,t,r);o={bootError:a.bootError,emitted:a.emitted}}return{installedGlobal:s.installedGlobal,bootError:s.bootError,emitted:s.emitted,degraded:o}}var Q=e=>({status:"pass",detail:e}),ee=(e,t)=>({status:"fail",detail:e,diagnostics:t}),te=e=>({status:"skip",detail:e}),u={pass:Q,fail:ee,skip:te};function ie(e,t){return E(e).errors.filter(r=>t.includes(r.code))}function C(e,t){return new RegExp(`name\\s*=\\s*["']${t}["']`,"i").test(e)}function I(e,t,r,l){return{id:e,area:"manifest",severity:"error",title:t,run:s=>{if(l&&!C(s.manifestHtml,l))return u.skip(`No ${l} declared`);const o=ie(s.manifestHtml,r);return o.length?u.fail(o[0].message,o):u.pass()}}}var se=[I("manifest/napplet-type","Declares a valid napplet-type",["missing-napplet-type","invalid-napplet-type"]),I("manifest/declared-naps","napplet-requires lists only real NAP domains",["unknown-required-nap"],"napplet-requires"),I("manifest/config-schema","Config schema is a draft-07 core subset",["invalid-config-schema"],"napplet-config-schema"),{id:"boot/sandbox-allow-scripts",area:"boot",severity:"error",title:"Runs under allow-scripts without allow-same-origin",run:e=>e.sandbox.allowScripts?e.sandbox.allowSameOrigin?u.fail("Napplet relied on allow-same-origin, which a shell must not grant"):u.pass():u.fail("Napplet was not loaded with allow-scripts")},{id:"boot/installs-global",area:"boot",severity:"error",title:"Completes the NAP-SHELL handshake (posts shell.ready)",run:e=>e.installedGlobal?u.pass():u.fail("window.napplet was not present after load (no NAP-SHELL shell.ready)")},{id:"boot/no-boot-error",area:"boot",severity:"error",title:"Boots without an uncaught error (observed via NAP-SHELL shell.ready)",run:e=>e.bootError?u.fail(e.bootError):u.pass()},{id:"boot/no-forbidden-globals",area:"boot",severity:"error",title:"Does not access forbidden globals (e.g. window.nostr)",run:e=>e.forbiddenGlobals.length?u.fail(`Accessed forbidden global(s): ${e.forbiddenGlobals.join(", ")}`,e.forbiddenGlobals):u.pass()},{id:"wire/envelope-well-formed",area:"wire",severity:"error",title:"Every emitted envelope is well-formed",run:e=>{if(e.emitted.length===0)return u.skip("Napplet emitted no envelopes");const t=e.emitted.filter(l=>!l.verdict.ok);if(t.length===0)return u.pass(`${e.emitted.length} envelope(s) valid`);const r=t.map(l=>({envelope:l.envelope,errors:l.verdict.errors}));return u.fail(`${t.length}/${e.emitted.length} emitted envelope(s) invalid`,r)}},{id:"wire/declared-naps-only",area:"wire",severity:"warning",title:"Only emits NAP domains it declared in napplet-requires",run:e=>{if(e.emitted.length===0)return u.skip("Napplet emitted no envelopes");if(!C(e.manifestHtml,"napplet-requires"))return u.skip("No napplet-requires to compare against");const t=new Set(E(e.manifestHtml).requires.map(s=>s.startsWith("nap:")?s.slice(4):s)),l=[...new Set(e.emitted.map(s=>s.verdict.domain).filter(s=>typeof s=="string"))].filter(s=>!t.has(s));return l.length?u.fail(`Emitted undeclared NAP domain(s): ${l.join(", ")}`,l):u.pass()}},{id:"degrade/supports-false",area:"degradation",severity:"error",title:"Does not crash when the NAP-SHELL environment offers no capabilities (supports() false)",run:e=>e.degraded?e.degraded.bootError?u.fail(`Crashed under a no-capability shell: ${e.degraded.bootError}`):u.pass():u.skip("Host did not run the degraded (no-capabilities) pass")},{id:"lifecycle/clean-teardown",area:"lifecycle",severity:"warning",title:"Tears down listeners on unload",run:e=>e.lifecycle?e.lifecycle.listenerLeak?u.fail("Message listeners remained after unload"):u.pass():u.skip("Lifecycle not measured")}];function re(e,t={}){const r=t.now??(()=>Date.now()),l=t.checks??se,s=r(),o=l.map(d=>{const m=d.run(e);return{id:d.id,area:d.area,severity:d.severity,title:d.title,status:m.status,detail:m.detail,diagnostics:m.diagnostics}}),a={total:o.length,passed:o.filter(d=>d.status==="pass").length,failed:o.filter(d=>d.status==="fail").length,skipped:o.filter(d=>d.status==="skip").length,errors:o.filter(d=>d.status==="fail"&&d.severity==="error").length,warnings:o.filter(d=>d.status==="fail"&&d.severity==="warning").length};return{napplet:E(e.manifestHtml).nappletType,startedAt:s,finishedAt:r(),checks:o,summary:a,ok:a.errors===0}}const ne=/\bwindow\s*\.\s*nostr\b|\bglobalThis\s*\.\s*nostr\b/,oe="/__conformance__/events",T=new URLSearchParams(location.search),D=T.get("live")==="1";function y(e){return String(e).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}const b=document.getElementById("app");function de(){return b.innerHTML=`
2
- <header class="masthead">
3
- <h1>napplet conformance ${D?'<span class="live-badge" id="live-badge">● live</span>':""}</h1>
4
- <p>Load a napplet by URL and run NAP protocol conformance live. Same engine as <code>napplet-conformance</code> CI.</p>
5
- </header>
6
- <form id="run-form" class="runbar">
7
- <input id="url" type="url" placeholder="https://localhost:5173/ (a napplet served with permissive CORS)" autocomplete="off" />
8
- <button id="run" type="submit">Run conformance</button>
9
- <button id="rerun" type="button" class="secondary" hidden>Re-run</button>
10
- </form>
11
- <p id="status" class="status" role="status"></p>
12
- <section id="output" class="output"></section>
13
- `,{urlInput:b.querySelector("#url"),runButton:b.querySelector("#run"),rerunButton:b.querySelector("#rerun"),liveBadge:b.querySelector("#live-badge")??document.createElement("span"),status:b.querySelector("#status"),output:b.querySelector("#output")}}const f=de();let h="",j=0;function k(e,t="info"){f.status.textContent=e,f.status.dataset.kind=t}function A(e){return String(e).padStart(2,"0")}function le(e){const t=new Date(e);return`${A(t.getHours())}:${A(t.getMinutes())}:${A(t.getSeconds())}`}async function ae(e){try{const r=await(await fetch(e,{mode:"cors"})).text();return{html:r,forbidden:ne.test(r)?["window.nostr"]:[],fetched:!0}}catch{return{html:"",forbidden:[],fetched:!1}}}function ue(e){const t=e.ok?"conformant":"non-conformant",l=[...new Set(e.checks.map(a=>a.area))].map(a=>{const p=e.checks.filter(d=>d.area===a).map(d=>{const m=d.severity==="warning"&&d.status==="fail"?' <span class="tag warn">warning</span>':"",v=d.detail?`<span class="detail">${y(d.detail)}</span>`:"";return`<li class="check ${d.status}"><span class="badge ${d.status}">${d.status}</span><span class="id">${y(d.id)}</span>${m}${v}</li>`}).join("");return`<div class="area"><h3>${y(a)}</h3><ul>${p}</ul></div>`}).join(""),s=e.summary,o=j>1?`<span class="run-stamp">run #${j} · ${le(e.finishedAt)}</span>`:"";return`
14
- <div class="verdict ${t}">
15
- <span class="result">${e.ok?"CONFORMANT":"NON-CONFORMANT"}</span>
16
- <span class="counts">${s.passed} passed · ${s.failed} failed · ${s.skipped} skipped${s.warnings?` · ${s.warnings} warning(s)`:""}</span>
17
- ${o}
18
- </div>
19
- <div class="checks">${l}</div>
20
- `}function ce(e){return e.length===0?'<p class="empty">No envelopes were emitted.</p>':`<ul class="envlog">${e.map(r=>{const l=r.verdict.ok?"ok":"bad",s=r.verdict.ok?"":`<div class="errs">${r.verdict.errors.map(o=>y(`${o.code}: ${o.message}`)).join("<br>")}</div>`;return`<li class="env ${l}"><code>${y(JSON.stringify(r.envelope))}</code>${s}</li>`}).join("")}</ul>`}function pe(e,t){if(!t)return`<p class="empty">Couldn't fetch the napplet HTML (likely CORS). Serve it with <code>Access-Control-Allow-Origin: *</code>, or use the CLI for the authoritative manifest checks.</p>`;const r=E(e),l=(s,o)=>`<tr><th>${y(s)}</th><td>${o?y(String(o)):"<em>—</em>"}</td></tr>`;return`
21
- <table class="manifest">
22
- ${l("napplet-type",r.nappletType)}
23
- ${l("requires",r.requires.join(", "))}
24
- </table>
25
- ${r.errors.length?`<div class="errs">${r.errors.map(s=>y(`${s.code}: ${s.message}`)).join("<br>")}</div>`:""}
26
- `}function fe(e,t,r,l){f.output.innerHTML=`
27
- ${ue(e)}
28
- <div class="panels">
29
- <section class="panel"><h2>Emitted envelopes (${t.length})</h2>${ce(t)}</section>
30
- <section class="panel"><h2>Manifest</h2>${pe(r,l)}</section>
31
- </div>
32
- `}async function $(e){h=e,f.runButton.disabled=!0,f.rerunButton.disabled=!0;try{k(`Fetching manifest for ${e} …`);const{html:t,forbidden:r,fetched:l}=await ae(e);k("Booting napplet in a sandboxed iframe …");const s=await X({url:e,readyTimeoutMs:5e3,settleMs:800,runDegraded:!0}),o=Y({manifestHtml:t,boot:s,forbiddenGlobals:r}),a=re(o);j+=1,k(a.ok?"Done — conformant.":"Done — non-conformant.",a.ok?"info":"error"),fe(a,s.emitted,t,l),f.rerunButton.hidden=!1}catch(t){k(`Error: ${t instanceof Error?t.message:String(t)}`,"error")}finally{f.runButton.disabled=!1,f.rerunButton.disabled=!1}}function me(){if(!D||typeof EventSource>"u")return;let e=!1;const t=new EventSource(oe);t.addEventListener("rerun",()=>{!h||e||(e=!0,setTimeout(()=>{e=!1,$(h)},50))}),t.addEventListener("open",()=>{f.liveBadge&&(f.liveBadge.dataset.state="connected")}),t.addEventListener("error",()=>{f.liveBadge&&(f.liveBadge.dataset.state="disconnected")})}f.runButton.parentElement.addEventListener("submit",e=>{e.preventDefault();const t=f.urlInput.value.trim();if(t){const r=new URL(location.href);r.searchParams.set("url",t),history.replaceState(null,"",r),$(t)}});f.rerunButton.addEventListener("click",()=>{h&&$(h)});me();const P=T.get("url");P&&(f.urlInput.value=P,$(P));