@napplet/conformance-cli 0.2.2 → 0.2.4

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", "cvm", "outbox", "upload", "intent"];
4
+ var NAP_DOMAINS = ["relay", "identity", "storage", "inc", "theme", "keys", "media", "notify", "config", "resource", "cvm", "outbox", "upload", "intent", "serial"];
5
5
  function createDispatch() {
6
6
  const handlers = /* @__PURE__ */ new Map();
7
7
  function registerNap2(domain, handler) {
@@ -176,7 +176,15 @@ var NappletConformanceHostBundle = (() => {
176
176
  "intent.invoke.result": { dir: "in" },
177
177
  "intent.available.result": { dir: "in" },
178
178
  "intent.handlers.result": { dir: "in" },
179
- "intent.changed": { dir: "in" }
179
+ "intent.changed": { dir: "in" },
180
+ // ── serial ───────────────────────────────────────────────────────────────
181
+ "serial.open": { dir: "out", fields: { ...ID, request: "object" } },
182
+ "serial.write": { dir: "out", fields: { ...ID, sessionId: "string", data: "array" } },
183
+ "serial.close": { dir: "out", fields: { ...ID, sessionId: "string" } },
184
+ "serial.open.result": { dir: "in" },
185
+ "serial.write.result": { dir: "in" },
186
+ "serial.close.result": { dir: "in" },
187
+ "serial.event": { dir: "in" }
180
188
  };
181
189
  function kindOf(value) {
182
190
  if (value === void 0) return "undefined";
@@ -391,7 +399,11 @@ var NappletConformanceHostBundle = (() => {
391
399
  // intent
392
400
  "intent.invoke": (e) => ok({ type: "intent.invoke.result", id: e.id, result: {} }),
393
401
  "intent.available": (e) => ok({ type: "intent.available.result", id: e.id, availability: {} }),
394
- "intent.handlers": (e) => ok({ type: "intent.handlers.result", id: e.id, handlers: [] })
402
+ "intent.handlers": (e) => ok({ type: "intent.handlers.result", id: e.id, handlers: [] }),
403
+ // serial
404
+ "serial.open": (e) => ok({ type: "serial.open.result", id: e.id, session: { id: `serial-${String(e.id)}`, state: "open" } }),
405
+ "serial.write": (e) => ok({ type: "serial.write.result", id: e.id }),
406
+ "serial.close": (e) => ok({ type: "serial.close.result", id: e.id })
395
407
  };
396
408
  function createReferenceShell(options = {}) {
397
409
  const now = options.now ?? (() => Date.now());
@@ -400,7 +412,6 @@ var NappletConformanceHostBundle = (() => {
400
412
  protocols: options.capabilities?.protocols ?? {}
401
413
  };
402
414
  const services = options.services ?? [];
403
- const klass = options.class ?? null;
404
415
  const records = [];
405
416
  function handle(envelope) {
406
417
  const type = envelope && typeof envelope === "object" && typeof envelope.type === "string" ? envelope.type : void 0;
@@ -409,8 +420,7 @@ var NappletConformanceHostBundle = (() => {
409
420
  {
410
421
  type: "shell.init",
411
422
  capabilities: { domains: capabilities.domains, protocols: capabilities.protocols },
412
- services,
413
- class: klass
423
+ services
414
424
  }
415
425
  ];
416
426
  }
@@ -422,7 +432,6 @@ var NappletConformanceHostBundle = (() => {
422
432
  return {
423
433
  capabilities,
424
434
  services,
425
- class: klass,
426
435
  get records() {
427
436
  return records;
428
437
  },
@@ -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","serial"],_=["shell"],a={id:"string"},G={"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"},"serial.open":{dir:"out",fields:{...a,request:"object"}},"serial.write":{dir:"out",fields:{...a,sessionId:"string",data:"array"}},"serial.close":{dir:"out",fields:{...a,sessionId:"string"}},"serial.open.result":{dir:"in"},"serial.write.result":{dir:"in"},"serial.close.result":{dir:"in"},"serial.event":{dir:"in"}};function B(e){if(e===void 0)return"undefined";if(e===null)return"null";if(Array.isArray(e))return"array";const t=typeof e;return t==="string"||t==="number"||t==="boolean"?t:t==="object"?"object":"present"}function K(e,t){return t==="present"?e!=null:B(e)===t}function U(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)||_.includes(r)))return{ok:!1,type:o,domain:r,errors:[{code:"unknown-domain",message:`"${r}" is not a known NAP domain`}]};const p=G[o];if(!p)return{ok:!1,type:o,domain:r,errors:[{code:"unknown-type",message:`"${o}" is not a known ${r} message type`}]};if(p.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,y]of Object.entries(p.fields??{})){if(!(n in s)||s[n]===void 0){t.push({code:"missing-field",message:`Required field "${n}" is missing`,field:n});continue}K(s[n],y)||t.push({code:"wrong-type",message:`Field "${n}" should be ${y} but is ${B(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._:-]*$/;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 p=/\bcontent\s*=\s*(?:"([^"]*)"|'([^']*)')/i.exec(i);return Z((p?.[1]??p?.[2]??"").trim())}}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 y=n.startsWith("nap:")?n.slice(4):n;P.includes(y)||s.push({code:"unknown-required-nap",message:`napplet-requires lists "${n}" which is not a known NAP domain`})}const p=S(e,"napplet-config-schema");if(p){let n;try{n=JSON.parse(p)}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)"}))}return{ok:s.length===0,nappletType:i,requires:d,errors:s,warnings:o}}var z="f".repeat(64),J="https://reference.invalid/blob",l=e=>[e],c=()=>[],V={"relay.subscribe":e=>l({type:"relay.eose",subId:e.subId}),"relay.close":c,"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:z}),"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":c,"inc.subscribe":e=>l({type:"inc.subscribe.result",id:e.id}),"inc.unsubscribe":c,"inc.channel.open":e=>l({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=>l({type:"inc.channel.list.result",id:e.id,channels:[]}),"inc.channel.close":c,"theme.get":e=>l({type:"theme.get.result",id:e.id,theme:{colors:{},mode:"dark"}}),"keys.forward":c,"keys.registerAction":e=>l({type:"keys.registerAction.result",id:e.id,actionId:`action-${String(e.id)}`}),"keys.unregisterAction":c,"media.session.create":e=>l({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=>l({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=>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":c,"config.openSettings":c,"resource.bytes":e=>l({type:"resource.bytes.result",id:e.id,blob:new Blob([]),mime:"application/octet-stream"}),"resource.cancel":c,"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":c,"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:J}}),"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:[]}),"serial.open":e=>l({type:"serial.open.result",id:e.id,session:{id:`serial-${String(e.id)}`,state:"open"}}),"serial.write":e=>l({type:"serial.write.result",id:e.id}),"serial.close":e=>l({type:"serial.close.result",id:e.id})};function W(e={}){const t=e.now??(()=>Date.now()),s={domains:e.capabilities?.domains??[...P],protocols:e.capabilities?.protocols??{}},o=e.services??[],i=[];function r(d){const p=d&&typeof d=="object"&&typeof d.type=="string"?d.type:void 0;if(p==="shell.ready")return[{type:"shell.init",capabilities:{domains:s.domains,protocols:s.protocols},services:o}];if(i.push({envelope:d,verdict:U(d),timestamp:t()}),!p)return[];const n=V[p];return n?n(d):[]}return{capabilities:s,services:o,get records(){return i},handle:r,reset(){i.length=0}}}function Y(e){return{manifestHtml:e.manifestHtml,sandbox:e.sandbox??{allowScripts:!0,allowSameOrigin:!1},installedGlobal:e.boot.installedGlobal,bootError:e.boot.bootError,emitted:e.boot.emitted,forbiddenGlobals:e.forbiddenGlobals??[],degraded:e.boot.degraded,lifecycle:e.lifecycle??null}}function j(e){return new Promise(t=>setTimeout(t,e))}async function x(e,t,s,o){const i=W(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 p=new Promise(b=>{d=b}),n=b=>{if(b.source!==r.contentWindow)return;const H=i.handle(b.data),D=b.source;for(const F of H)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 y=!1,v=null;const O=Symbol("timeout");await Promise.race([p.then(()=>"ready"),j(t.readyTimeoutMs).then(()=>O)])===O?v=`napplet did not boot (no shell.ready within ${t.readyTimeoutMs}ms; a conformant napplet must run under sandbox="allow-scripts" without allow-same-origin)`:(y=!0,await j(t.settleMs)),o.removeEventListener("message",n);try{r.remove()}catch{}return{installedGlobal:y,bootError:v,emitted:[...i.records]}}async function X(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 Q=e=>({status:"pass",detail:e}),ee=(e,t)=>({status:"fail",detail:e,diagnostics:t}),te=e=>({status:"skip",detail:e}),u={pass:Q,fail:ee,skip:te};function ie(e,t){return k(e).errors.filter(s=>t.includes(s.code))}function M(e,t){return new RegExp(`name\\s*=\\s*["']${t}["']`,"i").test(e)}function A(e,t,s,o){return{id:e,area:"manifest",severity:"error",title:t,run:i=>{if(o&&!M(i.manifestHtml,o))return u.skip(`No ${o} declared`);const r=ie(i.manifestHtml,s);return r.length?u.fail(r[0].message,r):u.pass()}}}var se=[A("manifest/napplet-type","Declares a valid napplet-type",["missing-napplet-type","invalid-napplet-type"]),A("manifest/declared-naps","napplet-requires lists only real NAP domains",["unknown-required-nap"],"napplet-requires"),A("manifest/config-schema","Config schema is a draft-07 core subset",["invalid-config-schema"],"napplet-config-schema"),{id:"boot/sandbox-allow-scripts",area:"boot",severity:"error",title:"Runs under allow-scripts without allow-same-origin",run:e=>e.sandbox.allowScripts?e.sandbox.allowSameOrigin?u.fail("Napplet relied on allow-same-origin, which a shell must not grant"):u.pass():u.fail("Napplet was not loaded with allow-scripts")},{id:"boot/installs-global",area:"boot",severity:"error",title:"Completes the NAP-SHELL handshake (posts shell.ready)",run:e=>e.installedGlobal?u.pass():u.fail("window.napplet was not present after load (no NAP-SHELL shell.ready)")},{id:"boot/no-boot-error",area:"boot",severity:"error",title:"Boots without an uncaught error (observed via NAP-SHELL shell.ready)",run:e=>e.bootError?u.fail(e.bootError):u.pass()},{id:"boot/no-forbidden-globals",area:"boot",severity:"error",title:"Does not access forbidden globals (e.g. window.nostr)",run:e=>e.forbiddenGlobals.length?u.fail(`Accessed forbidden global(s): ${e.forbiddenGlobals.join(", ")}`,e.forbiddenGlobals):u.pass()},{id:"wire/envelope-well-formed",area:"wire",severity:"error",title:"Every emitted envelope is well-formed",run:e=>{if(e.emitted.length===0)return u.skip("Napplet emitted no envelopes");const t=e.emitted.filter(o=>!o.verdict.ok);if(t.length===0)return u.pass(`${e.emitted.length} envelope(s) valid`);const s=t.map(o=>({envelope:o.envelope,errors:o.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(!M(e.manifestHtml,"napplet-requires"))return u.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?u.fail(`Emitted undeclared NAP domain(s): ${o.join(", ")}`,o):u.pass()}},{id:"degrade/supports-false",area:"degradation",severity:"error",title:"Does not crash when the NAP-SHELL environment offers no capabilities (supports() false)",run:e=>e.degraded?e.degraded.bootError?u.fail(`Crashed under a no-capability shell: ${e.degraded.bootError}`):u.pass():u.skip("Host did not run the degraded (no-capabilities) pass")},{id:"lifecycle/clean-teardown",area:"lifecycle",severity:"warning",title:"Tears down listeners on unload",run:e=>e.lifecycle?e.lifecycle.listenerLeak?u.fail("Message listeners remained after unload"):u.pass():u.skip("Lifecycle not measured")}];function re(e,t={}){const s=t.now??(()=>Date.now()),o=t.checks??se,i=s(),r=o.map(n=>{const y=n.run(e);return{id:n.id,area:n.area,severity:n.severity,title:n.title,status:y.status,detail:y.detail,diagnostics:y.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 ne=/\bwindow\s*\.\s*nostr\b|\bglobalThis\s*\.\s*nostr\b/,oe="/__conformance__/events",C=new URLSearchParams(location.search),T=C.get("live")==="1";function m(e){return String(e).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}const g=document.getElementById("app");function ae(){return g.innerHTML=`
2
+ <header class="masthead">
3
+ <h1>napplet conformance ${T?'<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 f=ae();let h="",I=0;function w(e,t="info"){f.status.textContent=e,f.status.dataset.kind=t}function N(e){return String(e).padStart(2,"0")}function le(e){const t=new Date(e);return`${N(t.getHours())}:${N(t.getMinutes())}:${N(t.getSeconds())}`}async function de(e){try{const s=await(await fetch(e,{mode:"cors"})).text();return{html:s,forbidden:ne.test(s)?["window.nostr"]:[],fetched:!0}}catch{return{html:"",forbidden:[],fetched:!1}}}function ue(e){const t=e.ok?"conformant":"non-conformant",o=[...new Set(e.checks.map(d=>d.area))].map(d=>{const p=e.checks.filter(n=>n.area===d).map(n=>{const y=n.severity==="warning"&&n.status==="fail"?' <span class="tag warn">warning</span>':"",v=n.detail?`<span class="detail">${m(n.detail)}</span>`:"";return`<li class="check ${n.status}"><span class="badge ${n.status}">${n.status}</span><span class="id">${m(n.id)}</span>${y}${v}</li>`}).join("");return`<div class="area"><h3>${m(d)}</h3><ul>${p}</ul></div>`}).join(""),i=e.summary,r=I>1?`<span class="run-stamp">run #${I} · ${le(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 ce(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=>m(`${r.code}: ${r.message}`)).join("<br>")}</div>`;return`<li class="env ${o}"><code>${m(JSON.stringify(s.envelope))}</code>${i}</li>`}).join("")}</ul>`}function pe(e,t){if(!t)return`<p class="empty">Couldn't fetch the napplet HTML (likely CORS). Serve it with <code>Access-Control-Allow-Origin: *</code>, or use the CLI for the authoritative manifest checks.</p>`;const s=k(e),o=(i,r)=>`<tr><th>${m(i)}</th><td>${r?m(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=>m(`${i.code}: ${i.message}`)).join("<br>")}</div>`:""}
26
+ `}function fe(e,t,s,o){f.output.innerHTML=`
27
+ ${ue(e)}
28
+ <div class="panels">
29
+ <section class="panel"><h2>Emitted envelopes (${t.length})</h2>${ce(t)}</section>
30
+ <section class="panel"><h2>Manifest</h2>${pe(s,o)}</section>
31
+ </div>
32
+ `}async function E(e){h=e,f.runButton.disabled=!0,f.rerunButton.disabled=!0;try{w(`Fetching manifest for ${e} …`);const{html:t,forbidden:s,fetched:o}=await de(e);w("Booting napplet in a sandboxed iframe …");const i=await X({url:e,readyTimeoutMs:5e3,settleMs:800,runDegraded:!0}),r=Y({manifestHtml:t,boot:i,forbiddenGlobals:s}),d=re(r);I+=1,w(d.ok?"Done — conformant.":"Done — non-conformant.",d.ok?"info":"error"),fe(d,i.emitted,t,o),f.rerunButton.hidden=!1}catch(t){w(`Error: ${t instanceof Error?t.message:String(t)}`,"error")}finally{f.runButton.disabled=!1,f.rerunButton.disabled=!1}}function ye(){if(!T||typeof EventSource>"u")return;let e=!1;const t=new EventSource(oe);t.addEventListener("rerun",()=>{!h||e||(e=!0,setTimeout(()=>{e=!1,E(h)},50))}),t.addEventListener("open",()=>{f.liveBadge&&(f.liveBadge.dataset.state="connected")}),t.addEventListener("error",()=>{f.liveBadge&&(f.liveBadge.dataset.state="disconnected")})}f.runButton.parentElement.addEventListener("submit",e=>{e.preventDefault();const t=f.urlInput.value.trim();if(t){const s=new URL(location.href);s.searchParams.set("url",t),history.replaceState(null,"",s),E(t)}});f.rerunButton.addEventListener("click",()=>{h&&E(h)});ye();const q=C.get("url");q&&(f.urlInput.value=q,E(q));
@@ -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-Cn9jwNt-.js"></script>
8
+ <script type="module" crossorigin src="/assets/index-ps53p3X8.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.2",
3
+ "version": "0.2.4",
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.2.1"
18
+ "@napplet/conformance": "^0.4.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.2"
24
+ "@napplet/conformance-web": "0.0.4"
25
25
  },
26
26
  "license": "MIT",
27
27
  "repository": {
@@ -1,32 +0,0 @@
1
- (function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const 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 I=["relay","identity","storage","inc","theme","keys","media","notify","config","resource","cvm","outbox","upload","intent"],_=["shell"],a={id:"string"},G={"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 B(e){if(e===void 0)return"undefined";if(e===null)return"null";if(Array.isArray(e))return"array";const t=typeof e;return t==="string"||t==="number"||t==="boolean"?t:t==="object"?"object":"present"}function K(e,t){return t==="present"?e!=null:B(e)===t}function U(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(!(I.includes(r)||_.includes(r)))return{ok:!1,type:o,domain:r,errors:[{code:"unknown-domain",message:`"${r}" is not a known NAP domain`}]};const c=G[o];if(!c)return{ok:!1,type:o,domain:r,errors:[{code:"unknown-type",message:`"${o}" is not a known ${r} message type`}]};if(c.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,f]of Object.entries(c.fields??{})){if(!(n in s)||s[n]===void 0){t.push({code:"missing-field",message:`Required field "${n}" is missing`,field:n});continue}K(s[n],f)||t.push({code:"wrong-type",message:`Field "${n}" should be ${f} but is ${B(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._:-]*$/;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 c=/\bcontent\s*=\s*(?:"([^"]*)"|'([^']*)')/i.exec(i);return Z((c?.[1]??c?.[2]??"").trim())}}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 f=n.startsWith("nap:")?n.slice(4):n;I.includes(f)||s.push({code:"unknown-required-nap",message:`napplet-requires lists "${n}" which is not a known NAP domain`})}const c=S(e,"napplet-config-schema");if(c){let n;try{n=JSON.parse(c)}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)"}))}return{ok:s.length===0,nappletType:i,requires:d,errors:s,warnings:o}}var z="f".repeat(64),J="https://reference.invalid/blob",l=e=>[e],p=()=>[],V={"relay.subscribe":e=>l({type:"relay.eose",subId:e.subId}),"relay.close":p,"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:z}),"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":p,"inc.subscribe":e=>l({type:"inc.subscribe.result",id:e.id}),"inc.unsubscribe":p,"inc.channel.open":e=>l({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=>l({type:"inc.channel.list.result",id:e.id,channels:[]}),"inc.channel.close":p,"theme.get":e=>l({type:"theme.get.result",id:e.id,theme:{colors:{},mode:"dark"}}),"keys.forward":p,"keys.registerAction":e=>l({type:"keys.registerAction.result",id:e.id,actionId:`action-${String(e.id)}`}),"keys.unregisterAction":p,"media.session.create":e=>l({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=>l({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=>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":p,"config.openSettings":p,"resource.bytes":e=>l({type:"resource.bytes.result",id:e.id,blob:new Blob([]),mime:"application/octet-stream"}),"resource.cancel":p,"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":p,"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:J}}),"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 W(e={}){const t=e.now??(()=>Date.now()),s={domains:e.capabilities?.domains??[...I],protocols:e.capabilities?.protocols??{}},o=e.services??[],i=e.class??null,r=[];function d(c){const n=c&&typeof c=="object"&&typeof c.type=="string"?c.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:c,verdict:U(c),timestamp:t()}),!n)return[];const f=V[n];return f?f(c):[]}return{capabilities:s,services:o,class:i,get records(){return r},handle:d,reset(){r.length=0}}}function Y(e){return{manifestHtml:e.manifestHtml,sandbox:e.sandbox??{allowScripts:!0,allowSameOrigin:!1},installedGlobal:e.boot.installedGlobal,bootError:e.boot.bootError,emitted:e.boot.emitted,forbiddenGlobals:e.forbiddenGlobals??[],degraded:e.boot.degraded,lifecycle:e.lifecycle??null}}function j(e){return new Promise(t=>setTimeout(t,e))}async function x(e,t,s,o){const i=W(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 c=new Promise(b=>{d=b}),n=b=>{if(b.source!==r.contentWindow)return;const H=i.handle(b.data),D=b.source;for(const F of H)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 f=!1,v=null;const O=Symbol("timeout");await Promise.race([c.then(()=>"ready"),j(t.readyTimeoutMs).then(()=>O)])===O?v=`napplet did not boot (no shell.ready within ${t.readyTimeoutMs}ms; a conformant napplet must run under sandbox="allow-scripts" without allow-same-origin)`:(f=!0,await j(t.settleMs)),o.removeEventListener("message",n);try{r.remove()}catch{}return{installedGlobal:f,bootError:v,emitted:[...i.records]}}async function X(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 Q=e=>({status:"pass",detail:e}),ee=(e,t)=>({status:"fail",detail:e,diagnostics:t}),te=e=>({status:"skip",detail:e}),u={pass:Q,fail:ee,skip:te};function ie(e,t){return k(e).errors.filter(s=>t.includes(s.code))}function M(e,t){return new RegExp(`name\\s*=\\s*["']${t}["']`,"i").test(e)}function A(e,t,s,o){return{id:e,area:"manifest",severity:"error",title:t,run:i=>{if(o&&!M(i.manifestHtml,o))return u.skip(`No ${o} declared`);const r=ie(i.manifestHtml,s);return r.length?u.fail(r[0].message,r):u.pass()}}}var se=[A("manifest/napplet-type","Declares a valid napplet-type",["missing-napplet-type","invalid-napplet-type"]),A("manifest/declared-naps","napplet-requires lists only real NAP domains",["unknown-required-nap"],"napplet-requires"),A("manifest/config-schema","Config schema is a draft-07 core subset",["invalid-config-schema"],"napplet-config-schema"),{id:"boot/sandbox-allow-scripts",area:"boot",severity:"error",title:"Runs under allow-scripts without allow-same-origin",run:e=>e.sandbox.allowScripts?e.sandbox.allowSameOrigin?u.fail("Napplet relied on allow-same-origin, which a shell must not grant"):u.pass():u.fail("Napplet was not loaded with allow-scripts")},{id:"boot/installs-global",area:"boot",severity:"error",title:"Completes the NAP-SHELL handshake (posts shell.ready)",run:e=>e.installedGlobal?u.pass():u.fail("window.napplet was not present after load (no NAP-SHELL shell.ready)")},{id:"boot/no-boot-error",area:"boot",severity:"error",title:"Boots without an uncaught error (observed via NAP-SHELL shell.ready)",run:e=>e.bootError?u.fail(e.bootError):u.pass()},{id:"boot/no-forbidden-globals",area:"boot",severity:"error",title:"Does not access forbidden globals (e.g. window.nostr)",run:e=>e.forbiddenGlobals.length?u.fail(`Accessed forbidden global(s): ${e.forbiddenGlobals.join(", ")}`,e.forbiddenGlobals):u.pass()},{id:"wire/envelope-well-formed",area:"wire",severity:"error",title:"Every emitted envelope is well-formed",run:e=>{if(e.emitted.length===0)return u.skip("Napplet emitted no envelopes");const t=e.emitted.filter(o=>!o.verdict.ok);if(t.length===0)return u.pass(`${e.emitted.length} envelope(s) valid`);const s=t.map(o=>({envelope:o.envelope,errors:o.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(!M(e.manifestHtml,"napplet-requires"))return u.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?u.fail(`Emitted undeclared NAP domain(s): ${o.join(", ")}`,o):u.pass()}},{id:"degrade/supports-false",area:"degradation",severity:"error",title:"Does not crash when the NAP-SHELL environment offers no capabilities (supports() false)",run:e=>e.degraded?e.degraded.bootError?u.fail(`Crashed under a no-capability shell: ${e.degraded.bootError}`):u.pass():u.skip("Host did not run the degraded (no-capabilities) pass")},{id:"lifecycle/clean-teardown",area:"lifecycle",severity:"warning",title:"Tears down listeners on unload",run:e=>e.lifecycle?e.lifecycle.listenerLeak?u.fail("Message listeners remained after unload"):u.pass():u.skip("Lifecycle not measured")}];function re(e,t={}){const s=t.now??(()=>Date.now()),o=t.checks??se,i=s(),r=o.map(n=>{const f=n.run(e);return{id:n.id,area:n.area,severity:n.severity,title:n.title,status:f.status,detail:f.detail,diagnostics:f.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 ne=/\bwindow\s*\.\s*nostr\b|\bglobalThis\s*\.\s*nostr\b/,oe="/__conformance__/events",C=new URLSearchParams(location.search),T=C.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 ae(){return g.innerHTML=`
2
- <header class="masthead">
3
- <h1>napplet conformance ${T?'<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=ae();let h="",P=0;function w(e,t="info"){m.status.textContent=e,m.status.dataset.kind=t}function N(e){return String(e).padStart(2,"0")}function le(e){const t=new Date(e);return`${N(t.getHours())}:${N(t.getMinutes())}:${N(t.getSeconds())}`}async function de(e){try{const s=await(await fetch(e,{mode:"cors"})).text();return{html:s,forbidden:ne.test(s)?["window.nostr"]:[],fetched:!0}}catch{return{html:"",forbidden:[],fetched:!1}}}function ue(e){const t=e.ok?"conformant":"non-conformant",o=[...new Set(e.checks.map(d=>d.area))].map(d=>{const c=e.checks.filter(n=>n.area===d).map(n=>{const f=n.severity==="warning"&&n.status==="fail"?' <span class="tag warn">warning</span>':"",v=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>${f}${v}</li>`}).join("");return`<div class="area"><h3>${y(d)}</h3><ul>${c}</ul></div>`}).join(""),i=e.summary,r=P>1?`<span class="run-stamp">run #${P} · ${le(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 ce(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 pe(e,t){if(!t)return`<p class="empty">Couldn't fetch the napplet HTML (likely CORS). Serve it with <code>Access-Control-Allow-Origin: *</code>, or use the CLI for the authoritative manifest checks.</p>`;const 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 fe(e,t,s,o){m.output.innerHTML=`
27
- ${ue(e)}
28
- <div class="panels">
29
- <section class="panel"><h2>Emitted envelopes (${t.length})</h2>${ce(t)}</section>
30
- <section class="panel"><h2>Manifest</h2>${pe(s,o)}</section>
31
- </div>
32
- `}async function E(e){h=e,m.runButton.disabled=!0,m.rerunButton.disabled=!0;try{w(`Fetching manifest for ${e} …`);const{html:t,forbidden:s,fetched:o}=await de(e);w("Booting napplet in a sandboxed iframe …");const i=await X({url:e,readyTimeoutMs:5e3,settleMs:800,runDegraded:!0}),r=Y({manifestHtml:t,boot:i,forbiddenGlobals:s}),d=re(r);P+=1,w(d.ok?"Done — conformant.":"Done — non-conformant.",d.ok?"info":"error"),fe(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 me(){if(!T||typeof EventSource>"u")return;let e=!1;const t=new EventSource(oe);t.addEventListener("rerun",()=>{!h||e||(e=!0,setTimeout(()=>{e=!1,E(h)},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",()=>{h&&E(h)});me();const q=C.get("url");q&&(m.urlInput.value=q,E(q));