@napplet/conformance-cli 0.2.18 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/host-bundle.global.js +228 -148
- package/dist/ui/assets/index-BAVQvBDn.js +109 -0
- package/dist/ui/index.html +1 -1
- package/package.json +4 -4
- package/dist/ui/assets/index-BRWua2-f.js +0 -113
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 napplet contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -136,7 +136,7 @@ var NappletConformanceHostBundle = (() => {
|
|
|
136
136
|
// ── resource ─────────────────────────────────────────────────────────────
|
|
137
137
|
"resource.info": { dir: "out", fields: { ...ID } },
|
|
138
138
|
"resource.bytes": { dir: "out", fields: { ...ID, url: "string" } },
|
|
139
|
-
"resource.bytesMany": { dir: "out", fields: { ...ID,
|
|
139
|
+
"resource.bytesMany": { dir: "out", fields: { ...ID, requests: "array" } },
|
|
140
140
|
"resource.cancel": { dir: "out", fields: { ...ID } },
|
|
141
141
|
"resource.info.result": { dir: "in" },
|
|
142
142
|
"resource.info.error": { dir: "in" },
|
|
@@ -185,7 +185,7 @@ var NappletConformanceHostBundle = (() => {
|
|
|
185
185
|
"intent.invoke": { dir: "out", fields: { ...ID, request: "object" } },
|
|
186
186
|
"intent.available": { dir: "out", fields: { ...ID, archetype: "string" } },
|
|
187
187
|
"intent.handlers": { dir: "out", fields: { ...ID } },
|
|
188
|
-
"intent.invoke.result": { dir: "in" },
|
|
188
|
+
"intent.invoke.result": { dir: "in", fields: { ...ID, result: "object" } },
|
|
189
189
|
"intent.available.result": { dir: "in" },
|
|
190
190
|
"intent.handlers.result": { dir: "in" },
|
|
191
191
|
"intent.changed": { dir: "in" },
|
|
@@ -336,6 +336,48 @@ var NappletConformanceHostBundle = (() => {
|
|
|
336
336
|
});
|
|
337
337
|
}
|
|
338
338
|
}
|
|
339
|
+
function validateServerHints(value, field, errors) {
|
|
340
|
+
if (Array.isArray(value) && value.every((server) => typeof server === "string")) return;
|
|
341
|
+
errors.push({
|
|
342
|
+
code: "wrong-type",
|
|
343
|
+
message: `Field "${field}" must be an array of strings`,
|
|
344
|
+
field
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
function validateResourceRequestFields(record, errors) {
|
|
348
|
+
if (record.servers !== void 0) validateServerHints(record.servers, "servers", errors);
|
|
349
|
+
}
|
|
350
|
+
function validateResourceBytesManyRequests(requests, errors) {
|
|
351
|
+
if (!Array.isArray(requests)) return;
|
|
352
|
+
if (requests.length === 0) {
|
|
353
|
+
errors.push({
|
|
354
|
+
code: "invalid-resource-request",
|
|
355
|
+
message: 'Field "requests" must contain at least one resource request',
|
|
356
|
+
field: "requests"
|
|
357
|
+
});
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
for (const [index, request] of requests.entries()) {
|
|
361
|
+
const field = `requests[${index}]`;
|
|
362
|
+
if (typeof request !== "object" || request === null || Array.isArray(request)) {
|
|
363
|
+
errors.push({
|
|
364
|
+
code: "invalid-resource-request",
|
|
365
|
+
message: `Field "${field}" must be a resource request object`,
|
|
366
|
+
field
|
|
367
|
+
});
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
const entry = request;
|
|
371
|
+
if (typeof entry.url !== "string") {
|
|
372
|
+
errors.push({
|
|
373
|
+
code: entry.url === void 0 ? "missing-field" : "wrong-type",
|
|
374
|
+
message: `Field "${field}.url" must be a string`,
|
|
375
|
+
field: `${field}.url`
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
if (entry.servers !== void 0) validateServerHints(entry.servers, `${field}.servers`, errors);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
339
381
|
function validateEnvelope(message) {
|
|
340
382
|
const errors = [];
|
|
341
383
|
if (typeof message !== "object" || message === null || Array.isArray(message)) {
|
|
@@ -396,6 +438,12 @@ var NappletConformanceHostBundle = (() => {
|
|
|
396
438
|
if (type === "intent.invoke") {
|
|
397
439
|
validateIntentInvokeRequest(record.request, errors);
|
|
398
440
|
}
|
|
441
|
+
if (type === "resource.bytes") {
|
|
442
|
+
validateResourceRequestFields(record, errors);
|
|
443
|
+
}
|
|
444
|
+
if (type === "resource.bytesMany") {
|
|
445
|
+
validateResourceBytesManyRequests(record.requests, errors);
|
|
446
|
+
}
|
|
399
447
|
return { ok: errors.length === 0, type, domain, direction: "out", errors };
|
|
400
448
|
}
|
|
401
449
|
var NAPPLET_KIND_SNAPSHOT = 5129;
|
|
@@ -413,14 +461,14 @@ var NappletConformanceHostBundle = (() => {
|
|
|
413
461
|
return NAPPLET_MANIFEST_KINDS.includes(kind);
|
|
414
462
|
}
|
|
415
463
|
function isSha256Hex(value) {
|
|
416
|
-
return typeof value === "string" && /^[0-9a-f]{64}
|
|
464
|
+
return typeof value === "string" && /^[0-9a-f]{64}$/.test(value);
|
|
417
465
|
}
|
|
418
466
|
function manifestDTag(event) {
|
|
419
|
-
const d = firstTag(event, "d")?.[1]
|
|
467
|
+
const d = firstTag(event, "d")?.[1];
|
|
420
468
|
return d || void 0;
|
|
421
469
|
}
|
|
422
470
|
function manifestRequires(event) {
|
|
423
|
-
return event.tags.filter((tag) => tag[0] === "
|
|
471
|
+
return event.tags.filter((tag) => tag[0] === "R").map((tag) => tag[1]?.trim() ?? "").filter(Boolean);
|
|
424
472
|
}
|
|
425
473
|
function validateManifestEvent(event) {
|
|
426
474
|
const errors = [];
|
|
@@ -429,6 +477,7 @@ var NappletConformanceHostBundle = (() => {
|
|
|
429
477
|
return {
|
|
430
478
|
ok: false,
|
|
431
479
|
requires: [],
|
|
480
|
+
optional: [],
|
|
432
481
|
errors: [{ code: "missing-manifest-event", message: "No NIP-5D manifest event was resolved" }],
|
|
433
482
|
warnings
|
|
434
483
|
};
|
|
@@ -441,53 +490,58 @@ var NappletConformanceHostBundle = (() => {
|
|
|
441
490
|
message: `Manifest event kind ${event.kind} is not a NIP-5D napplet kind`
|
|
442
491
|
});
|
|
443
492
|
}
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
});
|
|
449
|
-
} else if ((event.kind === NAPPLET_KIND_ROOT || event.kind === NAPPLET_KIND_SNAPSHOT) && dTag) {
|
|
450
|
-
errors.push({
|
|
451
|
-
code: "unexpected-d-tag",
|
|
452
|
-
message: `Napplet manifest kind ${event.kind} must not include a d tag`
|
|
453
|
-
});
|
|
493
|
+
const identifiers = event.tags.filter((tag) => tag[0] === "d");
|
|
494
|
+
if (event.kind === NAPPLET_KIND_NAMED && (identifiers.length !== 1 || !dTag)) {
|
|
495
|
+
errors.push({ code: "missing-d-tag", message: "Named manifests require exactly one non-empty d tag" });
|
|
496
|
+
} else if (event.kind !== NAPPLET_KIND_NAMED && identifiers.length > 0) {
|
|
497
|
+
errors.push({ code: "unexpected-d-tag", message: "Only named manifests carry a d tag" });
|
|
454
498
|
}
|
|
455
|
-
const
|
|
456
|
-
if (!
|
|
457
|
-
errors.push({
|
|
458
|
-
code: "missing-index-html",
|
|
459
|
-
message: "Napplet manifest must include a path tag for /index.html"
|
|
460
|
-
});
|
|
461
|
-
} else if (!isSha256Hex(indexPath[2])) {
|
|
462
|
-
errors.push({
|
|
463
|
-
code: "invalid-index-html-hash",
|
|
464
|
-
message: "The /index.html path tag must carry a 64-character sha256 hash"
|
|
465
|
-
});
|
|
499
|
+
const hashes = event.tags.filter((tag) => tag[0] === "x");
|
|
500
|
+
if (hashes.length !== 1 || hashes[0].length !== 2 || !isSha256Hex(hashes[0][1])) {
|
|
501
|
+
errors.push({ code: "invalid-artifact-hash", message: "Manifest requires exactly one x tag with the lowercase sha256 of /index.html" });
|
|
466
502
|
}
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
errors.push({
|
|
470
|
-
code: "invalid-required-nap",
|
|
471
|
-
message: `requires tag "${req}" must be a bare NAP domain such as "relay"`
|
|
472
|
-
});
|
|
473
|
-
continue;
|
|
474
|
-
}
|
|
475
|
-
if (!NAP_DOMAINS.includes(req)) {
|
|
476
|
-
errors.push({
|
|
477
|
-
code: "unknown-required-nap",
|
|
478
|
-
message: `requires tag "${req}" is not a known NAP domain`
|
|
479
|
-
});
|
|
480
|
-
}
|
|
503
|
+
if (typeof event.content !== "string" || !event.content.trim()) {
|
|
504
|
+
errors.push({ code: "missing-description", message: "Manifest content must contain a non-empty plain-text description" });
|
|
481
505
|
}
|
|
506
|
+
validateCapabilities(event, errors, warnings);
|
|
507
|
+
validateMetadata(event, errors, warnings);
|
|
482
508
|
return {
|
|
483
509
|
ok: errors.length === 0,
|
|
484
510
|
kind: event.kind,
|
|
485
511
|
dTag,
|
|
486
512
|
requires,
|
|
513
|
+
optional: event.tags.filter((tag) => tag[0] === "O").map((tag) => tag[1]).filter(Boolean),
|
|
487
514
|
errors,
|
|
488
515
|
warnings
|
|
489
516
|
};
|
|
490
517
|
}
|
|
518
|
+
function validateCapabilities(event, errors, warnings) {
|
|
519
|
+
for (const tag of event.tags.filter((tag2) => tag2[0] === "R" || tag2[0] === "O")) {
|
|
520
|
+
const domain = tag[1];
|
|
521
|
+
if (tag.length !== 2 || !domain || /[.:\s]/.test(domain) || domain.startsWith("NAP-")) {
|
|
522
|
+
errors.push({ code: tag[0] === "R" ? "invalid-required-nap" : "invalid-optional-nap", message: `${tag[0]} must name one bare NAP domain` });
|
|
523
|
+
} else if (!NAP_DOMAINS.includes(domain)) {
|
|
524
|
+
warnings.push({ code: "unknown-required-nap", message: `Domain "${domain}" is not in this tool's registry; check its NAP and runtime availability` });
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
function validateMetadata(event, errors, warnings) {
|
|
529
|
+
for (const name of ["title", "source", "a", "A"]) {
|
|
530
|
+
const tags = event.tags.filter((tag) => tag[0] === name);
|
|
531
|
+
if (tags.length > 1 || tags.length && (name === "a" || name === "A") && event.kind !== NAPPLET_KIND_SNAPSHOT) {
|
|
532
|
+
errors.push({ code: "invalid-metadata", message: `Invalid ${name} tag cardinality for kind ${event.kind}` });
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
for (const tag of event.tags.filter((tag2) => tag2[0] === "i")) {
|
|
536
|
+
if (!tag[1]?.trim() || tag[1].includes("?")) {
|
|
537
|
+
errors.push({ code: "invalid-metadata", message: "Intent advertisements must be non-empty queryless identities" });
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
const icons = event.tags.filter((tag) => tag[0] === "icon");
|
|
541
|
+
if (icons.length > 1 || icons.some((tag) => tag.length !== 3 || !isSha256Hex(tag[1]) || !["image/png", "image/jpeg", "image/webp"].includes(tag[2]))) {
|
|
542
|
+
warnings.push({ code: "invalid-icon", message: "Ignore invalid icon metadata and use generic artwork (NIP-5D \xA7Icon)" });
|
|
543
|
+
}
|
|
544
|
+
}
|
|
491
545
|
var REFERENCE_PUBKEY = "f".repeat(64);
|
|
492
546
|
var REFERENCE_ENDPOINT = { dTag: "reference-source" };
|
|
493
547
|
var REFERENCE_BLOB_URL = "https://reference.invalid/blob";
|
|
@@ -587,10 +641,11 @@ var NappletConformanceHostBundle = (() => {
|
|
|
587
641
|
"resource.bytesMany": (e) => ok({
|
|
588
642
|
type: "resource.bytesMany.result",
|
|
589
643
|
id: e.id,
|
|
590
|
-
items: Array.isArray(e.
|
|
644
|
+
items: Array.isArray(e.requests) ? e.requests.map((request) => {
|
|
645
|
+
const url = typeof request === "object" && request !== null && !Array.isArray(request) ? request.url : void 0;
|
|
591
646
|
const decoded = dataUrlToBlob(url);
|
|
592
647
|
return {
|
|
593
|
-
url,
|
|
648
|
+
url: typeof url === "string" ? url : "",
|
|
594
649
|
ok: true,
|
|
595
650
|
blob: decoded?.blob ?? new Blob([]),
|
|
596
651
|
mime: decoded?.mime ?? "application/octet-stream"
|
|
@@ -604,7 +659,8 @@ var NappletConformanceHostBundle = (() => {
|
|
|
604
659
|
schemes: [
|
|
605
660
|
{ scheme: "data", enabled: true },
|
|
606
661
|
{ scheme: "https", enabled: true }
|
|
607
|
-
]
|
|
662
|
+
],
|
|
663
|
+
maxServers: 8
|
|
608
664
|
}
|
|
609
665
|
}),
|
|
610
666
|
"resource.cancel": none,
|
|
@@ -738,118 +794,133 @@ var NappletConformanceHostBundle = (() => {
|
|
|
738
794
|
"fs.watch": (e) => ok({ type: "fs.watch.result", id: e.id, watchId: `watch-${String(e.id)}` }),
|
|
739
795
|
"fs.unwatch": (e) => ok({ type: "fs.unwatch.result", id: e.id })
|
|
740
796
|
};
|
|
741
|
-
function
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
const targetQueues = /* @__PURE__ */ new Map();
|
|
745
|
-
function queueDelivery(target, delivery) {
|
|
746
|
-
const queue = targetQueues.get(target);
|
|
747
|
-
if (queue) {
|
|
748
|
-
queue.push(delivery);
|
|
749
|
-
return;
|
|
750
|
-
}
|
|
751
|
-
targetQueues.set(target, [delivery]);
|
|
752
|
-
}
|
|
753
|
-
function takeDeliveries(target) {
|
|
754
|
-
const queue = targetQueues.get(target) ?? [];
|
|
755
|
-
targetQueues.delete(target);
|
|
756
|
-
return queue;
|
|
757
|
-
}
|
|
758
|
-
function unavailableIntent(id, archetype, action, error) {
|
|
759
|
-
return ok({
|
|
760
|
-
type: "intent.invoke.result",
|
|
761
|
-
id,
|
|
762
|
-
result: { ok: false, archetype, action, handled: false, error }
|
|
763
|
-
});
|
|
764
|
-
}
|
|
765
|
-
function handleIntentInvoke(_endpoint, env) {
|
|
766
|
-
const request = env.request;
|
|
767
|
-
if (typeof request !== "object" || request === null || Array.isArray(request)) {
|
|
768
|
-
return unavailableIntent(env.id, "", "open", "invalid intent request");
|
|
769
|
-
}
|
|
770
|
-
const intent = request;
|
|
771
|
-
const archetype = typeof intent.archetype === "string" ? intent.archetype : "";
|
|
772
|
-
const action = typeof intent.action === "string" ? intent.action : "open";
|
|
773
|
-
if (!archetype) {
|
|
774
|
-
return unavailableIntent(env.id, archetype, action, "intent request requires an archetype");
|
|
775
|
-
}
|
|
776
|
-
if (archetype !== "note") {
|
|
777
|
-
return unavailableIntent(env.id, archetype, action, "no handler");
|
|
778
|
-
}
|
|
779
|
-
if (intent.convention !== void 0 && intent.convention !== REFERENCE_CONVENTION) {
|
|
780
|
-
return unavailableIntent(env.id, archetype, action, "unsupported convention");
|
|
781
|
-
}
|
|
782
|
-
return ok({
|
|
783
|
-
type: "intent.invoke.result",
|
|
784
|
-
id: env.id,
|
|
785
|
-
result: {
|
|
786
|
-
ok: true,
|
|
787
|
-
archetype,
|
|
788
|
-
action,
|
|
789
|
-
handled: true,
|
|
790
|
-
handler: REFERENCE_HANDLER,
|
|
791
|
-
windowId: "reference-window",
|
|
792
|
-
convention: typeof intent.convention === "string" ? intent.convention : REFERENCE_CONVENTION
|
|
793
|
-
}
|
|
794
|
-
});
|
|
795
|
-
}
|
|
796
|
-
function intentAvailability(archetype) {
|
|
797
|
-
if (archetype !== "note") {
|
|
798
|
-
return { archetype, available: false, candidates: [], hasDefault: false };
|
|
799
|
-
}
|
|
800
|
-
return {
|
|
801
|
-
archetype,
|
|
802
|
-
available: true,
|
|
803
|
-
candidates: [{
|
|
804
|
-
dTag: REFERENCE_HANDLER,
|
|
805
|
-
actions: ["open"],
|
|
806
|
-
conventions: [REFERENCE_CONVENTION],
|
|
807
|
-
isDefault: true
|
|
808
|
-
}],
|
|
809
|
-
hasDefault: true
|
|
810
|
-
};
|
|
811
|
-
}
|
|
812
|
-
function handleFrom(endpoint, envelope) {
|
|
813
|
-
const type = envelope && typeof envelope === "object" && typeof envelope.type === "string" ? envelope.type : void 0;
|
|
797
|
+
function createReferenceHandler(options) {
|
|
798
|
+
return (endpoint, envelope) => {
|
|
799
|
+
const type = getEnvelopeType(envelope);
|
|
814
800
|
const verdict = validateEnvelope(envelope);
|
|
815
|
-
records.push({ envelope, verdict, timestamp: now() });
|
|
801
|
+
options.records.push({ envelope, verdict, timestamp: options.now() });
|
|
816
802
|
if (!type || !verdict.ok) return [];
|
|
817
803
|
const env = envelope;
|
|
818
|
-
if (type === "intent.invoke") return
|
|
804
|
+
if (type === "intent.invoke") return options.intents.handleInvoke(endpoint, env);
|
|
819
805
|
if (type === "intent.available") {
|
|
820
|
-
return ok({ type: "intent.available.result", id: env.id, availability:
|
|
806
|
+
return ok({ type: "intent.available.result", id: env.id, availability: options.intents.availability(env.archetype) });
|
|
821
807
|
}
|
|
822
808
|
if (type === "intent.handlers") {
|
|
823
|
-
return ok({ type: "intent.handlers.result", id: env.id, handlers: [
|
|
824
|
-
}
|
|
825
|
-
if (type === "inc.emit") {
|
|
826
|
-
if (env.topic === REFERENCE_CONVENTION) {
|
|
827
|
-
const event = {
|
|
828
|
-
type: "inc.event",
|
|
829
|
-
topic: env.topic,
|
|
830
|
-
sender: endpoint.dTag
|
|
831
|
-
};
|
|
832
|
-
if ("payload" in env) event.payload = env.payload;
|
|
833
|
-
queueDelivery(REFERENCE_SUBSCRIBER, event);
|
|
834
|
-
}
|
|
835
|
-
return [];
|
|
809
|
+
return ok({ type: "intent.handlers.result", id: env.id, handlers: [options.intents.availability("note")] });
|
|
836
810
|
}
|
|
811
|
+
if (type === "inc.emit") return handleIncEmit(endpoint, env, options.queueDelivery);
|
|
837
812
|
const responder = RESPONDERS[type];
|
|
838
813
|
return responder ? responder(env) : [];
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
function getEnvelopeType(envelope) {
|
|
817
|
+
if (!envelope || typeof envelope !== "object") return void 0;
|
|
818
|
+
const type = envelope.type;
|
|
819
|
+
return typeof type === "string" ? type : void 0;
|
|
820
|
+
}
|
|
821
|
+
function handleIncEmit(endpoint, env, queueDelivery) {
|
|
822
|
+
if (env.topic !== REFERENCE_CONVENTION) return [];
|
|
823
|
+
const event = {
|
|
824
|
+
type: "inc.event",
|
|
825
|
+
topic: env.topic,
|
|
826
|
+
sender: endpoint.dTag
|
|
827
|
+
};
|
|
828
|
+
if ("payload" in env) event.payload = env.payload;
|
|
829
|
+
queueDelivery(REFERENCE_SUBSCRIBER, event);
|
|
830
|
+
return [];
|
|
831
|
+
}
|
|
832
|
+
function unavailableIntent(id, archetype, action, error) {
|
|
833
|
+
return ok({
|
|
834
|
+
type: "intent.invoke.result",
|
|
835
|
+
id,
|
|
836
|
+
result: { ok: false, archetype, action, handled: false, error }
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
function createIntentHandlers() {
|
|
840
|
+
return { handleInvoke, availability };
|
|
841
|
+
}
|
|
842
|
+
function handleInvoke(_endpoint, env) {
|
|
843
|
+
const request = env.request;
|
|
844
|
+
if (typeof request !== "object" || request === null || Array.isArray(request)) {
|
|
845
|
+
return unavailableIntent(env.id, "", "open", "invalid intent request");
|
|
846
|
+
}
|
|
847
|
+
const intent = request;
|
|
848
|
+
const archetype = typeof intent.archetype === "string" ? intent.archetype : "";
|
|
849
|
+
const action = typeof intent.action === "string" ? intent.action : "open";
|
|
850
|
+
if (!archetype) {
|
|
851
|
+
return unavailableIntent(env.id, archetype, action, "intent request requires an archetype");
|
|
839
852
|
}
|
|
840
|
-
|
|
841
|
-
|
|
853
|
+
if (archetype !== "note") return unavailableIntent(env.id, archetype, action, "no handler");
|
|
854
|
+
if (intent.convention !== void 0 && intent.convention !== REFERENCE_CONVENTION) {
|
|
855
|
+
return unavailableIntent(env.id, archetype, action, "unsupported convention");
|
|
856
|
+
}
|
|
857
|
+
return ok({
|
|
858
|
+
type: "intent.invoke.result",
|
|
859
|
+
id: env.id,
|
|
860
|
+
result: {
|
|
861
|
+
ok: true,
|
|
862
|
+
archetype,
|
|
863
|
+
action,
|
|
864
|
+
handled: true,
|
|
865
|
+
handler: REFERENCE_HANDLER,
|
|
866
|
+
windowId: "reference-window",
|
|
867
|
+
convention: typeof intent.convention === "string" ? intent.convention : REFERENCE_CONVENTION
|
|
868
|
+
}
|
|
869
|
+
});
|
|
870
|
+
}
|
|
871
|
+
function availability(archetype) {
|
|
872
|
+
if (archetype !== "note") {
|
|
873
|
+
return { archetype, available: false, candidates: [], hasDefault: false };
|
|
842
874
|
}
|
|
875
|
+
return {
|
|
876
|
+
archetype,
|
|
877
|
+
available: true,
|
|
878
|
+
candidates: [{
|
|
879
|
+
dTag: REFERENCE_HANDLER,
|
|
880
|
+
actions: ["open"],
|
|
881
|
+
conventions: [REFERENCE_CONVENTION],
|
|
882
|
+
isDefault: true
|
|
883
|
+
}],
|
|
884
|
+
hasDefault: true
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
function createReferenceShell(options = {}) {
|
|
888
|
+
const records = [];
|
|
889
|
+
const deliveries = createDeliveryQueue();
|
|
890
|
+
const handleFrom = createReferenceHandler({
|
|
891
|
+
now: options.now ?? Date.now,
|
|
892
|
+
records,
|
|
893
|
+
queueDelivery: deliveries.queue,
|
|
894
|
+
intents: createIntentHandlers()
|
|
895
|
+
});
|
|
843
896
|
return {
|
|
844
897
|
get records() {
|
|
845
898
|
return records;
|
|
846
899
|
},
|
|
847
|
-
handle,
|
|
900
|
+
handle: (envelope) => handleFrom(REFERENCE_ENDPOINT, envelope),
|
|
848
901
|
handleFrom,
|
|
849
|
-
takeDeliveries,
|
|
902
|
+
takeDeliveries: deliveries.take,
|
|
850
903
|
reset() {
|
|
851
904
|
records.length = 0;
|
|
852
|
-
|
|
905
|
+
deliveries.clear();
|
|
906
|
+
}
|
|
907
|
+
};
|
|
908
|
+
}
|
|
909
|
+
function createDeliveryQueue() {
|
|
910
|
+
const queues = /* @__PURE__ */ new Map();
|
|
911
|
+
return {
|
|
912
|
+
queue(target, delivery) {
|
|
913
|
+
const queue = queues.get(target);
|
|
914
|
+
if (queue) queue.push(delivery);
|
|
915
|
+
else queues.set(target, [delivery]);
|
|
916
|
+
},
|
|
917
|
+
take(target) {
|
|
918
|
+
const queue = queues.get(target) ?? [];
|
|
919
|
+
queues.delete(target);
|
|
920
|
+
return queue;
|
|
921
|
+
},
|
|
922
|
+
clear() {
|
|
923
|
+
queues.clear();
|
|
853
924
|
}
|
|
854
925
|
};
|
|
855
926
|
}
|
|
@@ -902,11 +973,14 @@ var NappletConformanceHostBundle = (() => {
|
|
|
902
973
|
napplet.resource.info = () =>
|
|
903
974
|
request({ type: 'resource.info' }, 'resource.info.result', 'resource.info.error')
|
|
904
975
|
.then((msg) => msg.info);
|
|
905
|
-
napplet.resource.bytes = (url) =>
|
|
906
|
-
|
|
976
|
+
napplet.resource.bytes = (url, options) => {
|
|
977
|
+
const message = { type: 'resource.bytes', url: String(url) };
|
|
978
|
+
if (options && options.servers !== undefined) message.servers = options.servers;
|
|
979
|
+
return request(message, 'resource.bytes.result', 'resource.bytes.error')
|
|
907
980
|
.then((msg) => msg.blob);
|
|
908
|
-
|
|
909
|
-
|
|
981
|
+
};
|
|
982
|
+
napplet.resource.bytesMany = (requests) =>
|
|
983
|
+
request({ type: 'resource.bytesMany', requests: Array.from(requests || []) }, 'resource.bytesMany.result', 'resource.bytesMany.error')
|
|
910
984
|
.then((msg) => msg.items);
|
|
911
985
|
napplet.resource.bytesAsObjectURL = (url) => {
|
|
912
986
|
const handle = { url: '', revoke: () => {} };
|
|
@@ -1044,13 +1118,14 @@ var NappletConformanceHostBundle = (() => {
|
|
|
1044
1118
|
var skip = (detail) => ({ status: "skip", detail });
|
|
1045
1119
|
var result = { pass, fail, skip };
|
|
1046
1120
|
function manifestErrors(ctx, codes) {
|
|
1047
|
-
|
|
1121
|
+
const verdict = validateManifestEvent(ctx.manifestEvent);
|
|
1122
|
+
return [...verdict.errors, ...verdict.warnings].filter((e) => codes.includes(e.code));
|
|
1048
1123
|
}
|
|
1049
|
-
function manifestCheck(id, title, codes) {
|
|
1124
|
+
function manifestCheck(id, title, codes, severity = "error") {
|
|
1050
1125
|
return {
|
|
1051
1126
|
id,
|
|
1052
1127
|
area: "manifest",
|
|
1053
|
-
severity
|
|
1128
|
+
severity,
|
|
1054
1129
|
title,
|
|
1055
1130
|
run: (ctx) => {
|
|
1056
1131
|
if (!ctx.manifestEvent) return result.skip("No NIP-5D manifest event was resolved");
|
|
@@ -1067,13 +1142,18 @@ var NappletConformanceHostBundle = (() => {
|
|
|
1067
1142
|
"unexpected-d-tag"
|
|
1068
1143
|
]),
|
|
1069
1144
|
manifestCheck("manifest/index-html", "Manifest declares a hashed /index.html", [
|
|
1070
|
-
"
|
|
1071
|
-
"
|
|
1145
|
+
"invalid-artifact-hash",
|
|
1146
|
+
"missing-description",
|
|
1147
|
+
"invalid-metadata"
|
|
1072
1148
|
]),
|
|
1073
|
-
manifestCheck("manifest/requires", "
|
|
1149
|
+
manifestCheck("manifest/requires", "R/O tags name bare NAP domains", [
|
|
1074
1150
|
"invalid-required-nap",
|
|
1075
|
-
"
|
|
1151
|
+
"invalid-optional-nap"
|
|
1076
1152
|
]),
|
|
1153
|
+
// Registry coverage is a tooling advisory, not a protocol requirement.
|
|
1154
|
+
manifestCheck("manifest/requires-known", "R/O domains are in this toolchain registry (advisory)", [
|
|
1155
|
+
"unknown-required-nap"
|
|
1156
|
+
], "warning"),
|
|
1077
1157
|
// ── boot ─────────────────────────────────────────────────────────────────────
|
|
1078
1158
|
{
|
|
1079
1159
|
id: "boot/sandbox-allow-scripts",
|