@napplet/conformance-cli 0.2.12 → 0.2.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +12 -1
- package/dist/cli.js.map +1 -1
- package/dist/host-bundle.global.js +112 -13
- package/dist/ui/assets/index-DZ1po6iW.js +107 -0
- package/dist/ui/index.html +1 -1
- package/package.json +4 -3
- package/dist/ui/assets/index-B6ykjdo8.js +0 -46
package/dist/cli.js
CHANGED
|
@@ -230,7 +230,18 @@ import { readdir, readFile as readFile3, stat as stat3 } from "fs/promises";
|
|
|
230
230
|
import { join as join3, extname as extname3 } from "path";
|
|
231
231
|
var FORBIDDEN = [
|
|
232
232
|
{ label: "window.nostr", re: /\bwindow\s*\.\s*nostr\b/ },
|
|
233
|
-
{ label: "globalThis.nostr", re: /\bglobalThis\s*\.\s*nostr\b/ }
|
|
233
|
+
{ label: "globalThis.nostr", re: /\bglobalThis\s*\.\s*nostr\b/ },
|
|
234
|
+
{ label: "fetch", re: /\b(?:window\s*\.\s*|globalThis\s*\.\s*)?fetch\s*\(/ },
|
|
235
|
+
{ label: "XMLHttpRequest", re: /\b(?:new\s+)?XMLHttpRequest\s*\(/ },
|
|
236
|
+
{ label: "WebSocket", re: /\b(?:new\s+)?WebSocket\s*\(/ },
|
|
237
|
+
{ label: "localStorage", re: /\b(?:window\s*\.\s*)?localStorage\b/ },
|
|
238
|
+
{ label: "sessionStorage", re: /\b(?:window\s*\.\s*)?sessionStorage\b/ },
|
|
239
|
+
{ label: "indexedDB", re: /\b(?:window\s*\.\s*)?indexedDB\b/ },
|
|
240
|
+
{ label: "document.cookie", re: /\bdocument\s*\.\s*cookie\b/ },
|
|
241
|
+
{ label: "external img src", re: /<img\b[^>]*\bsrc\s*=\s*["']?https?:/i },
|
|
242
|
+
{ label: "external script src", re: /<script\b[^>]*\bsrc\s*=\s*["']?https?:/i },
|
|
243
|
+
{ label: "external stylesheet href", re: /<link\b[^>]*\brel\s*=\s*["']?stylesheet["']?[^>]*\bhref\s*=\s*["']?https?:/i },
|
|
244
|
+
{ label: "external stylesheet href", re: /<link\b[^>]*\bhref\s*=\s*["']?https?:[^>]*\brel\s*=\s*["']?stylesheet["']?/i }
|
|
234
245
|
];
|
|
235
246
|
var SCANNABLE = /* @__PURE__ */ new Set([".js", ".mjs", ".cjs", ".html", ".htm"]);
|
|
236
247
|
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", ".turbo", "coverage"]);
|
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 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"]}
|
|
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 napplet authority.\n *\n * A napplet runs in a `sandbox=\"allow-scripts\"` iframe at an opaque origin, so the\n * host cannot observe every forbidden browser surface at runtime. Instead we scan\n * served source for references to authority the shell owns: signer globals,\n * browser storage, direct network primitives, and external network-loaded assets.\n * This is a heuristic, but a true positive is meaningful: NIP-5D and the NAPs\n * route these capabilities through `window.napplet` domains.\n *\n * @packageDocumentation\n */\n\nimport { readdir, readFile, stat } from 'node:fs/promises';\nimport { join, extname } from 'node:path';\n\n/** Patterns considered forbidden, keyed by the label reported in the verdict. */\nconst FORBIDDEN: Array<{ label: string; re: RegExp }> = [\n { label: 'window.nostr', re: /\\bwindow\\s*\\.\\s*nostr\\b/ },\n { label: 'globalThis.nostr', re: /\\bglobalThis\\s*\\.\\s*nostr\\b/ },\n { label: 'fetch', re: /\\b(?:window\\s*\\.\\s*|globalThis\\s*\\.\\s*)?fetch\\s*\\(/ },\n { label: 'XMLHttpRequest', re: /\\b(?:new\\s+)?XMLHttpRequest\\s*\\(/ },\n { label: 'WebSocket', re: /\\b(?:new\\s+)?WebSocket\\s*\\(/ },\n { label: 'localStorage', re: /\\b(?:window\\s*\\.\\s*)?localStorage\\b/ },\n { label: 'sessionStorage', re: /\\b(?:window\\s*\\.\\s*)?sessionStorage\\b/ },\n { label: 'indexedDB', re: /\\b(?:window\\s*\\.\\s*)?indexedDB\\b/ },\n { label: 'document.cookie', re: /\\bdocument\\s*\\.\\s*cookie\\b/ },\n { label: 'external img src', re: /<img\\b[^>]*\\bsrc\\s*=\\s*[\"']?https?:/i },\n { label: 'external script src', re: /<script\\b[^>]*\\bsrc\\s*=\\s*[\"']?https?:/i },\n { label: 'external stylesheet href', re: /<link\\b[^>]*\\brel\\s*=\\s*[\"']?stylesheet[\"']?[^>]*\\bhref\\s*=\\s*[\"']?https?:/i },\n { label: 'external stylesheet href', re: /<link\\b[^>]*\\bhref\\s*=\\s*[\"']?https?:[^>]*\\brel\\s*=\\s*[\"']?stylesheet[\"']?/i },\n];\n\nconst SCANNABLE = new Set(['.js', '.mjs', '.cjs', '.html', '.htm']);\nconst SKIP_DIRS = new Set(['node_modules', '.git', '.turbo', 'coverage']);\n\nasync function collectFiles(dir: string, acc: string[]): Promise<void> {\n for (const entry of await readdir(dir, { withFileTypes: true })) {\n if (entry.isDirectory()) {\n if (SKIP_DIRS.has(entry.name)) continue;\n await collectFiles(join(dir, entry.name), acc);\n } else if (entry.isFile() && SCANNABLE.has(extname(entry.name).toLowerCase())) {\n acc.push(join(dir, entry.name));\n }\n }\n}\n\n/**\n * Scan a napplet directory for forbidden browser authority references.\n *\n * @param dir - The directory of served napplet files.\n * @returns The distinct forbidden labels found (e.g. `['window.nostr']`).\n */\nexport async function scanForbiddenGlobals(dir: string): Promise<string[]> {\n const info = await stat(dir).catch(() => null);\n if (!info?.isDirectory()) return [];\n const files: string[] = [];\n await collectFiles(dir, files);\n const found = new Set<string>();\n for (const file of files) {\n const text = await readFile(file, 'utf8').catch(() => '');\n for (const { label, re } of FORBIDDEN) {\n if (re.test(text)) found.add(label);\n }\n }\n return [...found];\n}\n","/**\n * @napplet/conformance-cli -- Resolve which local artifact to conformance-test.\n *\n * Kept in its own module (not `cli.ts`, whose top level runs `main()`) so tests\n * can import the resolver without launching the CLI.\n */\n\nimport { stat } from 'node:fs/promises';\nimport { join, resolve } from 'node:path';\n\nasync function isFile(path: string): Promise<boolean> {\n return (await stat(path).catch(() => null))?.isFile() ?? false;\n}\n\n/**\n * Resolve the directory + index.html for a local napplet target.\n *\n * A built napplet is the artifact we test, so `dist/index.html` is preferred over\n * a sibling `index.html`. When `target` is a project root, its source\n * `index.html` (a Vite entry referencing `/src/*.ts` that cannot run in the\n * opaque-origin `srcdoc` sandbox) would otherwise shadow the built single-file\n * output and make conformance test the wrong, never-booting artifact.\n */\nexport async function resolveNappletDir(target: string): Promise<{ dir: string; indexHtml: string }> {\n const base = resolve(target);\n const dist = join(base, 'dist');\n if (await isFile(join(dist, 'index.html'))) return { dir: dist, indexHtml: join(dist, 'index.html') };\n if (await isFile(join(base, 'index.html'))) return { dir: base, indexHtml: join(base, 'index.html') };\n throw new Error(`No index.html found in ${join(dist, 'index.html')} or ${join(base, 'index.html')}. Build the napplet first.`);\n}\n"],"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"]}
|
|
@@ -147,10 +147,18 @@ var NappletConformanceHostBundle = (() => {
|
|
|
147
147
|
"cvm.discover": { dir: "out", fields: { ...ID } },
|
|
148
148
|
"cvm.request": { dir: "out", fields: { ...ID, server: "object", message: "object" } },
|
|
149
149
|
"cvm.close": { dir: "out", fields: { ...ID, server: "object" } },
|
|
150
|
+
"cvm.registry.list": { dir: "out", fields: { ...ID } },
|
|
151
|
+
"cvm.registry.has": { dir: "out", fields: { ...ID, family: "string" } },
|
|
152
|
+
"cvm.registry.describe": { dir: "out", fields: { ...ID, family: "string" } },
|
|
153
|
+
"cvm.registry.call": { dir: "out", fields: { ...ID, family: "string", tool: "string" } },
|
|
150
154
|
"cvm.discover.result": { dir: "in" },
|
|
151
155
|
"cvm.request.result": { dir: "in" },
|
|
152
156
|
"cvm.close.result": { dir: "in" },
|
|
153
157
|
"cvm.event": { dir: "in" },
|
|
158
|
+
"cvm.registry.list.result": { dir: "in" },
|
|
159
|
+
"cvm.registry.has.result": { dir: "in" },
|
|
160
|
+
"cvm.registry.describe.result": { dir: "in" },
|
|
161
|
+
"cvm.registry.call.result": { dir: "in" },
|
|
154
162
|
// ── outbox ───────────────────────────────────────────────────────────────
|
|
155
163
|
"outbox.getEvent": { dir: "out", fields: { ...ID, eventId: "string" } },
|
|
156
164
|
"outbox.query": { dir: "out", fields: { ...ID, filters: "present" } },
|
|
@@ -161,7 +169,6 @@ var NappletConformanceHostBundle = (() => {
|
|
|
161
169
|
"outbox.getEvent.result": { dir: "in" },
|
|
162
170
|
"outbox.query.result": { dir: "in" },
|
|
163
171
|
"outbox.event": { dir: "in" },
|
|
164
|
-
"outbox.eose": { dir: "in" },
|
|
165
172
|
"outbox.closed": { dir: "in" },
|
|
166
173
|
"outbox.publish.result": { dir: "in" },
|
|
167
174
|
"outbox.resolveRelays.result": { dir: "in" },
|
|
@@ -397,6 +404,28 @@ var NappletConformanceHostBundle = (() => {
|
|
|
397
404
|
var REFERENCE_BLOB_URL = "https://reference.invalid/blob";
|
|
398
405
|
var ok = (v) => [v];
|
|
399
406
|
var none = () => [];
|
|
407
|
+
function dataUrlToBlob(url) {
|
|
408
|
+
if (typeof url !== "string" || !url.startsWith("data:")) return null;
|
|
409
|
+
const comma = url.indexOf(",");
|
|
410
|
+
if (comma < 0) {
|
|
411
|
+
return { blob: new Blob([], { type: "text/plain" }), mime: "text/plain" };
|
|
412
|
+
}
|
|
413
|
+
const meta = url.slice("data:".length, comma);
|
|
414
|
+
const body = url.slice(comma + 1);
|
|
415
|
+
const parts = meta.split(";").filter(Boolean);
|
|
416
|
+
const base64 = parts.includes("base64");
|
|
417
|
+
const mime = parts.find((part) => part.includes("/")) ?? "text/plain";
|
|
418
|
+
try {
|
|
419
|
+
if (base64) {
|
|
420
|
+
const binary = globalThis.atob(body);
|
|
421
|
+
const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
|
|
422
|
+
return { blob: new Blob([bytes], { type: mime }), mime };
|
|
423
|
+
}
|
|
424
|
+
return { blob: new Blob([decodeURIComponent(body)], { type: mime }), mime };
|
|
425
|
+
} catch {
|
|
426
|
+
return null;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
400
429
|
var RESPONDERS = {
|
|
401
430
|
// relay
|
|
402
431
|
"relay.subscribe": (e) => ok({ type: "relay.eose", subId: e.subId }),
|
|
@@ -453,16 +482,25 @@ var NappletConformanceHostBundle = (() => {
|
|
|
453
482
|
"config.unsubscribe": none,
|
|
454
483
|
"config.openSettings": none,
|
|
455
484
|
// resource
|
|
456
|
-
"resource.bytes": (e) =>
|
|
485
|
+
"resource.bytes": (e) => {
|
|
486
|
+
const decoded = dataUrlToBlob(e.url);
|
|
487
|
+
if (!decoded) {
|
|
488
|
+
return ok({ type: "resource.bytes.result", id: e.id, blob: new Blob([]), mime: "application/octet-stream" });
|
|
489
|
+
}
|
|
490
|
+
return ok({ type: "resource.bytes.result", id: e.id, blob: decoded.blob, mime: decoded.mime });
|
|
491
|
+
},
|
|
457
492
|
"resource.bytesMany": (e) => ok({
|
|
458
493
|
type: "resource.bytesMany.result",
|
|
459
494
|
id: e.id,
|
|
460
|
-
items: Array.isArray(e.urls) ? e.urls.map((url) =>
|
|
461
|
-
url
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
495
|
+
items: Array.isArray(e.urls) ? e.urls.map((url) => {
|
|
496
|
+
const decoded = dataUrlToBlob(url);
|
|
497
|
+
return {
|
|
498
|
+
url,
|
|
499
|
+
ok: true,
|
|
500
|
+
blob: decoded?.blob ?? new Blob([]),
|
|
501
|
+
mime: decoded?.mime ?? "application/octet-stream"
|
|
502
|
+
};
|
|
503
|
+
}) : []
|
|
466
504
|
}),
|
|
467
505
|
"resource.info": (e) => ok({
|
|
468
506
|
type: "resource.info.result",
|
|
@@ -480,9 +518,9 @@ var NappletConformanceHostBundle = (() => {
|
|
|
480
518
|
"cvm.request": (e) => ok({ type: "cvm.request.result", id: e.id, message: {} }),
|
|
481
519
|
"cvm.close": (e) => ok({ type: "cvm.close.result", id: e.id }),
|
|
482
520
|
// outbox
|
|
483
|
-
"outbox.getEvent": (e) => ok({ type: "outbox.getEvent.result", id: e.id
|
|
484
|
-
"outbox.query": (e) => ok({ type: "outbox.query.result", id: e.id, events: []
|
|
485
|
-
"outbox.subscribe": (e) => ok({ type: "outbox.
|
|
521
|
+
"outbox.getEvent": (e) => ok({ type: "outbox.getEvent.result", id: e.id }),
|
|
522
|
+
"outbox.query": (e) => ok({ type: "outbox.query.result", id: e.id, events: [] }),
|
|
523
|
+
"outbox.subscribe": (e) => ok({ type: "outbox.closed", subId: e.subId, reason: "reference shell complete" }),
|
|
486
524
|
"outbox.close": none,
|
|
487
525
|
"outbox.publish": (e) => ok({ type: "outbox.publish.result", id: e.id, ok: true }),
|
|
488
526
|
"outbox.resolveRelays": (e) => ok({ type: "outbox.resolveRelays.result", id: e.id, plan: {} }),
|
|
@@ -604,6 +642,67 @@ var NappletConformanceHostBundle = (() => {
|
|
|
604
642
|
const domains = ${JSON.stringify(domains)};
|
|
605
643
|
const napplet = {};
|
|
606
644
|
for (const domain of domains) napplet[domain] = {};
|
|
645
|
+
const pending = new Map();
|
|
646
|
+
let seq = 0;
|
|
647
|
+
const nextId = () => {
|
|
648
|
+
try {
|
|
649
|
+
if (window.crypto && typeof window.crypto.randomUUID === 'function') return window.crypto.randomUUID();
|
|
650
|
+
} catch {}
|
|
651
|
+
seq += 1;
|
|
652
|
+
return 'conformance-' + Date.now().toString(36) + '-' + seq.toString(36);
|
|
653
|
+
};
|
|
654
|
+
const request = (message, resultType, errorType) => new Promise((resolve, reject) => {
|
|
655
|
+
const id = message.id || nextId();
|
|
656
|
+
const timeout = window.setTimeout(() => {
|
|
657
|
+
pending.delete(id);
|
|
658
|
+
reject(new Error(message.type + ' timed out'));
|
|
659
|
+
}, 5000);
|
|
660
|
+
pending.set(id, { resolve, reject, resultType, errorType, timeout });
|
|
661
|
+
window.parent.postMessage({ ...message, id }, '*');
|
|
662
|
+
});
|
|
663
|
+
window.addEventListener('message', (event) => {
|
|
664
|
+
const msg = event.data;
|
|
665
|
+
if (!msg || typeof msg !== 'object' || typeof msg.id !== 'string') return;
|
|
666
|
+
const slot = pending.get(msg.id);
|
|
667
|
+
if (!slot) return;
|
|
668
|
+
if (msg.type !== slot.resultType && msg.type !== slot.errorType) return;
|
|
669
|
+
pending.delete(msg.id);
|
|
670
|
+
window.clearTimeout(slot.timeout);
|
|
671
|
+
if (msg.type === slot.errorType) {
|
|
672
|
+
slot.reject(new Error(msg.message ? msg.error + ': ' + msg.message : String(msg.error || 'request failed')));
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
slot.resolve(msg);
|
|
676
|
+
});
|
|
677
|
+
if (napplet.resource) {
|
|
678
|
+
napplet.resource.info = () =>
|
|
679
|
+
request({ type: 'resource.info' }, 'resource.info.result', 'resource.info.error')
|
|
680
|
+
.then((msg) => msg.info);
|
|
681
|
+
napplet.resource.bytes = (url) =>
|
|
682
|
+
request({ type: 'resource.bytes', url: String(url) }, 'resource.bytes.result', 'resource.bytes.error')
|
|
683
|
+
.then((msg) => msg.blob);
|
|
684
|
+
napplet.resource.bytesMany = (urls) =>
|
|
685
|
+
request({ type: 'resource.bytesMany', urls: Array.from(urls || []) }, 'resource.bytesMany.result', 'resource.bytesMany.error')
|
|
686
|
+
.then((msg) => msg.items);
|
|
687
|
+
napplet.resource.bytesAsObjectURL = (url) => {
|
|
688
|
+
const handle = { url: '', revoke: () => {} };
|
|
689
|
+
let objectUrl = '';
|
|
690
|
+
let revoked = false;
|
|
691
|
+
const ready = napplet.resource.bytes(url).then((blob) => {
|
|
692
|
+
if (revoked) return;
|
|
693
|
+
objectUrl = URL.createObjectURL(blob);
|
|
694
|
+
handle.url = objectUrl;
|
|
695
|
+
return objectUrl;
|
|
696
|
+
});
|
|
697
|
+
handle.revoke = () => {
|
|
698
|
+
if (revoked) return;
|
|
699
|
+
revoked = true;
|
|
700
|
+
if (objectUrl) URL.revokeObjectURL(objectUrl);
|
|
701
|
+
};
|
|
702
|
+
Object.defineProperty(handle, 'ready', { value: ready, enumerable: false });
|
|
703
|
+
return handle;
|
|
704
|
+
};
|
|
705
|
+
}
|
|
607
706
|
window.napplet = napplet;
|
|
608
707
|
const report = (reason) => {
|
|
609
708
|
try {
|
|
@@ -774,8 +873,8 @@ var NappletConformanceHostBundle = (() => {
|
|
|
774
873
|
id: "boot/no-forbidden-globals",
|
|
775
874
|
area: "boot",
|
|
776
875
|
severity: "error",
|
|
777
|
-
title: "Does not access forbidden
|
|
778
|
-
run: (ctx) => ctx.forbiddenGlobals.length ? result.fail(`Accessed forbidden
|
|
876
|
+
title: "Does not access forbidden browser authority surfaces",
|
|
877
|
+
run: (ctx) => ctx.forbiddenGlobals.length ? result.fail(`Accessed forbidden surface(s): ${ctx.forbiddenGlobals.join(", ")}`, ctx.forbiddenGlobals) : result.pass()
|
|
779
878
|
},
|
|
780
879
|
// ── wire ─────────────────────────────────────────────────────────────────────
|
|
781
880
|
{
|
|
@@ -0,0 +1,107 @@
|
|
|
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"]'))d(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"&&d(a)}).observe(document,{childList:!0,subtree:!0});function i(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 d(s){if(s.ep)return;s.ep=!0;const o=i(s);fetch(s.href,o)}})();var R=["relay","identity","storage","inc","theme","keys","media","notify","config","resource","cvm","outbox","upload","intent","ble","webrtc","link","count","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.registry.list":{dir:"out",fields:{...r}},"cvm.registry.has":{dir:"out",fields:{...r,family:"string"}},"cvm.registry.describe":{dir:"out",fields:{...r,family:"string"}},"cvm.registry.call":{dir:"out",fields:{...r,family:"string",tool:"string"}},"cvm.discover.result":{dir:"in"},"cvm.request.result":{dir:"in"},"cvm.close.result":{dir:"in"},"cvm.event":{dir:"in"},"cvm.registry.list.result":{dir:"in"},"cvm.registry.has.result":{dir:"in"},"cvm.registry.describe.result":{dir:"in"},"cvm.registry.call.result":{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.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"},"count.query":{dir:"out",fields:{...r,filters:"array"}},"count.query.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 M(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:M(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 i=e,d=i.type;if(typeof d!="string")return{ok:!1,errors:[{code:"missing-type",message:"Envelope is missing a string `type` field"}]};const s=d.indexOf(".");if(s<=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,s);if(!R.includes(o))return{ok:!1,type:d,domain:o,errors:[{code:"unknown-domain",message:`"${o}" is not a known NAP domain`}]};const c=F[d];if(!c)return{ok:!1,type:d,domain:o,errors:[{code:"unknown-type",message:`"${d}" is not a known ${o} message type`}]};if(c.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(c.fields??{})){if(!(l in i)||i[l]===void 0){t.push({code:"missing-field",message:`Required field "${l}" is missing`,field:l});continue}G(i[l],y)||t.push({code:"wrong-type",message:`Field "${l}" should be ${y} but is ${M(i[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 W(e){return e.replace(/&(#x?[0-9a-fA-F]+|[a-zA-Z]+);/g,(t,i)=>{if(i[0]==="#"){const s=i[1]==="x"||i[1]==="X"?parseInt(i.slice(2),16):parseInt(i.slice(1),10);return Number.isFinite(s)?String.fromCodePoint(s):t}return{amp:"&",lt:"<",gt:">",quot:'"',apos:"'"}[i]??t})}function S(e,t){const i=/<meta\b[^>]*>/gi;let d;for(;(d=i.exec(e))!==null;){const s=d[0],o=/\bname\s*=\s*(?:"([^"]*)"|'([^']*)')/i.exec(s);if((o?.[1]??o?.[2]??"").trim()!==t)continue;const c=/\bcontent\s*=\s*(?:"([^"]*)"|'([^']*)')/i.exec(s);return W((c?.[1]??c?.[2]??"").trim())}}function N(e){if(Array.isArray(e))return e.some(N);if(e&&typeof e=="object"){const t=e;return"pattern"in t?!0:Object.values(t).some(N)}return!1}function $(e,t={}){const i=[],d=[],s=S(e,"napplet-type");s?x.test(s)||i.push({code:"invalid-napplet-type",message:`napplet-type "${s}" must match ${x}`}):i.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;R.includes(y)||i.push({code:"unknown-required-nap",message:`napplet-requires lists "${l}" which is not a known NAP domain`})}const c=S(e,"napplet-config-schema");if(c){let l;try{l=JSON.parse(c)}catch{l=void 0,i.push({code:"invalid-config-schema",message:"napplet-config-schema is not valid JSON"})}l!==void 0&&(typeof l!="object"||l===null||Array.isArray(l)?i.push({code:"invalid-config-schema",message:"napplet-config-schema must be a JSON Schema object"}):N(l)&&i.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:i.length===0,nappletType:s,requires:a,errors:i,warnings:d}}var w="f".repeat(64),J="https://reference.invalid/blob",n=e=>[e],p=()=>[];function O(e){if(typeof e!="string"||!e.startsWith("data:"))return null;const t=e.indexOf(",");if(t<0)return{blob:new Blob([],{type:"text/plain"}),mime:"text/plain"};const i=e.slice(5,t),d=e.slice(t+1),s=i.split(";").filter(Boolean),o=s.includes("base64"),a=s.find(c=>c.includes("/"))??"text/plain";try{if(o){const c=globalThis.atob(d),l=Uint8Array.from(c,y=>y.charCodeAt(0));return{blob:new Blob([l],{type:a}),mime:a}}return{blob:new Blob([decodeURIComponent(d)],{type:a}),mime:a}}catch{return null}}var Z={"relay.subscribe":e=>n({type:"relay.eose",subId:e.subId}),"relay.close":p,"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":p,"inc.subscribe":e=>n({type:"inc.subscribe.result",id:e.id}),"inc.unsubscribe":p,"inc.channel.open":e=>n({type:"inc.channel.open.result",id:e.id,channelId:`chan-${String(e.id)}`,peer:"reference-peer"}),"inc.channel.emit":p,"inc.channel.broadcast":p,"inc.channel.list":e=>n({type:"inc.channel.list.result",id:e.id,channels:[]}),"inc.channel.close":p,"theme.get":e=>n({type:"theme.get.result",id:e.id,theme:{colors:{},mode:"dark"}}),"keys.forward":p,"keys.registerAction":e=>n({type:"keys.registerAction.result",id:e.id,actionId:`action-${String(e.id)}`}),"keys.unregisterAction":p,"media.session.create":e=>n({type:"media.session.create.result",id:e.id,sessionId:`session-${String(e.id)}`,owner:e.owner}),"media.session.update":p,"media.session.destroy":p,"media.state":p,"media.capabilities":p,"notify.send":e=>n({type:"notify.send.result",id:e.id,notificationId:`notif-${String(e.id)}`}),"notify.dismiss":p,"notify.badge":p,"notify.channel.register":p,"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":p,"config.openSettings":p,"resource.bytes":e=>{const t=O(e.url);return n(t?{type:"resource.bytes.result",id:e.id,blob:t.blob,mime:t.mime}:{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=>{const i=O(t);return{url:t,ok:!0,blob:i?.blob??new Blob([]),mime:i?.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":p,"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}),"outbox.query":e=>n({type:"outbox.query.result",id:e.id,events:[]}),"outbox.subscribe":e=>n({type:"outbox.closed",subId:e.subId,reason:"reference shell complete"}),"outbox.close":p,"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"}),"count.query":e=>n({type:"count.query.result",id:e.id,ok:!0,count:0}),"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()),i=[];function d(s){const o=s&&typeof s=="object"&&typeof s.type=="string"?s.type:void 0;if(i.push({envelope:s,verdict:K(s),timestamp:t()}),!o)return[];const a=Z[o];return a?a(s):[]}return{get records(){return i},handle:d,reset(){i.length=0}}}function V(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 B="__nappletConformance.error";function P(e){return new Promise(t=>setTimeout(t,e))}function Y(e){return e.replace(/&/g,"&").replace(/"/g,""").replace(/</g,"<").replace(/>/g,">")}function X(e){return`
|
|
2
|
+
(() => {
|
|
3
|
+
const domains = ${JSON.stringify(e)};
|
|
4
|
+
const napplet = {};
|
|
5
|
+
for (const domain of domains) napplet[domain] = {};
|
|
6
|
+
const pending = new Map();
|
|
7
|
+
let seq = 0;
|
|
8
|
+
const nextId = () => {
|
|
9
|
+
try {
|
|
10
|
+
if (window.crypto && typeof window.crypto.randomUUID === 'function') return window.crypto.randomUUID();
|
|
11
|
+
} catch {}
|
|
12
|
+
seq += 1;
|
|
13
|
+
return 'conformance-' + Date.now().toString(36) + '-' + seq.toString(36);
|
|
14
|
+
};
|
|
15
|
+
const request = (message, resultType, errorType) => new Promise((resolve, reject) => {
|
|
16
|
+
const id = message.id || nextId();
|
|
17
|
+
const timeout = window.setTimeout(() => {
|
|
18
|
+
pending.delete(id);
|
|
19
|
+
reject(new Error(message.type + ' timed out'));
|
|
20
|
+
}, 5000);
|
|
21
|
+
pending.set(id, { resolve, reject, resultType, errorType, timeout });
|
|
22
|
+
window.parent.postMessage({ ...message, id }, '*');
|
|
23
|
+
});
|
|
24
|
+
window.addEventListener('message', (event) => {
|
|
25
|
+
const msg = event.data;
|
|
26
|
+
if (!msg || typeof msg !== 'object' || typeof msg.id !== 'string') return;
|
|
27
|
+
const slot = pending.get(msg.id);
|
|
28
|
+
if (!slot) return;
|
|
29
|
+
if (msg.type !== slot.resultType && msg.type !== slot.errorType) return;
|
|
30
|
+
pending.delete(msg.id);
|
|
31
|
+
window.clearTimeout(slot.timeout);
|
|
32
|
+
if (msg.type === slot.errorType) {
|
|
33
|
+
slot.reject(new Error(msg.message ? msg.error + ': ' + msg.message : String(msg.error || 'request failed')));
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
slot.resolve(msg);
|
|
37
|
+
});
|
|
38
|
+
if (napplet.resource) {
|
|
39
|
+
napplet.resource.info = () =>
|
|
40
|
+
request({ type: 'resource.info' }, 'resource.info.result', 'resource.info.error')
|
|
41
|
+
.then((msg) => msg.info);
|
|
42
|
+
napplet.resource.bytes = (url) =>
|
|
43
|
+
request({ type: 'resource.bytes', url: String(url) }, 'resource.bytes.result', 'resource.bytes.error')
|
|
44
|
+
.then((msg) => msg.blob);
|
|
45
|
+
napplet.resource.bytesMany = (urls) =>
|
|
46
|
+
request({ type: 'resource.bytesMany', urls: Array.from(urls || []) }, 'resource.bytesMany.result', 'resource.bytesMany.error')
|
|
47
|
+
.then((msg) => msg.items);
|
|
48
|
+
napplet.resource.bytesAsObjectURL = (url) => {
|
|
49
|
+
const handle = { url: '', revoke: () => {} };
|
|
50
|
+
let objectUrl = '';
|
|
51
|
+
let revoked = false;
|
|
52
|
+
const ready = napplet.resource.bytes(url).then((blob) => {
|
|
53
|
+
if (revoked) return;
|
|
54
|
+
objectUrl = URL.createObjectURL(blob);
|
|
55
|
+
handle.url = objectUrl;
|
|
56
|
+
return objectUrl;
|
|
57
|
+
});
|
|
58
|
+
handle.revoke = () => {
|
|
59
|
+
if (revoked) return;
|
|
60
|
+
revoked = true;
|
|
61
|
+
if (objectUrl) URL.revokeObjectURL(objectUrl);
|
|
62
|
+
};
|
|
63
|
+
Object.defineProperty(handle, 'ready', { value: ready, enumerable: false });
|
|
64
|
+
return handle;
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
window.napplet = napplet;
|
|
68
|
+
const report = (reason) => {
|
|
69
|
+
try {
|
|
70
|
+
window.parent.postMessage({ type: '${B}', reason: String(reason && reason.message || reason) }, '*');
|
|
71
|
+
} catch {}
|
|
72
|
+
};
|
|
73
|
+
window.addEventListener('error', (event) => report(event.error || event.message));
|
|
74
|
+
window.addEventListener('unhandledrejection', (event) => report(event.reason));
|
|
75
|
+
})();
|
|
76
|
+
`}function Q(e,t,i){const d=`<base href="${Y(t)}">`,s=`<script>${X(i)}<\/script>`,o=/<head(?:\s[^>]*)?>/i.exec(e);if(o){const a=o.index+o[0].length;return`${e.slice(0,a)}${d}${s}${e.slice(a)}`}return`${d}${s}${e}`}function ee(e){if(!e.startsWith("data:"))return null;const t=e.indexOf(",");if(t<0)return"";const i=e.slice(0,t),d=e.slice(t+1);return i.endsWith(";base64")?atob(d):decodeURIComponent(d)}async function te(e){if(e==="about:blank")return"<!doctype html><html><head></head><body></body></html>";const t=ee(e);if(t!==null)return t;const i=await fetch(e);if(!i.ok)throw new Error(`Failed to fetch napplet HTML: ${i.status} ${i.statusText}`);return i.text()}async function L(e,t,i,d){const s=z(),o=i.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 c=f=>{if(f.source!==o.contentWindow)return;const h=f.data;if(h&&typeof h=="object"&&h.type===B){a=`napplet boot error: ${String(h.reason??"unknown error")}`;return}const D=s.handle(f.data),H=f.source;for(const _ of D)H?.postMessage(_,"*")};let l=!1;const y=new Promise(f=>{o.addEventListener("load",()=>{l=!0,f()},{once:!0})});d.addEventListener("message",c),(i.body??i.documentElement).appendChild(o);try{const f=await te(t.url);o.srcdoc=Q(f,t.url,e)}catch(f){a=f instanceof Error?f.message:String(f)}if(!a){const f=Symbol("timeout");await Promise.race([y.then(()=>"loaded"),P(t.readyTimeoutMs).then(()=>f)])===f||!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 P(t.settleMs)}d.removeEventListener("message",c);try{o.remove()}catch{}return{installedGlobal:a===null,bootError:a,emitted:[...s.records]}}async function re(e){const t=e.document??document,i=e.window??window,d={url:e.url,readyTimeoutMs:e.readyTimeoutMs??5e3,settleMs:e.settleMs,allowSameOrigin:e.allowSameOrigin??!1},s=await L(R,d,t,i);let o=null;{const a=await L([],d,t,i);o={bootError:a.bootError,emitted:a.emitted}}return{installedGlobal:s.installedGlobal,bootError:s.bootError,emitted:s.emitted,degraded:o}}var ie=e=>({status:"pass",detail:e}),se=(e,t)=>({status:"fail",detail:e,diagnostics:t}),ne=e=>({status:"skip",detail:e}),u={pass:ie,fail:se,skip:ne};function oe(e,t){return $(e).errors.filter(i=>t.includes(i.code))}function T(e,t){return new RegExp(`name\\s*=\\s*["']${t}["']`,"i").test(e)}function j(e,t,i,d){return{id:e,area:"manifest",severity:"error",title:t,run:s=>{if(d&&!T(s.manifestHtml,d))return u.skip(`No ${d} declared`);const o=oe(s.manifestHtml,i);return o.length?u.fail(o[0].message,o):u.pass()}}}var de=[j("manifest/napplet-type","Declares a valid napplet-type",["missing-napplet-type","invalid-napplet-type"]),j("manifest/declared-naps","napplet-requires lists only real NAP domains",["unknown-required-nap"],"napplet-requires"),j("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 browser authority surfaces",run:e=>e.forbiddenGlobals.length?u.fail(`Accessed forbidden surface(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 i=t.map(d=>({envelope:d.envelope,errors:d.verdict.errors}));return u.fail(`${t.length}/${e.emitted.length} emitted envelope(s) invalid`,i)}},{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(!T(e.manifestHtml,"napplet-requires"))return u.skip("No napplet-requires to compare against");const t=new Set($(e.manifestHtml).requires.map(s=>s.startsWith("nap:")?s.slice(4):s)),d=[...new Set(e.emitted.map(s=>s.verdict.domain).filter(s=>typeof s=="string"))].filter(s=>!t.has(s));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 le(e,t={}){const i=t.now??(()=>Date.now()),d=t.checks??de,s=i(),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:s,finishedAt:i(),checks:o,summary:a,ok:a.errors===0}}const ae=/\bwindow\s*\.\s*nostr\b|\bglobalThis\s*\.\s*nostr\b/,ue="/__conformance__/events",C=new URLSearchParams(location.search),U=C.get("live")==="1";function b(e){return String(e).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")}const g=document.getElementById("app");function ce(){return g.innerHTML=`
|
|
77
|
+
<header class="masthead">
|
|
78
|
+
<h1>napplet conformance ${U?'<span class="live-badge" id="live-badge">● live</span>':""}</h1>
|
|
79
|
+
<p>Load a napplet by URL and run NAP protocol conformance live. Same engine as <code>napplet-conformance</code> CI.</p>
|
|
80
|
+
</header>
|
|
81
|
+
<form id="run-form" class="runbar">
|
|
82
|
+
<input id="url" type="url" placeholder="https://localhost:5173/ (a napplet served with permissive CORS)" autocomplete="off" />
|
|
83
|
+
<button id="run" type="submit">Run conformance</button>
|
|
84
|
+
<button id="rerun" type="button" class="secondary" hidden>Re-run</button>
|
|
85
|
+
</form>
|
|
86
|
+
<p id="status" class="status" role="status"></p>
|
|
87
|
+
<section id="output" class="output"></section>
|
|
88
|
+
`,{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 m=ce();let v="",A=0;function k(e,t="info"){m.status.textContent=e,m.status.dataset.kind=t}function I(e){return String(e).padStart(2,"0")}function pe(e){const t=new Date(e);return`${I(t.getHours())}:${I(t.getMinutes())}:${I(t.getSeconds())}`}async function fe(e){try{const i=await(await fetch(e,{mode:"cors"})).text();return{html:i,forbidden:ae.test(i)?["window.nostr"]:[],fetched:!0}}catch{return{html:"",forbidden:[],fetched:!1}}}function me(e){const t=e.ok?"conformant":"non-conformant",d=[...new Set(e.checks.map(a=>a.area))].map(a=>{const c=e.checks.filter(l=>l.area===a).map(l=>{const y=l.severity==="warning"&&l.status==="fail"?' <span class="tag warn">warning</span>':"",f=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}${f}</li>`}).join("");return`<div class="area"><h3>${b(a)}</h3><ul>${c}</ul></div>`}).join(""),s=e.summary,o=A>1?`<span class="run-stamp">run #${A} · ${pe(e.finishedAt)}</span>`:"";return`
|
|
89
|
+
<div class="verdict ${t}">
|
|
90
|
+
<span class="result">${e.ok?"CONFORMANT":"NON-CONFORMANT"}</span>
|
|
91
|
+
<span class="counts">${s.passed} passed · ${s.failed} failed · ${s.skipped} skipped${s.warnings?` · ${s.warnings} warning(s)`:""}</span>
|
|
92
|
+
${o}
|
|
93
|
+
</div>
|
|
94
|
+
<div class="checks">${d}</div>
|
|
95
|
+
`}function ye(e){return e.length===0?'<p class="empty">No envelopes were emitted.</p>':`<ul class="envlog">${e.map(i=>{const d=i.verdict.ok?"ok":"bad",s=i.verdict.ok?"":`<div class="errs">${i.verdict.errors.map(o=>b(`${o.code}: ${o.message}`)).join("<br>")}</div>`;return`<li class="env ${d}"><code>${b(JSON.stringify(i.envelope))}</code>${s}</li>`}).join("")}</ul>`}function be(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 i=$(e),d=(s,o)=>`<tr><th>${b(s)}</th><td>${o?b(String(o)):"<em>—</em>"}</td></tr>`;return`
|
|
96
|
+
<table class="manifest">
|
|
97
|
+
${d("napplet-type",i.nappletType)}
|
|
98
|
+
${d("requires",i.requires.join(", "))}
|
|
99
|
+
</table>
|
|
100
|
+
${i.errors.length?`<div class="errs">${i.errors.map(s=>b(`${s.code}: ${s.message}`)).join("<br>")}</div>`:""}
|
|
101
|
+
`}function ge(e,t,i,d){m.output.innerHTML=`
|
|
102
|
+
${me(e)}
|
|
103
|
+
<div class="panels">
|
|
104
|
+
<section class="panel"><h2>Emitted envelopes (${t.length})</h2>${ye(t)}</section>
|
|
105
|
+
<section class="panel"><h2>Manifest</h2>${be(i,d)}</section>
|
|
106
|
+
</div>
|
|
107
|
+
`}async function E(e){v=e,m.runButton.disabled=!0,m.rerunButton.disabled=!0;try{k(`Fetching manifest for ${e} …`);const{html:t,forbidden:i,fetched:d}=await fe(e);k("Booting napplet in a sandboxed iframe …");const s=await re({url:e,readyTimeoutMs:5e3,settleMs:800,runDegraded:!0}),o=V({manifestHtml:t,boot:s,forbiddenGlobals:i}),a=le(o);A+=1,k(a.ok?"Done — conformant.":"Done — non-conformant.",a.ok?"info":"error"),ge(a,s.emitted,t,d),m.rerunButton.hidden=!1}catch(t){k(`Error: ${t instanceof Error?t.message:String(t)}`,"error")}finally{m.runButton.disabled=!1,m.rerunButton.disabled=!1}}function he(){if(!U||typeof EventSource>"u")return;let e=!1;const t=new EventSource(ue);t.addEventListener("rerun",()=>{!v||e||(e=!0,setTimeout(()=>{e=!1,E(v)},50))}),t.addEventListener("open",()=>{m.liveBadge&&(m.liveBadge.dataset.state="connected")}),t.addEventListener("error",()=>{m.liveBadge&&(m.liveBadge.dataset.state="disconnected")})}m.runButton.parentElement.addEventListener("submit",e=>{e.preventDefault();const t=m.urlInput.value.trim();if(t){const i=new URL(location.href);i.searchParams.set("url",t),history.replaceState(null,"",i),E(t)}});m.rerunButton.addEventListener("click",()=>{v&&E(v)});he();const q=C.get("url");q&&(m.urlInput.value=q,E(q));
|
package/dist/ui/index.html
CHANGED
|
@@ -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-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-DZ1po6iW.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.
|
|
3
|
+
"version": "0.2.14",
|
|
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,14 @@
|
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
17
|
"playwright": "^1.59.1",
|
|
18
|
-
"@napplet/conformance": "^0.
|
|
18
|
+
"@napplet/conformance": "^0.12.3"
|
|
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
|
-
"
|
|
24
|
+
"vitest": "^4.1.2",
|
|
25
|
+
"@napplet/conformance-web": "0.0.13"
|
|
25
26
|
},
|
|
26
27
|
"license": "MIT",
|
|
27
28
|
"repository": {
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const r of document.querySelectorAll('link[rel="modulepreload"]'))d(r);new MutationObserver(r=>{for(const o of r)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(r){const o={};return r.integrity&&(o.integrity=r.integrity),r.referrerPolicy&&(o.referrerPolicy=r.referrerPolicy),r.crossOrigin==="use-credentials"?o.credentials="include":r.crossOrigin==="anonymous"?o.credentials="omit":o.credentials="same-origin",o}function d(r){if(r.ep)return;r.ep=!0;const o=s(r);fetch(r.href,o)}})();var P=["relay","identity","storage","inc","theme","keys","media","notify","config","resource","cvm","outbox","upload","intent","ble","webrtc","link","count","lists","serial","common","dm"],i={id:"string"},F={"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.info":{dir:"out",fields:{...i}},"resource.bytes":{dir:"out",fields:{...i,url:"string"}},"resource.bytesMany":{dir:"out",fields:{...i,urls:"array"}},"resource.cancel":{dir:"out",fields:{...i}},"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:{...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.getEvent":{dir:"out",fields:{...i,eventId:"string"}},"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.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:{...i}},"upload.upload":{dir:"out",fields:{...i,request:"object"}},"upload.status":{dir:"out",fields:{...i,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:{...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"},"count.query":{dir:"out",fields:{...i,filters:"array"}},"count.query.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 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 r=d.indexOf(".");if(r<=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,r);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 r=s[1]==="x"||s[1]==="X"?parseInt(s.slice(2),16):parseInt(s.slice(1),10);return Number.isFinite(r)?String.fromCodePoint(r):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 r=d[0],o=/\bname\s*=\s*(?:"([^"]*)"|'([^']*)')/i.exec(r);if((o?.[1]??o?.[2]??"").trim()!==t)continue;const m=/\bcontent\s*=\s*(?:"([^"]*)"|'([^']*)')/i.exec(r);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=[],r=S(e,"napplet-type");r?x.test(r)||s.push({code:"invalid-napplet-type",message:`napplet-type "${r}" 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:r,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"}),"count.query":e=>n({type:"count.query.result",id:e.id,ok:!0,count:0}),"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(r){const o=r&&typeof r=="object"&&typeof r.type=="string"?r.type:void 0;if(s.push({envelope:r,verdict:K(r),timestamp:t()}),!o)return[];const a=W[o];return a?a(r):[]}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,"&").replace(/"/g,""").replace(/</g,"<").replace(/>/g,">")}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)}">`,r=`<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}${r}${e.slice(a)}`}return`${d}${r}${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 r=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=r.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:[...r.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},r=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:r.installedGlobal,bootError:r.bootError,emitted:r.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:r=>{if(d&&!B(r.manifestHtml,d))return u.skip(`No ${d} declared`);const o=ne(r.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(r=>r.startsWith("nap:")?r.slice(4):r)),d=[...new Set(e.emitted.map(r=>r.verdict.domain).filter(r=>typeof r=="string"))].filter(r=>!t.has(r));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,r=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:r,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,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")}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(""),r=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">${r.passed} passed · ${r.failed} failed · ${r.skipped} skipped${r.warnings?` · ${r.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",r=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>${r}</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=(r,o)=>`<tr><th>${b(r)}</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(r=>b(`${r.code}: ${r.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 r=await te({url:e,readyTimeoutMs:5e3,settleMs:800,runDegraded:!0}),o=z({manifestHtml:t,boot:r,forbiddenGlobals:s}),a=de(o);A+=1,k(a.ok?"Done — conformant.":"Done — non-conformant.",a.ok?"info":"error"),be(a,r.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));
|