@napplet/conformance-cli 0.2.3 → 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());
|
|
@@ -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,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")}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));
|
package/dist/ui/index.html
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>napplet conformance runtime</title>
|
|
7
7
|
<meta name="description" content="Load a napplet and run NAP protocol conformance checks live in your browser." />
|
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-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.
|
|
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.
|
|
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.
|
|
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 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,m]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],m)||t.push({code:"wrong-type",message:`Field "${n}" should be ${m} 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 m=n.startsWith("nap:")?n.slice(4):n;I.includes(m)||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:[]})};function W(e={}){const t=e.now??(()=>Date.now()),s={domains:e.capabilities?.domains??[...I],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 m=!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)`:(m=!0,await j(t.settleMs)),o.removeEventListener("message",n);try{r.remove()}catch{}return{installedGlobal:m,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 m=n.run(e);return{id:n.id,area:n.area,severity:n.severity,title:n.title,status:m.status,detail:m.detail,diagnostics:m.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,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")}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="",P=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 m=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>${m}${v}</li>`}).join("");return`<div class="area"><h3>${y(d)}</h3><ul>${p}</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){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);P+=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 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",()=>{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)});me();const q=C.get("url");q&&(f.urlInput.value=q,E(q));
|