@kehto/nip 0.5.1 → 0.6.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/README.md +11 -1
- package/dist/5d/index.d.ts +57 -46
- package/dist/5d/index.js +1 -1
- package/dist/{chunk-YIG2IRN2.js → chunk-3JJBAODT.js} +202 -98
- package/dist/chunk-3JJBAODT.js.map +1 -0
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-YIG2IRN2.js.map +0 -1
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ any NIP that isn't referenced.
|
|
|
11
11
|
| Subpath | NIP | What it provides |
|
|
12
12
|
|---------|-----|------------------|
|
|
13
13
|
| [`@kehto/nip/5a`](./src/5a/README.md) | NIP-5A | aggregate hash compute/verify over `path` tags (`computeAggregateHash`, `verifyAggregate`) |
|
|
14
|
-
| [`@kehto/nip/5d`](./src/5d/README.md) | NIP-5D | content-addressed napplet manifest resolution — parse, verify
|
|
14
|
+
| [`@kehto/nip/5d`](./src/5d/README.md) | NIP-5D | content-addressed napplet manifest resolution — parse, verify signature + artifact bytes (or legacy aggregate/blobs) (`resolveNapplet`) with optional Cache Storage artifact caching |
|
|
15
15
|
| [`@kehto/nip/51`](./src/51/README.md) | NIP-51 | lists & sets parser — mute/bookmarks/relay-sets/emoji-sets/… (`parseList`) |
|
|
16
16
|
| [`@kehto/nip/65`](./src/65/README.md) | NIP-65 | relay-list (kind 10002) parsing + outbox/inbox resolution (`createNip65Registry`) |
|
|
17
17
|
| [`@kehto/nip/66`](./src/66/README.md) | NIP-66 | kind-30166 relay-discovery aggregator (`createNip66Aggregator`) |
|
|
@@ -87,3 +87,13 @@ Every NIP module is:
|
|
|
87
87
|
4. Re-export it from `src/index.ts` for the barrel, and add the
|
|
88
88
|
`@kehto/nip/<n>` alias to the repo `vitest.config.ts`.
|
|
89
89
|
5. Add a row to the table above.
|
|
90
|
+
|
|
91
|
+
## NIP-5D event compatibility
|
|
92
|
+
|
|
93
|
+
Current manifests use a direct artifact `x` hash, plain-text `content`, independent
|
|
94
|
+
`z`/`i` routing declarations, and required `R` / optional `O` domains. Kehto also
|
|
95
|
+
accepts legacy aggregate events through an isolated compatibility adapter.
|
|
96
|
+
Existing `aggregateHash` host/cache/ACL fields carry the verified artifact hash
|
|
97
|
+
for current events; legacy identities keep their original aggregate. Both paths
|
|
98
|
+
verify signatures and bytes before runtime injection and `srcdoc` execution.
|
|
99
|
+
For the schema and removal boundary, see [event migration](https://kehto.github.io/web/docs/migrations/NIP-5D-EVENT-SCHEMA.html).
|
package/dist/5d/index.d.ts
CHANGED
|
@@ -233,22 +233,23 @@ declare class CacheStorageNappletArtifactCache implements NappletArtifactCache {
|
|
|
233
233
|
*/
|
|
234
234
|
declare function openNappletArtifactCache(options?: OpenNappletArtifactCacheOptions): Promise<CacheStorageNappletArtifactCache | undefined>;
|
|
235
235
|
|
|
236
|
+
/** Error codes for every napplet resolution failure path. */
|
|
237
|
+
type NappletResolutionErrorCode = 'invalid-signature' | 'invalid-manifest' | 'aggregate-mismatch' | 'blob-hash-mismatch' | 'blob-unavailable' | 'missing-index';
|
|
236
238
|
/**
|
|
237
|
-
*
|
|
238
|
-
*
|
|
239
|
-
*
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
*
|
|
248
|
-
*
|
|
249
|
-
*
|
|
250
|
-
*
|
|
251
|
-
*
|
|
239
|
+
* Thrown on any napplet resolution/verification failure. The `code` field
|
|
240
|
+
* identifies which guard rejected, so callers can fail closed without parsing
|
|
241
|
+
* the message.
|
|
242
|
+
*/
|
|
243
|
+
declare class NappletResolutionError extends Error {
|
|
244
|
+
readonly code: NappletResolutionErrorCode;
|
|
245
|
+
constructor(code: NappletResolutionErrorCode, message: string);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* `@kehto/nip/5d` — verify signed napplet manifests and their content bytes.
|
|
250
|
+
* Current events use one artifact x hash. The isolated legacy adapter accepts
|
|
251
|
+
* path tags plus an aggregate x marker. Both formats share signature checking,
|
|
252
|
+
* blob verification, caching, and verified srcdoc output.
|
|
252
253
|
* @module
|
|
253
254
|
*/
|
|
254
255
|
/** Snapshot manifest — regular event, immutable point-in-time release. */
|
|
@@ -268,26 +269,48 @@ declare const NAPPLET_KINDS: readonly number[];
|
|
|
268
269
|
declare function isNappletManifestKind(kind: number): boolean;
|
|
269
270
|
/**
|
|
270
271
|
* A parsed NIP-5D napplet manifest. All fields are derived from the manifest
|
|
271
|
-
* event
|
|
272
|
-
*
|
|
272
|
+
* event. Declarations are untrusted until {@link resolveNapplet} verifies both
|
|
273
|
+
* the signature and the content bytes.
|
|
273
274
|
*/
|
|
274
275
|
interface NappletManifest {
|
|
276
|
+
/** Event format selected by the parser. Absent only in older host-created values. */
|
|
277
|
+
format?: 'current' | 'legacy';
|
|
278
|
+
/** Signed artifact hash for current events; legacy aggregate for older events. */
|
|
279
|
+
artifactHash?: string;
|
|
280
|
+
/** Optional domains; absence never prevents loading and presence grants nothing. */
|
|
281
|
+
optional?: string[];
|
|
282
|
+
/** Independent current z declarations, preserved even without accepted intents. */
|
|
283
|
+
archetypeSlugs?: string[];
|
|
284
|
+
/** Accepted queryless intents and advertised parameter names from i tags. */
|
|
285
|
+
intents?: Array<{
|
|
286
|
+
identity: string;
|
|
287
|
+
parameters: string[];
|
|
288
|
+
}>;
|
|
289
|
+
/** Valid supported icon declaration. Hosts may keep generic artwork. */
|
|
290
|
+
icon?: {
|
|
291
|
+
sha256: string;
|
|
292
|
+
mimeType: 'image/png' | 'image/jpeg' | 'image/webp';
|
|
293
|
+
};
|
|
294
|
+
/** Snapshot provenance only; never resolved or trusted on its behalf. */
|
|
295
|
+
parent?: string;
|
|
296
|
+
/** Snapshot root provenance only. */
|
|
297
|
+
root?: string;
|
|
275
298
|
/** Manifest event kind (`5129` / `15129` / `35129`). */
|
|
276
299
|
kind: number;
|
|
277
300
|
/** Author hex public key. */
|
|
278
301
|
pubkey: string;
|
|
279
302
|
/** Named-napplet `d` identifier, or `''` for root/snapshot manifests. */
|
|
280
303
|
dTag: string;
|
|
281
|
-
/**
|
|
304
|
+
/** Normalized files: one /index.html for current events, legacy path entries otherwise. */
|
|
282
305
|
paths: PathEntry[];
|
|
283
|
-
/**
|
|
306
|
+
/** Compatibility name for the signed content identity (current artifact or legacy aggregate). */
|
|
284
307
|
aggregateHash: string;
|
|
285
308
|
/** Blossom server URL hints from `server` tags. */
|
|
286
309
|
servers: string[];
|
|
287
|
-
/**
|
|
310
|
+
/** Required bare domains from current R tags or legacy requires tags. */
|
|
288
311
|
requires: string[];
|
|
289
312
|
/**
|
|
290
|
-
*
|
|
313
|
+
* Routing projection: independent current z/i sets, or exact legacy archetype pairs.
|
|
291
314
|
*/
|
|
292
315
|
archetypes: Array<{
|
|
293
316
|
slug: string;
|
|
@@ -295,32 +318,18 @@ interface NappletManifest {
|
|
|
295
318
|
}>;
|
|
296
319
|
/** Optional human title. */
|
|
297
320
|
title?: string;
|
|
298
|
-
/**
|
|
321
|
+
/** Plain-text event content (legacy description tag for older events). */
|
|
299
322
|
description?: string;
|
|
300
323
|
/** Optional upstream source URL from the `source` tag. */
|
|
301
324
|
source?: string;
|
|
302
325
|
}
|
|
303
|
-
|
|
304
|
-
type NappletResolutionErrorCode = 'invalid-signature' | 'invalid-manifest' | 'aggregate-mismatch' | 'blob-hash-mismatch' | 'blob-unavailable' | 'missing-index';
|
|
305
|
-
/**
|
|
306
|
-
* Thrown on any napplet resolution/verification failure. The `code` field
|
|
307
|
-
* identifies which guard rejected, so callers can fail closed without parsing
|
|
308
|
-
* the message.
|
|
309
|
-
*/
|
|
310
|
-
declare class NappletResolutionError extends Error {
|
|
311
|
-
readonly code: NappletResolutionErrorCode;
|
|
312
|
-
constructor(code: NappletResolutionErrorCode, message: string);
|
|
313
|
-
}
|
|
326
|
+
|
|
314
327
|
/**
|
|
315
|
-
*
|
|
316
|
-
*
|
|
317
|
-
*
|
|
318
|
-
*
|
|
319
|
-
*
|
|
320
|
-
* @param event - A NIP-5D manifest event (`5129` / `15129` / `35129`)
|
|
321
|
-
* @returns The parsed manifest
|
|
322
|
-
* @throws {@link NappletResolutionError} `invalid-manifest` for a non-napplet
|
|
323
|
-
* kind, missing `path` tags, or a missing aggregate `x` tag
|
|
328
|
+
* Normalize a current or legacy NIP-5D event without verifying its signature.
|
|
329
|
+
* @param event - Candidate manifest; use resolveNapplet before trusting it.
|
|
330
|
+
* @returns Manifest with normalized identity, paths, and routing declarations.
|
|
331
|
+
* @example
|
|
332
|
+
* const manifest = parseNappletManifest(event);
|
|
324
333
|
*/
|
|
325
334
|
declare function parseNappletManifest(event: NostrEvent): NappletManifest;
|
|
326
335
|
/**
|
|
@@ -353,9 +362,11 @@ declare function verifyBlobHash(bytes: Uint8Array, sha256Hex: string): boolean;
|
|
|
353
362
|
declare function fetchBlob(servers: readonly string[], sha256Hex: string, fetchBytes: (url: string) => Promise<Uint8Array>): Promise<Uint8Array>;
|
|
354
363
|
/** A fully verified napplet, ready to inject via `iframe.srcdoc`. */
|
|
355
364
|
interface ResolvedNapplet {
|
|
365
|
+
/** Verified content identity; current artifact SHA-256 or legacy aggregate. */
|
|
366
|
+
artifactHash: string;
|
|
356
367
|
/** Computed `d` identifier (`''` for root/snapshot). */
|
|
357
368
|
dTag: string;
|
|
358
|
-
/**
|
|
369
|
+
/** Compatibility alias for artifactHash; preserves existing host and ACL APIs. */
|
|
359
370
|
aggregateHash: string;
|
|
360
371
|
/** Verified file bytes keyed by manifest path. */
|
|
361
372
|
files: Map<string, Uint8Array>;
|
|
@@ -379,14 +390,14 @@ interface ResolveNappletOptions {
|
|
|
379
390
|
/**
|
|
380
391
|
* Optional verified artifact cache. Cache hits are still re-verified against
|
|
381
392
|
* the manifest hash before use; cache writes happen only after the signature,
|
|
382
|
-
*
|
|
393
|
+
* content identity, and every blob hash have been verified.
|
|
383
394
|
*/
|
|
384
395
|
cache?: NappletArtifactCache;
|
|
385
396
|
}
|
|
386
397
|
/**
|
|
387
398
|
* Resolve a napplet end-to-end from a candidate manifest event: verify the
|
|
388
|
-
* signature, parse
|
|
389
|
-
*
|
|
399
|
+
* signature, parse its schema, fetch and hash its artifact, and return verified
|
|
400
|
+
* `/index.html`. Legacy events additionally verify their NIP-5A aggregate.
|
|
390
401
|
*
|
|
391
402
|
* The returned `(dTag, aggregateHash)` is computed from the verified bytes and
|
|
392
403
|
* is the napplet's identity. Any failure throws a {@link NappletResolutionError}
|
package/dist/5d/index.js
CHANGED
|
@@ -9,6 +9,199 @@ import { verifyEvent } from "nostr-tools/pure";
|
|
|
9
9
|
import { sha256 } from "@noble/hashes/sha2.js";
|
|
10
10
|
import { bytesToHex } from "@noble/hashes/utils.js";
|
|
11
11
|
|
|
12
|
+
// src/5d/errors.ts
|
|
13
|
+
var NappletResolutionError = class extends Error {
|
|
14
|
+
code;
|
|
15
|
+
constructor(code, message) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.name = "NappletResolutionError";
|
|
18
|
+
this.code = code;
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// src/5d/legacy-manifest.ts
|
|
23
|
+
function firstTagValue(tags, name) {
|
|
24
|
+
for (const tag of tags) {
|
|
25
|
+
if (tag[0] === name && typeof tag[1] === "string" && tag[1].length > 0) return tag[1];
|
|
26
|
+
}
|
|
27
|
+
return void 0;
|
|
28
|
+
}
|
|
29
|
+
function allTagValues(tags, name) {
|
|
30
|
+
const out = [];
|
|
31
|
+
for (const tag of tags) {
|
|
32
|
+
if (tag[0] === name && typeof tag[1] === "string" && tag[1].length > 0) out.push(tag[1]);
|
|
33
|
+
}
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
function archetypesFromTags(tags) {
|
|
37
|
+
const out = [];
|
|
38
|
+
for (const tag of tags) {
|
|
39
|
+
if (tag[0] !== "archetype") continue;
|
|
40
|
+
const slug = tag[1];
|
|
41
|
+
if (typeof slug !== "string" || !/^[a-z0-9][a-z0-9-]*$/.test(slug)) {
|
|
42
|
+
throw new NappletResolutionError(
|
|
43
|
+
"invalid-manifest",
|
|
44
|
+
"archetype slug must contain lowercase letters, numbers, and hyphens"
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
const convention = tag[2];
|
|
48
|
+
if (typeof convention !== "string" || convention.length === 0) {
|
|
49
|
+
throw new NappletResolutionError("invalid-manifest", "archetype convention is required");
|
|
50
|
+
}
|
|
51
|
+
if (/^NAP-\d+$/.test(convention)) {
|
|
52
|
+
throw new NappletResolutionError(
|
|
53
|
+
"invalid-manifest",
|
|
54
|
+
"numbered NAP identifier is not an archetype convention"
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
if (!/^napplet:[^/?#\s]+\/[^/?#\s]+$/.test(convention)) {
|
|
58
|
+
throw new NappletResolutionError(
|
|
59
|
+
"invalid-manifest",
|
|
60
|
+
"archetype convention must be a queryless napplet:<archetype>/<intent> identity"
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
if (tag.length !== 3) {
|
|
64
|
+
throw new NappletResolutionError(
|
|
65
|
+
"invalid-manifest",
|
|
66
|
+
"archetype tags must contain exactly slug and convention"
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
out.push({ slug, convention });
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
function parseLegacyManifest(event) {
|
|
74
|
+
const paths = pathEntriesFromTags(event.tags);
|
|
75
|
+
if (paths.length === 0) {
|
|
76
|
+
throw new NappletResolutionError("invalid-manifest", "manifest has no path tags");
|
|
77
|
+
}
|
|
78
|
+
const aggregateHash = aggregateTagValue(event.tags);
|
|
79
|
+
if (!aggregateHash) {
|
|
80
|
+
throw new NappletResolutionError("invalid-manifest", "manifest has no aggregate x tag");
|
|
81
|
+
}
|
|
82
|
+
const dTag = firstTagValue(event.tags, "d");
|
|
83
|
+
if (event.kind !== 35129 && dTag !== void 0) {
|
|
84
|
+
throw new NappletResolutionError("invalid-manifest", "only named manifests may carry a d tag");
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
format: "legacy",
|
|
88
|
+
artifactHash: aggregateHash,
|
|
89
|
+
optional: [],
|
|
90
|
+
kind: event.kind,
|
|
91
|
+
pubkey: event.pubkey,
|
|
92
|
+
dTag: dTag ?? "",
|
|
93
|
+
paths,
|
|
94
|
+
aggregateHash,
|
|
95
|
+
servers: allTagValues(event.tags, "server"),
|
|
96
|
+
requires: allTagValues(event.tags, "requires"),
|
|
97
|
+
archetypes: archetypesFromTags(event.tags),
|
|
98
|
+
title: firstTagValue(event.tags, "title"),
|
|
99
|
+
description: firstTagValue(event.tags, "description"),
|
|
100
|
+
source: firstTagValue(event.tags, "source")
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
function verifyLegacyAggregate(manifest) {
|
|
104
|
+
const recomputed = computeAggregateHash(manifest.paths);
|
|
105
|
+
if (recomputed !== manifest.aggregateHash) {
|
|
106
|
+
throw new NappletResolutionError(
|
|
107
|
+
"aggregate-mismatch",
|
|
108
|
+
`recomputed aggregate ${recomputed} != manifest ${manifest.aggregateHash}`
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
var warnedLegacyManifest = false;
|
|
113
|
+
function warnLegacyManifest() {
|
|
114
|
+
if (warnedLegacyManifest) return;
|
|
115
|
+
warnedLegacyManifest = true;
|
|
116
|
+
console.warn("[KEHTO_COMPAT_LEGACY_MANIFEST] Legacy aggregate manifest resolved. Republish with @napplet/vite-plugin >=0.15 using the current single-artifact schema. See docs/compatibility.md.");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// src/5d/current-manifest.ts
|
|
120
|
+
var HASH = /^[a-f0-9]{64}$/;
|
|
121
|
+
var DOMAIN = /^[a-z][a-z0-9-]*$/;
|
|
122
|
+
var SLUG = /^[a-z0-9][a-z0-9-]*$/;
|
|
123
|
+
var INTENT = /^napplet:[^/?#\s]+\/[^/?#\s]+$/;
|
|
124
|
+
function invalid(message) {
|
|
125
|
+
throw new NappletResolutionError("invalid-manifest", message);
|
|
126
|
+
}
|
|
127
|
+
function single(tags, name) {
|
|
128
|
+
const matches = tags.filter((tag) => tag[0] === name);
|
|
129
|
+
if (matches.length > 1 || matches.some((tag) => tag.length !== 2 || !tag[1])) {
|
|
130
|
+
invalid(`manifest ${name} tag must have one value and occur at most once`);
|
|
131
|
+
}
|
|
132
|
+
return matches[0]?.[1];
|
|
133
|
+
}
|
|
134
|
+
function declarations(tags, name, pattern) {
|
|
135
|
+
return [...new Set(tags.filter((tag) => tag[0] === name).map((tag) => {
|
|
136
|
+
if (tag.length !== 2 || !pattern.test(tag[1])) invalid(`invalid ${name} declaration`);
|
|
137
|
+
return tag[1];
|
|
138
|
+
}))];
|
|
139
|
+
}
|
|
140
|
+
function iconFromTags(tags) {
|
|
141
|
+
const icons = tags.filter((tag) => tag[0] === "icon");
|
|
142
|
+
if (icons.length !== 1) return void 0;
|
|
143
|
+
const [, hash, mimeType] = icons[0];
|
|
144
|
+
if (icons[0].length !== 3 || !HASH.test(hash)) return void 0;
|
|
145
|
+
if (mimeType !== "image/png" && mimeType !== "image/jpeg" && mimeType !== "image/webp") {
|
|
146
|
+
return void 0;
|
|
147
|
+
}
|
|
148
|
+
return { sha256: hash, mimeType };
|
|
149
|
+
}
|
|
150
|
+
function parseCurrentManifest(event) {
|
|
151
|
+
const tags = event.tags;
|
|
152
|
+
const artifactHash = single(tags, "x");
|
|
153
|
+
if (!artifactHash || !HASH.test(artifactHash)) invalid("invalid artifact x hash");
|
|
154
|
+
if (typeof event.content !== "string" || !event.content.trim()) {
|
|
155
|
+
invalid("manifest content must be a non-empty plain-text description");
|
|
156
|
+
}
|
|
157
|
+
const dTag = single(tags, "d");
|
|
158
|
+
if (event.kind === 35129 ? dTag === void 0 : dTag !== void 0) {
|
|
159
|
+
invalid("exactly one d tag is required only on named manifests");
|
|
160
|
+
}
|
|
161
|
+
const parent = single(tags, "a");
|
|
162
|
+
const root = single(tags, "A");
|
|
163
|
+
for (const address of [parent, root]) {
|
|
164
|
+
if (address === void 0) continue;
|
|
165
|
+
if (event.kind !== 5129 || !/^(35129:[a-f0-9]{64}:.+|15129:[a-f0-9]{64}:)$/.test(address)) {
|
|
166
|
+
invalid("lineage must be a napplet address on a snapshot");
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
const archetypeSlugs = declarations(tags, "z", SLUG);
|
|
170
|
+
const intents = tags.filter((tag) => tag[0] === "i").map((tag) => {
|
|
171
|
+
const [, identity, ...parameters] = tag;
|
|
172
|
+
if (!identity || !INTENT.test(identity) || parameters.some((parameter) => !parameter || /\s/.test(parameter))) {
|
|
173
|
+
invalid("invalid accepted intent or parameter name");
|
|
174
|
+
}
|
|
175
|
+
return { identity, parameters };
|
|
176
|
+
});
|
|
177
|
+
return {
|
|
178
|
+
format: "current",
|
|
179
|
+
kind: event.kind,
|
|
180
|
+
pubkey: event.pubkey,
|
|
181
|
+
dTag: dTag ?? "",
|
|
182
|
+
artifactHash,
|
|
183
|
+
aggregateHash: artifactHash,
|
|
184
|
+
paths: [{ path: "/index.html", sha256: artifactHash }],
|
|
185
|
+
servers: tags.filter((tag) => tag[0] === "server" && tag[1]).map((tag) => tag[1]),
|
|
186
|
+
requires: declarations(tags, "R", DOMAIN),
|
|
187
|
+
optional: declarations(tags, "O", DOMAIN),
|
|
188
|
+
archetypeSlugs,
|
|
189
|
+
intents,
|
|
190
|
+
// NAP-INTENT keeps roles and conventions orthogonal. The current schema
|
|
191
|
+
// advertises independent sets; legacy explicit pairings stay in its adapter.
|
|
192
|
+
archetypes: archetypeSlugs.flatMap((slug) => intents.map(({ identity }) => ({
|
|
193
|
+
slug,
|
|
194
|
+
convention: identity
|
|
195
|
+
}))),
|
|
196
|
+
title: single(tags, "title"),
|
|
197
|
+
description: event.content,
|
|
198
|
+
source: single(tags, "source"),
|
|
199
|
+
icon: iconFromTags(tags),
|
|
200
|
+
parent,
|
|
201
|
+
root
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
12
205
|
// src/5d/artifact-cache.ts
|
|
13
206
|
var MIB = 1024 * 1024;
|
|
14
207
|
var NAPPLET_ARTIFACT_CACHE_NAME = "kehto:napplet-artifacts:v1";
|
|
@@ -421,100 +614,15 @@ var NAPPLET_KINDS = [
|
|
|
421
614
|
function isNappletManifestKind(kind) {
|
|
422
615
|
return NAPPLET_KINDS.includes(kind);
|
|
423
616
|
}
|
|
424
|
-
var NappletResolutionError = class extends Error {
|
|
425
|
-
code;
|
|
426
|
-
constructor(code, message) {
|
|
427
|
-
super(message);
|
|
428
|
-
this.name = "NappletResolutionError";
|
|
429
|
-
this.code = code;
|
|
430
|
-
}
|
|
431
|
-
};
|
|
432
|
-
function firstTagValue(tags, name) {
|
|
433
|
-
for (const tag of tags) {
|
|
434
|
-
if (tag[0] === name && typeof tag[1] === "string" && tag[1].length > 0) return tag[1];
|
|
435
|
-
}
|
|
436
|
-
return void 0;
|
|
437
|
-
}
|
|
438
|
-
function allTagValues(tags, name) {
|
|
439
|
-
const out = [];
|
|
440
|
-
for (const tag of tags) {
|
|
441
|
-
if (tag[0] === name && typeof tag[1] === "string" && tag[1].length > 0) out.push(tag[1]);
|
|
442
|
-
}
|
|
443
|
-
return out;
|
|
444
|
-
}
|
|
445
|
-
function archetypesFromTags(tags) {
|
|
446
|
-
const out = [];
|
|
447
|
-
for (const tag of tags) {
|
|
448
|
-
if (tag[0] !== "archetype") continue;
|
|
449
|
-
const slug = tag[1];
|
|
450
|
-
if (typeof slug !== "string" || !/^[a-z0-9][a-z0-9-]*$/.test(slug)) {
|
|
451
|
-
throw new NappletResolutionError(
|
|
452
|
-
"invalid-manifest",
|
|
453
|
-
"archetype slug must contain lowercase letters, numbers, and hyphens"
|
|
454
|
-
);
|
|
455
|
-
}
|
|
456
|
-
const convention = tag[2];
|
|
457
|
-
if (typeof convention !== "string" || convention.length === 0) {
|
|
458
|
-
throw new NappletResolutionError("invalid-manifest", "archetype convention is required");
|
|
459
|
-
}
|
|
460
|
-
if (/^NAP-\d+$/.test(convention)) {
|
|
461
|
-
throw new NappletResolutionError(
|
|
462
|
-
"invalid-manifest",
|
|
463
|
-
"numbered NAP identifier is not an archetype convention"
|
|
464
|
-
);
|
|
465
|
-
}
|
|
466
|
-
const conventionMatch = /^napplet:([^/?#\s]+)\/([^/?#\s]+)$/.exec(convention);
|
|
467
|
-
if (!conventionMatch) {
|
|
468
|
-
throw new NappletResolutionError(
|
|
469
|
-
"invalid-manifest",
|
|
470
|
-
"archetype convention must be a queryless napplet:<archetype>/<intent> identity"
|
|
471
|
-
);
|
|
472
|
-
}
|
|
473
|
-
if (conventionMatch[1] !== slug) {
|
|
474
|
-
throw new NappletResolutionError(
|
|
475
|
-
"invalid-manifest",
|
|
476
|
-
"archetype slug must match the convention archetype"
|
|
477
|
-
);
|
|
478
|
-
}
|
|
479
|
-
if (tag.length !== 3) {
|
|
480
|
-
throw new NappletResolutionError(
|
|
481
|
-
"invalid-manifest",
|
|
482
|
-
"archetype tags must contain exactly slug and convention"
|
|
483
|
-
);
|
|
484
|
-
}
|
|
485
|
-
out.push({ slug, convention });
|
|
486
|
-
}
|
|
487
|
-
return out;
|
|
488
|
-
}
|
|
489
617
|
function parseNappletManifest(event) {
|
|
490
618
|
if (!isNappletManifestKind(event.kind)) {
|
|
491
619
|
throw new NappletResolutionError("invalid-manifest", `not a NIP-5D napplet kind: ${event.kind}`);
|
|
492
620
|
}
|
|
493
|
-
const
|
|
494
|
-
if (
|
|
495
|
-
throw new NappletResolutionError("invalid-manifest", "manifest
|
|
496
|
-
}
|
|
497
|
-
const aggregateHash = aggregateTagValue(event.tags);
|
|
498
|
-
if (!aggregateHash) {
|
|
499
|
-
throw new NappletResolutionError("invalid-manifest", "manifest has no aggregate x tag");
|
|
500
|
-
}
|
|
501
|
-
const dTag = firstTagValue(event.tags, "d");
|
|
502
|
-
if (event.kind !== NAPPLET_KIND_NAMED && dTag !== void 0) {
|
|
503
|
-
throw new NappletResolutionError("invalid-manifest", "only named manifests may carry a d tag");
|
|
621
|
+
const hashes = event.tags.filter((tag) => tag[0] === "x");
|
|
622
|
+
if (hashes.length !== 1) {
|
|
623
|
+
throw new NappletResolutionError("invalid-manifest", "manifest must carry exactly one x tag");
|
|
504
624
|
}
|
|
505
|
-
return
|
|
506
|
-
kind: event.kind,
|
|
507
|
-
pubkey: event.pubkey,
|
|
508
|
-
dTag: dTag ?? "",
|
|
509
|
-
paths,
|
|
510
|
-
aggregateHash,
|
|
511
|
-
servers: allTagValues(event.tags, "server"),
|
|
512
|
-
requires: allTagValues(event.tags, "requires"),
|
|
513
|
-
archetypes: archetypesFromTags(event.tags),
|
|
514
|
-
title: firstTagValue(event.tags, "title"),
|
|
515
|
-
description: firstTagValue(event.tags, "description"),
|
|
516
|
-
source: firstTagValue(event.tags, "source")
|
|
517
|
-
};
|
|
625
|
+
return hashes[0][2] === "aggregate" ? parseLegacyManifest(event) : parseCurrentManifest(event);
|
|
518
626
|
}
|
|
519
627
|
function verifyManifestSignature(event) {
|
|
520
628
|
try {
|
|
@@ -550,13 +658,7 @@ async function resolveNapplet(options) {
|
|
|
550
658
|
throw new NappletResolutionError("invalid-signature", "manifest signature is invalid");
|
|
551
659
|
}
|
|
552
660
|
const manifest = parseNappletManifest(event);
|
|
553
|
-
|
|
554
|
-
if (recomputed !== manifest.aggregateHash) {
|
|
555
|
-
throw new NappletResolutionError(
|
|
556
|
-
"aggregate-mismatch",
|
|
557
|
-
`recomputed aggregate ${recomputed} != manifest ${manifest.aggregateHash}`
|
|
558
|
-
);
|
|
559
|
-
}
|
|
661
|
+
if (manifest.format === "legacy") verifyLegacyAggregate(manifest);
|
|
560
662
|
const files = /* @__PURE__ */ new Map();
|
|
561
663
|
for (const entry of manifest.paths) {
|
|
562
664
|
let bytes = await cache?.readBlob(entry.sha256);
|
|
@@ -579,7 +681,9 @@ async function resolveNapplet(options) {
|
|
|
579
681
|
}
|
|
580
682
|
const indexHtml = textDecode(files.get(indexEntry.path));
|
|
581
683
|
await cache?.writeVerifiedResolution({ event, manifest, files, indexHtml });
|
|
684
|
+
if (manifest.format === "legacy") warnLegacyManifest();
|
|
582
685
|
return {
|
|
686
|
+
artifactHash: manifest.aggregateHash,
|
|
583
687
|
dTag: manifest.dTag,
|
|
584
688
|
aggregateHash: manifest.aggregateHash,
|
|
585
689
|
files,
|
|
@@ -589,6 +693,7 @@ async function resolveNapplet(options) {
|
|
|
589
693
|
}
|
|
590
694
|
|
|
591
695
|
export {
|
|
696
|
+
NappletResolutionError,
|
|
592
697
|
NAPPLET_ARTIFACT_CACHE_NAME,
|
|
593
698
|
coordinateKey,
|
|
594
699
|
isCoordinateFresh,
|
|
@@ -599,11 +704,10 @@ export {
|
|
|
599
704
|
NAPPLET_KIND_NAMED,
|
|
600
705
|
NAPPLET_KINDS,
|
|
601
706
|
isNappletManifestKind,
|
|
602
|
-
NappletResolutionError,
|
|
603
707
|
parseNappletManifest,
|
|
604
708
|
verifyManifestSignature,
|
|
605
709
|
verifyBlobHash,
|
|
606
710
|
fetchBlob,
|
|
607
711
|
resolveNapplet
|
|
608
712
|
};
|
|
609
|
-
//# sourceMappingURL=chunk-
|
|
713
|
+
//# sourceMappingURL=chunk-3JJBAODT.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/5d/index.ts","../src/5d/errors.ts","../src/5d/legacy-manifest.ts","../src/5d/current-manifest.ts","../src/5d/artifact-cache.ts"],"sourcesContent":["import type { NostrEvent } from 'nostr-tools';\nimport { verifyEvent } from 'nostr-tools/pure';\nimport { sha256 } from '@noble/hashes/sha2.js';\nimport { bytesToHex } from '@noble/hashes/utils.js';\nimport type { PathEntry } from '../5a/index.js';\nimport { NappletResolutionError } from './errors.js';\nimport { parseLegacyManifest, verifyLegacyAggregate, warnLegacyManifest } from './legacy-manifest.js';\nimport { parseCurrentManifest } from './current-manifest.js';\nimport type { NappletArtifactCache } from './artifact-cache.js';\nexport {\n CacheStorageNappletArtifactCache,\n NAPPLET_ARTIFACT_CACHE_NAME,\n coordinateKey,\n isCoordinateFresh,\n openNappletArtifactCache,\n type CachedCoordinate,\n type CacheStorageNappletArtifactCacheOptions,\n type CoordinateFreshnessOptions,\n type NappletAggregateIndexEntry,\n type NappletArtifactCacheIndex,\n type NappletArtifactCache,\n type NappletBlobIndexEntry,\n type NappletCacheDiagnostic,\n type OpenNappletArtifactCacheOptions,\n type WriteVerifiedResolutionInput,\n} from './artifact-cache.js';\n\n/**\n * `@kehto/nip/5d` — verify signed napplet manifests and their content bytes.\n * Current events use one artifact x hash. The isolated legacy adapter accepts\n * path tags plus an aggregate x marker. Both formats share signature checking,\n * blob verification, caching, and verified srcdoc output.\n * @module\n */\n\n/** Snapshot manifest — regular event, immutable point-in-time release. */\nexport const NAPPLET_KIND_SNAPSHOT = 5129;\n/** Root manifest — replaceable event, an author's latest unnamed napplet. */\nexport const NAPPLET_KIND_ROOT = 15129;\n/** Named manifest — addressable event (carries a `d` tag identifier). */\nexport const NAPPLET_KIND_NAMED = 35129;\n\n/** All three NIP-5D napplet manifest kinds. */\nexport const NAPPLET_KINDS: readonly number[] = [\n NAPPLET_KIND_SNAPSHOT,\n NAPPLET_KIND_ROOT,\n NAPPLET_KIND_NAMED,\n];\n\n/**\n * Whether `kind` is one of the three NIP-5D napplet manifest kinds.\n *\n * @param kind - A Nostr event kind\n * @returns `true` for `5129` / `15129` / `35129`\n */\nexport function isNappletManifestKind(kind: number): boolean {\n return NAPPLET_KINDS.includes(kind);\n}\n\n/**\n * A parsed NIP-5D napplet manifest. All fields are derived from the manifest\n * event. Declarations are untrusted until {@link resolveNapplet} verifies both\n * the signature and the content bytes.\n */\nexport interface NappletManifest {\n /** Event format selected by the parser. Absent only in older host-created values. */\n format?: 'current' | 'legacy';\n /** Signed artifact hash for current events; legacy aggregate for older events. */\n artifactHash?: string;\n /** Optional domains; absence never prevents loading and presence grants nothing. */\n optional?: string[];\n /** Independent current z declarations, preserved even without accepted intents. */\n archetypeSlugs?: string[];\n /** Accepted queryless intents and advertised parameter names from i tags. */\n intents?: Array<{ identity: string; parameters: string[] }>;\n /** Valid supported icon declaration. Hosts may keep generic artwork. */\n icon?: { sha256: string; mimeType: 'image/png' | 'image/jpeg' | 'image/webp' };\n /** Snapshot provenance only; never resolved or trusted on its behalf. */\n parent?: string;\n /** Snapshot root provenance only. */\n root?: string;\n /** Manifest event kind (`5129` / `15129` / `35129`). */\n kind: number;\n /** Author hex public key. */\n pubkey: string;\n /** Named-napplet `d` identifier, or `''` for root/snapshot manifests. */\n dTag: string;\n /** Normalized files: one /index.html for current events, legacy path entries otherwise. */\n paths: PathEntry[];\n /** Compatibility name for the signed content identity (current artifact or legacy aggregate). */\n aggregateHash: string;\n /** Blossom server URL hints from `server` tags. */\n servers: string[];\n /** Required bare domains from current R tags or legacy requires tags. */\n requires: string[];\n /**\n * Routing projection: independent current z/i sets, or exact legacy archetype pairs.\n */\n archetypes: Array<{ slug: string; convention: string }>;\n /** Optional human title. */\n title?: string;\n /** Plain-text event content (legacy description tag for older events). */\n description?: string;\n /** Optional upstream source URL from the `source` tag. */\n source?: string;\n}\n\nexport { NappletResolutionError, type NappletResolutionErrorCode } from './errors.js';\n\n/**\n * Normalize a current or legacy NIP-5D event without verifying its signature.\n * @param event - Candidate manifest; use resolveNapplet before trusting it.\n * @returns Manifest with normalized identity, paths, and routing declarations.\n * @example\n * const manifest = parseNappletManifest(event);\n */\nexport function parseNappletManifest(event: NostrEvent): NappletManifest {\n if (!isNappletManifestKind(event.kind)) {\n throw new NappletResolutionError('invalid-manifest', `not a NIP-5D napplet kind: ${event.kind}`);\n }\n const hashes = event.tags.filter((tag) => tag[0] === 'x');\n if (hashes.length !== 1) {\n throw new NappletResolutionError('invalid-manifest', 'manifest must carry exactly one x tag');\n }\n // Explicit format selection, never retry a malformed current event as legacy.\n return hashes[0][2] === 'aggregate'\n ? parseLegacyManifest(event)\n : parseCurrentManifest(event);\n}\n\n/**\n * Verify a manifest event's Nostr signature (id + schnorr sig).\n *\n * @param event - A manifest event\n * @returns `true` if the event is internally consistent and validly signed\n */\nexport function verifyManifestSignature(event: NostrEvent): boolean {\n try {\n return verifyEvent(event);\n } catch {\n return false;\n }\n}\n\n/**\n * Whether `bytes` hash to the expected lowercase-hex SHA-256.\n *\n * @param bytes - The blob bytes\n * @param sha256Hex - Expected lowercase-hex SHA-256\n * @returns `true` only on an exact match\n */\nexport function verifyBlobHash(bytes: Uint8Array, sha256Hex: string): boolean {\n return bytesToHex(sha256(bytes)) === sha256Hex;\n}\n\nfunction stripTrailingSlash(url: string): string {\n return url.endsWith('/') ? url.slice(0, -1) : url;\n}\n\n/**\n * Fetch a blob by SHA-256 from a list of Blossom servers, returning the first\n * server's bytes whose hash matches. The returned bytes are always re-verified\n * against `sha256Hex` — servers (and gateways) are never trusted.\n *\n * @param servers - Candidate Blossom server base URLs, tried in order\n * @param sha256Hex - The blob's lowercase-hex SHA-256\n * @param fetchBytes - Fetches raw bytes for a URL (`<server>/<sha256>`)\n * @returns The verified blob bytes\n * @throws {@link NappletResolutionError} `blob-unavailable` if no server serves\n * a hash-matching blob\n */\nexport async function fetchBlob(\n servers: readonly string[],\n sha256Hex: string,\n fetchBytes: (url: string) => Promise<Uint8Array>,\n): Promise<Uint8Array> {\n for (const server of servers) {\n const url = `${stripTrailingSlash(server)}/${sha256Hex}`;\n try {\n const bytes = await fetchBytes(url);\n if (verifyBlobHash(bytes, sha256Hex)) return bytes;\n } catch {\n // try the next server\n }\n }\n throw new NappletResolutionError('blob-unavailable', `no server served blob ${sha256Hex}`);\n}\n\n/** A fully verified napplet, ready to inject via `iframe.srcdoc`. */\nexport interface ResolvedNapplet {\n /** Verified content identity; current artifact SHA-256 or legacy aggregate. */\n artifactHash: string;\n /** Computed `d` identifier (`''` for root/snapshot). */\n dTag: string;\n /** Compatibility alias for artifactHash; preserves existing host and ACL APIs. */\n aggregateHash: string;\n /** Verified file bytes keyed by manifest path. */\n files: Map<string, Uint8Array>;\n /** The verified `/index.html` decoded to text. */\n indexHtml: string;\n /** The parsed manifest. */\n manifest: NappletManifest;\n}\n\n/** Options for {@link resolveNapplet}. */\nexport interface ResolveNappletOptions {\n /** The candidate manifest event (resolved from relays by the caller). */\n event: NostrEvent;\n /**\n * Fetch raw blob bytes by SHA-256. Backed by Blossom (or a gateway) by the\n * caller. The bytes are re-verified against the hash here, so the fetcher is\n * untrusted.\n */\n fetchBlob: (sha256Hex: string, servers: readonly string[]) => Promise<Uint8Array>;\n /** Decode blob bytes to text for `indexHtml` (default UTF-8). */\n textDecode?: (bytes: Uint8Array) => string;\n /**\n * Optional verified artifact cache. Cache hits are still re-verified against\n * the manifest hash before use; cache writes happen only after the signature,\n * content identity, and every blob hash have been verified.\n */\n cache?: NappletArtifactCache;\n}\n\nconst INDEX_PATHS = ['/index.html', 'index.html', '/'];\n\nfunction defaultDecode(bytes: Uint8Array): string {\n return new TextDecoder().decode(bytes);\n}\n\n/**\n * Resolve a napplet end-to-end from a candidate manifest event: verify the\n * signature, parse its schema, fetch and hash its artifact, and return verified\n * `/index.html`. Legacy events additionally verify their NIP-5A aggregate.\n *\n * The returned `(dTag, aggregateHash)` is computed from the verified bytes and\n * is the napplet's identity. Any failure throws a {@link NappletResolutionError}\n * — the caller must fail closed (never render unverified bytes).\n *\n * @param options - {@link ResolveNappletOptions}\n * @returns The {@link ResolvedNapplet}\n * @throws {@link NappletResolutionError} on signature, manifest, aggregate,\n * blob-hash, blob-availability, or missing-index failures\n *\n * @example\n * ```ts\n * import { resolveNapplet } from '@kehto/nip/5d';\n * const napplet = await resolveNapplet({ event, fetchBlob });\n * iframe.srcdoc = napplet.indexHtml; // sandbox=\"allow-scripts\", opaque origin\n * ```\n */\nexport async function resolveNapplet(options: ResolveNappletOptions): Promise<ResolvedNapplet> {\n const { event, fetchBlob: fetchBlobBytes, textDecode = defaultDecode, cache } = options;\n\n if (!verifyManifestSignature(event)) {\n throw new NappletResolutionError('invalid-signature', 'manifest signature is invalid');\n }\n\n const manifest = parseNappletManifest(event);\n\n if (manifest.format === 'legacy') verifyLegacyAggregate(manifest);\n\n const files = new Map<string, Uint8Array>();\n for (const entry of manifest.paths) {\n let bytes = await cache?.readBlob(entry.sha256);\n if (bytes && !verifyBlobHash(bytes, entry.sha256)) {\n await cache?.deleteBlob(entry.sha256);\n bytes = undefined;\n }\n bytes ??= await fetchBlobBytes(entry.sha256, manifest.servers);\n if (!verifyBlobHash(bytes, entry.sha256)) {\n throw new NappletResolutionError(\n 'blob-hash-mismatch',\n `blob for ${entry.path} does not match hash ${entry.sha256}`,\n );\n }\n files.set(entry.path, bytes);\n }\n\n const indexEntry = manifest.paths.find((e) => INDEX_PATHS.includes(e.path));\n if (!indexEntry) {\n throw new NappletResolutionError('missing-index', 'manifest has no /index.html entry');\n }\n\n const indexHtml = textDecode(files.get(indexEntry.path)!);\n await cache?.writeVerifiedResolution({ event, manifest, files, indexHtml });\n if (manifest.format === 'legacy') warnLegacyManifest();\n\n return {\n artifactHash: manifest.aggregateHash,\n dTag: manifest.dTag,\n aggregateHash: manifest.aggregateHash,\n files,\n indexHtml,\n manifest,\n };\n}\n","/** Error codes for every napplet resolution failure path. */\nexport type NappletResolutionErrorCode =\n | 'invalid-signature'\n | 'invalid-manifest'\n | 'aggregate-mismatch'\n | 'blob-hash-mismatch'\n | 'blob-unavailable'\n | 'missing-index';\n\n/**\n * Thrown on any napplet resolution/verification failure. The `code` field\n * identifies which guard rejected, so callers can fail closed without parsing\n * the message.\n */\nexport class NappletResolutionError extends Error {\n readonly code: NappletResolutionErrorCode;\n constructor(code: NappletResolutionErrorCode, message: string) {\n super(message);\n this.name = 'NappletResolutionError';\n this.code = code;\n }\n}\n\n","// Removal boundary for pre-0.15 @napplet/vite-plugin event compatibility.\nimport type { NostrEvent } from 'nostr-tools';\nimport { computeAggregateHash, pathEntriesFromTags, aggregateTagValue } from '../5a/index.js';\nimport type { NappletManifest } from './index.js';\nimport { NappletResolutionError } from './errors.js';\n\nfunction firstTagValue(tags: readonly (readonly string[])[], name: string): string | undefined {\n for (const tag of tags) {\n if (tag[0] === name && typeof tag[1] === 'string' && tag[1].length > 0) return tag[1];\n }\n return undefined;\n}\n\nfunction allTagValues(tags: readonly (readonly string[])[], name: string): string[] {\n const out: string[] = [];\n for (const tag of tags) {\n if (tag[0] === name && typeof tag[1] === 'string' && tag[1].length > 0) out.push(tag[1]);\n }\n return out;\n}\n\nfunction archetypesFromTags(\n tags: readonly (readonly string[])[],\n): Array<{ slug: string; convention: string }> {\n const out: Array<{ slug: string; convention: string }> = [];\n for (const tag of tags) {\n if (tag[0] !== 'archetype') continue;\n const slug = tag[1];\n if (typeof slug !== 'string' || !/^[a-z0-9][a-z0-9-]*$/.test(slug)) {\n throw new NappletResolutionError(\n 'invalid-manifest',\n 'archetype slug must contain lowercase letters, numbers, and hyphens',\n );\n }\n\n const convention = tag[2];\n if (typeof convention !== 'string' || convention.length === 0) {\n throw new NappletResolutionError('invalid-manifest', 'archetype convention is required');\n }\n if (/^NAP-\\d+$/.test(convention)) {\n throw new NappletResolutionError(\n 'invalid-manifest',\n 'numbered NAP identifier is not an archetype convention',\n );\n }\n if (!/^napplet:[^/?#\\s]+\\/[^/?#\\s]+$/.test(convention)) {\n throw new NappletResolutionError(\n 'invalid-manifest',\n 'archetype convention must be a queryless napplet:<archetype>/<intent> identity',\n );\n }\n // NAP-INTENT keeps a routing archetype and a payload convention\n // orthogonal: one convention may serve multiple archetypes and vice versa.\n\n if (tag.length !== 3) {\n throw new NappletResolutionError(\n 'invalid-manifest',\n 'archetype tags must contain exactly slug and convention',\n );\n }\n out.push({ slug, convention });\n }\n return out;\n}\n\nexport function parseLegacyManifest(event: NostrEvent): NappletManifest {\n const paths = pathEntriesFromTags(event.tags);\n if (paths.length === 0) {\n throw new NappletResolutionError('invalid-manifest', 'manifest has no path tags');\n }\n const aggregateHash = aggregateTagValue(event.tags);\n if (!aggregateHash) {\n throw new NappletResolutionError('invalid-manifest', 'manifest has no aggregate x tag');\n }\n const dTag = firstTagValue(event.tags, 'd');\n if (event.kind !== 35129 && dTag !== undefined) {\n throw new NappletResolutionError('invalid-manifest', 'only named manifests may carry a d tag');\n }\n return {\n format: 'legacy',\n artifactHash: aggregateHash,\n optional: [],\n kind: event.kind,\n pubkey: event.pubkey,\n dTag: dTag ?? '',\n paths,\n aggregateHash,\n servers: allTagValues(event.tags, 'server'),\n requires: allTagValues(event.tags, 'requires'),\n archetypes: archetypesFromTags(event.tags),\n title: firstTagValue(event.tags, 'title'),\n description: firstTagValue(event.tags, 'description'),\n source: firstTagValue(event.tags, 'source'),\n };\n}\n\n\n/** Compatibility-only aggregate check; current manifests never enter this path. */\nexport function verifyLegacyAggregate(manifest: NappletManifest): void {\n const recomputed = computeAggregateHash(manifest.paths);\n if (recomputed !== manifest.aggregateHash) {\n throw new NappletResolutionError('aggregate-mismatch',\n `recomputed aggregate ${recomputed} != manifest ${manifest.aggregateHash}`);\n }\n}\n\nlet warnedLegacyManifest = false;\n\n/** Warn once per module lifetime, after a legacy artifact has passed verification. */\nexport function warnLegacyManifest(): void {\n if (warnedLegacyManifest) return;\n warnedLegacyManifest = true;\n console.warn('[KEHTO_COMPAT_LEGACY_MANIFEST] Legacy aggregate manifest resolved. Republish with @napplet/vite-plugin >=0.15 using the current single-artifact schema. See docs/compatibility.md.');\n}\n","import type { NostrEvent } from 'nostr-tools';\nimport type { NappletManifest } from './index.js';\nimport { NappletResolutionError } from './errors.js';\n\nconst HASH = /^[a-f0-9]{64}$/;\nconst DOMAIN = /^[a-z][a-z0-9-]*$/;\nconst SLUG = /^[a-z0-9][a-z0-9-]*$/;\nconst INTENT = /^napplet:[^/?#\\s]+\\/[^/?#\\s]+$/;\n\nfunction invalid(message: string): never {\n throw new NappletResolutionError('invalid-manifest', message);\n}\n\nfunction single(tags: string[][], name: string): string | undefined {\n const matches = tags.filter((tag) => tag[0] === name);\n if (matches.length > 1 || matches.some((tag) => tag.length !== 2 || !tag[1])) {\n invalid(`manifest ${name} tag must have one value and occur at most once`);\n }\n return matches[0]?.[1];\n}\n\nfunction declarations(tags: string[][], name: string, pattern: RegExp): string[] {\n return [...new Set(tags.filter((tag) => tag[0] === name).map((tag) => {\n if (tag.length !== 2 || !pattern.test(tag[1])) invalid(`invalid ${name} declaration`);\n return tag[1];\n }))];\n}\n\nfunction iconFromTags(tags: string[][]): NappletManifest['icon'] {\n const icons = tags.filter((tag) => tag[0] === 'icon');\n if (icons.length !== 1) return undefined;\n const [, hash, mimeType] = icons[0];\n if (icons[0].length !== 3 || !HASH.test(hash)) return undefined;\n if (mimeType !== 'image/png' && mimeType !== 'image/jpeg' && mimeType !== 'image/webp') {\n return undefined;\n }\n // A declaration is not verified image data. Hosts retain generic artwork until\n // both its hash and decoded image format have been checked independently.\n return { sha256: hash, mimeType };\n}\n\n/** Parse the single-artifact schema from NIP-5D #2303, 020cb8b. */\nexport function parseCurrentManifest(event: NostrEvent): NappletManifest {\n const tags = event.tags;\n const artifactHash = single(tags, 'x');\n if (!artifactHash || !HASH.test(artifactHash)) invalid('invalid artifact x hash');\n if (typeof event.content !== 'string' || !event.content.trim()) {\n invalid('manifest content must be a non-empty plain-text description');\n }\n const dTag = single(tags, 'd');\n if (event.kind === 35129 ? dTag === undefined : dTag !== undefined) {\n invalid('exactly one d tag is required only on named manifests');\n }\n const parent = single(tags, 'a');\n const root = single(tags, 'A');\n for (const address of [parent, root]) {\n if (address === undefined) continue;\n if (event.kind !== 5129 || !/^(35129:[a-f0-9]{64}:.+|15129:[a-f0-9]{64}:)$/.test(address)) {\n invalid('lineage must be a napplet address on a snapshot');\n }\n }\n const archetypeSlugs = declarations(tags, 'z', SLUG);\n const intents = tags.filter((tag) => tag[0] === 'i').map((tag) => {\n const [, identity, ...parameters] = tag;\n if (!identity || !INTENT.test(identity)\n || parameters.some((parameter) => !parameter || /\\s/.test(parameter))) {\n invalid('invalid accepted intent or parameter name');\n }\n return { identity, parameters };\n });\n return {\n format: 'current',\n kind: event.kind,\n pubkey: event.pubkey,\n dTag: dTag ?? '',\n artifactHash,\n aggregateHash: artifactHash,\n paths: [{ path: '/index.html', sha256: artifactHash }],\n servers: tags.filter((tag) => tag[0] === 'server' && tag[1]).map((tag) => tag[1]),\n requires: declarations(tags, 'R', DOMAIN),\n optional: declarations(tags, 'O', DOMAIN),\n archetypeSlugs,\n intents,\n // NAP-INTENT keeps roles and conventions orthogonal. The current schema\n // advertises independent sets; legacy explicit pairings stay in its adapter.\n archetypes: archetypeSlugs.flatMap((slug) => intents.map(({ identity }) => ({\n slug, convention: identity,\n }))),\n title: single(tags, 'title'),\n description: event.content,\n source: single(tags, 'source'),\n icon: iconFromTags(tags),\n parent,\n root,\n };\n}\n","import type { NostrEvent } from 'nostr-tools';\nimport type { NappletManifest } from './index.js';\n\nconst MIB = 1024 * 1024;\n\n/** Versioned Cache Storage namespace for verified napplet artifacts. */\nexport const NAPPLET_ARTIFACT_CACHE_NAME = 'kehto:napplet-artifacts:v1';\nconst SCHEMA_VERSION = 1;\nconst DEFAULT_BASE_URL = 'https://kehto.invalid';\nconst DEFAULT_UNKNOWN_BUDGET_BYTES = 32 * MIB;\nconst DEFAULT_SOFT_CEILING_BYTES = 128 * MIB;\nconst DEFAULT_HARD_CEILING_BYTES = 256 * MIB;\nconst DEFAULT_FOREGROUND_TTL_MS = 15 * 60 * 1000;\nconst DEFAULT_BACKGROUND_TTL_MS = 60 * 60 * 1000;\n\nexport interface NappletCacheDiagnostic {\n /** Diagnostic category for cache open/read/write/delete/prune/storage-estimate failures. */\n type:\n | 'cache-open-failed'\n | 'cache-read-failed'\n | 'cache-write-failed'\n | 'cache-delete-failed'\n | 'cache-prune-failed'\n | 'storage-estimate-unavailable';\n /** Human-readable diagnostic detail suitable for logs. */\n message: string;\n /** Original thrown value, when one exists. */\n cause?: unknown;\n}\n\n/** Metadata tracked for one cached napplet artifact blob. */\nexport interface NappletBlobIndexEntry {\n /** Blob byte length. */\n size: number;\n /** Last read or write timestamp in milliseconds. */\n lastAccessed: number;\n /** Number of cached aggregate records currently referencing this blob. */\n refCount: number;\n}\n\n/** Metadata tracked for one verified aggregate cache record. */\nexport interface NappletAggregateIndexEntry {\n /** Manifest `d` tag. */\n dTag: string;\n /** Verified NIP-5A aggregate hash. */\n aggregateHash: string;\n /** Internal cache key derived from `dTag` and `aggregateHash`. */\n key: string;\n /** SHA-256 hashes of blobs referenced by this aggregate. */\n blobHashes: string[];\n /** Approximate stored aggregate-record byte size. */\n size: number;\n /** Last read or write timestamp in milliseconds. */\n lastAccessed: number;\n /** Coordinate cache key that currently points at this aggregate, if any. */\n coordinateKey?: string;\n /** Reserved pin marker for hosts that want to protect an aggregate. */\n pinned?: boolean;\n}\n\n/** Cached mapping from a NIP-5D coordinate to a verified aggregate. */\nexport interface CachedCoordinate {\n /** Internal coordinate key in the form `kind:pubkey:dTag`. */\n key: string;\n /** Napplet manifest event kind. */\n kind: number;\n /** Manifest author public key. */\n pubkey: string;\n /** Manifest `d` tag. */\n dTag: string;\n /** Verified aggregate hash for the coordinate. */\n aggregateHash: string;\n /** Timestamp when this coordinate last resolved successfully. */\n lastResolvedAt: number;\n /** Foreground refresh TTL in milliseconds. */\n foregroundTtlMs: number;\n /** Background refresh TTL in milliseconds. */\n backgroundTtlMs: number;\n}\n\n/** Serialized Cache Storage index for verified napplet artifacts. */\nexport interface NappletArtifactCacheIndex {\n /** Index schema version. */\n schemaVersion: number;\n /** Last index update timestamp in milliseconds. */\n updatedAt: number;\n /** Blob entries keyed by SHA-256 hash. */\n blobs: Record<string, NappletBlobIndexEntry>;\n /** Aggregate entries keyed by the internal aggregate key. */\n aggregates: Record<string, NappletAggregateIndexEntry>;\n /** Coordinate entries keyed by {@link coordinateKey}. */\n coordinates: Record<string, CachedCoordinate>;\n}\n\ninterface AggregateRecord {\n schemaVersion: number;\n dTag: string;\n aggregateHash: string;\n manifest: {\n kind: number;\n pubkey: string;\n dTag: string;\n paths: Array<{ path: string; sha256: string }>;\n requires: string[];\n title?: string;\n description?: string;\n };\n indexHtml: string;\n blobHashes: string[];\n writtenAt: number;\n}\n\nexport interface WriteVerifiedResolutionInput {\n /** Verified NIP-5D manifest event. */\n event: NostrEvent;\n /** Parsed manifest whose aggregate and paths have already been verified. */\n manifest: NappletManifest;\n /** File bytes keyed by manifest path. */\n files: ReadonlyMap<string, Uint8Array>;\n /** Verified `/index.html` bytes decoded as text. */\n indexHtml: string;\n /** Timestamp override for deterministic tests. */\n now?: number;\n}\n\n/** Freshness controls for reading a cached coordinate. */\nexport interface CoordinateFreshnessOptions {\n /** Timestamp override in milliseconds. */\n now?: number;\n /** Use the background TTL instead of the foreground TTL. */\n background?: boolean;\n}\n\n/** Cache interface for verified NIP-5D napplet artifacts. */\nexport interface NappletArtifactCache {\n /** Read one cached artifact blob by SHA-256 hash. */\n readBlob(sha256Hex: string): Promise<Uint8Array | undefined>;\n /** Delete one cached artifact blob by SHA-256 hash. */\n deleteBlob(sha256Hex: string): Promise<void>;\n /** Write a verified manifest resolution and all referenced blobs. */\n writeVerifiedResolution(input: WriteVerifiedResolutionInput): Promise<void>;\n /** Cache the latest aggregate for a NIP-5D coordinate. */\n writeCoordinate(input: {\n kind: number;\n pubkey: string;\n dTag: string;\n aggregateHash: string;\n lastResolvedAt?: number;\n foregroundTtlMs?: number;\n backgroundTtlMs?: number;\n }): Promise<void>;\n /** Read a cached coordinate with a computed freshness flag. */\n getCoordinate(input: {\n kind: number;\n pubkey: string;\n dTag: string;\n }, options?: CoordinateFreshnessOptions): Promise<(CachedCoordinate & { fresh: boolean }) | undefined>;\n /** Update aggregate/blob last-access timestamps. */\n touchAggregate(dTag: string, aggregateHash: string, now?: number): Promise<void>;\n /** Protect an aggregate from normal pruning while it is in active use. */\n markAggregateActive(dTag: string, aggregateHash: string): void;\n /** Release an aggregate previously marked active. */\n releaseAggregateActive(dTag: string, aggregateHash: string): void;\n /** Prune cache contents to the configured storage budget. */\n prune(now?: number): Promise<void>;\n}\n\n/** Constructor options for {@link CacheStorageNappletArtifactCache}. */\nexport interface CacheStorageNappletArtifactCacheOptions {\n /** Cache Storage implementation; defaults to `globalThis.caches`. */\n cacheStorage?: CacheStorage;\n /** Storage manager used for quota estimates; defaults to `navigator.storage`. */\n storage?: StorageManager;\n /** Cache Storage namespace. */\n cacheName?: string;\n /** Base URL used to construct synthetic cache keys. */\n baseUrl?: string;\n /** Clock used for timestamps. */\n now?: () => number;\n /** Diagnostic callback for non-fatal cache failures. */\n onDiagnostic?: (diagnostic: NappletCacheDiagnostic) => void;\n /** Budget used when storage estimates are unavailable. */\n unknownBudgetBytes?: number;\n /** Soft cache ceiling in bytes. */\n softCeilingBytes?: number;\n /** Hard cache ceiling in bytes. */\n hardCeilingBytes?: number;\n}\n\n/** Options for opening the browser Cache Storage artifact cache. */\nexport interface OpenNappletArtifactCacheOptions extends CacheStorageNappletArtifactCacheOptions {\n /** Return `undefined` when quota estimates are unavailable. */\n requireStorageEstimate?: boolean;\n}\n\nfunction emptyIndex(now: number): NappletArtifactCacheIndex {\n return {\n schemaVersion: SCHEMA_VERSION,\n updatedAt: now,\n blobs: {},\n aggregates: {},\n coordinates: {},\n };\n}\n\nfunction aggregateKey(dTag: string, aggregateHash: string): string {\n return `${encodeURIComponent(dTag)}:${aggregateHash}`;\n}\n\n/**\n * Build the stable index key for a NIP-5D coordinate.\n *\n * @param kind - Manifest event kind.\n * @param pubkey - Manifest author public key.\n * @param dTag - Manifest `d` tag.\n * @returns Cache key in the form `kind:pubkey:dTag`.\n */\nexport function coordinateKey(kind: number, pubkey: string, dTag: string): string {\n return `${kind}:${pubkey}:${encodeURIComponent(dTag)}`;\n}\n\n/**\n * Check whether a cached coordinate is still inside its foreground/background TTL.\n *\n * @param coordinate - Cached coordinate to inspect.\n * @param options - Optional clock and TTL mode overrides.\n * @returns `true` when the coordinate can be used without a network refresh.\n */\nexport function isCoordinateFresh(\n coordinate: CachedCoordinate,\n options: CoordinateFreshnessOptions = {},\n): boolean {\n const now = options.now ?? Date.now();\n const ttl = options.background ? coordinate.backgroundTtlMs : coordinate.foregroundTtlMs;\n return ttl === Number.POSITIVE_INFINITY || now - coordinate.lastResolvedAt <= ttl;\n}\n\nfunction byteLength(bytes: Uint8Array): number {\n return bytes.byteLength;\n}\n\nfunction encodeKeyPart(value: string): string {\n return encodeURIComponent(value).replaceAll('%2F', '%252F');\n}\n\nfunction cloneBytes(bytes: Uint8Array): Uint8Array {\n return new Uint8Array(bytes);\n}\n\nfunction responseBody(bytes: Uint8Array): ArrayBuffer {\n return cloneBytes(bytes).buffer as ArrayBuffer;\n}\n\nfunction normalizeBaseUrl(baseUrl: string): string {\n return baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`;\n}\n\nfunction resolveBaseUrl(baseUrl?: string): string {\n if (baseUrl) return normalizeBaseUrl(baseUrl);\n const maybeLocation = globalThis as typeof globalThis & { location?: { origin?: string } };\n if (maybeLocation.location?.origin) return normalizeBaseUrl(maybeLocation.location.origin);\n return normalizeBaseUrl(DEFAULT_BASE_URL);\n}\n\nasync function responseBytes(response: Response): Promise<Uint8Array> {\n return new Uint8Array(await response.arrayBuffer());\n}\n\nasync function responseJson<T>(response: Response): Promise<T> {\n return response.json() as Promise<T>;\n}\n\nfunction refCounts(index: NappletArtifactCacheIndex): Record<string, number> {\n const counts: Record<string, number> = {};\n for (const aggregate of Object.values(index.aggregates)) {\n for (const hash of aggregate.blobHashes) counts[hash] = (counts[hash] ?? 0) + 1;\n }\n return counts;\n}\n\nfunction cachedSize(index: NappletArtifactCacheIndex): number {\n const blobBytes = Object.values(index.blobs).reduce((sum, entry) => sum + entry.size, 0);\n const aggregateBytes = Object.values(index.aggregates).reduce((sum, entry) => sum + entry.size, 0);\n return blobBytes + aggregateBytes;\n}\n\n/**\n * Browser Cache Storage implementation of {@link NappletArtifactCache}.\n *\n * It stores verified blobs and aggregate records under synthetic URLs, keeps a\n * small JSON index, and prunes according to quota-derived budgets.\n */\nexport class CacheStorageNappletArtifactCache implements NappletArtifactCache {\n private readonly cacheStorage: CacheStorage;\n private readonly storage?: StorageManager;\n private readonly cacheName: string;\n private readonly baseUrl: string;\n private readonly now: () => number;\n private readonly activeAggregates = new Set<string>();\n private readonly onDiagnostic?: (diagnostic: NappletCacheDiagnostic) => void;\n private readonly unknownBudgetBytes: number;\n private readonly softCeilingBytes: number;\n private readonly hardCeilingBytes: number;\n private cachePromise?: Promise<Cache>;\n\n constructor(options: CacheStorageNappletArtifactCacheOptions = {}) {\n const maybeCacheStorage = options.cacheStorage ?? (globalThis as typeof globalThis & { caches?: CacheStorage }).caches;\n if (!maybeCacheStorage) {\n throw new Error('Cache Storage is unavailable');\n }\n this.cacheStorage = maybeCacheStorage;\n this.storage = options.storage ?? (globalThis.navigator as Navigator | undefined)?.storage;\n this.cacheName = options.cacheName ?? NAPPLET_ARTIFACT_CACHE_NAME;\n this.baseUrl = resolveBaseUrl(options.baseUrl);\n this.now = options.now ?? Date.now;\n this.onDiagnostic = options.onDiagnostic;\n this.unknownBudgetBytes = options.unknownBudgetBytes ?? DEFAULT_UNKNOWN_BUDGET_BYTES;\n this.softCeilingBytes = options.softCeilingBytes ?? DEFAULT_SOFT_CEILING_BYTES;\n this.hardCeilingBytes = options.hardCeilingBytes ?? DEFAULT_HARD_CEILING_BYTES;\n }\n\n async readBlob(sha256Hex: string): Promise<Uint8Array | undefined> {\n try {\n const cache = await this.open();\n const response = await cache.match(this.blobUrl(sha256Hex));\n if (!response) return undefined;\n const bytes = await responseBytes(response);\n const index = await this.readIndex(cache);\n const entry = index.blobs[sha256Hex];\n if (entry) {\n entry.lastAccessed = this.now();\n index.updatedAt = entry.lastAccessed;\n await this.writeIndex(cache, index);\n }\n return bytes;\n } catch (cause) {\n this.diagnostic({ type: 'cache-read-failed', message: `failed to read blob ${sha256Hex}`, cause });\n return undefined;\n }\n }\n\n async deleteBlob(sha256Hex: string): Promise<void> {\n try {\n const cache = await this.open();\n await cache.delete(this.blobUrl(sha256Hex));\n const index = await this.readIndex(cache);\n delete index.blobs[sha256Hex];\n index.updatedAt = this.now();\n await this.writeIndex(cache, index);\n } catch (cause) {\n this.diagnostic({ type: 'cache-delete-failed', message: `failed to delete blob ${sha256Hex}`, cause });\n }\n }\n\n async writeVerifiedResolution(input: WriteVerifiedResolutionInput): Promise<void> {\n try {\n await this.writeVerifiedResolutionOnce(input);\n } catch (firstCause) {\n try {\n await this.prune(input.now ?? this.now());\n await this.writeVerifiedResolutionOnce(input);\n } catch (cause) {\n this.diagnostic({\n type: 'cache-write-failed',\n message: 'failed to write verified napplet artifacts after prune retry',\n cause: cause ?? firstCause,\n });\n }\n }\n }\n\n async writeCoordinate(input: {\n kind: number;\n pubkey: string;\n dTag: string;\n aggregateHash: string;\n lastResolvedAt?: number;\n foregroundTtlMs?: number;\n backgroundTtlMs?: number;\n }): Promise<void> {\n const cache = await this.open();\n const now = this.now();\n const index = await this.readIndex(cache);\n const key = coordinateKey(input.kind, input.pubkey, input.dTag);\n index.coordinates[key] = {\n key,\n kind: input.kind,\n pubkey: input.pubkey,\n dTag: input.dTag,\n aggregateHash: input.aggregateHash,\n lastResolvedAt: input.lastResolvedAt ?? now,\n foregroundTtlMs: input.foregroundTtlMs ?? DEFAULT_FOREGROUND_TTL_MS,\n backgroundTtlMs: input.backgroundTtlMs ?? DEFAULT_BACKGROUND_TTL_MS,\n };\n index.updatedAt = now;\n await this.writeIndex(cache, index);\n }\n\n async getCoordinate(\n input: { kind: number; pubkey: string; dTag: string },\n options: CoordinateFreshnessOptions = {},\n ): Promise<(CachedCoordinate & { fresh: boolean }) | undefined> {\n const cache = await this.open();\n const index = await this.readIndex(cache);\n const coordinate = index.coordinates[coordinateKey(input.kind, input.pubkey, input.dTag)];\n if (!coordinate) return undefined;\n return { ...coordinate, fresh: isCoordinateFresh(coordinate, options) };\n }\n\n async touchAggregate(dTag: string, aggregateHash: string, now: number = this.now()): Promise<void> {\n const cache = await this.open();\n const index = await this.readIndex(cache);\n const key = aggregateKey(dTag, aggregateHash);\n const aggregate = index.aggregates[key];\n if (!aggregate) return;\n aggregate.lastAccessed = now;\n for (const hash of aggregate.blobHashes) {\n if (index.blobs[hash]) index.blobs[hash].lastAccessed = now;\n }\n index.updatedAt = now;\n await this.writeIndex(cache, index);\n }\n\n markAggregateActive(dTag: string, aggregateHash: string): void {\n this.activeAggregates.add(aggregateKey(dTag, aggregateHash));\n }\n\n releaseAggregateActive(dTag: string, aggregateHash: string): void {\n this.activeAggregates.delete(aggregateKey(dTag, aggregateHash));\n }\n\n async prune(now: number = this.now()): Promise<void> {\n try {\n const cache = await this.open();\n const index = await this.readIndex(cache);\n let size = cachedSize(index);\n const budget = await this.budgetBytes();\n const hardBudget = await this.hardBudgetBytes();\n const shouldPrune = size > budget || await this.originPressureHigh();\n if (!shouldPrune) return;\n\n this.rewriteRefCounts(index);\n\n for (const [hash, blob] of Object.entries(index.blobs)) {\n if (size <= budget) break;\n if (blob.refCount > 0) continue;\n await cache.delete(this.blobUrl(hash));\n delete index.blobs[hash];\n size -= blob.size;\n }\n\n const currentAggregateHashes = new Set(\n Object.values(index.coordinates).map((coordinate) => coordinate.aggregateHash),\n );\n const aggregates = Object.values(index.aggregates)\n .filter((entry) => !this.activeAggregates.has(entry.key))\n .sort((a, b) => {\n const currentA = currentAggregateHashes.has(a.aggregateHash) ? 1 : 0;\n const currentB = currentAggregateHashes.has(b.aggregateHash) ? 1 : 0;\n if (currentA !== currentB) return currentA - currentB;\n return a.lastAccessed - b.lastAccessed;\n });\n\n for (const aggregate of aggregates) {\n if (size <= budget) break;\n await cache.delete(this.aggregateUrl(aggregate.key));\n delete index.aggregates[aggregate.key];\n size -= aggregate.size;\n this.rewriteRefCounts(index);\n }\n\n if (size > hardBudget) {\n const active = Object.values(index.aggregates)\n .filter((entry) => this.activeAggregates.has(entry.key))\n .sort((a, b) => a.lastAccessed - b.lastAccessed);\n for (const aggregate of active) {\n if (size <= hardBudget) break;\n await cache.delete(this.aggregateUrl(aggregate.key));\n delete index.aggregates[aggregate.key];\n size -= aggregate.size;\n }\n }\n\n this.rewriteRefCounts(index);\n for (const [hash, blob] of Object.entries(index.blobs)) {\n if (blob.refCount === 0) {\n await cache.delete(this.blobUrl(hash));\n delete index.blobs[hash];\n }\n }\n index.updatedAt = now;\n await this.writeIndex(cache, index);\n } catch (cause) {\n this.diagnostic({ type: 'cache-prune-failed', message: 'failed to prune napplet artifact cache', cause });\n }\n }\n\n async snapshotIndex(): Promise<NappletArtifactCacheIndex> {\n return this.readIndex(await this.open());\n }\n\n private async writeVerifiedResolutionOnce(input: WriteVerifiedResolutionInput): Promise<void> {\n const cache = await this.open();\n const now = input.now ?? this.now();\n const index = await this.readIndex(cache);\n const blobHashes: string[] = [];\n\n for (const pathEntry of input.manifest.paths) {\n const bytes = input.files.get(pathEntry.path);\n if (!bytes) continue;\n blobHashes.push(pathEntry.sha256);\n await cache.put(this.blobUrl(pathEntry.sha256), new Response(responseBody(bytes)));\n index.blobs[pathEntry.sha256] = {\n size: byteLength(bytes),\n lastAccessed: now,\n refCount: index.blobs[pathEntry.sha256]?.refCount ?? 0,\n };\n }\n\n const key = aggregateKey(input.manifest.dTag, input.manifest.aggregateHash);\n const record: AggregateRecord = {\n schemaVersion: SCHEMA_VERSION,\n dTag: input.manifest.dTag,\n aggregateHash: input.manifest.aggregateHash,\n manifest: {\n kind: input.manifest.kind,\n pubkey: input.manifest.pubkey,\n dTag: input.manifest.dTag,\n paths: input.manifest.paths.map((entry) => ({ path: entry.path, sha256: entry.sha256 })),\n requires: [...input.manifest.requires],\n ...(input.manifest.title === undefined ? {} : { title: input.manifest.title }),\n ...(input.manifest.description === undefined ? {} : { description: input.manifest.description }),\n },\n indexHtml: input.indexHtml,\n blobHashes,\n writtenAt: now,\n };\n const aggregateBytes = new TextEncoder().encode(JSON.stringify(record));\n await cache.put(this.aggregateUrl(key), new Response(responseBody(aggregateBytes), {\n headers: { 'content-type': 'application/json' },\n }));\n\n const coordKey = coordinateKey(input.manifest.kind, input.manifest.pubkey, input.manifest.dTag);\n index.aggregates[key] = {\n dTag: input.manifest.dTag,\n aggregateHash: input.manifest.aggregateHash,\n key,\n blobHashes,\n size: byteLength(aggregateBytes),\n lastAccessed: now,\n coordinateKey: coordKey,\n };\n index.coordinates[coordKey] = {\n key: coordKey,\n kind: input.manifest.kind,\n pubkey: input.manifest.pubkey,\n dTag: input.manifest.dTag,\n aggregateHash: input.manifest.aggregateHash,\n lastResolvedAt: now,\n foregroundTtlMs: DEFAULT_FOREGROUND_TTL_MS,\n backgroundTtlMs: DEFAULT_BACKGROUND_TTL_MS,\n };\n\n this.rewriteRefCounts(index);\n index.updatedAt = now;\n await this.writeIndex(cache, index);\n await this.prune(now);\n }\n\n private async open(): Promise<Cache> {\n this.cachePromise ??= this.cacheStorage.open(this.cacheName);\n return this.cachePromise;\n }\n\n private indexUrl(): string {\n return new URL('__kehto_napplet_cache/index.json', this.baseUrl).toString();\n }\n\n private blobUrl(hash: string): string {\n return new URL(`__kehto_napplet_cache/blob/${encodeKeyPart(hash)}`, this.baseUrl).toString();\n }\n\n private aggregateUrl(key: string): string {\n return new URL(`__kehto_napplet_cache/aggregate/${encodeKeyPart(key)}.json`, this.baseUrl).toString();\n }\n\n private async readIndex(cache: Cache): Promise<NappletArtifactCacheIndex> {\n const response = await cache.match(this.indexUrl());\n if (!response) return emptyIndex(this.now());\n try {\n const index = await responseJson<NappletArtifactCacheIndex>(response);\n if (index.schemaVersion !== SCHEMA_VERSION) return emptyIndex(this.now());\n return index;\n } catch {\n return emptyIndex(this.now());\n }\n }\n\n private async writeIndex(cache: Cache, index: NappletArtifactCacheIndex): Promise<void> {\n await cache.put(this.indexUrl(), new Response(JSON.stringify(index), {\n headers: { 'content-type': 'application/json' },\n }));\n }\n\n private rewriteRefCounts(index: NappletArtifactCacheIndex): void {\n const counts = refCounts(index);\n for (const [hash, blob] of Object.entries(index.blobs)) {\n blob.refCount = counts[hash] ?? 0;\n }\n }\n\n private async budgetBytes(): Promise<number> {\n const estimate = await this.estimateStorage();\n if (!estimate?.quota) return this.unknownBudgetBytes;\n return Math.min(this.softCeilingBytes, Math.floor(estimate.quota * 0.1));\n }\n\n private async hardBudgetBytes(): Promise<number> {\n const estimate = await this.estimateStorage();\n if (!estimate?.quota) return this.unknownBudgetBytes;\n return Math.min(this.hardCeilingBytes, Math.floor(estimate.quota * 0.2));\n }\n\n private async originPressureHigh(): Promise<boolean> {\n const estimate = await this.estimateStorage();\n if (!estimate?.quota || estimate.usage === undefined) return false;\n return estimate.usage / estimate.quota >= 0.8;\n }\n\n private async estimateStorage(): Promise<StorageEstimate | undefined> {\n if (!this.storage?.estimate) return undefined;\n try {\n return this.storage.estimate();\n } catch (cause) {\n this.diagnostic({\n type: 'storage-estimate-unavailable',\n message: 'storage estimate failed; using unknown quota cache budget',\n cause,\n });\n return undefined;\n }\n }\n\n private diagnostic(diagnostic: NappletCacheDiagnostic): void {\n this.onDiagnostic?.(diagnostic);\n }\n}\n\n/**\n * Open the browser Cache Storage artifact cache when the host supports it.\n *\n * @param options - Cache construction options and diagnostics callback.\n * @returns A cache instance, or `undefined` when Cache Storage is unavailable.\n */\nexport async function openNappletArtifactCache(\n options: OpenNappletArtifactCacheOptions = {},\n): Promise<CacheStorageNappletArtifactCache | undefined> {\n const cacheStorage = options.cacheStorage ?? (globalThis as typeof globalThis & { caches?: CacheStorage }).caches;\n if (!cacheStorage) return undefined;\n const storage = options.storage ?? (globalThis.navigator as Navigator | undefined)?.storage;\n if (options.requireStorageEstimate && !storage?.estimate) {\n options.onDiagnostic?.({\n type: 'storage-estimate-unavailable',\n message: 'storage estimates are unavailable; using network-only loading',\n });\n return undefined;\n }\n try {\n const cache = new CacheStorageNappletArtifactCache({ ...options, cacheStorage, storage });\n await cache.snapshotIndex();\n return cache;\n } catch (cause) {\n options.onDiagnostic?.({\n type: 'cache-open-failed',\n message: 'Cache Storage napplet artifact cache could not be opened',\n cause,\n });\n return undefined;\n }\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/5d/index.ts","../src/5d/artifact-cache.ts"],"sourcesContent":["import type { NostrEvent } from 'nostr-tools';\nimport { verifyEvent } from 'nostr-tools/pure';\nimport { sha256 } from '@noble/hashes/sha2.js';\nimport { bytesToHex } from '@noble/hashes/utils.js';\nimport {\n computeAggregateHash,\n pathEntriesFromTags,\n aggregateTagValue,\n type PathEntry,\n} from '../5a/index.js';\nimport type { NappletArtifactCache } from './artifact-cache.js';\nexport {\n CacheStorageNappletArtifactCache,\n NAPPLET_ARTIFACT_CACHE_NAME,\n coordinateKey,\n isCoordinateFresh,\n openNappletArtifactCache,\n type CachedCoordinate,\n type CacheStorageNappletArtifactCacheOptions,\n type CoordinateFreshnessOptions,\n type NappletAggregateIndexEntry,\n type NappletArtifactCacheIndex,\n type NappletArtifactCache,\n type NappletBlobIndexEntry,\n type NappletCacheDiagnostic,\n type OpenNappletArtifactCacheOptions,\n type WriteVerifiedResolutionInput,\n} from './artifact-cache.js';\n\n/**\n * `@kehto/nip/5d` — NIP-5D napplet manifest resolution.\n *\n * NIP-5D (`dskvr/nips` branch `nip/5d`) defines content-addressed \"napplet\"\n * web applets published as Nostr events that reference their files by hash\n * (NIP-5A `path` tags + an aggregate `x` tag). A runtime resolves a napplet by:\n *\n * 1. verifying the manifest event's signature,\n * 2. recomputing the NIP-5A aggregate from its `path` tags and checking the\n * `[\"x\",\"<hex>\",\"aggregate\"]` tag,\n * 3. fetching each referenced blob (e.g. from Blossom) and verifying its hash,\n * 4. assembling the verified `/index.html`.\n *\n * The napplet's identity is the `(dTag, aggregateHash)` tuple **computed** from\n * these verified bytes — it is never accepted from a host or gateway.\n *\n * @module\n */\n\n/** Snapshot manifest — regular event, immutable point-in-time release. */\nexport const NAPPLET_KIND_SNAPSHOT = 5129;\n/** Root manifest — replaceable event, an author's latest unnamed napplet. */\nexport const NAPPLET_KIND_ROOT = 15129;\n/** Named manifest — addressable event (carries a `d` tag identifier). */\nexport const NAPPLET_KIND_NAMED = 35129;\n\n/** All three NIP-5D napplet manifest kinds. */\nexport const NAPPLET_KINDS: readonly number[] = [\n NAPPLET_KIND_SNAPSHOT,\n NAPPLET_KIND_ROOT,\n NAPPLET_KIND_NAMED,\n];\n\n/**\n * Whether `kind` is one of the three NIP-5D napplet manifest kinds.\n *\n * @param kind - A Nostr event kind\n * @returns `true` for `5129` / `15129` / `35129`\n */\nexport function isNappletManifestKind(kind: number): boolean {\n return NAPPLET_KINDS.includes(kind);\n}\n\n/**\n * A parsed NIP-5D napplet manifest. All fields are derived from the manifest\n * event's tags; the aggregate is the declared `x` tag (verify it separately\n * with {@link resolveNapplet} or `@kehto/nip/5a` `verifyAggregate`).\n */\nexport interface NappletManifest {\n /** Manifest event kind (`5129` / `15129` / `35129`). */\n kind: number;\n /** Author hex public key. */\n pubkey: string;\n /** Named-napplet `d` identifier, or `''` for root/snapshot manifests. */\n dTag: string;\n /** File path entries from `path` tags. */\n paths: PathEntry[];\n /** Declared aggregate hash from the `[\"x\",\"<hex>\",\"aggregate\"]` tag. */\n aggregateHash: string;\n /** Blossom server URL hints from `server` tags. */\n servers: string[];\n /** Short NAP capability names from `requires` tags. */\n requires: string[];\n /**\n * Convention contracts this napplet fulfills, from `archetype` manifest tags.\n */\n archetypes: Array<{ slug: string; convention: string }>;\n /** Optional human title. */\n title?: string;\n /** Optional human description. */\n description?: string;\n /** Optional upstream source URL from the `source` tag. */\n source?: string;\n}\n\n/** Error codes for every napplet resolution failure path. */\nexport type NappletResolutionErrorCode =\n | 'invalid-signature'\n | 'invalid-manifest'\n | 'aggregate-mismatch'\n | 'blob-hash-mismatch'\n | 'blob-unavailable'\n | 'missing-index';\n\n/**\n * Thrown on any napplet resolution/verification failure. The `code` field\n * identifies which guard rejected, so callers can fail closed without parsing\n * the message.\n */\nexport class NappletResolutionError extends Error {\n readonly code: NappletResolutionErrorCode;\n constructor(code: NappletResolutionErrorCode, message: string) {\n super(message);\n this.name = 'NappletResolutionError';\n this.code = code;\n }\n}\n\nfunction firstTagValue(tags: readonly (readonly string[])[], name: string): string | undefined {\n for (const tag of tags) {\n if (tag[0] === name && typeof tag[1] === 'string' && tag[1].length > 0) return tag[1];\n }\n return undefined;\n}\n\nfunction allTagValues(tags: readonly (readonly string[])[], name: string): string[] {\n const out: string[] = [];\n for (const tag of tags) {\n if (tag[0] === name && typeof tag[1] === 'string' && tag[1].length > 0) out.push(tag[1]);\n }\n return out;\n}\n\nfunction archetypesFromTags(\n tags: readonly (readonly string[])[],\n): Array<{ slug: string; convention: string }> {\n const out: Array<{ slug: string; convention: string }> = [];\n for (const tag of tags) {\n if (tag[0] !== 'archetype') continue;\n const slug = tag[1];\n if (typeof slug !== 'string' || !/^[a-z0-9][a-z0-9-]*$/.test(slug)) {\n throw new NappletResolutionError(\n 'invalid-manifest',\n 'archetype slug must contain lowercase letters, numbers, and hyphens',\n );\n }\n\n const convention = tag[2];\n if (typeof convention !== 'string' || convention.length === 0) {\n throw new NappletResolutionError('invalid-manifest', 'archetype convention is required');\n }\n if (/^NAP-\\d+$/.test(convention)) {\n throw new NappletResolutionError(\n 'invalid-manifest',\n 'numbered NAP identifier is not an archetype convention',\n );\n }\n const conventionMatch = /^napplet:([^/?#\\s]+)\\/([^/?#\\s]+)$/.exec(convention);\n if (!conventionMatch) {\n throw new NappletResolutionError(\n 'invalid-manifest',\n 'archetype convention must be a queryless napplet:<archetype>/<intent> identity',\n );\n }\n if (conventionMatch[1] !== slug) {\n throw new NappletResolutionError(\n 'invalid-manifest',\n 'archetype slug must match the convention archetype',\n );\n }\n\n if (tag.length !== 3) {\n throw new NappletResolutionError(\n 'invalid-manifest',\n 'archetype tags must contain exactly slug and convention',\n );\n }\n out.push({ slug, convention });\n }\n return out;\n}\n\n/**\n * Parse a NIP-5D manifest event into a {@link NappletManifest}.\n *\n * Does not verify the signature, aggregate, or blobs — use {@link resolveNapplet}\n * for full verification.\n *\n * @param event - A NIP-5D manifest event (`5129` / `15129` / `35129`)\n * @returns The parsed manifest\n * @throws {@link NappletResolutionError} `invalid-manifest` for a non-napplet\n * kind, missing `path` tags, or a missing aggregate `x` tag\n */\nexport function parseNappletManifest(event: NostrEvent): NappletManifest {\n if (!isNappletManifestKind(event.kind)) {\n throw new NappletResolutionError('invalid-manifest', `not a NIP-5D napplet kind: ${event.kind}`);\n }\n const paths = pathEntriesFromTags(event.tags);\n if (paths.length === 0) {\n throw new NappletResolutionError('invalid-manifest', 'manifest has no path tags');\n }\n const aggregateHash = aggregateTagValue(event.tags);\n if (!aggregateHash) {\n throw new NappletResolutionError('invalid-manifest', 'manifest has no aggregate x tag');\n }\n const dTag = firstTagValue(event.tags, 'd');\n if (event.kind !== NAPPLET_KIND_NAMED && dTag !== undefined) {\n throw new NappletResolutionError('invalid-manifest', 'only named manifests may carry a d tag');\n }\n return {\n kind: event.kind,\n pubkey: event.pubkey,\n dTag: dTag ?? '',\n paths,\n aggregateHash,\n servers: allTagValues(event.tags, 'server'),\n requires: allTagValues(event.tags, 'requires'),\n archetypes: archetypesFromTags(event.tags),\n title: firstTagValue(event.tags, 'title'),\n description: firstTagValue(event.tags, 'description'),\n source: firstTagValue(event.tags, 'source'),\n };\n}\n\n/**\n * Verify a manifest event's Nostr signature (id + schnorr sig).\n *\n * @param event - A manifest event\n * @returns `true` if the event is internally consistent and validly signed\n */\nexport function verifyManifestSignature(event: NostrEvent): boolean {\n try {\n return verifyEvent(event);\n } catch {\n return false;\n }\n}\n\n/**\n * Whether `bytes` hash to the expected lowercase-hex SHA-256.\n *\n * @param bytes - The blob bytes\n * @param sha256Hex - Expected lowercase-hex SHA-256\n * @returns `true` only on an exact match\n */\nexport function verifyBlobHash(bytes: Uint8Array, sha256Hex: string): boolean {\n return bytesToHex(sha256(bytes)) === sha256Hex;\n}\n\nfunction stripTrailingSlash(url: string): string {\n return url.endsWith('/') ? url.slice(0, -1) : url;\n}\n\n/**\n * Fetch a blob by SHA-256 from a list of Blossom servers, returning the first\n * server's bytes whose hash matches. The returned bytes are always re-verified\n * against `sha256Hex` — servers (and gateways) are never trusted.\n *\n * @param servers - Candidate Blossom server base URLs, tried in order\n * @param sha256Hex - The blob's lowercase-hex SHA-256\n * @param fetchBytes - Fetches raw bytes for a URL (`<server>/<sha256>`)\n * @returns The verified blob bytes\n * @throws {@link NappletResolutionError} `blob-unavailable` if no server serves\n * a hash-matching blob\n */\nexport async function fetchBlob(\n servers: readonly string[],\n sha256Hex: string,\n fetchBytes: (url: string) => Promise<Uint8Array>,\n): Promise<Uint8Array> {\n for (const server of servers) {\n const url = `${stripTrailingSlash(server)}/${sha256Hex}`;\n try {\n const bytes = await fetchBytes(url);\n if (verifyBlobHash(bytes, sha256Hex)) return bytes;\n } catch {\n // try the next server\n }\n }\n throw new NappletResolutionError('blob-unavailable', `no server served blob ${sha256Hex}`);\n}\n\n/** A fully verified napplet, ready to inject via `iframe.srcdoc`. */\nexport interface ResolvedNapplet {\n /** Computed `d` identifier (`''` for root/snapshot). */\n dTag: string;\n /** Computed (and verified) aggregate hash — the content address. */\n aggregateHash: string;\n /** Verified file bytes keyed by manifest path. */\n files: Map<string, Uint8Array>;\n /** The verified `/index.html` decoded to text. */\n indexHtml: string;\n /** The parsed manifest. */\n manifest: NappletManifest;\n}\n\n/** Options for {@link resolveNapplet}. */\nexport interface ResolveNappletOptions {\n /** The candidate manifest event (resolved from relays by the caller). */\n event: NostrEvent;\n /**\n * Fetch raw blob bytes by SHA-256. Backed by Blossom (or a gateway) by the\n * caller. The bytes are re-verified against the hash here, so the fetcher is\n * untrusted.\n */\n fetchBlob: (sha256Hex: string, servers: readonly string[]) => Promise<Uint8Array>;\n /** Decode blob bytes to text for `indexHtml` (default UTF-8). */\n textDecode?: (bytes: Uint8Array) => string;\n /**\n * Optional verified artifact cache. Cache hits are still re-verified against\n * the manifest hash before use; cache writes happen only after the signature,\n * aggregate, and every blob hash have been verified.\n */\n cache?: NappletArtifactCache;\n}\n\nconst INDEX_PATHS = ['/index.html', 'index.html', '/'];\n\nfunction defaultDecode(bytes: Uint8Array): string {\n return new TextDecoder().decode(bytes);\n}\n\n/**\n * Resolve a napplet end-to-end from a candidate manifest event: verify the\n * signature, parse the manifest, verify the NIP-5A aggregate, fetch and verify\n * every blob, then assemble the verified `/index.html`.\n *\n * The returned `(dTag, aggregateHash)` is computed from the verified bytes and\n * is the napplet's identity. Any failure throws a {@link NappletResolutionError}\n * — the caller must fail closed (never render unverified bytes).\n *\n * @param options - {@link ResolveNappletOptions}\n * @returns The {@link ResolvedNapplet}\n * @throws {@link NappletResolutionError} on signature, manifest, aggregate,\n * blob-hash, blob-availability, or missing-index failures\n *\n * @example\n * ```ts\n * import { resolveNapplet } from '@kehto/nip/5d';\n * const napplet = await resolveNapplet({ event, fetchBlob });\n * iframe.srcdoc = napplet.indexHtml; // sandbox=\"allow-scripts\", opaque origin\n * ```\n */\nexport async function resolveNapplet(options: ResolveNappletOptions): Promise<ResolvedNapplet> {\n const { event, fetchBlob: fetchBlobBytes, textDecode = defaultDecode, cache } = options;\n\n if (!verifyManifestSignature(event)) {\n throw new NappletResolutionError('invalid-signature', 'manifest signature is invalid');\n }\n\n const manifest = parseNappletManifest(event);\n\n const recomputed = computeAggregateHash(manifest.paths);\n if (recomputed !== manifest.aggregateHash) {\n throw new NappletResolutionError(\n 'aggregate-mismatch',\n `recomputed aggregate ${recomputed} != manifest ${manifest.aggregateHash}`,\n );\n }\n\n const files = new Map<string, Uint8Array>();\n for (const entry of manifest.paths) {\n let bytes = await cache?.readBlob(entry.sha256);\n if (bytes && !verifyBlobHash(bytes, entry.sha256)) {\n await cache?.deleteBlob(entry.sha256);\n bytes = undefined;\n }\n bytes ??= await fetchBlobBytes(entry.sha256, manifest.servers);\n if (!verifyBlobHash(bytes, entry.sha256)) {\n throw new NappletResolutionError(\n 'blob-hash-mismatch',\n `blob for ${entry.path} does not match hash ${entry.sha256}`,\n );\n }\n files.set(entry.path, bytes);\n }\n\n const indexEntry = manifest.paths.find((e) => INDEX_PATHS.includes(e.path));\n if (!indexEntry) {\n throw new NappletResolutionError('missing-index', 'manifest has no /index.html entry');\n }\n\n const indexHtml = textDecode(files.get(indexEntry.path)!);\n await cache?.writeVerifiedResolution({ event, manifest, files, indexHtml });\n\n return {\n dTag: manifest.dTag,\n aggregateHash: manifest.aggregateHash,\n files,\n indexHtml,\n manifest,\n };\n}\n","import type { NostrEvent } from 'nostr-tools';\nimport type { NappletManifest } from './index.js';\n\nconst MIB = 1024 * 1024;\n\n/** Versioned Cache Storage namespace for verified napplet artifacts. */\nexport const NAPPLET_ARTIFACT_CACHE_NAME = 'kehto:napplet-artifacts:v1';\nconst SCHEMA_VERSION = 1;\nconst DEFAULT_BASE_URL = 'https://kehto.invalid';\nconst DEFAULT_UNKNOWN_BUDGET_BYTES = 32 * MIB;\nconst DEFAULT_SOFT_CEILING_BYTES = 128 * MIB;\nconst DEFAULT_HARD_CEILING_BYTES = 256 * MIB;\nconst DEFAULT_FOREGROUND_TTL_MS = 15 * 60 * 1000;\nconst DEFAULT_BACKGROUND_TTL_MS = 60 * 60 * 1000;\n\nexport interface NappletCacheDiagnostic {\n /** Diagnostic category for cache open/read/write/delete/prune/storage-estimate failures. */\n type:\n | 'cache-open-failed'\n | 'cache-read-failed'\n | 'cache-write-failed'\n | 'cache-delete-failed'\n | 'cache-prune-failed'\n | 'storage-estimate-unavailable';\n /** Human-readable diagnostic detail suitable for logs. */\n message: string;\n /** Original thrown value, when one exists. */\n cause?: unknown;\n}\n\n/** Metadata tracked for one cached napplet artifact blob. */\nexport interface NappletBlobIndexEntry {\n /** Blob byte length. */\n size: number;\n /** Last read or write timestamp in milliseconds. */\n lastAccessed: number;\n /** Number of cached aggregate records currently referencing this blob. */\n refCount: number;\n}\n\n/** Metadata tracked for one verified aggregate cache record. */\nexport interface NappletAggregateIndexEntry {\n /** Manifest `d` tag. */\n dTag: string;\n /** Verified NIP-5A aggregate hash. */\n aggregateHash: string;\n /** Internal cache key derived from `dTag` and `aggregateHash`. */\n key: string;\n /** SHA-256 hashes of blobs referenced by this aggregate. */\n blobHashes: string[];\n /** Approximate stored aggregate-record byte size. */\n size: number;\n /** Last read or write timestamp in milliseconds. */\n lastAccessed: number;\n /** Coordinate cache key that currently points at this aggregate, if any. */\n coordinateKey?: string;\n /** Reserved pin marker for hosts that want to protect an aggregate. */\n pinned?: boolean;\n}\n\n/** Cached mapping from a NIP-5D coordinate to a verified aggregate. */\nexport interface CachedCoordinate {\n /** Internal coordinate key in the form `kind:pubkey:dTag`. */\n key: string;\n /** Napplet manifest event kind. */\n kind: number;\n /** Manifest author public key. */\n pubkey: string;\n /** Manifest `d` tag. */\n dTag: string;\n /** Verified aggregate hash for the coordinate. */\n aggregateHash: string;\n /** Timestamp when this coordinate last resolved successfully. */\n lastResolvedAt: number;\n /** Foreground refresh TTL in milliseconds. */\n foregroundTtlMs: number;\n /** Background refresh TTL in milliseconds. */\n backgroundTtlMs: number;\n}\n\n/** Serialized Cache Storage index for verified napplet artifacts. */\nexport interface NappletArtifactCacheIndex {\n /** Index schema version. */\n schemaVersion: number;\n /** Last index update timestamp in milliseconds. */\n updatedAt: number;\n /** Blob entries keyed by SHA-256 hash. */\n blobs: Record<string, NappletBlobIndexEntry>;\n /** Aggregate entries keyed by the internal aggregate key. */\n aggregates: Record<string, NappletAggregateIndexEntry>;\n /** Coordinate entries keyed by {@link coordinateKey}. */\n coordinates: Record<string, CachedCoordinate>;\n}\n\ninterface AggregateRecord {\n schemaVersion: number;\n dTag: string;\n aggregateHash: string;\n manifest: {\n kind: number;\n pubkey: string;\n dTag: string;\n paths: Array<{ path: string; sha256: string }>;\n requires: string[];\n title?: string;\n description?: string;\n };\n indexHtml: string;\n blobHashes: string[];\n writtenAt: number;\n}\n\nexport interface WriteVerifiedResolutionInput {\n /** Verified NIP-5D manifest event. */\n event: NostrEvent;\n /** Parsed manifest whose aggregate and paths have already been verified. */\n manifest: NappletManifest;\n /** File bytes keyed by manifest path. */\n files: ReadonlyMap<string, Uint8Array>;\n /** Verified `/index.html` bytes decoded as text. */\n indexHtml: string;\n /** Timestamp override for deterministic tests. */\n now?: number;\n}\n\n/** Freshness controls for reading a cached coordinate. */\nexport interface CoordinateFreshnessOptions {\n /** Timestamp override in milliseconds. */\n now?: number;\n /** Use the background TTL instead of the foreground TTL. */\n background?: boolean;\n}\n\n/** Cache interface for verified NIP-5D napplet artifacts. */\nexport interface NappletArtifactCache {\n /** Read one cached artifact blob by SHA-256 hash. */\n readBlob(sha256Hex: string): Promise<Uint8Array | undefined>;\n /** Delete one cached artifact blob by SHA-256 hash. */\n deleteBlob(sha256Hex: string): Promise<void>;\n /** Write a verified manifest resolution and all referenced blobs. */\n writeVerifiedResolution(input: WriteVerifiedResolutionInput): Promise<void>;\n /** Cache the latest aggregate for a NIP-5D coordinate. */\n writeCoordinate(input: {\n kind: number;\n pubkey: string;\n dTag: string;\n aggregateHash: string;\n lastResolvedAt?: number;\n foregroundTtlMs?: number;\n backgroundTtlMs?: number;\n }): Promise<void>;\n /** Read a cached coordinate with a computed freshness flag. */\n getCoordinate(input: {\n kind: number;\n pubkey: string;\n dTag: string;\n }, options?: CoordinateFreshnessOptions): Promise<(CachedCoordinate & { fresh: boolean }) | undefined>;\n /** Update aggregate/blob last-access timestamps. */\n touchAggregate(dTag: string, aggregateHash: string, now?: number): Promise<void>;\n /** Protect an aggregate from normal pruning while it is in active use. */\n markAggregateActive(dTag: string, aggregateHash: string): void;\n /** Release an aggregate previously marked active. */\n releaseAggregateActive(dTag: string, aggregateHash: string): void;\n /** Prune cache contents to the configured storage budget. */\n prune(now?: number): Promise<void>;\n}\n\n/** Constructor options for {@link CacheStorageNappletArtifactCache}. */\nexport interface CacheStorageNappletArtifactCacheOptions {\n /** Cache Storage implementation; defaults to `globalThis.caches`. */\n cacheStorage?: CacheStorage;\n /** Storage manager used for quota estimates; defaults to `navigator.storage`. */\n storage?: StorageManager;\n /** Cache Storage namespace. */\n cacheName?: string;\n /** Base URL used to construct synthetic cache keys. */\n baseUrl?: string;\n /** Clock used for timestamps. */\n now?: () => number;\n /** Diagnostic callback for non-fatal cache failures. */\n onDiagnostic?: (diagnostic: NappletCacheDiagnostic) => void;\n /** Budget used when storage estimates are unavailable. */\n unknownBudgetBytes?: number;\n /** Soft cache ceiling in bytes. */\n softCeilingBytes?: number;\n /** Hard cache ceiling in bytes. */\n hardCeilingBytes?: number;\n}\n\n/** Options for opening the browser Cache Storage artifact cache. */\nexport interface OpenNappletArtifactCacheOptions extends CacheStorageNappletArtifactCacheOptions {\n /** Return `undefined` when quota estimates are unavailable. */\n requireStorageEstimate?: boolean;\n}\n\nfunction emptyIndex(now: number): NappletArtifactCacheIndex {\n return {\n schemaVersion: SCHEMA_VERSION,\n updatedAt: now,\n blobs: {},\n aggregates: {},\n coordinates: {},\n };\n}\n\nfunction aggregateKey(dTag: string, aggregateHash: string): string {\n return `${encodeURIComponent(dTag)}:${aggregateHash}`;\n}\n\n/**\n * Build the stable index key for a NIP-5D coordinate.\n *\n * @param kind - Manifest event kind.\n * @param pubkey - Manifest author public key.\n * @param dTag - Manifest `d` tag.\n * @returns Cache key in the form `kind:pubkey:dTag`.\n */\nexport function coordinateKey(kind: number, pubkey: string, dTag: string): string {\n return `${kind}:${pubkey}:${encodeURIComponent(dTag)}`;\n}\n\n/**\n * Check whether a cached coordinate is still inside its foreground/background TTL.\n *\n * @param coordinate - Cached coordinate to inspect.\n * @param options - Optional clock and TTL mode overrides.\n * @returns `true` when the coordinate can be used without a network refresh.\n */\nexport function isCoordinateFresh(\n coordinate: CachedCoordinate,\n options: CoordinateFreshnessOptions = {},\n): boolean {\n const now = options.now ?? Date.now();\n const ttl = options.background ? coordinate.backgroundTtlMs : coordinate.foregroundTtlMs;\n return ttl === Number.POSITIVE_INFINITY || now - coordinate.lastResolvedAt <= ttl;\n}\n\nfunction byteLength(bytes: Uint8Array): number {\n return bytes.byteLength;\n}\n\nfunction encodeKeyPart(value: string): string {\n return encodeURIComponent(value).replaceAll('%2F', '%252F');\n}\n\nfunction cloneBytes(bytes: Uint8Array): Uint8Array {\n return new Uint8Array(bytes);\n}\n\nfunction responseBody(bytes: Uint8Array): ArrayBuffer {\n return cloneBytes(bytes).buffer as ArrayBuffer;\n}\n\nfunction normalizeBaseUrl(baseUrl: string): string {\n return baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`;\n}\n\nfunction resolveBaseUrl(baseUrl?: string): string {\n if (baseUrl) return normalizeBaseUrl(baseUrl);\n const maybeLocation = globalThis as typeof globalThis & { location?: { origin?: string } };\n if (maybeLocation.location?.origin) return normalizeBaseUrl(maybeLocation.location.origin);\n return normalizeBaseUrl(DEFAULT_BASE_URL);\n}\n\nasync function responseBytes(response: Response): Promise<Uint8Array> {\n return new Uint8Array(await response.arrayBuffer());\n}\n\nasync function responseJson<T>(response: Response): Promise<T> {\n return response.json() as Promise<T>;\n}\n\nfunction refCounts(index: NappletArtifactCacheIndex): Record<string, number> {\n const counts: Record<string, number> = {};\n for (const aggregate of Object.values(index.aggregates)) {\n for (const hash of aggregate.blobHashes) counts[hash] = (counts[hash] ?? 0) + 1;\n }\n return counts;\n}\n\nfunction cachedSize(index: NappletArtifactCacheIndex): number {\n const blobBytes = Object.values(index.blobs).reduce((sum, entry) => sum + entry.size, 0);\n const aggregateBytes = Object.values(index.aggregates).reduce((sum, entry) => sum + entry.size, 0);\n return blobBytes + aggregateBytes;\n}\n\n/**\n * Browser Cache Storage implementation of {@link NappletArtifactCache}.\n *\n * It stores verified blobs and aggregate records under synthetic URLs, keeps a\n * small JSON index, and prunes according to quota-derived budgets.\n */\nexport class CacheStorageNappletArtifactCache implements NappletArtifactCache {\n private readonly cacheStorage: CacheStorage;\n private readonly storage?: StorageManager;\n private readonly cacheName: string;\n private readonly baseUrl: string;\n private readonly now: () => number;\n private readonly activeAggregates = new Set<string>();\n private readonly onDiagnostic?: (diagnostic: NappletCacheDiagnostic) => void;\n private readonly unknownBudgetBytes: number;\n private readonly softCeilingBytes: number;\n private readonly hardCeilingBytes: number;\n private cachePromise?: Promise<Cache>;\n\n constructor(options: CacheStorageNappletArtifactCacheOptions = {}) {\n const maybeCacheStorage = options.cacheStorage ?? (globalThis as typeof globalThis & { caches?: CacheStorage }).caches;\n if (!maybeCacheStorage) {\n throw new Error('Cache Storage is unavailable');\n }\n this.cacheStorage = maybeCacheStorage;\n this.storage = options.storage ?? (globalThis.navigator as Navigator | undefined)?.storage;\n this.cacheName = options.cacheName ?? NAPPLET_ARTIFACT_CACHE_NAME;\n this.baseUrl = resolveBaseUrl(options.baseUrl);\n this.now = options.now ?? Date.now;\n this.onDiagnostic = options.onDiagnostic;\n this.unknownBudgetBytes = options.unknownBudgetBytes ?? DEFAULT_UNKNOWN_BUDGET_BYTES;\n this.softCeilingBytes = options.softCeilingBytes ?? DEFAULT_SOFT_CEILING_BYTES;\n this.hardCeilingBytes = options.hardCeilingBytes ?? DEFAULT_HARD_CEILING_BYTES;\n }\n\n async readBlob(sha256Hex: string): Promise<Uint8Array | undefined> {\n try {\n const cache = await this.open();\n const response = await cache.match(this.blobUrl(sha256Hex));\n if (!response) return undefined;\n const bytes = await responseBytes(response);\n const index = await this.readIndex(cache);\n const entry = index.blobs[sha256Hex];\n if (entry) {\n entry.lastAccessed = this.now();\n index.updatedAt = entry.lastAccessed;\n await this.writeIndex(cache, index);\n }\n return bytes;\n } catch (cause) {\n this.diagnostic({ type: 'cache-read-failed', message: `failed to read blob ${sha256Hex}`, cause });\n return undefined;\n }\n }\n\n async deleteBlob(sha256Hex: string): Promise<void> {\n try {\n const cache = await this.open();\n await cache.delete(this.blobUrl(sha256Hex));\n const index = await this.readIndex(cache);\n delete index.blobs[sha256Hex];\n index.updatedAt = this.now();\n await this.writeIndex(cache, index);\n } catch (cause) {\n this.diagnostic({ type: 'cache-delete-failed', message: `failed to delete blob ${sha256Hex}`, cause });\n }\n }\n\n async writeVerifiedResolution(input: WriteVerifiedResolutionInput): Promise<void> {\n try {\n await this.writeVerifiedResolutionOnce(input);\n } catch (firstCause) {\n try {\n await this.prune(input.now ?? this.now());\n await this.writeVerifiedResolutionOnce(input);\n } catch (cause) {\n this.diagnostic({\n type: 'cache-write-failed',\n message: 'failed to write verified napplet artifacts after prune retry',\n cause: cause ?? firstCause,\n });\n }\n }\n }\n\n async writeCoordinate(input: {\n kind: number;\n pubkey: string;\n dTag: string;\n aggregateHash: string;\n lastResolvedAt?: number;\n foregroundTtlMs?: number;\n backgroundTtlMs?: number;\n }): Promise<void> {\n const cache = await this.open();\n const now = this.now();\n const index = await this.readIndex(cache);\n const key = coordinateKey(input.kind, input.pubkey, input.dTag);\n index.coordinates[key] = {\n key,\n kind: input.kind,\n pubkey: input.pubkey,\n dTag: input.dTag,\n aggregateHash: input.aggregateHash,\n lastResolvedAt: input.lastResolvedAt ?? now,\n foregroundTtlMs: input.foregroundTtlMs ?? DEFAULT_FOREGROUND_TTL_MS,\n backgroundTtlMs: input.backgroundTtlMs ?? DEFAULT_BACKGROUND_TTL_MS,\n };\n index.updatedAt = now;\n await this.writeIndex(cache, index);\n }\n\n async getCoordinate(\n input: { kind: number; pubkey: string; dTag: string },\n options: CoordinateFreshnessOptions = {},\n ): Promise<(CachedCoordinate & { fresh: boolean }) | undefined> {\n const cache = await this.open();\n const index = await this.readIndex(cache);\n const coordinate = index.coordinates[coordinateKey(input.kind, input.pubkey, input.dTag)];\n if (!coordinate) return undefined;\n return { ...coordinate, fresh: isCoordinateFresh(coordinate, options) };\n }\n\n async touchAggregate(dTag: string, aggregateHash: string, now: number = this.now()): Promise<void> {\n const cache = await this.open();\n const index = await this.readIndex(cache);\n const key = aggregateKey(dTag, aggregateHash);\n const aggregate = index.aggregates[key];\n if (!aggregate) return;\n aggregate.lastAccessed = now;\n for (const hash of aggregate.blobHashes) {\n if (index.blobs[hash]) index.blobs[hash].lastAccessed = now;\n }\n index.updatedAt = now;\n await this.writeIndex(cache, index);\n }\n\n markAggregateActive(dTag: string, aggregateHash: string): void {\n this.activeAggregates.add(aggregateKey(dTag, aggregateHash));\n }\n\n releaseAggregateActive(dTag: string, aggregateHash: string): void {\n this.activeAggregates.delete(aggregateKey(dTag, aggregateHash));\n }\n\n async prune(now: number = this.now()): Promise<void> {\n try {\n const cache = await this.open();\n const index = await this.readIndex(cache);\n let size = cachedSize(index);\n const budget = await this.budgetBytes();\n const hardBudget = await this.hardBudgetBytes();\n const shouldPrune = size > budget || await this.originPressureHigh();\n if (!shouldPrune) return;\n\n this.rewriteRefCounts(index);\n\n for (const [hash, blob] of Object.entries(index.blobs)) {\n if (size <= budget) break;\n if (blob.refCount > 0) continue;\n await cache.delete(this.blobUrl(hash));\n delete index.blobs[hash];\n size -= blob.size;\n }\n\n const currentAggregateHashes = new Set(\n Object.values(index.coordinates).map((coordinate) => coordinate.aggregateHash),\n );\n const aggregates = Object.values(index.aggregates)\n .filter((entry) => !this.activeAggregates.has(entry.key))\n .sort((a, b) => {\n const currentA = currentAggregateHashes.has(a.aggregateHash) ? 1 : 0;\n const currentB = currentAggregateHashes.has(b.aggregateHash) ? 1 : 0;\n if (currentA !== currentB) return currentA - currentB;\n return a.lastAccessed - b.lastAccessed;\n });\n\n for (const aggregate of aggregates) {\n if (size <= budget) break;\n await cache.delete(this.aggregateUrl(aggregate.key));\n delete index.aggregates[aggregate.key];\n size -= aggregate.size;\n this.rewriteRefCounts(index);\n }\n\n if (size > hardBudget) {\n const active = Object.values(index.aggregates)\n .filter((entry) => this.activeAggregates.has(entry.key))\n .sort((a, b) => a.lastAccessed - b.lastAccessed);\n for (const aggregate of active) {\n if (size <= hardBudget) break;\n await cache.delete(this.aggregateUrl(aggregate.key));\n delete index.aggregates[aggregate.key];\n size -= aggregate.size;\n }\n }\n\n this.rewriteRefCounts(index);\n for (const [hash, blob] of Object.entries(index.blobs)) {\n if (blob.refCount === 0) {\n await cache.delete(this.blobUrl(hash));\n delete index.blobs[hash];\n }\n }\n index.updatedAt = now;\n await this.writeIndex(cache, index);\n } catch (cause) {\n this.diagnostic({ type: 'cache-prune-failed', message: 'failed to prune napplet artifact cache', cause });\n }\n }\n\n async snapshotIndex(): Promise<NappletArtifactCacheIndex> {\n return this.readIndex(await this.open());\n }\n\n private async writeVerifiedResolutionOnce(input: WriteVerifiedResolutionInput): Promise<void> {\n const cache = await this.open();\n const now = input.now ?? this.now();\n const index = await this.readIndex(cache);\n const blobHashes: string[] = [];\n\n for (const pathEntry of input.manifest.paths) {\n const bytes = input.files.get(pathEntry.path);\n if (!bytes) continue;\n blobHashes.push(pathEntry.sha256);\n await cache.put(this.blobUrl(pathEntry.sha256), new Response(responseBody(bytes)));\n index.blobs[pathEntry.sha256] = {\n size: byteLength(bytes),\n lastAccessed: now,\n refCount: index.blobs[pathEntry.sha256]?.refCount ?? 0,\n };\n }\n\n const key = aggregateKey(input.manifest.dTag, input.manifest.aggregateHash);\n const record: AggregateRecord = {\n schemaVersion: SCHEMA_VERSION,\n dTag: input.manifest.dTag,\n aggregateHash: input.manifest.aggregateHash,\n manifest: {\n kind: input.manifest.kind,\n pubkey: input.manifest.pubkey,\n dTag: input.manifest.dTag,\n paths: input.manifest.paths.map((entry) => ({ path: entry.path, sha256: entry.sha256 })),\n requires: [...input.manifest.requires],\n ...(input.manifest.title === undefined ? {} : { title: input.manifest.title }),\n ...(input.manifest.description === undefined ? {} : { description: input.manifest.description }),\n },\n indexHtml: input.indexHtml,\n blobHashes,\n writtenAt: now,\n };\n const aggregateBytes = new TextEncoder().encode(JSON.stringify(record));\n await cache.put(this.aggregateUrl(key), new Response(responseBody(aggregateBytes), {\n headers: { 'content-type': 'application/json' },\n }));\n\n const coordKey = coordinateKey(input.manifest.kind, input.manifest.pubkey, input.manifest.dTag);\n index.aggregates[key] = {\n dTag: input.manifest.dTag,\n aggregateHash: input.manifest.aggregateHash,\n key,\n blobHashes,\n size: byteLength(aggregateBytes),\n lastAccessed: now,\n coordinateKey: coordKey,\n };\n index.coordinates[coordKey] = {\n key: coordKey,\n kind: input.manifest.kind,\n pubkey: input.manifest.pubkey,\n dTag: input.manifest.dTag,\n aggregateHash: input.manifest.aggregateHash,\n lastResolvedAt: now,\n foregroundTtlMs: DEFAULT_FOREGROUND_TTL_MS,\n backgroundTtlMs: DEFAULT_BACKGROUND_TTL_MS,\n };\n\n this.rewriteRefCounts(index);\n index.updatedAt = now;\n await this.writeIndex(cache, index);\n await this.prune(now);\n }\n\n private async open(): Promise<Cache> {\n this.cachePromise ??= this.cacheStorage.open(this.cacheName);\n return this.cachePromise;\n }\n\n private indexUrl(): string {\n return new URL('__kehto_napplet_cache/index.json', this.baseUrl).toString();\n }\n\n private blobUrl(hash: string): string {\n return new URL(`__kehto_napplet_cache/blob/${encodeKeyPart(hash)}`, this.baseUrl).toString();\n }\n\n private aggregateUrl(key: string): string {\n return new URL(`__kehto_napplet_cache/aggregate/${encodeKeyPart(key)}.json`, this.baseUrl).toString();\n }\n\n private async readIndex(cache: Cache): Promise<NappletArtifactCacheIndex> {\n const response = await cache.match(this.indexUrl());\n if (!response) return emptyIndex(this.now());\n try {\n const index = await responseJson<NappletArtifactCacheIndex>(response);\n if (index.schemaVersion !== SCHEMA_VERSION) return emptyIndex(this.now());\n return index;\n } catch {\n return emptyIndex(this.now());\n }\n }\n\n private async writeIndex(cache: Cache, index: NappletArtifactCacheIndex): Promise<void> {\n await cache.put(this.indexUrl(), new Response(JSON.stringify(index), {\n headers: { 'content-type': 'application/json' },\n }));\n }\n\n private rewriteRefCounts(index: NappletArtifactCacheIndex): void {\n const counts = refCounts(index);\n for (const [hash, blob] of Object.entries(index.blobs)) {\n blob.refCount = counts[hash] ?? 0;\n }\n }\n\n private async budgetBytes(): Promise<number> {\n const estimate = await this.estimateStorage();\n if (!estimate?.quota) return this.unknownBudgetBytes;\n return Math.min(this.softCeilingBytes, Math.floor(estimate.quota * 0.1));\n }\n\n private async hardBudgetBytes(): Promise<number> {\n const estimate = await this.estimateStorage();\n if (!estimate?.quota) return this.unknownBudgetBytes;\n return Math.min(this.hardCeilingBytes, Math.floor(estimate.quota * 0.2));\n }\n\n private async originPressureHigh(): Promise<boolean> {\n const estimate = await this.estimateStorage();\n if (!estimate?.quota || estimate.usage === undefined) return false;\n return estimate.usage / estimate.quota >= 0.8;\n }\n\n private async estimateStorage(): Promise<StorageEstimate | undefined> {\n if (!this.storage?.estimate) return undefined;\n try {\n return this.storage.estimate();\n } catch (cause) {\n this.diagnostic({\n type: 'storage-estimate-unavailable',\n message: 'storage estimate failed; using unknown quota cache budget',\n cause,\n });\n return undefined;\n }\n }\n\n private diagnostic(diagnostic: NappletCacheDiagnostic): void {\n this.onDiagnostic?.(diagnostic);\n }\n}\n\n/**\n * Open the browser Cache Storage artifact cache when the host supports it.\n *\n * @param options - Cache construction options and diagnostics callback.\n * @returns A cache instance, or `undefined` when Cache Storage is unavailable.\n */\nexport async function openNappletArtifactCache(\n options: OpenNappletArtifactCacheOptions = {},\n): Promise<CacheStorageNappletArtifactCache | undefined> {\n const cacheStorage = options.cacheStorage ?? (globalThis as typeof globalThis & { caches?: CacheStorage }).caches;\n if (!cacheStorage) return undefined;\n const storage = options.storage ?? (globalThis.navigator as Navigator | undefined)?.storage;\n if (options.requireStorageEstimate && !storage?.estimate) {\n options.onDiagnostic?.({\n type: 'storage-estimate-unavailable',\n message: 'storage estimates are unavailable; using network-only loading',\n });\n return undefined;\n }\n try {\n const cache = new CacheStorageNappletArtifactCache({ ...options, cacheStorage, storage });\n await cache.snapshotIndex();\n return cache;\n } catch (cause) {\n options.onDiagnostic?.({\n type: 'cache-open-failed',\n message: 'Cache Storage napplet artifact cache could not be opened',\n cause,\n });\n return undefined;\n }\n}\n"],"mappings":"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","names":[]}
|