@napplet/conformance-cli 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  var NappletConformanceHostBundle = (() => {
3
3
  // ../core/dist/index.js
4
- var NAP_DOMAINS = ["relay", "identity", "storage", "inc", "theme", "keys", "media", "notify", "config", "resource", "connect", "class", "cvm", "outbox", "upload", "intent"];
4
+ var NAP_DOMAINS = ["relay", "identity", "storage", "inc", "theme", "keys", "media", "notify", "config", "resource", "cvm", "outbox", "upload", "intent"];
5
5
  function createDispatch() {
6
6
  const handlers = /* @__PURE__ */ new Map();
7
7
  function registerNap2(domain, handler) {
@@ -29,122 +29,15 @@ var NappletConformanceHostBundle = (() => {
29
29
  var dispatch = _default.dispatch;
30
30
  var getRegisteredDomains = _default.getRegisteredDomains;
31
31
 
32
- // ../nap/dist/chunk-ESA5HG5Q.js
33
- var ERR_PREFIX = "[@napplet/nap/connect]";
34
- var ACCEPTED_SCHEMES = /* @__PURE__ */ new Set(["https", "wss", "http", "ws"]);
35
- var DEFAULT_PORTS = {
36
- https: "443",
37
- wss: "443",
38
- http: "80",
39
- ws: "80"
40
- };
41
- function splitOriginScheme(origin) {
42
- const schemeIdx = origin.indexOf("://");
43
- if (schemeIdx < 0) {
44
- throw new Error(`${ERR_PREFIX} origin missing scheme separator '://': ${origin}`);
45
- }
46
- const scheme = origin.slice(0, schemeIdx);
47
- if (scheme !== scheme.toLowerCase()) {
48
- throw new Error(`${ERR_PREFIX} scheme must be lowercase: ${origin}`);
49
- }
50
- if (!ACCEPTED_SCHEMES.has(scheme)) {
51
- throw new Error(`${ERR_PREFIX} scheme must be one of https|wss|http|ws: ${origin}`);
52
- }
53
- return { scheme, rest: origin.slice(schemeIdx + 3) };
54
- }
55
- function assertOriginRest(rest, origin) {
56
- if (rest.includes("/")) {
57
- throw new Error(`${ERR_PREFIX} origin MUST NOT contain a path: ${origin}`);
58
- }
59
- if (rest.includes("?")) {
60
- throw new Error(`${ERR_PREFIX} origin MUST NOT contain a query: ${origin}`);
61
- }
62
- if (rest.includes("#")) {
63
- throw new Error(`${ERR_PREFIX} origin MUST NOT contain a fragment: ${origin}`);
64
- }
65
- if (rest.includes("*")) {
66
- throw new Error(`${ERR_PREFIX} wildcard origins are not permitted: ${origin}`);
67
- }
68
- if (rest.startsWith("[")) {
69
- throw new Error(`${ERR_PREFIX} IPv6 literal origins are out of v1 scope: ${origin}`);
70
- }
71
- }
72
- function parseHost(scheme, rest, origin) {
73
- let host = rest;
74
- const portColonIdx = rest.lastIndexOf(":");
75
- if (portColonIdx >= 0) {
76
- host = rest.slice(0, portColonIdx);
77
- const port = rest.slice(portColonIdx + 1);
78
- if (!/^[0-9]+$/.test(port)) {
79
- throw new Error(`${ERR_PREFIX} port must be decimal digits: ${origin}`);
80
- }
81
- const portNum = Number(port);
82
- if (portNum < 1 || portNum > 65535) {
83
- throw new Error(`${ERR_PREFIX} port out of range 1-65535: ${origin}`);
84
- }
85
- if (DEFAULT_PORTS[scheme] === port) {
86
- throw new Error(`${ERR_PREFIX} default port ${port} MUST be omitted for scheme ${scheme}: ${origin}`);
87
- }
88
- }
89
- validateHostBase(host, origin);
90
- return host;
91
- }
92
- function validateHostBase(host, origin) {
93
- if (host.includes(":")) {
94
- throw new Error(`${ERR_PREFIX} IPv6 literal origins are out of v1 scope: ${origin}`);
95
- }
96
- if (host.length === 0) {
97
- throw new Error(`${ERR_PREFIX} host is empty: ${origin}`);
98
- }
99
- if (host !== host.toLowerCase()) {
100
- throw new Error(`${ERR_PREFIX} host MUST be lowercase: ${origin}`);
101
- }
102
- if (!/^[\x00-\x7F]+$/.test(host)) {
103
- throw new Error(`${ERR_PREFIX} non-ASCII host MUST be Punycode-encoded (xn--...) before calling: ${origin}`);
104
- }
105
- }
106
- function validateIpv4Host(host, origin) {
107
- if (!/^[0-9]+(\.[0-9]+){3}$/.test(host)) return false;
108
- for (const oct of host.split(".")) {
109
- if (oct.length > 1 && oct.startsWith("0")) {
110
- throw new Error(`${ERR_PREFIX} IPv4 octet MUST NOT have leading zeros: ${origin}`);
111
- }
112
- const n = Number(oct);
113
- if (!Number.isInteger(n) || n < 0 || n > 255) {
114
- throw new Error(`${ERR_PREFIX} IPv4 octet out of range 0-255: ${origin}`);
115
- }
116
- }
117
- return true;
118
- }
119
- function validateDnsHost(host, origin) {
120
- for (const label of host.split(".")) {
121
- if (label.length === 0 || label.length > 63) {
122
- throw new Error(`${ERR_PREFIX} invalid host label length: ${origin}`);
123
- }
124
- if (!/^[a-z0-9-]+$/.test(label)) {
125
- throw new Error(`${ERR_PREFIX} host label contains invalid characters: ${origin}`);
126
- }
127
- if (label.startsWith("-") || label.endsWith("-")) {
128
- throw new Error(`${ERR_PREFIX} host label MUST NOT start or end with hyphen: ${origin}`);
129
- }
130
- }
131
- }
132
- function normalizeConnectOrigin(origin) {
133
- if (typeof origin !== "string" || origin.length === 0) {
134
- throw new Error(`${ERR_PREFIX} origin must be a non-empty string`);
135
- }
136
- const { scheme, rest } = splitOriginScheme(origin);
137
- assertOriginRest(rest, origin);
138
- const host = parseHost(scheme, rest, origin);
139
- if (!validateIpv4Host(host, origin)) {
140
- validateDnsHost(host, origin);
141
- }
142
- return origin;
143
- }
144
-
145
32
  // ../conformance/dist/index.js
33
+ var FOUNDATIONAL_DOMAINS = ["shell"];
146
34
  var ID = { id: "string" };
147
35
  var ENVELOPE_SPECS = {
36
+ // ── shell (NAP-SHELL — foundational handshake) ─────────────────────────────
37
+ // `shell.ready` is a bare liveness ping (no required fields); `shell.init` is
38
+ // the runtime's environment reply (inbound — a napplet must not emit it).
39
+ "shell.ready": { dir: "out" },
40
+ "shell.init": { dir: "in" },
148
41
  // ── relay ────────────────────────────────────────────────────────────────
149
42
  "relay.subscribe": { dir: "out", fields: { ...ID, subId: "string", filters: "array" } },
150
43
  "relay.close": { dir: "out", fields: { ...ID, subId: "string" } },
@@ -250,8 +143,6 @@ var NappletConformanceHostBundle = (() => {
250
143
  "resource.cancel": { dir: "out", fields: { ...ID } },
251
144
  "resource.bytes.result": { dir: "in" },
252
145
  "resource.bytes.error": { dir: "in" },
253
- // ── class (shell-initiated only) ─────────────────────────────────────────
254
- "class.assigned": { dir: "in" },
255
146
  // ── cvm ──────────────────────────────────────────────────────────────────
256
147
  "cvm.discover": { dir: "out", fields: { ...ID } },
257
148
  "cvm.request": { dir: "out", fields: { ...ID, server: "object", message: "object" } },
@@ -315,7 +206,8 @@ var NappletConformanceHostBundle = (() => {
315
206
  return { ok: false, type, errors: [{ code: "malformed-type", message: `Envelope type "${type}" is not in domain.action form` }] };
316
207
  }
317
208
  const domain = type.slice(0, dotIndex);
318
- if (!NAP_DOMAINS.includes(domain)) {
209
+ const isKnownDomain = NAP_DOMAINS.includes(domain) || FOUNDATIONAL_DOMAINS.includes(domain);
210
+ if (!isKnownDomain) {
319
211
  return { ok: false, type, domain, errors: [{ code: "unknown-domain", message: `"${domain}" is not a known NAP domain` }] };
320
212
  }
321
213
  const spec = ENVELOPE_SPECS[type];
@@ -347,13 +239,22 @@ var NappletConformanceHostBundle = (() => {
347
239
  return { ok: errors.length === 0, type, domain, direction: "out", errors };
348
240
  }
349
241
  var NAPPLET_TYPE_RE = /^[a-z0-9][a-z0-9._:-]*$/;
350
- var AGGREGATE_HASH_RE = /^[0-9a-f]{64}$/;
351
242
  var NON_EXECUTING_SCRIPT_TYPES = /* @__PURE__ */ new Set([
352
243
  "application/json",
353
244
  "application/ld+json",
354
245
  "importmap",
355
246
  "speculationrules"
356
247
  ]);
248
+ function decodeHtmlEntities(value) {
249
+ return value.replace(/&(#x?[0-9a-fA-F]+|[a-zA-Z]+);/g, (match, entity) => {
250
+ if (entity[0] === "#") {
251
+ const code = entity[1] === "x" || entity[1] === "X" ? parseInt(entity.slice(2), 16) : parseInt(entity.slice(1), 10);
252
+ return Number.isFinite(code) ? String.fromCodePoint(code) : match;
253
+ }
254
+ const named = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'" };
255
+ return named[entity] ?? match;
256
+ });
257
+ }
357
258
  function readMeta(html, name) {
358
259
  const tagRe = /<meta\b[^>]*>/gi;
359
260
  let tag;
@@ -363,7 +264,7 @@ var NappletConformanceHostBundle = (() => {
363
264
  const metaName = (nameMatch?.[1] ?? nameMatch?.[2] ?? "").trim();
364
265
  if (metaName !== name) continue;
365
266
  const contentMatch = /\bcontent\s*=\s*(?:"([^"]*)"|'([^']*)')/i.exec(block);
366
- return (contentMatch?.[1] ?? contentMatch?.[2] ?? "").trim();
267
+ return decodeHtmlEntities((contentMatch?.[1] ?? contentMatch?.[2] ?? "").trim());
367
268
  }
368
269
  return void 0;
369
270
  }
@@ -398,8 +299,7 @@ var NappletConformanceHostBundle = (() => {
398
299
  }
399
300
  return false;
400
301
  }
401
- function validateManifest(html, options = {}) {
402
- const requireAggregateHash = options.requireAggregateHash ?? true;
302
+ function validateManifest(html, _options = {}) {
403
303
  const errors = [];
404
304
  const warnings = [];
405
305
  const nappletType = readMeta(html, "napplet-type");
@@ -408,14 +308,6 @@ var NappletConformanceHostBundle = (() => {
408
308
  } else if (!NAPPLET_TYPE_RE.test(nappletType)) {
409
309
  errors.push({ code: "invalid-napplet-type", message: `napplet-type "${nappletType}" must match ${NAPPLET_TYPE_RE}` });
410
310
  }
411
- const aggregateHash = readMeta(html, "napplet-aggregate-hash");
412
- if (!aggregateHash) {
413
- if (requireAggregateHash) {
414
- errors.push({ code: "missing-aggregate-hash", message: 'No <meta name="napplet-aggregate-hash"> \u2014 the build plugin injects it; build before testing' });
415
- }
416
- } else if (!AGGREGATE_HASH_RE.test(aggregateHash)) {
417
- errors.push({ code: "invalid-aggregate-hash", message: `napplet-aggregate-hash "${aggregateHash}" is not a 64-char lowercase hex digest` });
418
- }
419
311
  const requiresRaw = readMeta(html, "napplet-requires");
420
312
  const requires = requiresRaw ? requiresRaw.split(",").map((s) => s.trim()).filter(Boolean) : [];
421
313
  for (const req of requires) {
@@ -441,26 +333,13 @@ var NappletConformanceHostBundle = (() => {
441
333
  }
442
334
  }
443
335
  }
444
- const connectRaw = readMeta(html, "napplet-connect-requires");
445
- const connectTokens = connectRaw ? connectRaw.split(/\s+/).map((s) => s.trim()).filter(Boolean) : [];
446
- const connectOrigins = [];
447
- for (const origin of connectTokens) {
448
- try {
449
- connectOrigins.push(normalizeConnectOrigin(origin));
450
- } catch (err) {
451
- const reason = err instanceof Error ? err.message : String(err);
452
- errors.push({ code: "invalid-connect-origin", message: `connect origin "${origin}" is invalid: ${reason}` });
453
- }
454
- }
455
336
  for (const offender of findInlineScripts(html)) {
456
337
  errors.push({ code: "inline-script", message: `Inline <script> is forbidden under script-src 'self': ${offender}` });
457
338
  }
458
339
  return {
459
340
  ok: errors.length === 0,
460
341
  nappletType,
461
- aggregateHash,
462
342
  requires,
463
- connectOrigins,
464
343
  errors,
465
344
  warnings
466
345
  };
@@ -548,14 +427,23 @@ var NappletConformanceHostBundle = (() => {
548
427
  function createReferenceShell(options = {}) {
549
428
  const now = options.now ?? (() => Date.now());
550
429
  const capabilities = {
551
- naps: options.capabilities?.naps ?? [...NAP_DOMAINS],
552
- sandbox: options.capabilities?.sandbox ?? []
430
+ domains: options.capabilities?.domains ?? [...NAP_DOMAINS],
431
+ protocols: options.capabilities?.protocols ?? {}
553
432
  };
433
+ const services = options.services ?? [];
434
+ const klass = options.class ?? null;
554
435
  const records = [];
555
436
  function handle(envelope) {
556
437
  const type = envelope && typeof envelope === "object" && typeof envelope.type === "string" ? envelope.type : void 0;
557
438
  if (type === "shell.ready") {
558
- return [{ type: "shell.init", capabilities: { naps: capabilities.naps, sandbox: capabilities.sandbox } }];
439
+ return [
440
+ {
441
+ type: "shell.init",
442
+ capabilities: { domains: capabilities.domains, protocols: capabilities.protocols },
443
+ services,
444
+ class: klass
445
+ }
446
+ ];
559
447
  }
560
448
  records.push({ envelope, verdict: validateEnvelope(envelope), timestamp: now() });
561
449
  if (!type) return [];
@@ -564,6 +452,8 @@ var NappletConformanceHostBundle = (() => {
564
452
  }
565
453
  return {
566
454
  capabilities,
455
+ services,
456
+ class: klass,
567
457
  get records() {
568
458
  return records;
569
459
  },
@@ -629,7 +519,7 @@ var NappletConformanceHostBundle = (() => {
629
519
  const primary = await bootOnce(void 0, resolved, doc, win);
630
520
  let degraded = null;
631
521
  if (options.runDegraded ?? true) {
632
- const d = await bootOnce({ naps: [], sandbox: [] }, resolved, doc, win);
522
+ const d = await bootOnce({ domains: [], protocols: {} }, resolved, doc, win);
633
523
  degraded = { bootError: d.bootError, emitted: d.emitted };
634
524
  }
635
525
  return {
@@ -665,10 +555,8 @@ var NappletConformanceHostBundle = (() => {
665
555
  var CHECKS = [
666
556
  // ── manifest ───────────────────────────────────────────────────────────────
667
557
  manifestCheck("manifest/napplet-type", "Declares a valid napplet-type", ["missing-napplet-type", "invalid-napplet-type"]),
668
- manifestCheck("manifest/aggregate-hash", "Build injected a valid aggregate hash", ["missing-aggregate-hash", "invalid-aggregate-hash"]),
669
558
  manifestCheck("manifest/declared-naps", "napplet-requires lists only real NAP domains", ["unknown-required-nap"], "napplet-requires"),
670
559
  manifestCheck("manifest/config-schema", "Config schema is a draft-07 core subset", ["invalid-config-schema"], "napplet-config-schema"),
671
- manifestCheck("manifest/connect-origins", "Declared connect origins are valid", ["invalid-connect-origin"], "napplet-connect-requires"),
672
560
  {
673
561
  id: "manifest/no-inline-scripts",
674
562
  area: "manifest",
@@ -695,14 +583,18 @@ var NappletConformanceHostBundle = (() => {
695
583
  id: "boot/installs-global",
696
584
  area: "boot",
697
585
  severity: "error",
698
- title: "Installs window.napplet",
699
- run: (ctx) => ctx.installedGlobal ? result.pass() : result.fail("window.napplet was not present after load")
586
+ // NAP-SHELL: the observable boot signal across the opaque sandbox is the
587
+ // shim posting `shell.ready`. Receiving it proves window.napplet installed.
588
+ title: "Completes the NAP-SHELL handshake (posts shell.ready)",
589
+ run: (ctx) => ctx.installedGlobal ? result.pass() : result.fail("window.napplet was not present after load (no NAP-SHELL shell.ready)")
700
590
  },
701
591
  {
702
592
  id: "boot/no-boot-error",
703
593
  area: "boot",
704
594
  severity: "error",
705
- title: "Boots without an uncaught error",
595
+ // Boot is observed via the NAP-SHELL `shell.ready` signal; its absence within
596
+ // the timeout (or a crash before it) surfaces here as a boot error.
597
+ title: "Boots without an uncaught error (observed via NAP-SHELL shell.ready)",
706
598
  run: (ctx) => ctx.bootError ? result.fail(ctx.bootError) : result.pass()
707
599
  },
708
600
  {
@@ -747,7 +639,10 @@ var NappletConformanceHostBundle = (() => {
747
639
  id: "degrade/supports-false",
748
640
  area: "degradation",
749
641
  severity: "error",
750
- title: "Does not crash when shell.supports() is false",
642
+ // NAP-SHELL: when the runtime advertises an empty environment (no domains/
643
+ // protocols), every shell.supports() returns false; a conformant napplet must
644
+ // degrade rather than crash.
645
+ title: "Does not crash when the NAP-SHELL environment offers no capabilities (supports() false)",
751
646
  run: (ctx) => {
752
647
  if (!ctx.degraded) return result.skip("Host did not run the degraded (no-capabilities) pass");
753
648
  return ctx.degraded.bootError ? result.fail(`Crashed under a no-capability shell: ${ctx.degraded.bootError}`) : result.pass();
@@ -0,0 +1,32 @@
1
+ (function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const i of document.querySelectorAll('link[rel="modulepreload"]'))o(i);new MutationObserver(i=>{for(const r of i)if(r.type==="childList")for(const d of r.addedNodes)d.tagName==="LINK"&&d.rel==="modulepreload"&&o(d)}).observe(document,{childList:!0,subtree:!0});function s(i){const r={};return i.integrity&&(r.integrity=i.integrity),i.referrerPolicy&&(r.referrerPolicy=i.referrerPolicy),i.crossOrigin==="use-credentials"?r.credentials="include":i.crossOrigin==="anonymous"?r.credentials="omit":r.credentials="same-origin",r}function o(i){if(i.ep)return;i.ep=!0;const r=s(i);fetch(i.href,r)}})();var P=["relay","identity","storage","inc","theme","keys","media","notify","config","resource","cvm","outbox","upload","intent"],G=["shell"],a={id:"string"},K={"shell.ready":{dir:"out"},"shell.init":{dir:"in"},"relay.subscribe":{dir:"out",fields:{...a,subId:"string",filters:"array"}},"relay.close":{dir:"out",fields:{...a,subId:"string"}},"relay.publish":{dir:"out",fields:{...a,event:"object"}},"relay.publishEncrypted":{dir:"out",fields:{...a,event:"object",recipient:"string"}},"relay.query":{dir:"out",fields:{...a,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:{...a}},"identity.getRelays":{dir:"out",fields:{...a}},"identity.getProfile":{dir:"out",fields:{...a}},"identity.getFollows":{dir:"out",fields:{...a}},"identity.getList":{dir:"out",fields:{...a,listType:"string"}},"identity.getZaps":{dir:"out",fields:{...a}},"identity.getMutes":{dir:"out",fields:{...a}},"identity.getBlocked":{dir:"out",fields:{...a}},"identity.getBadges":{dir:"out",fields:{...a}},"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:{...a,key:"string"}},"storage.set":{dir:"out",fields:{...a,key:"string",value:"string"}},"storage.remove":{dir:"out",fields:{...a,key:"string"}},"storage.keys":{dir:"out",fields:{...a}},"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:{...a,topic:"string"}},"inc.unsubscribe":{dir:"out",fields:{topic:"string"}},"inc.channel.open":{dir:"out",fields:{...a,target:"string"}},"inc.channel.emit":{dir:"out",fields:{channelId:"string"}},"inc.channel.broadcast":{dir:"out"},"inc.channel.list":{dir:"out",fields:{...a}},"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:{...a}},"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:{...a,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:{...a,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:{...a,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:{...a}},"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:{...a,schema:"object"}},"config.get":{dir:"out",fields:{...a}},"config.subscribe":{dir:"out"},"config.unsubscribe":{dir:"out"},"config.openSettings":{dir:"out"},"config.registerSchema.result":{dir:"in"},"config.values":{dir:"in"},"config.schemaError":{dir:"in"},"resource.bytes":{dir:"out",fields:{...a,url:"string"}},"resource.cancel":{dir:"out",fields:{...a}},"resource.bytes.result":{dir:"in"},"resource.bytes.error":{dir:"in"},"cvm.discover":{dir:"out",fields:{...a}},"cvm.request":{dir:"out",fields:{...a,server:"object",message:"object"}},"cvm.close":{dir:"out",fields:{...a,server:"object"}},"cvm.discover.result":{dir:"in"},"cvm.request.result":{dir:"in"},"cvm.close.result":{dir:"in"},"cvm.event":{dir:"in"},"outbox.query":{dir:"out",fields:{...a,filters:"present"}},"outbox.subscribe":{dir:"out",fields:{...a,subId:"string",filters:"present"}},"outbox.close":{dir:"out",fields:{...a,subId:"string"}},"outbox.publish":{dir:"out",fields:{...a,event:"object"}},"outbox.resolveRelays":{dir:"out",fields:{...a,target:"present"}},"outbox.query.result":{dir:"in"},"outbox.event":{dir:"in"},"outbox.eose":{dir:"in"},"outbox.closed":{dir:"in"},"outbox.publish.result":{dir:"in"},"outbox.resolveRelays.result":{dir:"in"},"upload.upload":{dir:"out",fields:{...a,request:"object"}},"upload.status":{dir:"out",fields:{...a,uploadId:"string"}},"upload.upload.result":{dir:"in"},"upload.status.result":{dir:"in"},"upload.status.changed":{dir:"in"},"intent.invoke":{dir:"out",fields:{...a,request:"object"}},"intent.available":{dir:"out",fields:{...a,archetype:"string"}},"intent.handlers":{dir:"out",fields:{...a}},"intent.invoke.result":{dir:"in"},"intent.available.result":{dir:"in"},"intent.handlers.result":{dir:"in"},"intent.changed":{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 U(e,t){return t==="present"?e!=null:M(e)===t}function V(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,o=s.type;if(typeof o!="string")return{ok:!1,errors:[{code:"missing-type",message:"Envelope is missing a string `type` field"}]};const i=o.indexOf(".");if(i<=0)return{ok:!1,type:o,errors:[{code:"malformed-type",message:`Envelope type "${o}" is not in domain.action form`}]};const r=o.slice(0,i);if(!(P.includes(r)||G.includes(r)))return{ok:!1,type:o,domain:r,errors:[{code:"unknown-domain",message:`"${r}" is not a known NAP domain`}]};const u=K[o];if(!u)return{ok:!1,type:o,domain:r,errors:[{code:"unknown-type",message:`"${o}" is not a known ${r} message type`}]};if(u.dir==="in")return{ok:!1,type:o,domain:r,direction:"in",errors:[{code:"inbound-type-emitted",message:`"${o}" is a shell→napplet message; a napplet must not emit it`}]};for(const[n,p]of Object.entries(u.fields??{})){if(!(n in s)||s[n]===void 0){t.push({code:"missing-field",message:`Required field "${n}" is missing`,field:n});continue}U(s[n],p)||t.push({code:"wrong-type",message:`Field "${n}" should be ${p} but is ${M(s[n])}`,field:n})}return{ok:t.length===0,type:o,domain:r,direction:"out",errors:t}}var R=/^[a-z0-9][a-z0-9._:-]*$/,Z=new Set(["application/json","application/ld+json","importmap","speculationrules"]);function z(e){return e.replace(/&(#x?[0-9a-fA-F]+|[a-zA-Z]+);/g,(t,s)=>{if(s[0]==="#"){const i=s[1]==="x"||s[1]==="X"?parseInt(s.slice(2),16):parseInt(s.slice(1),10);return Number.isFinite(i)?String.fromCodePoint(i):t}return{amp:"&",lt:"<",gt:">",quot:'"',apos:"'"}[s]??t})}function S(e,t){const s=/<meta\b[^>]*>/gi;let o;for(;(o=s.exec(e))!==null;){const i=o[0],r=/\bname\s*=\s*(?:"([^"]*)"|'([^']*)')/i.exec(i);if((r?.[1]??r?.[2]??"").trim()!==t)continue;const u=/\bcontent\s*=\s*(?:"([^"]*)"|'([^']*)')/i.exec(i);return z((u?.[1]??u?.[2]??"").trim())}}function J(e){const t=e.replace(/<!--[\s\S]*?-->/g,""),s=/<script\b([\s\S]*?)>/gi,o=[];let i;for(;(i=s.exec(t))!==null;){const r=i[0],d=i[1]??"",u=/\bsrc\s*=\s*(?:"([^"]*)"|'([^']*)')/i.exec(d);if(u){if((u[1]??u[2]??"").trim().length>0)continue;o.push(r);continue}const n=/\btype\s*=\s*(?:"([^"]*)"|'([^']*)')/i.exec(d),p=(n?.[1]??n?.[2]??"").trim().toLowerCase();p&&Z.has(p)||o.push(r)}return o}function L(e){if(Array.isArray(e))return e.some(L);if(e&&typeof e=="object"){const t=e;return"pattern"in t?!0:Object.values(t).some(L)}return!1}function k(e,t={}){const s=[],o=[],i=S(e,"napplet-type");i?R.test(i)||s.push({code:"invalid-napplet-type",message:`napplet-type "${i}" must match ${R}`}):s.push({code:"missing-napplet-type",message:'No <meta name="napplet-type"> declared'});const r=S(e,"napplet-requires"),d=r?r.split(",").map(n=>n.trim()).filter(Boolean):[];for(const n of d){const p=n.startsWith("nap:")?n.slice(4):n;P.includes(p)||s.push({code:"unknown-required-nap",message:`napplet-requires lists "${n}" which is not a known NAP domain`})}const u=S(e,"napplet-config-schema");if(u){let n;try{n=JSON.parse(u)}catch{n=void 0,s.push({code:"invalid-config-schema",message:"napplet-config-schema is not valid JSON"})}n!==void 0&&(typeof n!="object"||n===null||Array.isArray(n)?s.push({code:"invalid-config-schema",message:"napplet-config-schema must be a JSON Schema object"}):L(n)&&s.push({code:"invalid-config-schema",message:"napplet-config-schema uses the `pattern` keyword, which the draft-07 core subset excludes (CVE-2025-69873)"}))}for(const n of J(e))s.push({code:"inline-script",message:`Inline <script> is forbidden under script-src 'self': ${n}`});return{ok:s.length===0,nappletType:i,requires:d,errors:s,warnings:o}}var W="f".repeat(64),Y="https://reference.invalid/blob",l=e=>[e],f=()=>[],X={"relay.subscribe":e=>l({type:"relay.eose",subId:e.subId}),"relay.close":f,"relay.publish":e=>l({type:"relay.publish.result",id:e.id,ok:!0,event:e.event}),"relay.publishEncrypted":e=>l({type:"relay.publishEncrypted.result",id:e.id,ok:!0,event:e.event}),"relay.query":e=>l({type:"relay.query.result",id:e.id,events:[]}),"identity.getPublicKey":e=>l({type:"identity.getPublicKey.result",id:e.id,pubkey:W}),"identity.getRelays":e=>l({type:"identity.getRelays.result",id:e.id,relays:{}}),"identity.getProfile":e=>l({type:"identity.getProfile.result",id:e.id,profile:null}),"identity.getFollows":e=>l({type:"identity.getFollows.result",id:e.id,pubkeys:[]}),"identity.getList":e=>l({type:"identity.getList.result",id:e.id,entries:[]}),"identity.getZaps":e=>l({type:"identity.getZaps.result",id:e.id,zaps:[]}),"identity.getMutes":e=>l({type:"identity.getMutes.result",id:e.id,pubkeys:[]}),"identity.getBlocked":e=>l({type:"identity.getBlocked.result",id:e.id,pubkeys:[]}),"identity.getBadges":e=>l({type:"identity.getBadges.result",id:e.id,badges:[]}),"storage.get":e=>l({type:"storage.get.result",id:e.id,value:null}),"storage.set":e=>l({type:"storage.set.result",id:e.id}),"storage.remove":e=>l({type:"storage.remove.result",id:e.id}),"storage.keys":e=>l({type:"storage.keys.result",id:e.id,keys:[]}),"inc.emit":f,"inc.subscribe":e=>l({type:"inc.subscribe.result",id:e.id}),"inc.unsubscribe":f,"inc.channel.open":e=>l({type:"inc.channel.open.result",id:e.id,channelId:`chan-${String(e.id)}`,peer:"reference-peer"}),"inc.channel.emit":f,"inc.channel.broadcast":f,"inc.channel.list":e=>l({type:"inc.channel.list.result",id:e.id,channels:[]}),"inc.channel.close":f,"theme.get":e=>l({type:"theme.get.result",id:e.id,theme:{colors:{},mode:"dark"}}),"keys.forward":f,"keys.registerAction":e=>l({type:"keys.registerAction.result",id:e.id,actionId:`action-${String(e.id)}`}),"keys.unregisterAction":f,"media.session.create":e=>l({type:"media.session.create.result",id:e.id,sessionId:`session-${String(e.id)}`,owner:e.owner}),"media.session.update":f,"media.session.destroy":f,"media.state":f,"media.capabilities":f,"notify.send":e=>l({type:"notify.send.result",id:e.id,notificationId:`notif-${String(e.id)}`}),"notify.dismiss":f,"notify.badge":f,"notify.channel.register":f,"notify.permission.request":e=>l({type:"notify.permission.result",id:e.id,granted:!0}),"config.registerSchema":e=>l({type:"config.registerSchema.result",id:e.id,ok:!0}),"config.get":e=>l({type:"config.values",id:e.id,values:{}}),"config.subscribe":()=>l({type:"config.values",values:{}}),"config.unsubscribe":f,"config.openSettings":f,"resource.bytes":e=>l({type:"resource.bytes.result",id:e.id,blob:new Blob([]),mime:"application/octet-stream"}),"resource.cancel":f,"cvm.discover":e=>l({type:"cvm.discover.result",id:e.id,servers:[]}),"cvm.request":e=>l({type:"cvm.request.result",id:e.id,message:{}}),"cvm.close":e=>l({type:"cvm.close.result",id:e.id}),"outbox.query":e=>l({type:"outbox.query.result",id:e.id,events:[],relays:{}}),"outbox.subscribe":e=>l({type:"outbox.eose",subId:e.subId}),"outbox.close":f,"outbox.publish":e=>l({type:"outbox.publish.result",id:e.id,ok:!0}),"outbox.resolveRelays":e=>l({type:"outbox.resolveRelays.result",id:e.id,plan:{}}),"upload.upload":e=>l({type:"upload.upload.result",id:e.id,result:{url:Y}}),"upload.status":e=>l({type:"upload.status.result",id:e.id,status:{}}),"intent.invoke":e=>l({type:"intent.invoke.result",id:e.id,result:{}}),"intent.available":e=>l({type:"intent.available.result",id:e.id,availability:{}}),"intent.handlers":e=>l({type:"intent.handlers.result",id:e.id,handlers:[]})};function Q(e={}){const t=e.now??(()=>Date.now()),s={domains:e.capabilities?.domains??[...P],protocols:e.capabilities?.protocols??{}},o=e.services??[],i=e.class??null,r=[];function d(u){const n=u&&typeof u=="object"&&typeof u.type=="string"?u.type:void 0;if(n==="shell.ready")return[{type:"shell.init",capabilities:{domains:s.domains,protocols:s.protocols},services:o,class:i}];if(r.push({envelope:u,verdict:V(u),timestamp:t()}),!n)return[];const p=X[n];return p?p(u):[]}return{capabilities:s,services:o,class:i,get records(){return r},handle:d,reset(){r.length=0}}}function ee(e){return{manifestHtml:e.manifestHtml,sandbox:e.sandbox??{allowScripts:!0,allowSameOrigin:!1},installedGlobal:e.boot.installedGlobal,bootError:e.boot.bootError,emitted:e.boot.emitted,forbiddenGlobals:e.forbiddenGlobals??[],degraded:e.boot.degraded,lifecycle:e.lifecycle??null}}function j(e){return new Promise(t=>setTimeout(t,e))}async function x(e,t,s,o){const i=Q(e?{capabilities:e}:void 0),r=s.createElement("iframe");r.setAttribute("sandbox",t.allowSameOrigin?"allow-scripts allow-same-origin":"allow-scripts"),r.setAttribute("aria-hidden","true"),r.style.position="absolute",r.style.width="1px",r.style.height="1px",r.style.left="-9999px";let d;const u=new Promise(b=>{d=b}),n=b=>{if(b.source!==r.contentWindow)return;const D=i.handle(b.data),_=b.source;for(const F of D)_?.postMessage(F,"*");const $=b.data;$&&typeof $=="object"&&$.type==="shell.ready"&&d()};o.addEventListener("message",n),(s.body??s.documentElement).appendChild(r),r.setAttribute("src",t.url);let p=!1,h=null;const O=Symbol("timeout");await Promise.race([u.then(()=>"ready"),j(t.readyTimeoutMs).then(()=>O)])===O?h=`napplet did not boot (no shell.ready within ${t.readyTimeoutMs}ms; a conformant napplet must run under sandbox="allow-scripts" without allow-same-origin)`:(p=!0,await j(t.settleMs)),o.removeEventListener("message",n);try{r.remove()}catch{}return{installedGlobal:p,bootError:h,emitted:[...i.records]}}async function te(e){const t=e.document??document,s=e.window??window,o={url:e.url,readyTimeoutMs:e.readyTimeoutMs??5e3,settleMs:e.settleMs,allowSameOrigin:e.allowSameOrigin??!1},i=await x(void 0,o,t,s);let r=null;{const d=await x({domains:[],protocols:{}},o,t,s);r={bootError:d.bootError,emitted:d.emitted}}return{installedGlobal:i.installedGlobal,bootError:i.bootError,emitted:i.emitted,degraded:r}}var ie=e=>({status:"pass",detail:e}),se=(e,t)=>({status:"fail",detail:e,diagnostics:t}),re=e=>({status:"skip",detail:e}),c={pass:ie,fail:se,skip:re};function B(e,t){return k(e).errors.filter(s=>t.includes(s.code))}function C(e,t){return new RegExp(`name\\s*=\\s*["']${t}["']`,"i").test(e)}function N(e,t,s,o){return{id:e,area:"manifest",severity:"error",title:t,run:i=>{if(o&&!C(i.manifestHtml,o))return c.skip(`No ${o} declared`);const r=B(i.manifestHtml,s);return r.length?c.fail(r[0].message,r):c.pass()}}}var ne=[N("manifest/napplet-type","Declares a valid napplet-type",["missing-napplet-type","invalid-napplet-type"]),N("manifest/declared-naps","napplet-requires lists only real NAP domains",["unknown-required-nap"],"napplet-requires"),N("manifest/config-schema","Config schema is a draft-07 core subset",["invalid-config-schema"],"napplet-config-schema"),{id:"manifest/no-inline-scripts",area:"manifest",severity:"error",title:"No inline <script> (incompatible with script-src 'self')",run:e=>{const t=B(e.manifestHtml,["inline-script"]);return t.length?c.fail(`${t.length} inline script(s) found`,t):c.pass()}},{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?c.fail("Napplet relied on allow-same-origin, which a shell must not grant"):c.pass():c.fail("Napplet was not loaded with allow-scripts")},{id:"boot/installs-global",area:"boot",severity:"error",title:"Completes the NAP-SHELL handshake (posts shell.ready)",run:e=>e.installedGlobal?c.pass():c.fail("window.napplet was not present after load (no NAP-SHELL shell.ready)")},{id:"boot/no-boot-error",area:"boot",severity:"error",title:"Boots without an uncaught error (observed via NAP-SHELL shell.ready)",run:e=>e.bootError?c.fail(e.bootError):c.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?c.fail(`Accessed forbidden global(s): ${e.forbiddenGlobals.join(", ")}`,e.forbiddenGlobals):c.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 c.skip("Napplet emitted no envelopes");const t=e.emitted.filter(o=>!o.verdict.ok);if(t.length===0)return c.pass(`${e.emitted.length} envelope(s) valid`);const s=t.map(o=>({envelope:o.envelope,errors:o.verdict.errors}));return c.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 c.skip("Napplet emitted no envelopes");if(!C(e.manifestHtml,"napplet-requires"))return c.skip("No napplet-requires to compare against");const t=new Set(k(e.manifestHtml).requires.map(i=>i.startsWith("nap:")?i.slice(4):i)),o=[...new Set(e.emitted.map(i=>i.verdict.domain).filter(i=>typeof i=="string"))].filter(i=>!t.has(i));return o.length?c.fail(`Emitted undeclared NAP domain(s): ${o.join(", ")}`,o):c.pass()}},{id:"degrade/supports-false",area:"degradation",severity:"error",title:"Does not crash when the NAP-SHELL environment offers no capabilities (supports() false)",run:e=>e.degraded?e.degraded.bootError?c.fail(`Crashed under a no-capability shell: ${e.degraded.bootError}`):c.pass():c.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?c.fail("Message listeners remained after unload"):c.pass():c.skip("Lifecycle not measured")}];function oe(e,t={}){const s=t.now??(()=>Date.now()),o=t.checks??ne,i=s(),r=o.map(n=>{const p=n.run(e);return{id:n.id,area:n.area,severity:n.severity,title:n.title,status:p.status,detail:p.detail,diagnostics:p.diagnostics}}),d={total:r.length,passed:r.filter(n=>n.status==="pass").length,failed:r.filter(n=>n.status==="fail").length,skipped:r.filter(n=>n.status==="skip").length,errors:r.filter(n=>n.status==="fail"&&n.severity==="error").length,warnings:r.filter(n=>n.status==="fail"&&n.severity==="warning").length};return{napplet:k(e.manifestHtml).nappletType,startedAt:i,finishedAt:s(),checks:r,summary:d,ok:d.errors===0}}const ae=/\bwindow\s*\.\s*nostr\b|\bglobalThis\s*\.\s*nostr\b/,le="/__conformance__/events",T=new URLSearchParams(location.search),H=T.get("live")==="1";function y(e){return String(e).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}const g=document.getElementById("app");function de(){return g.innerHTML=`
2
+ <header class="masthead">
3
+ <h1>napplet conformance ${H?'<span class="live-badge" id="live-badge">● live</span>':""}</h1>
4
+ <p>Load a napplet by URL and run NAP protocol conformance live. Same engine as <code>napplet-conformance</code> CI.</p>
5
+ </header>
6
+ <form id="run-form" class="runbar">
7
+ <input id="url" type="url" placeholder="https://localhost:5173/ (a napplet served with permissive CORS)" autocomplete="off" />
8
+ <button id="run" type="submit">Run conformance</button>
9
+ <button id="rerun" type="button" class="secondary" hidden>Re-run</button>
10
+ </form>
11
+ <p id="status" class="status" role="status"></p>
12
+ <section id="output" class="output"></section>
13
+ `,{urlInput: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=de();let v="",q=0;function w(e,t="info"){m.status.textContent=e,m.status.dataset.kind=t}function A(e){return String(e).padStart(2,"0")}function ce(e){const t=new Date(e);return`${A(t.getHours())}:${A(t.getMinutes())}:${A(t.getSeconds())}`}async function ue(e){try{const s=await(await fetch(e,{mode:"cors"})).text();return{html:s,forbidden:ae.test(s)?["window.nostr"]:[],fetched:!0}}catch{return{html:"",forbidden:[],fetched:!1}}}function pe(e){const t=e.ok?"conformant":"non-conformant",o=[...new Set(e.checks.map(d=>d.area))].map(d=>{const u=e.checks.filter(n=>n.area===d).map(n=>{const p=n.severity==="warning"&&n.status==="fail"?' <span class="tag warn">warning</span>':"",h=n.detail?`<span class="detail">${y(n.detail)}</span>`:"";return`<li class="check ${n.status}"><span class="badge ${n.status}">${n.status}</span><span class="id">${y(n.id)}</span>${p}${h}</li>`}).join("");return`<div class="area"><h3>${y(d)}</h3><ul>${u}</ul></div>`}).join(""),i=e.summary,r=q>1?`<span class="run-stamp">run #${q} · ${ce(e.finishedAt)}</span>`:"";return`
14
+ <div class="verdict ${t}">
15
+ <span class="result">${e.ok?"CONFORMANT":"NON-CONFORMANT"}</span>
16
+ <span class="counts">${i.passed} passed · ${i.failed} failed · ${i.skipped} skipped${i.warnings?` · ${i.warnings} warning(s)`:""}</span>
17
+ ${r}
18
+ </div>
19
+ <div class="checks">${o}</div>
20
+ `}function fe(e){return e.length===0?'<p class="empty">No envelopes were emitted.</p>':`<ul class="envlog">${e.map(s=>{const o=s.verdict.ok?"ok":"bad",i=s.verdict.ok?"":`<div class="errs">${s.verdict.errors.map(r=>y(`${r.code}: ${r.message}`)).join("<br>")}</div>`;return`<li class="env ${o}"><code>${y(JSON.stringify(s.envelope))}</code>${i}</li>`}).join("")}</ul>`}function me(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=k(e),o=(i,r)=>`<tr><th>${y(i)}</th><td>${r?y(String(r)):"<em>—</em>"}</td></tr>`;return`
21
+ <table class="manifest">
22
+ ${o("napplet-type",s.nappletType)}
23
+ ${o("requires",s.requires.join(", "))}
24
+ </table>
25
+ ${s.errors.length?`<div class="errs">${s.errors.map(i=>y(`${i.code}: ${i.message}`)).join("<br>")}</div>`:""}
26
+ `}function ye(e,t,s,o){m.output.innerHTML=`
27
+ ${pe(e)}
28
+ <div class="panels">
29
+ <section class="panel"><h2>Emitted envelopes (${t.length})</h2>${fe(t)}</section>
30
+ <section class="panel"><h2>Manifest</h2>${me(s,o)}</section>
31
+ </div>
32
+ `}async function E(e){v=e,m.runButton.disabled=!0,m.rerunButton.disabled=!0;try{w(`Fetching manifest for ${e} …`);const{html:t,forbidden:s,fetched:o}=await ue(e);w("Booting napplet in a sandboxed iframe …");const i=await te({url:e,readyTimeoutMs:5e3,settleMs:800,runDegraded:!0}),r=ee({manifestHtml:t,boot:i,forbiddenGlobals:s}),d=oe(r);q+=1,w(d.ok?"Done — conformant.":"Done — non-conformant.",d.ok?"info":"error"),ye(d,i.emitted,t,o),m.rerunButton.hidden=!1}catch(t){w(`Error: ${t instanceof Error?t.message:String(t)}`,"error")}finally{m.runButton.disabled=!1,m.rerunButton.disabled=!1}}function ge(){if(!H||typeof EventSource>"u")return;let e=!1;const t=new EventSource(le);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 s=new URL(location.href);s.searchParams.set("url",t),history.replaceState(null,"",s),E(t)}});m.rerunButton.addEventListener("click",()=>{v&&E(v)});ge();const I=T.get("url");I&&(m.urlInput.value=I,E(I));
@@ -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-iVwcmekY.js"></script>
8
+ <script type="module" crossorigin src="/assets/index-DlgPjL-r.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.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Headless CLI that runs @napplet/conformance against a napplet in real Chromium via Playwright — the `test:conformance` runner",
5
5
  "type": "module",
6
6
  "bin": {
@@ -15,13 +15,13 @@
15
15
  },
16
16
  "dependencies": {
17
17
  "playwright": "^1.59.1",
18
- "@napplet/conformance": "^0.1.0"
18
+ "@napplet/conformance": "^0.2.0"
19
19
  },
20
20
  "devDependencies": {
21
21
  "@types/node": "^22.15.0",
22
22
  "tsup": "^8.5.0",
23
23
  "typescript": "^5.9.3",
24
- "@napplet/conformance-web": "0.0.1"
24
+ "@napplet/conformance-web": "0.0.2"
25
25
  },
26
26
  "license": "MIT",
27
27
  "repository": {
@@ -1,34 +0,0 @@
1
- (function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const n of document.querySelectorAll('link[rel="modulepreload"]'))i(n);new MutationObserver(n=>{for(const r of n)if(r.type==="childList")for(const a of r.addedNodes)a.tagName==="LINK"&&a.rel==="modulepreload"&&i(a)}).observe(document,{childList:!0,subtree:!0});function s(n){const r={};return n.integrity&&(r.integrity=n.integrity),n.referrerPolicy&&(r.referrerPolicy=n.referrerPolicy),n.crossOrigin==="use-credentials"?r.credentials="include":n.crossOrigin==="anonymous"?r.credentials="omit":r.credentials="same-origin",r}function i(n){if(n.ep)return;n.ep=!0;const r=s(n);fetch(n.href,r)}})();var j=["relay","identity","storage","inc","theme","keys","media","notify","config","resource","connect","class","cvm","outbox","upload","intent"],f="[@napplet/nap/connect]",K=new Set(["https","wss","http","ws"]),W={https:"443",wss:"443",http:"80",ws:"80"};function z(e){const t=e.indexOf("://");if(t<0)throw new Error(`${f} origin missing scheme separator '://': ${e}`);const s=e.slice(0,t);if(s!==s.toLowerCase())throw new Error(`${f} scheme must be lowercase: ${e}`);if(!K.has(s))throw new Error(`${f} scheme must be one of https|wss|http|ws: ${e}`);return{scheme:s,rest:e.slice(t+3)}}function V(e,t){if(e.includes("/"))throw new Error(`${f} origin MUST NOT contain a path: ${t}`);if(e.includes("?"))throw new Error(`${f} origin MUST NOT contain a query: ${t}`);if(e.includes("#"))throw new Error(`${f} origin MUST NOT contain a fragment: ${t}`);if(e.includes("*"))throw new Error(`${f} wildcard origins are not permitted: ${t}`);if(e.startsWith("["))throw new Error(`${f} IPv6 literal origins are out of v1 scope: ${t}`)}function J(e,t,s){let i=t;const n=t.lastIndexOf(":");if(n>=0){i=t.slice(0,n);const r=t.slice(n+1);if(!/^[0-9]+$/.test(r))throw new Error(`${f} port must be decimal digits: ${s}`);const a=Number(r);if(a<1||a>65535)throw new Error(`${f} port out of range 1-65535: ${s}`);if(W[e]===r)throw new Error(`${f} default port ${r} MUST be omitted for scheme ${e}: ${s}`)}return Z(i,s),i}function Z(e,t){if(e.includes(":"))throw new Error(`${f} IPv6 literal origins are out of v1 scope: ${t}`);if(e.length===0)throw new Error(`${f} host is empty: ${t}`);if(e!==e.toLowerCase())throw new Error(`${f} host MUST be lowercase: ${t}`);if(!/^[\x00-\x7F]+$/.test(e))throw new Error(`${f} non-ASCII host MUST be Punycode-encoded (xn--...) before calling: ${t}`)}function Y(e,t){if(!/^[0-9]+(\.[0-9]+){3}$/.test(e))return!1;for(const s of e.split(".")){if(s.length>1&&s.startsWith("0"))throw new Error(`${f} IPv4 octet MUST NOT have leading zeros: ${t}`);const i=Number(s);if(!Number.isInteger(i)||i<0||i>255)throw new Error(`${f} IPv4 octet out of range 0-255: ${t}`)}return!0}function X(e,t){for(const s of e.split(".")){if(s.length===0||s.length>63)throw new Error(`${f} invalid host label length: ${t}`);if(!/^[a-z0-9-]+$/.test(s))throw new Error(`${f} host label contains invalid characters: ${t}`);if(s.startsWith("-")||s.endsWith("-"))throw new Error(`${f} host label MUST NOT start or end with hyphen: ${t}`)}}function Q(e){if(typeof e!="string"||e.length===0)throw new Error(`${f} origin must be a non-empty string`);const{scheme:t,rest:s}=z(e);V(s,e);const i=J(t,s,e);return Y(i,e)||X(i,e),e}var o={id:"string"},ee={"relay.subscribe":{dir:"out",fields:{...o,subId:"string",filters:"array"}},"relay.close":{dir:"out",fields:{...o,subId:"string"}},"relay.publish":{dir:"out",fields:{...o,event:"object"}},"relay.publishEncrypted":{dir:"out",fields:{...o,event:"object",recipient:"string"}},"relay.query":{dir:"out",fields:{...o,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:{...o}},"identity.getRelays":{dir:"out",fields:{...o}},"identity.getProfile":{dir:"out",fields:{...o}},"identity.getFollows":{dir:"out",fields:{...o}},"identity.getList":{dir:"out",fields:{...o,listType:"string"}},"identity.getZaps":{dir:"out",fields:{...o}},"identity.getMutes":{dir:"out",fields:{...o}},"identity.getBlocked":{dir:"out",fields:{...o}},"identity.getBadges":{dir:"out",fields:{...o}},"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:{...o,key:"string"}},"storage.set":{dir:"out",fields:{...o,key:"string",value:"string"}},"storage.remove":{dir:"out",fields:{...o,key:"string"}},"storage.keys":{dir:"out",fields:{...o}},"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:{...o,topic:"string"}},"inc.unsubscribe":{dir:"out",fields:{topic:"string"}},"inc.channel.open":{dir:"out",fields:{...o,target:"string"}},"inc.channel.emit":{dir:"out",fields:{channelId:"string"}},"inc.channel.broadcast":{dir:"out"},"inc.channel.list":{dir:"out",fields:{...o}},"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:{...o}},"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:{...o,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:{...o,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:{...o,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:{...o}},"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:{...o,schema:"object"}},"config.get":{dir:"out",fields:{...o}},"config.subscribe":{dir:"out"},"config.unsubscribe":{dir:"out"},"config.openSettings":{dir:"out"},"config.registerSchema.result":{dir:"in"},"config.values":{dir:"in"},"config.schemaError":{dir:"in"},"resource.bytes":{dir:"out",fields:{...o,url:"string"}},"resource.cancel":{dir:"out",fields:{...o}},"resource.bytes.result":{dir:"in"},"resource.bytes.error":{dir:"in"},"class.assigned":{dir:"in"},"cvm.discover":{dir:"out",fields:{...o}},"cvm.request":{dir:"out",fields:{...o,server:"object",message:"object"}},"cvm.close":{dir:"out",fields:{...o,server:"object"}},"cvm.discover.result":{dir:"in"},"cvm.request.result":{dir:"in"},"cvm.close.result":{dir:"in"},"cvm.event":{dir:"in"},"outbox.query":{dir:"out",fields:{...o,filters:"present"}},"outbox.subscribe":{dir:"out",fields:{...o,subId:"string",filters:"present"}},"outbox.close":{dir:"out",fields:{...o,subId:"string"}},"outbox.publish":{dir:"out",fields:{...o,event:"object"}},"outbox.resolveRelays":{dir:"out",fields:{...o,target:"present"}},"outbox.query.result":{dir:"in"},"outbox.event":{dir:"in"},"outbox.eose":{dir:"in"},"outbox.closed":{dir:"in"},"outbox.publish.result":{dir:"in"},"outbox.resolveRelays.result":{dir:"in"},"upload.upload":{dir:"out",fields:{...o,request:"object"}},"upload.status":{dir:"out",fields:{...o,uploadId:"string"}},"upload.upload.result":{dir:"in"},"upload.status.result":{dir:"in"},"upload.status.changed":{dir:"in"},"intent.invoke":{dir:"out",fields:{...o,request:"object"}},"intent.available":{dir:"out",fields:{...o,archetype:"string"}},"intent.handlers":{dir:"out",fields:{...o}},"intent.invoke.result":{dir:"in"},"intent.available.result":{dir:"in"},"intent.handlers.result":{dir:"in"},"intent.changed":{dir:"in"}};function H(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 te(e,t){return t==="present"?e!=null:H(e)===t}function ie(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,i=s.type;if(typeof i!="string")return{ok:!1,errors:[{code:"missing-type",message:"Envelope is missing a string `type` field"}]};const n=i.indexOf(".");if(n<=0)return{ok:!1,type:i,errors:[{code:"malformed-type",message:`Envelope type "${i}" is not in domain.action form`}]};const r=i.slice(0,n);if(!j.includes(r))return{ok:!1,type:i,domain:r,errors:[{code:"unknown-domain",message:`"${r}" is not a known NAP domain`}]};const a=ee[i];if(!a)return{ok:!1,type:i,domain:r,errors:[{code:"unknown-type",message:`"${i}" is not a known ${r} message type`}]};if(a.dir==="in")return{ok:!1,type:i,domain:r,direction:"in",errors:[{code:"inbound-type-emitted",message:`"${i}" is a shell→napplet message; a napplet must not emit it`}]};for(const[p,d]of Object.entries(a.fields??{})){if(!(p in s)||s[p]===void 0){t.push({code:"missing-field",message:`Required field "${p}" is missing`,field:p});continue}te(s[p],d)||t.push({code:"wrong-type",message:`Field "${p}" should be ${d} but is ${H(s[p])}`,field:p})}return{ok:t.length===0,type:i,domain:r,direction:"out",errors:t}}var C=/^[a-z0-9][a-z0-9._:-]*$/,se=/^[0-9a-f]{64}$/,re=new Set(["application/json","application/ld+json","importmap","speculationrules"]);function $(e,t){const s=/<meta\b[^>]*>/gi;let i;for(;(i=s.exec(e))!==null;){const n=i[0],r=/\bname\s*=\s*(?:"([^"]*)"|'([^']*)')/i.exec(n);if((r?.[1]??r?.[2]??"").trim()!==t)continue;const p=/\bcontent\s*=\s*(?:"([^"]*)"|'([^']*)')/i.exec(n);return(p?.[1]??p?.[2]??"").trim()}}function ne(e){const t=e.replace(/<!--[\s\S]*?-->/g,""),s=/<script\b([\s\S]*?)>/gi,i=[];let n;for(;(n=s.exec(t))!==null;){const r=n[0],a=n[1]??"",p=/\bsrc\s*=\s*(?:"([^"]*)"|'([^']*)')/i.exec(a);if(p){if((p[1]??p[2]??"").trim().length>0)continue;i.push(r);continue}const d=/\btype\s*=\s*(?:"([^"]*)"|'([^']*)')/i.exec(a),y=(d?.[1]??d?.[2]??"").trim().toLowerCase();y&&re.has(y)||i.push(r)}return i}function T(e){if(Array.isArray(e))return e.some(T);if(e&&typeof e=="object"){const t=e;return"pattern"in t?!0:Object.values(t).some(T)}return!1}function N(e,t={}){const s=t.requireAggregateHash??!0,i=[],n=[],r=$(e,"napplet-type");r?C.test(r)||i.push({code:"invalid-napplet-type",message:`napplet-type "${r}" must match ${C}`}):i.push({code:"missing-napplet-type",message:'No <meta name="napplet-type"> declared'});const a=$(e,"napplet-aggregate-hash");a?se.test(a)||i.push({code:"invalid-aggregate-hash",message:`napplet-aggregate-hash "${a}" is not a 64-char lowercase hex digest`}):s&&i.push({code:"missing-aggregate-hash",message:'No <meta name="napplet-aggregate-hash"> — the build plugin injects it; build before testing'});const p=$(e,"napplet-requires"),d=p?p.split(",").map(c=>c.trim()).filter(Boolean):[];for(const c of d){const v=c.startsWith("nap:")?c.slice(4):c;j.includes(v)||i.push({code:"unknown-required-nap",message:`napplet-requires lists "${c}" which is not a known NAP domain`})}const y=$(e,"napplet-config-schema");if(y){let c;try{c=JSON.parse(y)}catch{c=void 0,i.push({code:"invalid-config-schema",message:"napplet-config-schema is not valid JSON"})}c!==void 0&&(typeof c!="object"||c===null||Array.isArray(c)?i.push({code:"invalid-config-schema",message:"napplet-config-schema must be a JSON Schema object"}):T(c)&&i.push({code:"invalid-config-schema",message:"napplet-config-schema uses the `pattern` keyword, which the draft-07 core subset excludes (CVE-2025-69873)"}))}const h=$(e,"napplet-connect-requires"),S=h?h.split(/\s+/).map(c=>c.trim()).filter(Boolean):[],q=[];for(const c of S)try{q.push(Q(c))}catch(v){const x=v instanceof Error?v.message:String(v);i.push({code:"invalid-connect-origin",message:`connect origin "${c}" is invalid: ${x}`})}for(const c of ne(e))i.push({code:"inline-script",message:`Inline <script> is forbidden under script-src 'self': ${c}`});return{ok:i.length===0,nappletType:r,aggregateHash:a,requires:d,connectOrigins:q,errors:i,warnings:n}}var oe="f".repeat(64),ae="https://reference.invalid/blob",l=e=>[e],g=()=>[],le={"relay.subscribe":e=>l({type:"relay.eose",subId:e.subId}),"relay.close":g,"relay.publish":e=>l({type:"relay.publish.result",id:e.id,ok:!0,event:e.event}),"relay.publishEncrypted":e=>l({type:"relay.publishEncrypted.result",id:e.id,ok:!0,event:e.event}),"relay.query":e=>l({type:"relay.query.result",id:e.id,events:[]}),"identity.getPublicKey":e=>l({type:"identity.getPublicKey.result",id:e.id,pubkey:oe}),"identity.getRelays":e=>l({type:"identity.getRelays.result",id:e.id,relays:{}}),"identity.getProfile":e=>l({type:"identity.getProfile.result",id:e.id,profile:null}),"identity.getFollows":e=>l({type:"identity.getFollows.result",id:e.id,pubkeys:[]}),"identity.getList":e=>l({type:"identity.getList.result",id:e.id,entries:[]}),"identity.getZaps":e=>l({type:"identity.getZaps.result",id:e.id,zaps:[]}),"identity.getMutes":e=>l({type:"identity.getMutes.result",id:e.id,pubkeys:[]}),"identity.getBlocked":e=>l({type:"identity.getBlocked.result",id:e.id,pubkeys:[]}),"identity.getBadges":e=>l({type:"identity.getBadges.result",id:e.id,badges:[]}),"storage.get":e=>l({type:"storage.get.result",id:e.id,value:null}),"storage.set":e=>l({type:"storage.set.result",id:e.id}),"storage.remove":e=>l({type:"storage.remove.result",id:e.id}),"storage.keys":e=>l({type:"storage.keys.result",id:e.id,keys:[]}),"inc.emit":g,"inc.subscribe":e=>l({type:"inc.subscribe.result",id:e.id}),"inc.unsubscribe":g,"inc.channel.open":e=>l({type:"inc.channel.open.result",id:e.id,channelId:`chan-${String(e.id)}`,peer:"reference-peer"}),"inc.channel.emit":g,"inc.channel.broadcast":g,"inc.channel.list":e=>l({type:"inc.channel.list.result",id:e.id,channels:[]}),"inc.channel.close":g,"theme.get":e=>l({type:"theme.get.result",id:e.id,theme:{colors:{},mode:"dark"}}),"keys.forward":g,"keys.registerAction":e=>l({type:"keys.registerAction.result",id:e.id,actionId:`action-${String(e.id)}`}),"keys.unregisterAction":g,"media.session.create":e=>l({type:"media.session.create.result",id:e.id,sessionId:`session-${String(e.id)}`,owner:e.owner}),"media.session.update":g,"media.session.destroy":g,"media.state":g,"media.capabilities":g,"notify.send":e=>l({type:"notify.send.result",id:e.id,notificationId:`notif-${String(e.id)}`}),"notify.dismiss":g,"notify.badge":g,"notify.channel.register":g,"notify.permission.request":e=>l({type:"notify.permission.result",id:e.id,granted:!0}),"config.registerSchema":e=>l({type:"config.registerSchema.result",id:e.id,ok:!0}),"config.get":e=>l({type:"config.values",id:e.id,values:{}}),"config.subscribe":()=>l({type:"config.values",values:{}}),"config.unsubscribe":g,"config.openSettings":g,"resource.bytes":e=>l({type:"resource.bytes.result",id:e.id,blob:new Blob([]),mime:"application/octet-stream"}),"resource.cancel":g,"cvm.discover":e=>l({type:"cvm.discover.result",id:e.id,servers:[]}),"cvm.request":e=>l({type:"cvm.request.result",id:e.id,message:{}}),"cvm.close":e=>l({type:"cvm.close.result",id:e.id}),"outbox.query":e=>l({type:"outbox.query.result",id:e.id,events:[],relays:{}}),"outbox.subscribe":e=>l({type:"outbox.eose",subId:e.subId}),"outbox.close":g,"outbox.publish":e=>l({type:"outbox.publish.result",id:e.id,ok:!0}),"outbox.resolveRelays":e=>l({type:"outbox.resolveRelays.result",id:e.id,plan:{}}),"upload.upload":e=>l({type:"upload.upload.result",id:e.id,result:{url:ae}}),"upload.status":e=>l({type:"upload.status.result",id:e.id,status:{}}),"intent.invoke":e=>l({type:"intent.invoke.result",id:e.id,result:{}}),"intent.available":e=>l({type:"intent.available.result",id:e.id,availability:{}}),"intent.handlers":e=>l({type:"intent.handlers.result",id:e.id,handlers:[]})};function de(e={}){const t=e.now??(()=>Date.now()),s={naps:e.capabilities?.naps??[...j],sandbox:e.capabilities?.sandbox??[]},i=[];function n(r){const a=r&&typeof r=="object"&&typeof r.type=="string"?r.type:void 0;if(a==="shell.ready")return[{type:"shell.init",capabilities:{naps:s.naps,sandbox:s.sandbox}}];if(i.push({envelope:r,verdict:ie(r),timestamp:t()}),!a)return[];const p=le[a];return p?p(r):[]}return{capabilities:s,get records(){return i},handle:n,reset(){i.length=0}}}function ce(e){return{manifestHtml:e.manifestHtml,sandbox:e.sandbox??{allowScripts:!0,allowSameOrigin:!1},installedGlobal:e.boot.installedGlobal,bootError:e.boot.bootError,emitted:e.boot.emitted,forbiddenGlobals:e.forbiddenGlobals??[],degraded:e.boot.degraded,lifecycle:e.lifecycle??null}}function L(e){return new Promise(t=>setTimeout(t,e))}async function B(e,t,s,i){const n=de(e?{capabilities:e}:void 0),r=s.createElement("iframe");r.setAttribute("sandbox",t.allowSameOrigin?"allow-scripts allow-same-origin":"allow-scripts"),r.setAttribute("aria-hidden","true"),r.style.position="absolute",r.style.width="1px",r.style.height="1px",r.style.left="-9999px";let a;const p=new Promise(c=>{a=c}),d=c=>{if(c.source!==r.contentWindow)return;const v=n.handle(c.data),x=c.source;for(const U of v)x?.postMessage(U,"*");const A=c.data;A&&typeof A=="object"&&A.type==="shell.ready"&&a()};i.addEventListener("message",d),(s.body??s.documentElement).appendChild(r),r.setAttribute("src",t.url);let y=!1,h=null;const S=Symbol("timeout");await Promise.race([p.then(()=>"ready"),L(t.readyTimeoutMs).then(()=>S)])===S?h=`napplet did not boot (no shell.ready within ${t.readyTimeoutMs}ms; a conformant napplet must run under sandbox="allow-scripts" without allow-same-origin)`:(y=!0,await L(t.settleMs)),i.removeEventListener("message",d);try{r.remove()}catch{}return{installedGlobal:y,bootError:h,emitted:[...n.records]}}async function ue(e){const t=e.document??document,s=e.window??window,i={url:e.url,readyTimeoutMs:e.readyTimeoutMs??5e3,settleMs:e.settleMs,allowSameOrigin:e.allowSameOrigin??!1},n=await B(void 0,i,t,s);let r=null;{const a=await B({naps:[],sandbox:[]},i,t,s);r={bootError:a.bootError,emitted:a.emitted}}return{installedGlobal:n.installedGlobal,bootError:n.bootError,emitted:n.emitted,degraded:r}}var pe=e=>({status:"pass",detail:e}),fe=(e,t)=>({status:"fail",detail:e,diagnostics:t}),ge=e=>({status:"skip",detail:e}),u={pass:pe,fail:fe,skip:ge};function _(e,t){return N(e).errors.filter(s=>t.includes(s.code))}function D(e,t){return new RegExp(`name\\s*=\\s*["']${t}["']`,"i").test(e)}function E(e,t,s,i){return{id:e,area:"manifest",severity:"error",title:t,run:n=>{if(i&&!D(n.manifestHtml,i))return u.skip(`No ${i} declared`);const r=_(n.manifestHtml,s);return r.length?u.fail(r[0].message,r):u.pass()}}}var me=[E("manifest/napplet-type","Declares a valid napplet-type",["missing-napplet-type","invalid-napplet-type"]),E("manifest/aggregate-hash","Build injected a valid aggregate hash",["missing-aggregate-hash","invalid-aggregate-hash"]),E("manifest/declared-naps","napplet-requires lists only real NAP domains",["unknown-required-nap"],"napplet-requires"),E("manifest/config-schema","Config schema is a draft-07 core subset",["invalid-config-schema"],"napplet-config-schema"),E("manifest/connect-origins","Declared connect origins are valid",["invalid-connect-origin"],"napplet-connect-requires"),{id:"manifest/no-inline-scripts",area:"manifest",severity:"error",title:"No inline <script> (incompatible with script-src 'self')",run:e=>{const t=_(e.manifestHtml,["inline-script"]);return t.length?u.fail(`${t.length} inline script(s) found`,t):u.pass()}},{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:"Installs window.napplet",run:e=>e.installedGlobal?u.pass():u.fail("window.napplet was not present after 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(i=>!i.verdict.ok);if(t.length===0)return u.pass(`${e.emitted.length} envelope(s) valid`);const s=t.map(i=>({envelope:i.envelope,errors:i.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(!D(e.manifestHtml,"napplet-requires"))return u.skip("No napplet-requires to compare against");const t=new Set(N(e.manifestHtml).requires.map(n=>n.startsWith("nap:")?n.slice(4):n)),i=[...new Set(e.emitted.map(n=>n.verdict.domain).filter(n=>typeof n=="string"))].filter(n=>!t.has(n));return i.length?u.fail(`Emitted undeclared NAP domain(s): ${i.join(", ")}`,i):u.pass()}},{id:"degrade/supports-false",area:"degradation",severity:"error",title:"Does not crash when shell.supports() is false",run:e=>e.degraded?e.degraded.bootError?u.fail(`Crashed under a no-capability shell: ${e.degraded.bootError}`):u.pass():u.skip("Host did not run the degraded (no-capabilities) pass")},{id:"lifecycle/clean-teardown",area:"lifecycle",severity:"warning",title:"Tears down listeners on unload",run:e=>e.lifecycle?e.lifecycle.listenerLeak?u.fail("Message listeners remained after unload"):u.pass():u.skip("Lifecycle not measured")}];function ye(e,t={}){const s=t.now??(()=>Date.now()),i=t.checks??me,n=s(),r=i.map(d=>{const y=d.run(e);return{id:d.id,area:d.area,severity:d.severity,title:d.title,status:y.status,detail:y.detail,diagnostics:y.diagnostics}}),a={total:r.length,passed:r.filter(d=>d.status==="pass").length,failed:r.filter(d=>d.status==="fail").length,skipped:r.filter(d=>d.status==="skip").length,errors:r.filter(d=>d.status==="fail"&&d.severity==="error").length,warnings:r.filter(d=>d.status==="fail"&&d.severity==="warning").length};return{napplet:N(e.manifestHtml).nappletType,startedAt:n,finishedAt:s(),checks:r,summary:a,ok:a.errors===0}}const he=/\bwindow\s*\.\s*nostr\b|\bglobalThis\s*\.\s*nostr\b/,be="/__conformance__/events",F=new URLSearchParams(location.search),G=F.get("live")==="1";function b(e){return String(e).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}const w=document.getElementById("app");function ve(){return w.innerHTML=`
2
- <header class="masthead">
3
- <h1>napplet conformance ${G?'<span class="live-badge" id="live-badge">● live</span>':""}</h1>
4
- <p>Load a napplet by URL and run NAP protocol conformance live. Same engine as <code>napplet-conformance</code> CI.</p>
5
- </header>
6
- <form id="run-form" class="runbar">
7
- <input id="url" type="url" placeholder="https://localhost:5173/ (a napplet served with permissive CORS)" autocomplete="off" />
8
- <button id="run" type="submit">Run conformance</button>
9
- <button id="rerun" type="button" class="secondary" hidden>Re-run</button>
10
- </form>
11
- <p id="status" class="status" role="status"></p>
12
- <section id="output" class="output"></section>
13
- `,{urlInput:w.querySelector("#url"),runButton:w.querySelector("#run"),rerunButton:w.querySelector("#rerun"),liveBadge:w.querySelector("#live-badge")??document.createElement("span"),status:w.querySelector("#status"),output:w.querySelector("#output")}}const m=ve();let k="",M=0;function I(e,t="info"){m.status.textContent=e,m.status.dataset.kind=t}function R(e){return String(e).padStart(2,"0")}function we(e){const t=new Date(e);return`${R(t.getHours())}:${R(t.getMinutes())}:${R(t.getSeconds())}`}async function $e(e){try{const s=await(await fetch(e,{mode:"cors"})).text();return{html:s,forbidden:he.test(s)?["window.nostr"]:[],fetched:!0}}catch{return{html:"",forbidden:[],fetched:!1}}}function Ee(e){const t=e.ok?"conformant":"non-conformant",i=[...new Set(e.checks.map(a=>a.area))].map(a=>{const p=e.checks.filter(d=>d.area===a).map(d=>{const y=d.severity==="warning"&&d.status==="fail"?' <span class="tag warn">warning</span>':"",h=d.detail?`<span class="detail">${b(d.detail)}</span>`:"";return`<li class="check ${d.status}"><span class="badge ${d.status}">${d.status}</span><span class="id">${b(d.id)}</span>${y}${h}</li>`}).join("");return`<div class="area"><h3>${b(a)}</h3><ul>${p}</ul></div>`}).join(""),n=e.summary,r=M>1?`<span class="run-stamp">run #${M} · ${we(e.finishedAt)}</span>`:"";return`
14
- <div class="verdict ${t}">
15
- <span class="result">${e.ok?"CONFORMANT":"NON-CONFORMANT"}</span>
16
- <span class="counts">${n.passed} passed · ${n.failed} failed · ${n.skipped} skipped${n.warnings?` · ${n.warnings} warning(s)`:""}</span>
17
- ${r}
18
- </div>
19
- <div class="checks">${i}</div>
20
- `}function ke(e){return e.length===0?'<p class="empty">No envelopes were emitted.</p>':`<ul class="envlog">${e.map(s=>{const i=s.verdict.ok?"ok":"bad",n=s.verdict.ok?"":`<div class="errs">${s.verdict.errors.map(r=>b(`${r.code}: ${r.message}`)).join("<br>")}</div>`;return`<li class="env ${i}"><code>${b(JSON.stringify(s.envelope))}</code>${n}</li>`}).join("")}</ul>`}function Se(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=N(e),i=(n,r)=>`<tr><th>${b(n)}</th><td>${r?b(String(r)):"<em>—</em>"}</td></tr>`;return`
21
- <table class="manifest">
22
- ${i("napplet-type",s.nappletType)}
23
- ${i("aggregate-hash",s.aggregateHash)}
24
- ${i("requires",s.requires.join(", "))}
25
- ${i("connect origins",s.connectOrigins.join(", "))}
26
- </table>
27
- ${s.errors.length?`<div class="errs">${s.errors.map(n=>b(`${n.code}: ${n.message}`)).join("<br>")}</div>`:""}
28
- `}function Ie(e,t,s,i){m.output.innerHTML=`
29
- ${Ee(e)}
30
- <div class="panels">
31
- <section class="panel"><h2>Emitted envelopes (${t.length})</h2>${ke(t)}</section>
32
- <section class="panel"><h2>Manifest</h2>${Se(s,i)}</section>
33
- </div>
34
- `}async function O(e){k=e,m.runButton.disabled=!0,m.rerunButton.disabled=!0;try{I(`Fetching manifest for ${e} …`);const{html:t,forbidden:s,fetched:i}=await $e(e);I("Booting napplet in a sandboxed iframe …");const n=await ue({url:e,readyTimeoutMs:5e3,settleMs:800,runDegraded:!0}),r=ce({manifestHtml:t,boot:n,forbiddenGlobals:s}),a=ye(r);M+=1,I(a.ok?"Done — conformant.":"Done — non-conformant.",a.ok?"info":"error"),Ie(a,n.emitted,t,i),m.rerunButton.hidden=!1}catch(t){I(`Error: ${t instanceof Error?t.message:String(t)}`,"error")}finally{m.runButton.disabled=!1,m.rerunButton.disabled=!1}}function Ne(){if(!G||typeof EventSource>"u")return;let e=!1;const t=new EventSource(be);t.addEventListener("rerun",()=>{!k||e||(e=!0,setTimeout(()=>{e=!1,O(k)},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 s=new URL(location.href);s.searchParams.set("url",t),history.replaceState(null,"",s),O(t)}});m.rerunButton.addEventListener("click",()=>{k&&O(k)});Ne();const P=F.get("url");P&&(m.urlInput.value=P,O(P));