@kehto/nip 0.1.0 → 0.4.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.
@@ -0,0 +1,83 @@
1
+ // src/89/index.ts
2
+ var HANDLER_INFORMATION_KIND = 31990;
3
+ var HANDLER_RECOMMENDATION_KIND = 31989;
4
+ var HANDLER_PLATFORMS = ["web", "ios", "android", "macos", "windows", "linux"];
5
+ var PLATFORM_SET = new Set(HANDLER_PLATFORMS);
6
+ function parseHandlerInformation(event) {
7
+ let identifier = "";
8
+ const kinds = [];
9
+ const seenKinds = /* @__PURE__ */ new Set();
10
+ const platforms = {};
11
+ for (const tag of event.tags) {
12
+ const name = tag[0];
13
+ if (name === "d") {
14
+ identifier = tag[1] ?? "";
15
+ } else if (name === "k") {
16
+ const k = Number(tag[1]);
17
+ if (Number.isInteger(k) && !seenKinds.has(k)) {
18
+ seenKinds.add(k);
19
+ kinds.push(k);
20
+ }
21
+ } else if (PLATFORM_SET.has(name) && typeof tag[1] === "string" && tag[1].length > 0) {
22
+ const platform = name;
23
+ (platforms[platform] ??= []).push({ url: tag[1], entity: tag[2] });
24
+ }
25
+ }
26
+ return {
27
+ pubkey: event.pubkey,
28
+ identifier,
29
+ address: `${HANDLER_INFORMATION_KIND}:${event.pubkey}:${identifier}`,
30
+ kinds,
31
+ metadata: parseJsonObject(event.content),
32
+ platforms
33
+ };
34
+ }
35
+ function handlesKind(info, kind) {
36
+ return info.kinds.includes(kind);
37
+ }
38
+ function buildHandlerUrl(target, values) {
39
+ const template = typeof target === "string" ? target : target.url;
40
+ if (template.includes("<bech32>") && values.bech32 === void 0) return void 0;
41
+ if (template.includes("<raw>") && values.raw === void 0) return void 0;
42
+ return template.replace(/<bech32>/g, values.bech32 ?? "").replace(/<raw>/g, values.raw ?? "");
43
+ }
44
+ function parseHandlerRecommendation(event) {
45
+ let recommendedKind;
46
+ const recommendations = [];
47
+ for (const tag of event.tags) {
48
+ if (tag[0] === "d") {
49
+ const k = Number(tag[1]);
50
+ recommendedKind = Number.isInteger(k) ? k : void 0;
51
+ } else if (tag[0] === "a" && typeof tag[1] === "string" && tag[1].startsWith(`${HANDLER_INFORMATION_KIND}:`)) {
52
+ const [, handlerPubkey, identifier] = tag[1].split(":");
53
+ recommendations.push({
54
+ address: tag[1],
55
+ handlerPubkey: handlerPubkey || void 0,
56
+ identifier: identifier ?? void 0,
57
+ relay: tag[2] || void 0,
58
+ platform: tag[3] || void 0
59
+ });
60
+ }
61
+ }
62
+ return { pubkey: event.pubkey, recommendedKind, recommendations };
63
+ }
64
+ function parseJsonObject(content) {
65
+ if (!content) return {};
66
+ try {
67
+ const parsed = JSON.parse(content);
68
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
69
+ } catch {
70
+ return {};
71
+ }
72
+ }
73
+
74
+ export {
75
+ HANDLER_INFORMATION_KIND,
76
+ HANDLER_RECOMMENDATION_KIND,
77
+ HANDLER_PLATFORMS,
78
+ parseHandlerInformation,
79
+ handlesKind,
80
+ buildHandlerUrl,
81
+ parseHandlerRecommendation
82
+ };
83
+ //# sourceMappingURL=chunk-BCTIL523.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/89/index.ts"],"sourcesContent":["import type { NostrEvent } from 'nostr-tools';\n\n/**\n * `@kehto/nip/89` — NIP-89 application-handler discovery.\n *\n * NIP-89 lets clients answer \"who can open an event of kind N?\" without\n * hardcoding app knowledge — essential for any runtime that hosts or launches\n * other apps (the napplet model, \"open with…\" menus, unknown-kind fallbacks).\n *\n * Two event kinds:\n * - **kind 31990 — handler information.** An app advertises which kinds it\n * handles (`k` tags) and per-platform URL templates to open an entity\n * (`[\"web\", \"https://app/e/<bech32>\", \"nevent\"]`).\n * - **kind 31989 — handler recommendation.** A user recommends a handler for a\n * given kind via `a` tags pointing at 31990 addresses.\n *\n * `nostr-tools` ships no NIP-89 helper. This module provides pure parsers for\n * both kinds plus URL-template expansion — no relay, signer, or framework\n * coupling.\n */\n\n/** Kind 31990 — handler information (an app describing what it can open). */\nexport const HANDLER_INFORMATION_KIND = 31990;\n/** Kind 31989 — handler recommendation (a user endorsing a handler for a kind). */\nexport const HANDLER_RECOMMENDATION_KIND = 31989;\n\n/** Platform tag names defined by NIP-89 for handler URL templates. */\nexport const HANDLER_PLATFORMS = ['web', 'ios', 'android', 'macos', 'windows', 'linux'] as const;\n/** One of the NIP-89 platform identifiers. */\nexport type HandlerPlatform = (typeof HANDLER_PLATFORMS)[number];\n\nconst PLATFORM_SET = new Set<string>(HANDLER_PLATFORMS);\n\n/**\n * One platform-specific way to open an entity, from a 31990 platform tag.\n *\n * @example\n * ```ts\n * // tag [\"web\", \"https://app.example/e/<bech32>\", \"nevent\"]\n * const target: HandlerTarget = { url: 'https://app.example/e/<bech32>', entity: 'nevent' };\n * ```\n */\nexport interface HandlerTarget {\n /** URL template containing a `<bech32>` and/or `<raw>` placeholder. */\n url: string;\n /** Optional NIP-19 entity type this template is for (e.g. `'nevent'`, `'naddr'`, `'nprofile'`). */\n entity: string | undefined;\n}\n\n/**\n * A parsed kind-31990 handler-information event.\n *\n * @example\n * ```ts\n * const info = parseHandlerInformation(event);\n * info.kinds; // [1, 30023]\n * info.metadata.name; // 'My Reader'\n * info.platforms.web; // [{ url: 'https://app/<bech32>', entity: 'nevent' }]\n * ```\n */\nexport interface HandlerInformation {\n /** Handler author pubkey. */\n pubkey: string;\n /** `d`-tag identifier (handler instance id). */\n identifier: string;\n /** Addressable coordinate `31990:pubkey:d`. */\n address: string;\n /** Event kinds the handler declares support for (from `k` tags, de-duplicated). */\n kinds: number[];\n /** Parsed kind-0-style metadata from `content` (name, display_name, picture, about, …). `{}` if absent/invalid. */\n metadata: Record<string, unknown>;\n /** Per-platform URL templates, keyed by {@link HandlerPlatform}. Only platforms present in the event appear. */\n platforms: Partial<Record<HandlerPlatform, HandlerTarget[]>>;\n}\n\n/**\n * Parse a kind-31990 handler-information event.\n *\n * @param event - A kind-31990 event (kind is not re-validated)\n * @returns The structured handler information\n *\n * @example\n * ```ts\n * import { parseHandlerInformation, handlesKind } from '@kehto/nip/89';\n * const info = parseHandlerInformation(event);\n * if (handlesKind(info, 1)) openWith(info);\n * ```\n */\nexport function parseHandlerInformation(event: NostrEvent): HandlerInformation {\n let identifier = '';\n const kinds: number[] = [];\n const seenKinds = new Set<number>();\n const platforms: Partial<Record<HandlerPlatform, HandlerTarget[]>> = {};\n\n for (const tag of event.tags) {\n const name = tag[0];\n if (name === 'd') {\n identifier = tag[1] ?? '';\n } else if (name === 'k') {\n const k = Number(tag[1]);\n if (Number.isInteger(k) && !seenKinds.has(k)) {\n seenKinds.add(k);\n kinds.push(k);\n }\n } else if (PLATFORM_SET.has(name) && typeof tag[1] === 'string' && tag[1].length > 0) {\n const platform = name as HandlerPlatform;\n (platforms[platform] ??= []).push({ url: tag[1], entity: tag[2] });\n }\n }\n\n return {\n pubkey: event.pubkey,\n identifier,\n address: `${HANDLER_INFORMATION_KIND}:${event.pubkey}:${identifier}`,\n kinds,\n metadata: parseJsonObject(event.content),\n platforms,\n };\n}\n\n/**\n * Whether a handler declares support for `kind`.\n *\n * @param info - Parsed handler information\n * @param kind - Event kind to check\n */\nexport function handlesKind(info: HandlerInformation, kind: number): boolean {\n return info.kinds.includes(kind);\n}\n\n/**\n * Substitution values for {@link buildHandlerUrl}.\n *\n * NIP-89 templates use `<bech32>` (NIP-19 encoded entity) and/or `<raw>`\n * (hex id / address coordinate). Supply whichever the template needs.\n */\nexport interface HandlerEntityValues {\n /** NIP-19 bech32 entity (e.g. an `nevent…`, `naddr…`, `nprofile…`). */\n bech32?: string;\n /** Raw value (hex event id, or `kind:pubkey:d` coordinate). */\n raw?: string;\n}\n\n/**\n * Expand a handler URL template by substituting its `<bech32>` / `<raw>`\n * placeholders.\n *\n * Returns `undefined` when the template requires a placeholder you didn't\n * supply (e.g. it contains `<bech32>` but `values.bech32` is missing) — so a\n * caller can fall through to the next target instead of producing a broken URL.\n *\n * @param target - A {@link HandlerTarget} or a raw template string\n * @param values - The `<bech32>` / `<raw>` substitutions\n * @returns The expanded URL, or `undefined` if a needed placeholder is absent\n *\n * @example\n * ```ts\n * buildHandlerUrl({ url: 'https://app/e/<bech32>', entity: 'nevent' }, { bech32: 'nevent1…' });\n * // 'https://app/e/nevent1…'\n * ```\n */\nexport function buildHandlerUrl(target: HandlerTarget | string, values: HandlerEntityValues): string | undefined {\n const template = typeof target === 'string' ? target : target.url;\n if (template.includes('<bech32>') && values.bech32 === undefined) return undefined;\n if (template.includes('<raw>') && values.raw === undefined) return undefined;\n return template\n .replace(/<bech32>/g, values.bech32 ?? '')\n .replace(/<raw>/g, values.raw ?? '');\n}\n\n/** One handler endorsement parsed from a 31989 `a` tag. */\nexport interface HandlerRecommendationEntry {\n /** The recommended handler's 31990 coordinate `31990:pubkey:d`. */\n address: string;\n /** Handler author pubkey extracted from the coordinate, if well-formed. */\n handlerPubkey: string | undefined;\n /** Handler `d`-tag identifier extracted from the coordinate, if present. */\n identifier: string | undefined;\n /** Optional relay-hint where the 31990 event can be found (`a`-tag 3rd element). */\n relay: string | undefined;\n /** Optional platform marker the handler is recommended for (`a`-tag 4th element). */\n platform: string | undefined;\n}\n\n/**\n * A parsed kind-31989 handler-recommendation event.\n *\n * @example\n * ```ts\n * const rec = parseHandlerRecommendation(event);\n * rec.recommendedKind; // 1\n * rec.recommendations[0].address; // '31990:pubkey:d'\n * ```\n */\nexport interface HandlerRecommendation {\n /** Recommendation author pubkey. */\n pubkey: string;\n /** The kind being recommended for, parsed from the `d` tag; `undefined` if non-numeric/missing. */\n recommendedKind: number | undefined;\n /** Endorsed handlers, in tag order. */\n recommendations: HandlerRecommendationEntry[];\n}\n\n/**\n * Parse a kind-31989 handler-recommendation event.\n *\n * The `d` tag carries the kind being recommended for; each `a` tag (whose\n * coordinate starts with `31990:`) endorses one handler, optionally with a\n * relay hint and platform marker.\n *\n * @param event - A kind-31989 event (kind is not re-validated)\n * @returns The structured recommendation\n */\nexport function parseHandlerRecommendation(event: NostrEvent): HandlerRecommendation {\n let recommendedKind: number | undefined;\n const recommendations: HandlerRecommendationEntry[] = [];\n\n for (const tag of event.tags) {\n if (tag[0] === 'd') {\n const k = Number(tag[1]);\n recommendedKind = Number.isInteger(k) ? k : undefined;\n } else if (tag[0] === 'a' && typeof tag[1] === 'string' && tag[1].startsWith(`${HANDLER_INFORMATION_KIND}:`)) {\n const [, handlerPubkey, identifier] = tag[1].split(':');\n recommendations.push({\n address: tag[1],\n handlerPubkey: handlerPubkey || undefined,\n identifier: identifier ?? undefined,\n relay: tag[2] || undefined,\n platform: tag[3] || undefined,\n });\n }\n }\n\n return { pubkey: event.pubkey, recommendedKind, recommendations };\n}\n\nfunction parseJsonObject(content: string): Record<string, unknown> {\n if (!content) return {};\n try {\n const parsed: unknown = JSON.parse(content);\n return parsed && typeof parsed === 'object' && !Array.isArray(parsed)\n ? (parsed as Record<string, unknown>)\n : {};\n } catch {\n return {};\n }\n}\n"],"mappings":";AAsBO,IAAM,2BAA2B;AAEjC,IAAM,8BAA8B;AAGpC,IAAM,oBAAoB,CAAC,OAAO,OAAO,WAAW,SAAS,WAAW,OAAO;AAItF,IAAM,eAAe,IAAI,IAAY,iBAAiB;AAyD/C,SAAS,wBAAwB,OAAuC;AAC7E,MAAI,aAAa;AACjB,QAAM,QAAkB,CAAC;AACzB,QAAM,YAAY,oBAAI,IAAY;AAClC,QAAM,YAA+D,CAAC;AAEtE,aAAW,OAAO,MAAM,MAAM;AAC5B,UAAM,OAAO,IAAI,CAAC;AAClB,QAAI,SAAS,KAAK;AAChB,mBAAa,IAAI,CAAC,KAAK;AAAA,IACzB,WAAW,SAAS,KAAK;AACvB,YAAM,IAAI,OAAO,IAAI,CAAC,CAAC;AACvB,UAAI,OAAO,UAAU,CAAC,KAAK,CAAC,UAAU,IAAI,CAAC,GAAG;AAC5C,kBAAU,IAAI,CAAC;AACf,cAAM,KAAK,CAAC;AAAA,MACd;AAAA,IACF,WAAW,aAAa,IAAI,IAAI,KAAK,OAAO,IAAI,CAAC,MAAM,YAAY,IAAI,CAAC,EAAE,SAAS,GAAG;AACpF,YAAM,WAAW;AACjB,OAAC,UAAU,QAAQ,MAAM,CAAC,GAAG,KAAK,EAAE,KAAK,IAAI,CAAC,GAAG,QAAQ,IAAI,CAAC,EAAE,CAAC;AAAA,IACnE;AAAA,EACF;AAEA,SAAO;AAAA,IACL,QAAQ,MAAM;AAAA,IACd;AAAA,IACA,SAAS,GAAG,wBAAwB,IAAI,MAAM,MAAM,IAAI,UAAU;AAAA,IAClE;AAAA,IACA,UAAU,gBAAgB,MAAM,OAAO;AAAA,IACvC;AAAA,EACF;AACF;AAQO,SAAS,YAAY,MAA0B,MAAuB;AAC3E,SAAO,KAAK,MAAM,SAAS,IAAI;AACjC;AAiCO,SAAS,gBAAgB,QAAgC,QAAiD;AAC/G,QAAM,WAAW,OAAO,WAAW,WAAW,SAAS,OAAO;AAC9D,MAAI,SAAS,SAAS,UAAU,KAAK,OAAO,WAAW,OAAW,QAAO;AACzE,MAAI,SAAS,SAAS,OAAO,KAAK,OAAO,QAAQ,OAAW,QAAO;AACnE,SAAO,SACJ,QAAQ,aAAa,OAAO,UAAU,EAAE,EACxC,QAAQ,UAAU,OAAO,OAAO,EAAE;AACvC;AA6CO,SAAS,2BAA2B,OAA0C;AACnF,MAAI;AACJ,QAAM,kBAAgD,CAAC;AAEvD,aAAW,OAAO,MAAM,MAAM;AAC5B,QAAI,IAAI,CAAC,MAAM,KAAK;AAClB,YAAM,IAAI,OAAO,IAAI,CAAC,CAAC;AACvB,wBAAkB,OAAO,UAAU,CAAC,IAAI,IAAI;AAAA,IAC9C,WAAW,IAAI,CAAC,MAAM,OAAO,OAAO,IAAI,CAAC,MAAM,YAAY,IAAI,CAAC,EAAE,WAAW,GAAG,wBAAwB,GAAG,GAAG;AAC5G,YAAM,CAAC,EAAE,eAAe,UAAU,IAAI,IAAI,CAAC,EAAE,MAAM,GAAG;AACtD,sBAAgB,KAAK;AAAA,QACnB,SAAS,IAAI,CAAC;AAAA,QACd,eAAe,iBAAiB;AAAA,QAChC,YAAY,cAAc;AAAA,QAC1B,OAAO,IAAI,CAAC,KAAK;AAAA,QACjB,UAAU,IAAI,CAAC,KAAK;AAAA,MACtB,CAAC;AAAA,IACH;AAAA,EACF;AAEA,SAAO,EAAE,QAAQ,MAAM,QAAQ,iBAAiB,gBAAgB;AAClE;AAEA,SAAS,gBAAgB,SAA0C;AACjE,MAAI,CAAC,QAAS,QAAO,CAAC;AACtB,MAAI;AACF,UAAM,SAAkB,KAAK,MAAM,OAAO;AAC1C,WAAO,UAAU,OAAO,WAAW,YAAY,CAAC,MAAM,QAAQ,MAAM,IAC/D,SACD,CAAC;AAAA,EACP,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AACF;","names":[]}
@@ -0,0 +1,572 @@
1
+ import {
2
+ aggregateTagValue,
3
+ computeAggregateHash,
4
+ pathEntriesFromTags
5
+ } from "./chunk-2J3CSGKL.js";
6
+
7
+ // src/5d/index.ts
8
+ import { verifyEvent } from "nostr-tools/pure";
9
+ import { sha256 } from "@noble/hashes/sha2.js";
10
+ import { bytesToHex } from "@noble/hashes/utils.js";
11
+
12
+ // src/5d/artifact-cache.ts
13
+ var MIB = 1024 * 1024;
14
+ var NAPPLET_ARTIFACT_CACHE_NAME = "kehto:napplet-artifacts:v1";
15
+ var SCHEMA_VERSION = 1;
16
+ var DEFAULT_BASE_URL = "https://kehto.invalid";
17
+ var DEFAULT_UNKNOWN_BUDGET_BYTES = 32 * MIB;
18
+ var DEFAULT_SOFT_CEILING_BYTES = 128 * MIB;
19
+ var DEFAULT_HARD_CEILING_BYTES = 256 * MIB;
20
+ var DEFAULT_FOREGROUND_TTL_MS = 15 * 60 * 1e3;
21
+ var DEFAULT_BACKGROUND_TTL_MS = 60 * 60 * 1e3;
22
+ function emptyIndex(now) {
23
+ return {
24
+ schemaVersion: SCHEMA_VERSION,
25
+ updatedAt: now,
26
+ blobs: {},
27
+ aggregates: {},
28
+ coordinates: {}
29
+ };
30
+ }
31
+ function aggregateKey(dTag, aggregateHash) {
32
+ return `${encodeURIComponent(dTag)}:${aggregateHash}`;
33
+ }
34
+ function coordinateKey(kind, pubkey, dTag) {
35
+ return `${kind}:${pubkey}:${encodeURIComponent(dTag)}`;
36
+ }
37
+ function isCoordinateFresh(coordinate, options = {}) {
38
+ const now = options.now ?? Date.now();
39
+ const ttl = options.background ? coordinate.backgroundTtlMs : coordinate.foregroundTtlMs;
40
+ return ttl === Number.POSITIVE_INFINITY || now - coordinate.lastResolvedAt <= ttl;
41
+ }
42
+ function byteLength(bytes) {
43
+ return bytes.byteLength;
44
+ }
45
+ function encodeKeyPart(value) {
46
+ return encodeURIComponent(value).replaceAll("%2F", "%252F");
47
+ }
48
+ function cloneBytes(bytes) {
49
+ return new Uint8Array(bytes);
50
+ }
51
+ function responseBody(bytes) {
52
+ return cloneBytes(bytes).buffer;
53
+ }
54
+ function normalizeBaseUrl(baseUrl) {
55
+ return baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
56
+ }
57
+ function resolveBaseUrl(baseUrl) {
58
+ if (baseUrl) return normalizeBaseUrl(baseUrl);
59
+ const maybeLocation = globalThis;
60
+ if (maybeLocation.location?.origin) return normalizeBaseUrl(maybeLocation.location.origin);
61
+ return normalizeBaseUrl(DEFAULT_BASE_URL);
62
+ }
63
+ async function responseBytes(response) {
64
+ return new Uint8Array(await response.arrayBuffer());
65
+ }
66
+ async function responseJson(response) {
67
+ return response.json();
68
+ }
69
+ function refCounts(index) {
70
+ const counts = {};
71
+ for (const aggregate of Object.values(index.aggregates)) {
72
+ for (const hash of aggregate.blobHashes) counts[hash] = (counts[hash] ?? 0) + 1;
73
+ }
74
+ return counts;
75
+ }
76
+ function cachedSize(index) {
77
+ const blobBytes = Object.values(index.blobs).reduce((sum, entry) => sum + entry.size, 0);
78
+ const aggregateBytes = Object.values(index.aggregates).reduce((sum, entry) => sum + entry.size, 0);
79
+ return blobBytes + aggregateBytes;
80
+ }
81
+ var CacheStorageNappletArtifactCache = class {
82
+ cacheStorage;
83
+ storage;
84
+ cacheName;
85
+ baseUrl;
86
+ now;
87
+ activeAggregates = /* @__PURE__ */ new Set();
88
+ onDiagnostic;
89
+ unknownBudgetBytes;
90
+ softCeilingBytes;
91
+ hardCeilingBytes;
92
+ cachePromise;
93
+ constructor(options = {}) {
94
+ const maybeCacheStorage = options.cacheStorage ?? globalThis.caches;
95
+ if (!maybeCacheStorage) {
96
+ throw new Error("Cache Storage is unavailable");
97
+ }
98
+ this.cacheStorage = maybeCacheStorage;
99
+ this.storage = options.storage ?? globalThis.navigator?.storage;
100
+ this.cacheName = options.cacheName ?? NAPPLET_ARTIFACT_CACHE_NAME;
101
+ this.baseUrl = resolveBaseUrl(options.baseUrl);
102
+ this.now = options.now ?? Date.now;
103
+ this.onDiagnostic = options.onDiagnostic;
104
+ this.unknownBudgetBytes = options.unknownBudgetBytes ?? DEFAULT_UNKNOWN_BUDGET_BYTES;
105
+ this.softCeilingBytes = options.softCeilingBytes ?? DEFAULT_SOFT_CEILING_BYTES;
106
+ this.hardCeilingBytes = options.hardCeilingBytes ?? DEFAULT_HARD_CEILING_BYTES;
107
+ }
108
+ async readBlob(sha256Hex) {
109
+ try {
110
+ const cache = await this.open();
111
+ const response = await cache.match(this.blobUrl(sha256Hex));
112
+ if (!response) return void 0;
113
+ const bytes = await responseBytes(response);
114
+ const index = await this.readIndex(cache);
115
+ const entry = index.blobs[sha256Hex];
116
+ if (entry) {
117
+ entry.lastAccessed = this.now();
118
+ index.updatedAt = entry.lastAccessed;
119
+ await this.writeIndex(cache, index);
120
+ }
121
+ return bytes;
122
+ } catch (cause) {
123
+ this.diagnostic({ type: "cache-read-failed", message: `failed to read blob ${sha256Hex}`, cause });
124
+ return void 0;
125
+ }
126
+ }
127
+ async deleteBlob(sha256Hex) {
128
+ try {
129
+ const cache = await this.open();
130
+ await cache.delete(this.blobUrl(sha256Hex));
131
+ const index = await this.readIndex(cache);
132
+ delete index.blobs[sha256Hex];
133
+ index.updatedAt = this.now();
134
+ await this.writeIndex(cache, index);
135
+ } catch (cause) {
136
+ this.diagnostic({ type: "cache-delete-failed", message: `failed to delete blob ${sha256Hex}`, cause });
137
+ }
138
+ }
139
+ async writeVerifiedResolution(input) {
140
+ try {
141
+ await this.writeVerifiedResolutionOnce(input);
142
+ } catch (firstCause) {
143
+ try {
144
+ await this.prune(input.now ?? this.now());
145
+ await this.writeVerifiedResolutionOnce(input);
146
+ } catch (cause) {
147
+ this.diagnostic({
148
+ type: "cache-write-failed",
149
+ message: "failed to write verified napplet artifacts after prune retry",
150
+ cause: cause ?? firstCause
151
+ });
152
+ }
153
+ }
154
+ }
155
+ async writeCoordinate(input) {
156
+ const cache = await this.open();
157
+ const now = this.now();
158
+ const index = await this.readIndex(cache);
159
+ const key = coordinateKey(input.kind, input.pubkey, input.dTag);
160
+ index.coordinates[key] = {
161
+ key,
162
+ kind: input.kind,
163
+ pubkey: input.pubkey,
164
+ dTag: input.dTag,
165
+ aggregateHash: input.aggregateHash,
166
+ lastResolvedAt: input.lastResolvedAt ?? now,
167
+ foregroundTtlMs: input.foregroundTtlMs ?? DEFAULT_FOREGROUND_TTL_MS,
168
+ backgroundTtlMs: input.backgroundTtlMs ?? DEFAULT_BACKGROUND_TTL_MS
169
+ };
170
+ index.updatedAt = now;
171
+ await this.writeIndex(cache, index);
172
+ }
173
+ async getCoordinate(input, options = {}) {
174
+ const cache = await this.open();
175
+ const index = await this.readIndex(cache);
176
+ const coordinate = index.coordinates[coordinateKey(input.kind, input.pubkey, input.dTag)];
177
+ if (!coordinate) return void 0;
178
+ return { ...coordinate, fresh: isCoordinateFresh(coordinate, options) };
179
+ }
180
+ async touchAggregate(dTag, aggregateHash, now = this.now()) {
181
+ const cache = await this.open();
182
+ const index = await this.readIndex(cache);
183
+ const key = aggregateKey(dTag, aggregateHash);
184
+ const aggregate = index.aggregates[key];
185
+ if (!aggregate) return;
186
+ aggregate.lastAccessed = now;
187
+ for (const hash of aggregate.blobHashes) {
188
+ if (index.blobs[hash]) index.blobs[hash].lastAccessed = now;
189
+ }
190
+ index.updatedAt = now;
191
+ await this.writeIndex(cache, index);
192
+ }
193
+ markAggregateActive(dTag, aggregateHash) {
194
+ this.activeAggregates.add(aggregateKey(dTag, aggregateHash));
195
+ }
196
+ releaseAggregateActive(dTag, aggregateHash) {
197
+ this.activeAggregates.delete(aggregateKey(dTag, aggregateHash));
198
+ }
199
+ async prune(now = this.now()) {
200
+ try {
201
+ const cache = await this.open();
202
+ const index = await this.readIndex(cache);
203
+ let size = cachedSize(index);
204
+ const budget = await this.budgetBytes();
205
+ const hardBudget = await this.hardBudgetBytes();
206
+ const shouldPrune = size > budget || await this.originPressureHigh();
207
+ if (!shouldPrune) return;
208
+ this.rewriteRefCounts(index);
209
+ for (const [hash, blob] of Object.entries(index.blobs)) {
210
+ if (size <= budget) break;
211
+ if (blob.refCount > 0) continue;
212
+ await cache.delete(this.blobUrl(hash));
213
+ delete index.blobs[hash];
214
+ size -= blob.size;
215
+ }
216
+ const currentAggregateHashes = new Set(
217
+ Object.values(index.coordinates).map((coordinate) => coordinate.aggregateHash)
218
+ );
219
+ const aggregates = Object.values(index.aggregates).filter((entry) => !this.activeAggregates.has(entry.key)).sort((a, b) => {
220
+ const currentA = currentAggregateHashes.has(a.aggregateHash) ? 1 : 0;
221
+ const currentB = currentAggregateHashes.has(b.aggregateHash) ? 1 : 0;
222
+ if (currentA !== currentB) return currentA - currentB;
223
+ return a.lastAccessed - b.lastAccessed;
224
+ });
225
+ for (const aggregate of aggregates) {
226
+ if (size <= budget) break;
227
+ await cache.delete(this.aggregateUrl(aggregate.key));
228
+ delete index.aggregates[aggregate.key];
229
+ size -= aggregate.size;
230
+ this.rewriteRefCounts(index);
231
+ }
232
+ if (size > hardBudget) {
233
+ const active = Object.values(index.aggregates).filter((entry) => this.activeAggregates.has(entry.key)).sort((a, b) => a.lastAccessed - b.lastAccessed);
234
+ for (const aggregate of active) {
235
+ if (size <= hardBudget) break;
236
+ await cache.delete(this.aggregateUrl(aggregate.key));
237
+ delete index.aggregates[aggregate.key];
238
+ size -= aggregate.size;
239
+ }
240
+ }
241
+ this.rewriteRefCounts(index);
242
+ for (const [hash, blob] of Object.entries(index.blobs)) {
243
+ if (blob.refCount === 0) {
244
+ await cache.delete(this.blobUrl(hash));
245
+ delete index.blobs[hash];
246
+ }
247
+ }
248
+ index.updatedAt = now;
249
+ await this.writeIndex(cache, index);
250
+ } catch (cause) {
251
+ this.diagnostic({ type: "cache-prune-failed", message: "failed to prune napplet artifact cache", cause });
252
+ }
253
+ }
254
+ async snapshotIndex() {
255
+ return this.readIndex(await this.open());
256
+ }
257
+ async writeVerifiedResolutionOnce(input) {
258
+ const cache = await this.open();
259
+ const now = input.now ?? this.now();
260
+ const index = await this.readIndex(cache);
261
+ const blobHashes = [];
262
+ for (const pathEntry of input.manifest.paths) {
263
+ const bytes = input.files.get(pathEntry.path);
264
+ if (!bytes) continue;
265
+ blobHashes.push(pathEntry.sha256);
266
+ await cache.put(this.blobUrl(pathEntry.sha256), new Response(responseBody(bytes)));
267
+ index.blobs[pathEntry.sha256] = {
268
+ size: byteLength(bytes),
269
+ lastAccessed: now,
270
+ refCount: index.blobs[pathEntry.sha256]?.refCount ?? 0
271
+ };
272
+ }
273
+ const key = aggregateKey(input.manifest.dTag, input.manifest.aggregateHash);
274
+ const record = {
275
+ schemaVersion: SCHEMA_VERSION,
276
+ dTag: input.manifest.dTag,
277
+ aggregateHash: input.manifest.aggregateHash,
278
+ manifest: {
279
+ kind: input.manifest.kind,
280
+ pubkey: input.manifest.pubkey,
281
+ dTag: input.manifest.dTag,
282
+ paths: input.manifest.paths.map((entry) => ({ path: entry.path, sha256: entry.sha256 })),
283
+ requires: [...input.manifest.requires],
284
+ ...input.manifest.title === void 0 ? {} : { title: input.manifest.title },
285
+ ...input.manifest.description === void 0 ? {} : { description: input.manifest.description }
286
+ },
287
+ indexHtml: input.indexHtml,
288
+ blobHashes,
289
+ writtenAt: now
290
+ };
291
+ const aggregateBytes = new TextEncoder().encode(JSON.stringify(record));
292
+ await cache.put(this.aggregateUrl(key), new Response(responseBody(aggregateBytes), {
293
+ headers: { "content-type": "application/json" }
294
+ }));
295
+ const coordKey = coordinateKey(input.manifest.kind, input.manifest.pubkey, input.manifest.dTag);
296
+ index.aggregates[key] = {
297
+ dTag: input.manifest.dTag,
298
+ aggregateHash: input.manifest.aggregateHash,
299
+ key,
300
+ blobHashes,
301
+ size: byteLength(aggregateBytes),
302
+ lastAccessed: now,
303
+ coordinateKey: coordKey
304
+ };
305
+ index.coordinates[coordKey] = {
306
+ key: coordKey,
307
+ kind: input.manifest.kind,
308
+ pubkey: input.manifest.pubkey,
309
+ dTag: input.manifest.dTag,
310
+ aggregateHash: input.manifest.aggregateHash,
311
+ lastResolvedAt: now,
312
+ foregroundTtlMs: DEFAULT_FOREGROUND_TTL_MS,
313
+ backgroundTtlMs: DEFAULT_BACKGROUND_TTL_MS
314
+ };
315
+ this.rewriteRefCounts(index);
316
+ index.updatedAt = now;
317
+ await this.writeIndex(cache, index);
318
+ await this.prune(now);
319
+ }
320
+ async open() {
321
+ this.cachePromise ??= this.cacheStorage.open(this.cacheName);
322
+ return this.cachePromise;
323
+ }
324
+ indexUrl() {
325
+ return new URL("__kehto_napplet_cache/index.json", this.baseUrl).toString();
326
+ }
327
+ blobUrl(hash) {
328
+ return new URL(`__kehto_napplet_cache/blob/${encodeKeyPart(hash)}`, this.baseUrl).toString();
329
+ }
330
+ aggregateUrl(key) {
331
+ return new URL(`__kehto_napplet_cache/aggregate/${encodeKeyPart(key)}.json`, this.baseUrl).toString();
332
+ }
333
+ async readIndex(cache) {
334
+ const response = await cache.match(this.indexUrl());
335
+ if (!response) return emptyIndex(this.now());
336
+ try {
337
+ const index = await responseJson(response);
338
+ if (index.schemaVersion !== SCHEMA_VERSION) return emptyIndex(this.now());
339
+ return index;
340
+ } catch {
341
+ return emptyIndex(this.now());
342
+ }
343
+ }
344
+ async writeIndex(cache, index) {
345
+ await cache.put(this.indexUrl(), new Response(JSON.stringify(index), {
346
+ headers: { "content-type": "application/json" }
347
+ }));
348
+ }
349
+ rewriteRefCounts(index) {
350
+ const counts = refCounts(index);
351
+ for (const [hash, blob] of Object.entries(index.blobs)) {
352
+ blob.refCount = counts[hash] ?? 0;
353
+ }
354
+ }
355
+ async budgetBytes() {
356
+ const estimate = await this.estimateStorage();
357
+ if (!estimate?.quota) return this.unknownBudgetBytes;
358
+ return Math.min(this.softCeilingBytes, Math.floor(estimate.quota * 0.1));
359
+ }
360
+ async hardBudgetBytes() {
361
+ const estimate = await this.estimateStorage();
362
+ if (!estimate?.quota) return this.unknownBudgetBytes;
363
+ return Math.min(this.hardCeilingBytes, Math.floor(estimate.quota * 0.2));
364
+ }
365
+ async originPressureHigh() {
366
+ const estimate = await this.estimateStorage();
367
+ if (!estimate?.quota || estimate.usage === void 0) return false;
368
+ return estimate.usage / estimate.quota >= 0.8;
369
+ }
370
+ async estimateStorage() {
371
+ if (!this.storage?.estimate) return void 0;
372
+ try {
373
+ return this.storage.estimate();
374
+ } catch (cause) {
375
+ this.diagnostic({
376
+ type: "storage-estimate-unavailable",
377
+ message: "storage estimate failed; using unknown quota cache budget",
378
+ cause
379
+ });
380
+ return void 0;
381
+ }
382
+ }
383
+ diagnostic(diagnostic) {
384
+ this.onDiagnostic?.(diagnostic);
385
+ }
386
+ };
387
+ async function openNappletArtifactCache(options = {}) {
388
+ const cacheStorage = options.cacheStorage ?? globalThis.caches;
389
+ if (!cacheStorage) return void 0;
390
+ const storage = options.storage ?? globalThis.navigator?.storage;
391
+ if (options.requireStorageEstimate && !storage?.estimate) {
392
+ options.onDiagnostic?.({
393
+ type: "storage-estimate-unavailable",
394
+ message: "storage estimates are unavailable; using network-only loading"
395
+ });
396
+ return void 0;
397
+ }
398
+ try {
399
+ const cache = new CacheStorageNappletArtifactCache({ ...options, cacheStorage, storage });
400
+ await cache.snapshotIndex();
401
+ return cache;
402
+ } catch (cause) {
403
+ options.onDiagnostic?.({
404
+ type: "cache-open-failed",
405
+ message: "Cache Storage napplet artifact cache could not be opened",
406
+ cause
407
+ });
408
+ return void 0;
409
+ }
410
+ }
411
+
412
+ // src/5d/index.ts
413
+ var NAPPLET_KIND_SNAPSHOT = 5129;
414
+ var NAPPLET_KIND_ROOT = 15129;
415
+ var NAPPLET_KIND_NAMED = 35129;
416
+ var NAPPLET_KINDS = [
417
+ NAPPLET_KIND_SNAPSHOT,
418
+ NAPPLET_KIND_ROOT,
419
+ NAPPLET_KIND_NAMED
420
+ ];
421
+ function isNappletManifestKind(kind) {
422
+ return NAPPLET_KINDS.includes(kind);
423
+ }
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
+ if (typeof tag[1] !== "string" || tag[1].length === 0) continue;
450
+ out.push(
451
+ typeof tag[2] === "string" && tag[2].length > 0 ? { slug: tag[1], nap: tag[2] } : { slug: tag[1] }
452
+ );
453
+ }
454
+ return out;
455
+ }
456
+ function parseNappletManifest(event) {
457
+ if (!isNappletManifestKind(event.kind)) {
458
+ throw new NappletResolutionError("invalid-manifest", `not a NIP-5D napplet kind: ${event.kind}`);
459
+ }
460
+ const paths = pathEntriesFromTags(event.tags);
461
+ if (paths.length === 0) {
462
+ throw new NappletResolutionError("invalid-manifest", "manifest has no path tags");
463
+ }
464
+ const aggregateHash = aggregateTagValue(event.tags);
465
+ if (!aggregateHash) {
466
+ throw new NappletResolutionError("invalid-manifest", "manifest has no aggregate x tag");
467
+ }
468
+ return {
469
+ kind: event.kind,
470
+ pubkey: event.pubkey,
471
+ dTag: firstTagValue(event.tags, "d") ?? "",
472
+ paths,
473
+ aggregateHash,
474
+ servers: allTagValues(event.tags, "server"),
475
+ requires: allTagValues(event.tags, "requires"),
476
+ archetypes: archetypesFromTags(event.tags),
477
+ title: firstTagValue(event.tags, "title"),
478
+ description: firstTagValue(event.tags, "description"),
479
+ source: firstTagValue(event.tags, "source")
480
+ };
481
+ }
482
+ function verifyManifestSignature(event) {
483
+ try {
484
+ return verifyEvent(event);
485
+ } catch {
486
+ return false;
487
+ }
488
+ }
489
+ function verifyBlobHash(bytes, sha256Hex) {
490
+ return bytesToHex(sha256(bytes)) === sha256Hex;
491
+ }
492
+ function stripTrailingSlash(url) {
493
+ return url.endsWith("/") ? url.slice(0, -1) : url;
494
+ }
495
+ async function fetchBlob(servers, sha256Hex, fetchBytes) {
496
+ for (const server of servers) {
497
+ const url = `${stripTrailingSlash(server)}/${sha256Hex}`;
498
+ try {
499
+ const bytes = await fetchBytes(url);
500
+ if (verifyBlobHash(bytes, sha256Hex)) return bytes;
501
+ } catch {
502
+ }
503
+ }
504
+ throw new NappletResolutionError("blob-unavailable", `no server served blob ${sha256Hex}`);
505
+ }
506
+ var INDEX_PATHS = ["/index.html", "index.html", "/"];
507
+ function defaultDecode(bytes) {
508
+ return new TextDecoder().decode(bytes);
509
+ }
510
+ async function resolveNapplet(options) {
511
+ const { event, fetchBlob: fetchBlobBytes, textDecode = defaultDecode, cache } = options;
512
+ if (!verifyManifestSignature(event)) {
513
+ throw new NappletResolutionError("invalid-signature", "manifest signature is invalid");
514
+ }
515
+ const manifest = parseNappletManifest(event);
516
+ const recomputed = computeAggregateHash(manifest.paths);
517
+ if (recomputed !== manifest.aggregateHash) {
518
+ throw new NappletResolutionError(
519
+ "aggregate-mismatch",
520
+ `recomputed aggregate ${recomputed} != manifest ${manifest.aggregateHash}`
521
+ );
522
+ }
523
+ const files = /* @__PURE__ */ new Map();
524
+ for (const entry of manifest.paths) {
525
+ let bytes = await cache?.readBlob(entry.sha256);
526
+ if (bytes && !verifyBlobHash(bytes, entry.sha256)) {
527
+ await cache?.deleteBlob(entry.sha256);
528
+ bytes = void 0;
529
+ }
530
+ bytes ??= await fetchBlobBytes(entry.sha256, manifest.servers);
531
+ if (!verifyBlobHash(bytes, entry.sha256)) {
532
+ throw new NappletResolutionError(
533
+ "blob-hash-mismatch",
534
+ `blob for ${entry.path} does not match hash ${entry.sha256}`
535
+ );
536
+ }
537
+ files.set(entry.path, bytes);
538
+ }
539
+ const indexEntry = manifest.paths.find((e) => INDEX_PATHS.includes(e.path));
540
+ if (!indexEntry) {
541
+ throw new NappletResolutionError("missing-index", "manifest has no /index.html entry");
542
+ }
543
+ const indexHtml = textDecode(files.get(indexEntry.path));
544
+ await cache?.writeVerifiedResolution({ event, manifest, files, indexHtml });
545
+ return {
546
+ dTag: manifest.dTag,
547
+ aggregateHash: manifest.aggregateHash,
548
+ files,
549
+ indexHtml,
550
+ manifest
551
+ };
552
+ }
553
+
554
+ export {
555
+ NAPPLET_ARTIFACT_CACHE_NAME,
556
+ coordinateKey,
557
+ isCoordinateFresh,
558
+ CacheStorageNappletArtifactCache,
559
+ openNappletArtifactCache,
560
+ NAPPLET_KIND_SNAPSHOT,
561
+ NAPPLET_KIND_ROOT,
562
+ NAPPLET_KIND_NAMED,
563
+ NAPPLET_KINDS,
564
+ isNappletManifestKind,
565
+ NappletResolutionError,
566
+ parseNappletManifest,
567
+ verifyManifestSignature,
568
+ verifyBlobHash,
569
+ fetchBlob,
570
+ resolveNapplet
571
+ };
572
+ //# sourceMappingURL=chunk-CDBUCFLP.js.map