@kehto/nip 0.4.1 → 0.4.2

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.
@@ -16,6 +16,8 @@ import { NostrEvent } from 'nostr-tools';
16
16
  * widely-compatible parser that turns either family into a structured
17
17
  * {@link Nip51List}, plus item accessors and an injected-decryptor path for
18
18
  * private items (so the package stays crypto-free and framework-agnostic).
19
+ *
20
+ * @module
19
21
  */
20
22
  /** Canonical NIP-51 **standard list** kinds (replaceable, one per author). */
21
23
  declare const LIST_KINDS: Readonly<Record<number, string>>;
package/dist/51/index.js CHANGED
@@ -8,7 +8,7 @@ import {
8
8
  isSetKind,
9
9
  listKindName,
10
10
  parseList
11
- } from "../chunk-2XYRP2S6.js";
11
+ } from "../chunk-NDOAX5PE.js";
12
12
  export {
13
13
  ITEM_TAGS,
14
14
  LIST_KINDS,
package/dist/5a/index.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  computeAggregateHash,
4
4
  pathEntriesFromTags,
5
5
  verifyAggregate
6
- } from "../chunk-2J3CSGKL.js";
6
+ } from "../chunk-JT5WCCCX.js";
7
7
  export {
8
8
  aggregateTagValue,
9
9
  computeAggregateHash,
@@ -4,57 +4,101 @@ import { PathEntry } from '../5a/index.js';
4
4
  /** Versioned Cache Storage namespace for verified napplet artifacts. */
5
5
  declare const NAPPLET_ARTIFACT_CACHE_NAME = "kehto:napplet-artifacts:v1";
6
6
  interface NappletCacheDiagnostic {
7
+ /** Diagnostic category for cache open/read/write/delete/prune/storage-estimate failures. */
7
8
  type: 'cache-open-failed' | 'cache-read-failed' | 'cache-write-failed' | 'cache-delete-failed' | 'cache-prune-failed' | 'storage-estimate-unavailable';
9
+ /** Human-readable diagnostic detail suitable for logs. */
8
10
  message: string;
11
+ /** Original thrown value, when one exists. */
9
12
  cause?: unknown;
10
13
  }
14
+ /** Metadata tracked for one cached napplet artifact blob. */
11
15
  interface NappletBlobIndexEntry {
16
+ /** Blob byte length. */
12
17
  size: number;
18
+ /** Last read or write timestamp in milliseconds. */
13
19
  lastAccessed: number;
20
+ /** Number of cached aggregate records currently referencing this blob. */
14
21
  refCount: number;
15
22
  }
23
+ /** Metadata tracked for one verified aggregate cache record. */
16
24
  interface NappletAggregateIndexEntry {
25
+ /** Manifest `d` tag. */
17
26
  dTag: string;
27
+ /** Verified NIP-5A aggregate hash. */
18
28
  aggregateHash: string;
29
+ /** Internal cache key derived from `dTag` and `aggregateHash`. */
19
30
  key: string;
31
+ /** SHA-256 hashes of blobs referenced by this aggregate. */
20
32
  blobHashes: string[];
33
+ /** Approximate stored aggregate-record byte size. */
21
34
  size: number;
35
+ /** Last read or write timestamp in milliseconds. */
22
36
  lastAccessed: number;
37
+ /** Coordinate cache key that currently points at this aggregate, if any. */
23
38
  coordinateKey?: string;
39
+ /** Reserved pin marker for hosts that want to protect an aggregate. */
24
40
  pinned?: boolean;
25
41
  }
42
+ /** Cached mapping from a NIP-5D coordinate to a verified aggregate. */
26
43
  interface CachedCoordinate {
44
+ /** Internal coordinate key in the form `kind:pubkey:dTag`. */
27
45
  key: string;
46
+ /** Napplet manifest event kind. */
28
47
  kind: number;
48
+ /** Manifest author public key. */
29
49
  pubkey: string;
50
+ /** Manifest `d` tag. */
30
51
  dTag: string;
52
+ /** Verified aggregate hash for the coordinate. */
31
53
  aggregateHash: string;
54
+ /** Timestamp when this coordinate last resolved successfully. */
32
55
  lastResolvedAt: number;
56
+ /** Foreground refresh TTL in milliseconds. */
33
57
  foregroundTtlMs: number;
58
+ /** Background refresh TTL in milliseconds. */
34
59
  backgroundTtlMs: number;
35
60
  }
61
+ /** Serialized Cache Storage index for verified napplet artifacts. */
36
62
  interface NappletArtifactCacheIndex {
63
+ /** Index schema version. */
37
64
  schemaVersion: number;
65
+ /** Last index update timestamp in milliseconds. */
38
66
  updatedAt: number;
67
+ /** Blob entries keyed by SHA-256 hash. */
39
68
  blobs: Record<string, NappletBlobIndexEntry>;
69
+ /** Aggregate entries keyed by the internal aggregate key. */
40
70
  aggregates: Record<string, NappletAggregateIndexEntry>;
71
+ /** Coordinate entries keyed by {@link coordinateKey}. */
41
72
  coordinates: Record<string, CachedCoordinate>;
42
73
  }
43
74
  interface WriteVerifiedResolutionInput {
75
+ /** Verified NIP-5D manifest event. */
44
76
  event: NostrEvent;
77
+ /** Parsed manifest whose aggregate and paths have already been verified. */
45
78
  manifest: NappletManifest;
79
+ /** File bytes keyed by manifest path. */
46
80
  files: ReadonlyMap<string, Uint8Array>;
81
+ /** Verified `/index.html` bytes decoded as text. */
47
82
  indexHtml: string;
83
+ /** Timestamp override for deterministic tests. */
48
84
  now?: number;
49
85
  }
86
+ /** Freshness controls for reading a cached coordinate. */
50
87
  interface CoordinateFreshnessOptions {
88
+ /** Timestamp override in milliseconds. */
51
89
  now?: number;
90
+ /** Use the background TTL instead of the foreground TTL. */
52
91
  background?: boolean;
53
92
  }
93
+ /** Cache interface for verified NIP-5D napplet artifacts. */
54
94
  interface NappletArtifactCache {
95
+ /** Read one cached artifact blob by SHA-256 hash. */
55
96
  readBlob(sha256Hex: string): Promise<Uint8Array | undefined>;
97
+ /** Delete one cached artifact blob by SHA-256 hash. */
56
98
  deleteBlob(sha256Hex: string): Promise<void>;
99
+ /** Write a verified manifest resolution and all referenced blobs. */
57
100
  writeVerifiedResolution(input: WriteVerifiedResolutionInput): Promise<void>;
101
+ /** Cache the latest aggregate for a NIP-5D coordinate. */
58
102
  writeCoordinate(input: {
59
103
  kind: number;
60
104
  pubkey: string;
@@ -64,6 +108,7 @@ interface NappletArtifactCache {
64
108
  foregroundTtlMs?: number;
65
109
  backgroundTtlMs?: number;
66
110
  }): Promise<void>;
111
+ /** Read a cached coordinate with a computed freshness flag. */
67
112
  getCoordinate(input: {
68
113
  kind: number;
69
114
  pubkey: string;
@@ -71,27 +116,64 @@ interface NappletArtifactCache {
71
116
  }, options?: CoordinateFreshnessOptions): Promise<(CachedCoordinate & {
72
117
  fresh: boolean;
73
118
  }) | undefined>;
119
+ /** Update aggregate/blob last-access timestamps. */
74
120
  touchAggregate(dTag: string, aggregateHash: string, now?: number): Promise<void>;
121
+ /** Protect an aggregate from normal pruning while it is in active use. */
75
122
  markAggregateActive(dTag: string, aggregateHash: string): void;
123
+ /** Release an aggregate previously marked active. */
76
124
  releaseAggregateActive(dTag: string, aggregateHash: string): void;
125
+ /** Prune cache contents to the configured storage budget. */
77
126
  prune(now?: number): Promise<void>;
78
127
  }
128
+ /** Constructor options for {@link CacheStorageNappletArtifactCache}. */
79
129
  interface CacheStorageNappletArtifactCacheOptions {
130
+ /** Cache Storage implementation; defaults to `globalThis.caches`. */
80
131
  cacheStorage?: CacheStorage;
132
+ /** Storage manager used for quota estimates; defaults to `navigator.storage`. */
81
133
  storage?: StorageManager;
134
+ /** Cache Storage namespace. */
82
135
  cacheName?: string;
136
+ /** Base URL used to construct synthetic cache keys. */
83
137
  baseUrl?: string;
138
+ /** Clock used for timestamps. */
84
139
  now?: () => number;
140
+ /** Diagnostic callback for non-fatal cache failures. */
85
141
  onDiagnostic?: (diagnostic: NappletCacheDiagnostic) => void;
142
+ /** Budget used when storage estimates are unavailable. */
86
143
  unknownBudgetBytes?: number;
144
+ /** Soft cache ceiling in bytes. */
87
145
  softCeilingBytes?: number;
146
+ /** Hard cache ceiling in bytes. */
88
147
  hardCeilingBytes?: number;
89
148
  }
149
+ /** Options for opening the browser Cache Storage artifact cache. */
90
150
  interface OpenNappletArtifactCacheOptions extends CacheStorageNappletArtifactCacheOptions {
151
+ /** Return `undefined` when quota estimates are unavailable. */
91
152
  requireStorageEstimate?: boolean;
92
153
  }
154
+ /**
155
+ * Build the stable index key for a NIP-5D coordinate.
156
+ *
157
+ * @param kind - Manifest event kind.
158
+ * @param pubkey - Manifest author public key.
159
+ * @param dTag - Manifest `d` tag.
160
+ * @returns Cache key in the form `kind:pubkey:dTag`.
161
+ */
93
162
  declare function coordinateKey(kind: number, pubkey: string, dTag: string): string;
163
+ /**
164
+ * Check whether a cached coordinate is still inside its foreground/background TTL.
165
+ *
166
+ * @param coordinate - Cached coordinate to inspect.
167
+ * @param options - Optional clock and TTL mode overrides.
168
+ * @returns `true` when the coordinate can be used without a network refresh.
169
+ */
94
170
  declare function isCoordinateFresh(coordinate: CachedCoordinate, options?: CoordinateFreshnessOptions): boolean;
171
+ /**
172
+ * Browser Cache Storage implementation of {@link NappletArtifactCache}.
173
+ *
174
+ * It stores verified blobs and aggregate records under synthetic URLs, keeps a
175
+ * small JSON index, and prunes according to quota-derived budgets.
176
+ */
95
177
  declare class CacheStorageNappletArtifactCache implements NappletArtifactCache {
96
178
  private readonly cacheStorage;
97
179
  private readonly storage?;
@@ -143,6 +225,12 @@ declare class CacheStorageNappletArtifactCache implements NappletArtifactCache {
143
225
  private estimateStorage;
144
226
  private diagnostic;
145
227
  }
228
+ /**
229
+ * Open the browser Cache Storage artifact cache when the host supports it.
230
+ *
231
+ * @param options - Cache construction options and diagnostics callback.
232
+ * @returns A cache instance, or `undefined` when Cache Storage is unavailable.
233
+ */
146
234
  declare function openNappletArtifactCache(options?: OpenNappletArtifactCacheOptions): Promise<CacheStorageNappletArtifactCache | undefined>;
147
235
 
148
236
  /**
@@ -160,6 +248,8 @@ declare function openNappletArtifactCache(options?: OpenNappletArtifactCacheOpti
160
248
  *
161
249
  * The napplet's identity is the `(dTag, aggregateHash)` tuple **computed** from
162
250
  * these verified bytes — it is never accepted from a host or gateway.
251
+ *
252
+ * @module
163
253
  */
164
254
  /** Snapshot manifest — regular event, immutable point-in-time release. */
165
255
  declare const NAPPLET_KIND_SNAPSHOT = 5129;
package/dist/5d/index.js CHANGED
@@ -15,8 +15,8 @@ import {
15
15
  resolveNapplet,
16
16
  verifyBlobHash,
17
17
  verifyManifestSignature
18
- } from "../chunk-CDBUCFLP.js";
19
- import "../chunk-2J3CSGKL.js";
18
+ } from "../chunk-NRUVARZL.js";
19
+ import "../chunk-JT5WCCCX.js";
20
20
  export {
21
21
  CacheStorageNappletArtifactCache,
22
22
  NAPPLET_ARTIFACT_CACHE_NAME,
@@ -20,6 +20,8 @@ import { NostrEvent } from 'nostr-tools';
20
20
  * ["r", "wss://relay.example.com", "read"] → read-only
21
21
  * ["r", "wss://relay.example.com", "write"] → write-only
22
22
  * ```
23
+ *
24
+ * @module
23
25
  */
24
26
  /**
25
27
  * A single relay entry parsed from a NIP-65 kind-10002 `r` tag.
package/dist/65/index.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  parseNip65RelayList,
4
4
  selectReadRelays,
5
5
  selectWriteRelays
6
- } from "../chunk-UUMBCHTA.js";
6
+ } from "../chunk-PSENX3MS.js";
7
7
  export {
8
8
  createNip65Registry,
9
9
  parseNip65RelayList,
@@ -1,5 +1,11 @@
1
1
  import { NostrEvent } from 'nostr-tools';
2
2
 
3
+ /**
4
+ * `@kehto/nip/66` — NIP-66 relay discovery aggregation helpers.
5
+ *
6
+ * @module
7
+ */
8
+
3
9
  /**
4
10
  * Minimal relay-pool contract `@kehto/nip/66` depends on. Consumers implement
5
11
  * this against their pool library of choice (nostr-tools SimplePool,
package/dist/66/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  DEFAULT_RELAY_ATTRIBUTE_GROUPS,
3
3
  createNip66Aggregator
4
- } from "../chunk-DADZ6NUI.js";
4
+ } from "../chunk-PEKQVWZY.js";
5
5
  export {
6
6
  DEFAULT_RELAY_ATTRIBUTE_GROUPS,
7
7
  createNip66Aggregator
@@ -17,6 +17,8 @@ import { NostrEvent } from 'nostr-tools';
17
17
  * `nostr-tools` ships no NIP-89 helper. This module provides pure parsers for
18
18
  * both kinds plus URL-template expansion — no relay, signer, or framework
19
19
  * coupling.
20
+ *
21
+ * @module
20
22
  */
21
23
  /** Kind 31990 — handler information (an app describing what it can open). */
22
24
  declare const HANDLER_INFORMATION_KIND = 31990;
package/dist/89/index.js CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  handlesKind,
7
7
  parseHandlerInformation,
8
8
  parseHandlerRecommendation
9
- } from "../chunk-BCTIL523.js";
9
+ } from "../chunk-7X33QYEY.js";
10
10
  export {
11
11
  HANDLER_INFORMATION_KIND,
12
12
  HANDLER_PLATFORMS,
@@ -80,4 +80,4 @@ export {
80
80
  buildHandlerUrl,
81
81
  parseHandlerRecommendation
82
82
  };
83
- //# sourceMappingURL=chunk-BCTIL523.js.map
83
+ //# sourceMappingURL=chunk-7X33QYEY.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 * @module\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":";AAwBO,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":[]}
@@ -36,4 +36,4 @@ export {
36
36
  aggregateTagValue,
37
37
  verifyAggregate
38
38
  };
39
- //# sourceMappingURL=chunk-2J3CSGKL.js.map
39
+ //# sourceMappingURL=chunk-JT5WCCCX.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/5a/index.ts"],"sourcesContent":["import { sha256 } from '@noble/hashes/sha2.js';\nimport { bytesToHex } from '@noble/hashes/utils.js';\n\n/**\n * `@kehto/nip/5a` — NIP-5A \"Aggregate Hash\" computation and verification.\n *\n * NIP-5A (`nostr-protocol/nips` PR #2287) defines a content address for a set\n * of published files. Each file is declared by a `path` tag\n * (`[\"path\", \"<abs-path>\", \"<sha256>\"]`) and the set is summarized by a single\n * aggregate `x` tag (`[\"x\", \"<hex>\", \"aggregate\"]`).\n *\n * The aggregate is computed by:\n * 1. taking every `path` entry as the line `\"<sha256> <abs-path>\\n\"`,\n * 2. sorting the lines in ascending lexicographic order,\n * 3. concatenating them as UTF-8 bytes,\n * 4. hashing with SHA-256, encoded as lowercase hex.\n *\n * @module\n */\n\n// TextEncoder is available in all JS runtimes but not in the ES2022 lib types.\ndeclare class TextEncoder { encode(input: string): Uint8Array; }\n\n/**\n * One published file: an absolute path and the lowercase-hex SHA-256 of its\n * bytes. Parsed from a NIP-5A `[\"path\", \"<abs-path>\", \"<sha256>\"]` tag.\n *\n * @example\n * ```ts\n * const entry: PathEntry = { path: '/index.html', sha256: '186ea5…1c99' };\n * ```\n */\nexport interface PathEntry {\n /** Absolute path of the file (the `path` tag's second element). */\n path: string;\n /** Lowercase-hex SHA-256 of the file bytes (the `path` tag's third element). */\n sha256: string;\n}\n\n/**\n * Compute the NIP-5A aggregate hash over a set of {@link PathEntry} files.\n *\n * Lines (`\"<sha256> <abs-path>\\n\"`) are sorted before hashing, so the result is\n * independent of the input order.\n *\n * @param entries - The file path entries (any order)\n * @returns The aggregate hash as lowercase hex\n *\n * @example\n * ```ts\n * import { computeAggregateHash } from '@kehto/nip/5a';\n * const hex = computeAggregateHash([{ path: '/index.html', sha256: 'aa…' }]);\n * ```\n */\nexport function computeAggregateHash(entries: readonly PathEntry[]): string {\n const lines = entries.map((entry) => `${entry.sha256} ${entry.path}\\n`);\n lines.sort();\n return bytesToHex(sha256(new TextEncoder().encode(lines.join(''))));\n}\n\n/**\n * Extract {@link PathEntry} objects from an event's tags, keeping only\n * well-formed `[\"path\", \"<abs-path>\", \"<sha256>\"]` tags in tag order.\n *\n * @param tags - The manifest event's tags\n * @returns Parsed path entries (malformed `path` tags are skipped)\n */\nexport function pathEntriesFromTags(tags: readonly (readonly string[])[]): PathEntry[] {\n const entries: PathEntry[] = [];\n for (const tag of tags) {\n if (tag[0] === 'path' && typeof tag[1] === 'string' && typeof tag[2] === 'string'\n && tag[1].length > 0 && tag[2].length > 0) {\n entries.push({ path: tag[1], sha256: tag[2] });\n }\n }\n return entries;\n}\n\n/**\n * Read the aggregate hash declared by an event's `[\"x\", \"<hex>\", \"aggregate\"]`\n * tag.\n *\n * @param tags - The manifest event's tags\n * @returns The declared aggregate hex, or `undefined` if no aggregate `x` tag\n */\nexport function aggregateTagValue(tags: readonly (readonly string[])[]): string | undefined {\n for (const tag of tags) {\n if (tag[0] === 'x' && tag[2] === 'aggregate' && typeof tag[1] === 'string' && tag[1].length > 0) {\n return tag[1];\n }\n }\n return undefined;\n}\n\n/**\n * Verify that an event's declared aggregate `x` tag equals the aggregate\n * recomputed from its `path` tags.\n *\n * @param tags - The manifest event's tags\n * @returns `true` only when an aggregate `x` tag is present and matches the\n * recomputed value; `false` otherwise (mismatch or missing aggregate tag)\n *\n * @example\n * ```ts\n * import { verifyAggregate } from '@kehto/nip/5a';\n * if (!verifyAggregate(manifestEvent.tags)) throw new Error('aggregate mismatch');\n * ```\n */\nexport function verifyAggregate(tags: readonly (readonly string[])[]): boolean {\n const declared = aggregateTagValue(tags);\n if (!declared) return false;\n return computeAggregateHash(pathEntriesFromTags(tags)) === declared;\n}\n"],"mappings":";AAAA,SAAS,cAAc;AACvB,SAAS,kBAAkB;AAqDpB,SAAS,qBAAqB,SAAuC;AAC1E,QAAM,QAAQ,QAAQ,IAAI,CAAC,UAAU,GAAG,MAAM,MAAM,IAAI,MAAM,IAAI;AAAA,CAAI;AACtE,QAAM,KAAK;AACX,SAAO,WAAW,OAAO,IAAI,YAAY,EAAE,OAAO,MAAM,KAAK,EAAE,CAAC,CAAC,CAAC;AACpE;AASO,SAAS,oBAAoB,MAAmD;AACrF,QAAM,UAAuB,CAAC;AAC9B,aAAW,OAAO,MAAM;AACtB,QAAI,IAAI,CAAC,MAAM,UAAU,OAAO,IAAI,CAAC,MAAM,YAAY,OAAO,IAAI,CAAC,MAAM,YACpE,IAAI,CAAC,EAAE,SAAS,KAAK,IAAI,CAAC,EAAE,SAAS,GAAG;AAC3C,cAAQ,KAAK,EAAE,MAAM,IAAI,CAAC,GAAG,QAAQ,IAAI,CAAC,EAAE,CAAC;AAAA,IAC/C;AAAA,EACF;AACA,SAAO;AACT;AASO,SAAS,kBAAkB,MAA0D;AAC1F,aAAW,OAAO,MAAM;AACtB,QAAI,IAAI,CAAC,MAAM,OAAO,IAAI,CAAC,MAAM,eAAe,OAAO,IAAI,CAAC,MAAM,YAAY,IAAI,CAAC,EAAE,SAAS,GAAG;AAC/F,aAAO,IAAI,CAAC;AAAA,IACd;AAAA,EACF;AACA,SAAO;AACT;AAgBO,SAAS,gBAAgB,MAA+C;AAC7E,QAAM,WAAW,kBAAkB,IAAI;AACvC,MAAI,CAAC,SAAU,QAAO;AACtB,SAAO,qBAAqB,oBAAoB,IAAI,CAAC,MAAM;AAC7D;","names":[]}
@@ -105,4 +105,4 @@ export {
105
105
  getTagValues,
106
106
  decryptPrivateItems
107
107
  };
108
- //# sourceMappingURL=chunk-2XYRP2S6.js.map
108
+ //# sourceMappingURL=chunk-NDOAX5PE.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/51/index.ts"],"sourcesContent":["import type { NostrEvent } from 'nostr-tools';\n\n/**\n * `@kehto/nip/51` — NIP-51 list & set parsing.\n *\n * NIP-51 organizes user data into two families of events whose items live in\n * tags (public) and, optionally, in NIP-44-encrypted content (private):\n *\n * - **Standard lists** — replaceable, kinds `10000`–`10101`. One per user per\n * kind (e.g. mute list, bookmarks, blocked relays, DM relays).\n * - **Sets** — addressable / parameterized-replaceable, kinds `30000`–`30030`.\n * Many per user, distinguished by a `d` identifier (e.g. follow sets, relay\n * sets, emoji sets, curation sets).\n *\n * `nostr-tools` ships no NIP-51 helper. This module provides a single\n * widely-compatible parser that turns either family into a structured\n * {@link Nip51List}, plus item accessors and an injected-decryptor path for\n * private items (so the package stays crypto-free and framework-agnostic).\n *\n * @module\n */\n\n/** Canonical NIP-51 **standard list** kinds (replaceable, one per author). */\nexport const LIST_KINDS: Readonly<Record<number, string>> = {\n 10000: 'mute',\n 10001: 'pinned-notes',\n 10003: 'bookmarks',\n 10004: 'communities',\n 10005: 'public-chats',\n 10006: 'blocked-relays',\n 10007: 'search-relays',\n 10009: 'simple-groups',\n 10012: 'relay-feeds',\n 10015: 'interests',\n 10020: 'media-follows',\n 10030: 'emojis',\n 10050: 'dm-relays',\n 10101: 'good-wiki-authors',\n 10102: 'good-wiki-relays',\n};\n\n/** Canonical NIP-51 **set** kinds (addressable, many per author by `d`). */\nexport const SET_KINDS: Readonly<Record<number, string>> = {\n 30000: 'follow-sets',\n 30002: 'relay-sets',\n 30003: 'bookmark-sets',\n 30004: 'curation-sets',\n 30005: 'video-curation-sets',\n 30007: 'kind-mute-sets',\n 30015: 'interest-sets',\n 30019: 'release-sets',\n 30020: 'release-artifact-sets',\n 30030: 'emoji-sets',\n 30063: 'release-artifact-sets',\n 30267: 'app-curation-sets',\n};\n\n/**\n * Item-bearing NIP-51 tag names — the tags that carry list *content* (as\n * opposed to metadata tags like `d`, `title`, `image`, `description`).\n */\nexport const ITEM_TAGS = ['e', 'p', 'a', 't', 'r', 'word', 'emoji', 'relay', 'g', 'i', 'k'] as const;\n\n/** Whether `kind` is a NIP-51 standard list kind (`10000`–`10101`). */\nexport function isListKind(kind: number): boolean {\n return Object.prototype.hasOwnProperty.call(LIST_KINDS, kind);\n}\n\n/** Whether `kind` is a NIP-51 set kind (`30000`–`30030`). */\nexport function isSetKind(kind: number): boolean {\n return Object.prototype.hasOwnProperty.call(SET_KINDS, kind);\n}\n\n/**\n * Friendly name for a NIP-51 list/set kind (e.g. `10000` → `'mute'`,\n * `30030` → `'emoji-sets'`), or `undefined` if `kind` is not a known NIP-51 kind.\n */\nexport function listKindName(kind: number): string | undefined {\n return LIST_KINDS[kind] ?? SET_KINDS[kind];\n}\n\n/**\n * A parsed NIP-51 list or set.\n *\n * `publicItems` holds the item-bearing tags verbatim. `privateItems` is\n * populated only after {@link decryptPrivateItems} runs; until then it is\n * `undefined` (the encrypted payload is left untouched in {@link Nip51List.encryptedContent}).\n *\n * @example\n * ```ts\n * const list = parseList(kind10000MuteEvent);\n * list.kind; // 10000\n * list.type; // 'mute'\n * list.family; // 'list'\n * getTagValues(list.publicItems, 'p'); // muted pubkeys (public portion)\n * ```\n */\nexport interface Nip51List {\n /** Event kind. */\n kind: number;\n /** `'list'` for standard lists (10000–10101), `'set'` for sets (30000–30030), `'unknown'` otherwise. */\n family: 'list' | 'set' | 'unknown';\n /** Friendly kind name from {@link listKindName}, or `undefined` for unknown kinds. */\n type: string | undefined;\n /** Author pubkey. */\n pubkey: string;\n /** Set identifier from the `d` tag (sets only; `''` for standard lists / missing). */\n identifier: string;\n /** Addressable coordinate `kind:pubkey:d` for sets; `undefined` for standard lists. */\n address: string | undefined;\n /** Optional `title` tag (sets). */\n title: string | undefined;\n /** Optional `image` tag (sets). */\n image: string | undefined;\n /** Optional `description` tag (sets). */\n description: string | undefined;\n /** Item-bearing tags verbatim (the public portion of the list). */\n publicItems: string[][];\n /** Raw NIP-44-encrypted content, if any (the private portion). */\n encryptedContent: string | undefined;\n /** Decrypted private item tags — `undefined` until {@link decryptPrivateItems} succeeds. */\n privateItems: string[][] | undefined;\n}\n\nconst META_TAGS = new Set(['d', 'title', 'image', 'description', 'name', 'summary', 'alt']);\n\n/**\n * Parse a NIP-51 list or set event into a structured {@link Nip51List}.\n *\n * Works for any kind: known list/set kinds get a `type` and correct `family`;\n * unknown kinds still parse (`family: 'unknown'`) so forward-compatible callers\n * can handle new list kinds before this table is updated.\n *\n * Item-bearing tags (see {@link ITEM_TAGS}) become {@link Nip51List.publicItems};\n * metadata tags (`d`, `title`, `image`, `description`, …) are lifted into named\n * fields. The encrypted `content` is preserved verbatim in\n * {@link Nip51List.encryptedContent} — call {@link decryptPrivateItems} with a\n * NIP-44 decryptor to populate {@link Nip51List.privateItems}.\n *\n * @param event - A NIP-51 list or set event\n * @returns The structured list/set\n *\n * @example\n * ```ts\n * import { parseList, getTagValues } from '@kehto/nip/51';\n * const bookmarks = parseList(kind10003Event);\n * const notes = getTagValues(bookmarks.publicItems, 'e');\n * ```\n */\nexport function parseList(event: NostrEvent): Nip51List {\n const family: Nip51List['family'] = isListKind(event.kind)\n ? 'list'\n : isSetKind(event.kind)\n ? 'set'\n : 'unknown';\n\n const itemTagSet = new Set<string>(ITEM_TAGS);\n const publicItems: string[][] = [];\n let identifier = '';\n let title: string | undefined;\n let image: string | undefined;\n let description: string | undefined;\n\n for (const tag of event.tags) {\n const name = tag[0];\n if (name === 'd') identifier = tag[1] ?? '';\n else if (name === 'title') title = tag[1];\n else if (name === 'image') image = tag[1];\n else if (name === 'description') description = tag[1];\n else if (itemTagSet.has(name) && !META_TAGS.has(name)) publicItems.push([...tag]);\n }\n\n const isSet = family === 'set';\n const address = isSet ? `${event.kind}:${event.pubkey}:${identifier}` : undefined;\n const encryptedContent = event.content && event.content.length > 0 ? event.content : undefined;\n\n return {\n kind: event.kind,\n family,\n type: listKindName(event.kind),\n pubkey: event.pubkey,\n identifier,\n address,\n title,\n image,\n description,\n publicItems,\n encryptedContent,\n privateItems: undefined,\n };\n}\n\n/**\n * Collect the values (`tag[1]`) of every item tag with the given name.\n *\n * @param items - Item tags ({@link Nip51List.publicItems} or {@link Nip51List.privateItems})\n * @param tagName - Tag name to filter on (e.g. `'p'`, `'e'`, `'a'`, `'t'`, `'word'`)\n * @returns The second element of each matching tag (missing values skipped)\n *\n * @example\n * ```ts\n * const mutedPubkeys = getTagValues(list.publicItems, 'p');\n * const mutedWords = getTagValues(list.publicItems, 'word');\n * ```\n */\nexport function getTagValues(items: ReadonlyArray<ReadonlyArray<string>>, tagName: string): string[] {\n const out: string[] = [];\n for (const tag of items) {\n if (tag[0] === tagName && typeof tag[1] === 'string') out.push(tag[1]);\n }\n return out;\n}\n\n/**\n * A NIP-44 decryptor: takes the event's `content` and the author pubkey and\n * returns the plaintext (a JSON array of tags). Inject your signer's\n * `nip44.decrypt` (e.g. a NIP-07 `window.nostr.nip44`, a bunker, or\n * `nostr-tools/nip44`) so this package carries no crypto dependency.\n *\n * @example\n * ```ts\n * const decrypt: Nip44Decryptor = (content, author) =>\n * window.nostr!.nip44!.decrypt(author, content);\n * ```\n */\nexport type Nip44Decryptor = (content: string, authorPubkey: string) => string | Promise<string>;\n\n/**\n * Decrypt the private portion of a list/set and populate\n * {@link Nip51List.privateItems}.\n *\n * NIP-51 stores private items as a NIP-44-encrypted JSON array of tags in the\n * event `content`, encrypted to the author's own key. This helper runs the\n * injected {@link Nip44Decryptor}, parses the plaintext as `string[][]`, and\n * returns a **new** {@link Nip51List} with `privateItems` set (the input is not\n * mutated). Lists with no encrypted content return a copy with\n * `privateItems: []`.\n *\n * Throws if the decryptor throws or the plaintext is not a JSON tag array — so\n * callers can distinguish \"no private items\" from \"couldn't read private items\".\n *\n * @param list - A parsed list from {@link parseList}\n * @param decrypt - Injected NIP-44 decryptor\n * @returns A copy of `list` with `privateItems` populated\n *\n * @example\n * ```ts\n * const withPrivate = await decryptPrivateItems(list, (c, a) => signer.nip44.decrypt(a, c));\n * const privatelyMuted = getTagValues(withPrivate.privateItems!, 'p');\n * ```\n */\nexport async function decryptPrivateItems(list: Nip51List, decrypt: Nip44Decryptor): Promise<Nip51List> {\n if (!list.encryptedContent) {\n return { ...list, privateItems: [] };\n }\n const plaintext = await decrypt(list.encryptedContent, list.pubkey);\n const parsed: unknown = JSON.parse(plaintext);\n if (!Array.isArray(parsed) || !parsed.every((t) => Array.isArray(t) && t.every((s) => typeof s === 'string'))) {\n throw new Error('NIP-51: decrypted private items are not a string[][] tag array');\n }\n return { ...list, privateItems: parsed as string[][] };\n}\n"],"mappings":"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","names":[]}
@@ -2,7 +2,7 @@ import {
2
2
  aggregateTagValue,
3
3
  computeAggregateHash,
4
4
  pathEntriesFromTags
5
- } from "./chunk-2J3CSGKL.js";
5
+ } from "./chunk-JT5WCCCX.js";
6
6
 
7
7
  // src/5d/index.ts
8
8
  import { verifyEvent } from "nostr-tools/pure";
@@ -569,4 +569,4 @@ export {
569
569
  fetchBlob,
570
570
  resolveNapplet
571
571
  };
572
- //# sourceMappingURL=chunk-CDBUCFLP.js.map
572
+ //# sourceMappingURL=chunk-NRUVARZL.js.map
@@ -0,0 +1 @@
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 * Archetype slugs this napplet fulfills, from `archetype` manifest tags; the\n * optional `nap` is the recommended default wire protocol (the 3rd tag element).\n */\n archetypes: Array<{ slug: string; nap?: 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; nap?: string }> {\n const out: Array<{ slug: string; nap?: string }> = [];\n for (const tag of tags) {\n if (tag[0] !== 'archetype') continue;\n if (typeof tag[1] !== 'string' || tag[1].length === 0) continue;\n out.push(\n typeof tag[2] === 'string' && tag[2].length > 0\n ? { slug: tag[1], nap: tag[2] }\n : { slug: tag[1] },\n );\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 return {\n kind: event.kind,\n pubkey: event.pubkey,\n dTag: firstTagValue(event.tags, 'd') ?? '',\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":[]}
@@ -138,4 +138,4 @@ export {
138
138
  DEFAULT_RELAY_ATTRIBUTE_GROUPS,
139
139
  createNip66Aggregator
140
140
  };
141
- //# sourceMappingURL=chunk-DADZ6NUI.js.map
141
+ //# sourceMappingURL=chunk-PEKQVWZY.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/66/index.ts"],"sourcesContent":["/**\n * `@kehto/nip/66` — NIP-66 relay discovery aggregation helpers.\n *\n * @module\n */\nimport type { NostrEvent } from 'nostr-tools';\n\n/**\n * Minimal relay-pool contract `@kehto/nip/66` depends on. Consumers implement\n * this against their pool library of choice (nostr-tools SimplePool,\n * applesauce-relay, @snort/worker-relay, etc.). Intentionally smaller than\n * RxJS-style observable pools — one callback, one unsubscribe handle.\n *\n * @example\n * ```ts\n * import { SimplePool } from 'nostr-tools/pool';\n * const pool = new SimplePool();\n * const adapter: Nip66RelayPool = {\n * subscribe: (relays, filter, onEvent) => {\n * const sub = pool.subscribeMany(relays, [filter], { onevent: onEvent });\n * return () => sub.close();\n * },\n * };\n * ```\n */\nexport interface Nip66RelayPool {\n /**\n * Open a kind-30166 subscription against a set of monitor relays.\n *\n * @param relays - WebSocket URLs to subscribe against (the aggregator passes its bootstrap list)\n * @param filter - A kind-30166 filter, optionally narrowed with `#n` network tags\n * @param onEvent - Called synchronously with every matching event; the aggregator parses each one\n * @returns An unsubscribe handle the aggregator calls on `resync()` or teardown\n */\n subscribe(\n relays: ReadonlyArray<string>,\n filter: Nip66Filter,\n onEvent: (event: NostrEvent) => void,\n ): () => void;\n}\n\n/**\n * Minimal filter shape for kind-30166 subscriptions. `#n` optionally narrows\n * by network type (e.g. `['clearnet']`, `['clearnet', 'tor']`).\n *\n * @example\n * ```ts\n * const filter: Nip66Filter = { kinds: [30166], '#n': ['clearnet'] };\n * ```\n */\nexport type Nip66Filter = { kinds: [30166]; '#n'?: ReadonlyArray<string> };\n\n/**\n * Options for {@link createNip66Aggregator}.\n *\n * @example\n * ```ts\n * const options: Nip66AggregatorOptions = {\n * pool: myPoolAdapter,\n * bootstrap: ['wss://monitor1.example.com', 'wss://monitor2.example.com'],\n * networks: ['clearnet'],\n * };\n * ```\n */\nexport interface Nip66AggregatorOptions {\n /** A {@link Nip66RelayPool} adapter supplied by the consumer. */\n pool: Nip66RelayPool;\n /** Required non-empty list of monitor relay URLs to subscribe against. No default ships in the package. */\n bootstrap: ReadonlyArray<string>;\n /** Optional network-type narrowing passed to the filter as `#n` (e.g. `['clearnet', 'tor']`). Omit to receive every network. */\n networks?: ReadonlyArray<string>;\n /**\n * Optional named relay-attribute groups for interpreting ad hoc kind-30166 tags.\n * Omit to use {@link DEFAULT_RELAY_ATTRIBUTE_GROUPS}.\n */\n relayAttributeGroups?: RelayAttributeGroups;\n}\n\n/** A tag/value matcher for ad hoc relay attributes carried on kind-30166 events. */\nexport interface RelayAttributeMatcher {\n /** Tag name to match, for example `T`, `t`, or `k`. */\n tag: string;\n /** Optional accepted values. If omitted, any value for the tag matches. */\n values?: ReadonlyArray<string>;\n}\n\n/** Named groups of relay-attribute matchers. Matchers within a group are OR-ed. */\nexport type RelayAttributeGroups = Readonly<Record<string, ReadonlyArray<RelayAttributeMatcher>>>;\n\n/** Parsed attributes observed for one NIP-66 relay URL. */\nexport interface Nip66RelayAttributes {\n /** Raw tag copies from the latest observed kind-30166 event for the relay. */\n tags: ReadonlyArray<ReadonlyArray<string>>;\n /** Tag-value index, keyed by tag name. */\n values: ReadonlyMap<string, ReadonlySet<string>>;\n /** Parsed uppercase `N` tag support for convenience. */\n supportedNips: ReadonlySet<number>;\n}\n\nexport const DEFAULT_RELAY_ATTRIBUTE_GROUPS: RelayAttributeGroups = {\n Indexer: [\n { tag: 'T', values: ['Indexer', 'indexer'] },\n { tag: 't', values: ['Indexer', 'indexer'] },\n { tag: 'k', values: ['10002'] },\n ],\n RelayIndexer: [\n { tag: 'T', values: ['RelayIndexer', 'relay-indexer', 'relay_indexer'] },\n { tag: 't', values: ['RelayIndexer', 'relay-indexer', 'relay_indexer'] },\n { tag: 'k', values: ['30166', '10166'] },\n ],\n};\n\n/**\n * Aggregator handle returned by {@link createNip66Aggregator}. Each call\n * returns a fresh instance with its own closure-scoped state — multi-instance\n * safe (unlike a module-globals pattern).\n *\n * @example\n * ```ts\n * const aggregator = createNip66Aggregator({ pool, bootstrap: ['wss://monitor.example.com'] });\n * aggregator.start();\n * const relays = aggregator.getRelaySet();\n * ```\n */\nexport interface Nip66Aggregator {\n /** Begin subscribing against the bootstrap relay set. Idempotent — calling twice is a no-op. */\n start(): void;\n /** Tear down the current subscription, clear accumulated state, and re-subscribe. Call when `networks` semantics change upstream. */\n resync(): void;\n /**\n * Tear down the current subscription. Idempotent — calling without a prior\n * `start()` is a safe no-op. Preserves accumulated state (`getRelaySet`,\n * `relaySupportsNip`) — use `resync()` if you need to clear state AND\n * re-subscribe in one step. A subsequent `start()` re-subscribes against\n * the bootstrap relays (identical contract to the first start).\n *\n * Wire this to your consumer's teardown path (browser: `beforeunload`;\n * React: effect cleanup; Electron: `app.before-quit`) — the aggregator\n * holds a pool subscription that otherwise lives for the tab lifetime\n * (PITFALLS.md M-03).\n */\n stop(): void;\n /** The current set of relay URLs discovered from kind-30166 `d`-tags. Empty until `start()` fires at least one event. */\n getRelaySet(): ReadonlySet<string>;\n /**\n * Relay URLs whose `N`-tags declare support for the given NIP number.\n *\n * @param nip - NIP number to query support for (e.g. 44 for NIP-44 encrypted DMs)\n * @returns Array of relay URLs supporting that NIP\n */\n getRelaysSupportingNip(nip: number): string[];\n /**\n * Whether the relay at `url` has been seen with an `N`-tag declaring support for `nip`.\n *\n * @param url - Relay WebSocket URL to check\n * @param nip - NIP number to query support for\n * @returns `true` iff the relay has been observed with a matching `N`-tag\n */\n relaySupportsNip(url: string, nip: number): boolean;\n /**\n * Return parsed relay attributes for a relay URL, if that relay has been\n * observed in a kind-30166 event.\n */\n getRelayAttributes(url: string): Nip66RelayAttributes | undefined;\n /**\n * Return relays matching any supplied ad hoc attribute matcher.\n *\n * @param matchers - Tag/value matchers, OR semantics\n */\n getRelaysMatchingAttributes(matchers: ReadonlyArray<RelayAttributeMatcher>): string[];\n /**\n * Return relays in a named attribute group. Uses the group table passed to\n * `createNip66Aggregator`, or {@link DEFAULT_RELAY_ATTRIBUTE_GROUPS}.\n */\n getRelaysForAttributeGroup(groupName: string, groups?: RelayAttributeGroups): string[];\n}\n\n/**\n * Create a NIP-66 kind-30166 relay-discovery aggregator with closure-scoped state.\n *\n * Subscribes to kind-30166 events via the injected {@link Nip66RelayPool},\n * extracts relay URLs from `d`-tags, and parses `N`-tags to track which NIP\n * numbers each relay supports. Multi-instance safe — each factory call owns\n * its own `Set` + `Map`; multiple aggregators share nothing at module scope.\n *\n * The aggregator is framework-agnostic: it knows nothing about negentropy\n * (NIP-77), OPFS caching, worker relays, or network-type config stores.\n * Consumers inject those concerns through their {@link Nip66RelayPool}\n * implementation. `start()` is synchronous and idempotent — consumers\n * schedule their own cadence (delayed start, retry, etc.).\n *\n * @param options - Pool adapter + bootstrap monitor relays (+ optional network filter)\n * @returns A fresh {@link Nip66Aggregator} handle\n *\n * @example\n * ```ts\n * import { createNip66Aggregator } from '@kehto/nip/66';\n * import { SimplePool } from 'nostr-tools/pool';\n *\n * const pool = new SimplePool();\n * const aggregator = createNip66Aggregator({\n * pool: {\n * subscribe: (relays, filter, onEvent) => {\n * const sub = pool.subscribeMany(relays, [filter], { onevent: onEvent });\n * return () => sub.close();\n * },\n * },\n * bootstrap: ['wss://monitor1.example.com'],\n * });\n * aggregator.start();\n * // Later — inside a ShellAdapter hook:\n * const suggestions = Array.from(aggregator.getRelaySet());\n * ```\n */\nexport function createNip66Aggregator(options: Nip66AggregatorOptions): Nip66Aggregator {\n const relaySet = new Set<string>();\n const relaySupportedNips = new Map<string, Set<number>>();\n const relayAttributeTags = new Map<string, string[][]>();\n const relayAttributeValues = new Map<string, Map<string, Set<string>>>();\n const relayAttributeGroups = options.relayAttributeGroups ?? DEFAULT_RELAY_ATTRIBUTE_GROUPS;\n let unsubscribe: (() => void) | null = null;\n\n function buildFilter(): Nip66Filter {\n const filter: Nip66Filter = { kinds: [30166] };\n if (options.networks && options.networks.length > 0) {\n filter['#n'] = options.networks;\n }\n return filter;\n }\n\n function processEvent(event: NostrEvent): void {\n const dTag = event.tags.find((t) => t[0] === 'd');\n const relayUrl = dTag?.[1];\n if (!relayUrl) return;\n relaySet.add(relayUrl);\n captureRelayAttributes(relayUrl, event.tags);\n\n const nips = new Set<number>();\n for (const tag of event.tags) {\n if (tag[0] === 'N' && tag[1]) {\n const nipNum = parseInt(tag[1], 10);\n if (!Number.isNaN(nipNum)) nips.add(nipNum);\n }\n }\n if (nips.size > 0) relaySupportedNips.set(relayUrl, nips);\n }\n\n function captureRelayAttributes(relayUrl: string, tags: string[][]): void {\n const tagCopies = tags.map((tag) => [...tag]);\n const values = new Map<string, Set<string>>();\n for (const tag of tags) {\n const [name, value] = tag;\n if (!name || value === undefined) continue;\n let bucket = values.get(name);\n if (!bucket) {\n bucket = new Set<string>();\n values.set(name, bucket);\n }\n bucket.add(value);\n }\n relayAttributeTags.set(relayUrl, tagCopies);\n relayAttributeValues.set(relayUrl, values);\n }\n\n function start(): void {\n if (unsubscribe) return; // idempotent — already subscribed\n unsubscribe = options.pool.subscribe(options.bootstrap, buildFilter(), processEvent);\n }\n\n function resync(): void {\n if (unsubscribe) {\n unsubscribe();\n unsubscribe = null;\n }\n relaySet.clear();\n relaySupportedNips.clear();\n relayAttributeTags.clear();\n relayAttributeValues.clear();\n unsubscribe = options.pool.subscribe(options.bootstrap, buildFilter(), processEvent);\n }\n\n function stop(): void {\n if (unsubscribe) {\n unsubscribe();\n unsubscribe = null;\n }\n }\n\n function getRelaySet(): ReadonlySet<string> {\n return relaySet;\n }\n\n function getRelaysSupportingNip(nip: number): string[] {\n const result: string[] = [];\n for (const [url, nips] of relaySupportedNips) {\n if (nips.has(nip)) result.push(url);\n }\n return result;\n }\n\n function relaySupportsNip(url: string, nip: number): boolean {\n return relaySupportedNips.get(url)?.has(nip) ?? false;\n }\n\n function getRelayAttributes(url: string): Nip66RelayAttributes | undefined {\n const tagCopies = relayAttributeTags.get(url);\n const values = relayAttributeValues.get(url);\n if (!tagCopies || !values) return undefined;\n return {\n tags: tagCopies.map((tag) => [...tag]),\n values: new Map(Array.from(values, ([tag, tagValues]) => [tag, new Set(tagValues)])),\n supportedNips: new Set(relaySupportedNips.get(url) ?? []),\n };\n }\n\n function matcherMatches(url: string, matcher: RelayAttributeMatcher): boolean {\n const values = relayAttributeValues.get(url)?.get(matcher.tag);\n if (!values || values.size === 0) return false;\n if (!matcher.values || matcher.values.length === 0) return true;\n const accepted = new Set(matcher.values.map((value) => value.toLowerCase()));\n for (const value of values) {\n if (accepted.has(value.toLowerCase())) return true;\n }\n return false;\n }\n\n function getRelaysMatchingAttributes(matchers: ReadonlyArray<RelayAttributeMatcher>): string[] {\n const result: string[] = [];\n for (const url of relaySet) {\n if (matchers.some((matcher) => matcherMatches(url, matcher))) result.push(url);\n }\n return result;\n }\n\n function getRelaysForAttributeGroup(groupName: string, groups?: RelayAttributeGroups): string[] {\n const group = (groups ?? relayAttributeGroups)[groupName] ?? [];\n return getRelaysMatchingAttributes(group);\n }\n\n return {\n start,\n resync,\n stop,\n getRelaySet,\n getRelaysSupportingNip,\n relaySupportsNip,\n getRelayAttributes,\n getRelaysMatchingAttributes,\n getRelaysForAttributeGroup,\n };\n}\n"],"mappings":"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","names":[]}
@@ -72,4 +72,4 @@ export {
72
72
  selectReadRelays,
73
73
  createNip65Registry
74
74
  };
75
- //# sourceMappingURL=chunk-UUMBCHTA.js.map
75
+ //# sourceMappingURL=chunk-PSENX3MS.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/65/index.ts"],"sourcesContent":["import type { NostrEvent } from 'nostr-tools';\n\n/**\n * `@kehto/nip/65` — NIP-65 relay-list (kind 10002) parsing and outbox-model\n * relay resolution.\n *\n * NIP-65 defines kind-10002 replaceable events whose `r` tags declare which\n * relays an author reads from and writes to — the \"outbox model\" (a.k.a.\n * \"gossip\"). A client that wants to *reach* an author publishes to (or reads\n * from) that author's advertised relays instead of a fixed relay set.\n *\n * `nostr-tools` ships no NIP-65 helper, so this module fills the gap with a\n * pure parser plus a closure-scoped registry (no module-global state, so\n * multiple runtimes in one process never alias each other — same multi-instance\n * contract as `@kehto/nip/66`).\n *\n * Tag grammar:\n * ```text\n * [\"r\", \"wss://relay.example.com\"] → read AND write\n * [\"r\", \"wss://relay.example.com\", \"read\"] → read-only\n * [\"r\", \"wss://relay.example.com\", \"write\"] → write-only\n * ```\n *\n * @module\n */\n\n/**\n * A single relay entry parsed from a NIP-65 kind-10002 `r` tag.\n *\n * @example\n * ```ts\n * const entry: RelayEntry = { url: 'wss://relay.example.com', read: true, write: false };\n * ```\n */\nexport interface RelayEntry {\n /** Relay WebSocket URL from the `r` tag's second element. */\n url: string;\n /** Whether the author reads (receives events) from this relay. */\n read: boolean;\n /** Whether the author writes (publishes events) to this relay. */\n write: boolean;\n}\n\n/**\n * Parse a NIP-65 kind-10002 relay-list event into structured {@link RelayEntry}\n * objects.\n *\n * Keeps tags where `tag[0] === 'r'` and `tag[1]` is a string URL. The optional\n * third element is the marker (`'read'`, `'write'`, or absent for both). Any\n * marker other than the two canonical strings is treated as \"unmarked\" (read +\n * write), matching how lenient NIP-65 consumers behave in practice.\n *\n * No URL normalization or hostname de-duplication is performed — the relay URL\n * is preserved verbatim. Normalize with a NIP-66 dataset (`@kehto/nip/66`) or a\n * dedicated relay-URL normalizer if you need canonical hostnames.\n *\n * @param event - A kind-10002 relay-list event (kind is not re-validated; pass the right event)\n * @returns Array of relay entries in tag order; duplicate URLs are preserved\n *\n * @example\n * ```ts\n * import { parseNip65RelayList } from '@kehto/nip/65';\n * const entries = parseNip65RelayList(kind10002Event);\n * // [{ url: 'wss://a', read: true, write: true }, { url: 'wss://b', read: true, write: false }]\n * ```\n */\nexport function parseNip65RelayList(event: NostrEvent): RelayEntry[] {\n return event.tags.flatMap((tag) => {\n if (tag[0] !== 'r' || typeof tag[1] !== 'string' || tag[1].length === 0) return [];\n const marker = tag[2];\n // Canonical markers are 'read' and 'write'; anything else (absent or a\n // typo) is treated as unmarked → both, so a listed relay is never silently\n // dropped from the outbox/inbox set.\n const read = marker !== 'write';\n const write = marker !== 'read';\n return [{ url: tag[1], read, write }];\n });\n}\n\n/**\n * Select the write (outbox) relay URLs from one author's parsed relay list,\n * de-duplicated and in first-seen order.\n *\n * @param entries - Parsed entries from {@link parseNip65RelayList}\n * @returns De-duplicated write-relay URLs\n *\n * @example\n * ```ts\n * const outbox = selectWriteRelays(parseNip65RelayList(event));\n * ```\n */\nexport function selectWriteRelays(entries: ReadonlyArray<RelayEntry>): string[] {\n return dedupe(entries.filter((e) => e.write).map((e) => e.url));\n}\n\n/**\n * Select the read (inbox) relay URLs from one author's parsed relay list,\n * de-duplicated and in first-seen order.\n *\n * @param entries - Parsed entries from {@link parseNip65RelayList}\n * @returns De-duplicated read-relay URLs\n */\nexport function selectReadRelays(entries: ReadonlyArray<RelayEntry>): string[] {\n return dedupe(entries.filter((e) => e.read).map((e) => e.url));\n}\n\n/**\n * A closure-scoped registry of parsed NIP-65 relay lists keyed by author\n * pubkey. Returned by {@link createNip65Registry}. Each registry owns its own\n * `Map` — two registries never share state (unlike a module-globals pattern).\n *\n * @example\n * ```ts\n * const registry = createNip65Registry();\n * registry.ingest(event); // store by event.pubkey\n * const outbox = registry.resolveOutboxRelays([authorA, authorB]);\n * ```\n */\nexport interface Nip65Registry {\n /**\n * Parse and store a kind-10002 event under its own `pubkey`. Overwrites any\n * previously stored list for that author (kind-10002 is replaceable).\n *\n * @param event - A kind-10002 relay-list event\n * @returns The parsed entries that were stored\n */\n ingest(event: NostrEvent): RelayEntry[];\n /**\n * Store an already-parsed relay list for an explicit pubkey. Use when you\n * parsed elsewhere or want to seed the registry without an event object.\n *\n * @param pubkey - Author hex public key\n * @param entries - Parsed relay entries\n */\n setRelayList(pubkey: string, entries: ReadonlyArray<RelayEntry>): void;\n /**\n * Get the stored relay list for an author, or `undefined` if none seen.\n *\n * @param pubkey - Author hex public key\n */\n getRelayList(pubkey: string): RelayEntry[] | undefined;\n /** Whether a relay list has been stored for `pubkey`. */\n has(pubkey: string): boolean;\n /**\n * Resolve write (outbox) relay URLs across the given authors, de-duplicated.\n * Authors with no stored list contribute nothing. Empty input or all-unknown\n * authors yield `[]`.\n *\n * @param pubkeys - Author hex public keys\n * @returns De-duplicated outbox relay URLs in first-seen order\n */\n resolveOutboxRelays(pubkeys: ReadonlyArray<string>): string[];\n /**\n * Resolve read (inbox) relay URLs across the given authors, de-duplicated.\n *\n * @param pubkeys - Author hex public keys\n * @returns De-duplicated inbox relay URLs in first-seen order\n */\n resolveReadRelays(pubkeys: ReadonlyArray<string>): string[];\n /** Drop a single author's stored relay list. Returns `true` if one existed. */\n delete(pubkey: string): boolean;\n /** Clear every stored relay list. */\n clear(): void;\n}\n\n/**\n * Create a closure-scoped {@link Nip65Registry} for accumulating kind-10002\n * relay lists and resolving outbox/inbox relays.\n *\n * Framework-agnostic: it knows nothing about relay pools, subscriptions, or\n * storage. Feed it kind-10002 events (e.g. from your pool's `onevent`\n * callback) via {@link Nip65Registry.ingest} and query the outbox model with\n * {@link Nip65Registry.resolveOutboxRelays}.\n *\n * @returns A fresh registry with its own state\n *\n * @example\n * ```ts\n * import { createNip65Registry } from '@kehto/nip/65';\n * import { SimplePool } from 'nostr-tools/pool';\n *\n * const registry = createNip65Registry();\n * const pool = new SimplePool();\n * pool.subscribeMany(discoveryRelays, [{ kinds: [10002], authors }], {\n * onevent: (e) => registry.ingest(e),\n * });\n * // Later, before publishing on behalf of `author`:\n * const outbox = registry.resolveOutboxRelays([author]);\n * ```\n */\nexport function createNip65Registry(): Nip65Registry {\n const lists = new Map<string, RelayEntry[]>();\n\n function setRelayList(pubkey: string, entries: ReadonlyArray<RelayEntry>): void {\n lists.set(pubkey, entries.map((e) => ({ ...e })));\n }\n\n function ingest(event: NostrEvent): RelayEntry[] {\n const entries = parseNip65RelayList(event);\n setRelayList(event.pubkey, entries);\n return entries;\n }\n\n function getRelayList(pubkey: string): RelayEntry[] | undefined {\n const entries = lists.get(pubkey);\n return entries ? entries.map((e) => ({ ...e })) : undefined;\n }\n\n function resolve(pubkeys: ReadonlyArray<string>, want: 'read' | 'write'): string[] {\n const urls: string[] = [];\n const seen = new Set<string>();\n for (const pubkey of pubkeys) {\n const entries = lists.get(pubkey);\n if (!entries) continue;\n for (const entry of entries) {\n if (entry[want] && !seen.has(entry.url)) {\n seen.add(entry.url);\n urls.push(entry.url);\n }\n }\n }\n return urls;\n }\n\n return {\n ingest,\n setRelayList,\n getRelayList,\n has: (pubkey) => lists.has(pubkey),\n resolveOutboxRelays: (pubkeys) => resolve(pubkeys, 'write'),\n resolveReadRelays: (pubkeys) => resolve(pubkeys, 'read'),\n delete: (pubkey) => lists.delete(pubkey),\n clear: () => lists.clear(),\n };\n}\n\nfunction dedupe(urls: ReadonlyArray<string>): string[] {\n const seen = new Set<string>();\n const out: string[] = [];\n for (const url of urls) {\n if (!seen.has(url)) {\n seen.add(url);\n out.push(url);\n }\n }\n return out;\n}\n"],"mappings":";AAkEO,SAAS,oBAAoB,OAAiC;AACnE,SAAO,MAAM,KAAK,QAAQ,CAAC,QAAQ;AACjC,QAAI,IAAI,CAAC,MAAM,OAAO,OAAO,IAAI,CAAC,MAAM,YAAY,IAAI,CAAC,EAAE,WAAW,EAAG,QAAO,CAAC;AACjF,UAAM,SAAS,IAAI,CAAC;AAIpB,UAAM,OAAO,WAAW;AACxB,UAAM,QAAQ,WAAW;AACzB,WAAO,CAAC,EAAE,KAAK,IAAI,CAAC,GAAG,MAAM,MAAM,CAAC;AAAA,EACtC,CAAC;AACH;AAcO,SAAS,kBAAkB,SAA8C;AAC9E,SAAO,OAAO,QAAQ,OAAO,CAAC,MAAM,EAAE,KAAK,EAAE,IAAI,CAAC,MAAM,EAAE,GAAG,CAAC;AAChE;AASO,SAAS,iBAAiB,SAA8C;AAC7E,SAAO,OAAO,QAAQ,OAAO,CAAC,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,MAAM,EAAE,GAAG,CAAC;AAC/D;AAsFO,SAAS,sBAAqC;AACnD,QAAM,QAAQ,oBAAI,IAA0B;AAE5C,WAAS,aAAa,QAAgB,SAA0C;AAC9E,UAAM,IAAI,QAAQ,QAAQ,IAAI,CAAC,OAAO,EAAE,GAAG,EAAE,EAAE,CAAC;AAAA,EAClD;AAEA,WAAS,OAAO,OAAiC;AAC/C,UAAM,UAAU,oBAAoB,KAAK;AACzC,iBAAa,MAAM,QAAQ,OAAO;AAClC,WAAO;AAAA,EACT;AAEA,WAAS,aAAa,QAA0C;AAC9D,UAAM,UAAU,MAAM,IAAI,MAAM;AAChC,WAAO,UAAU,QAAQ,IAAI,CAAC,OAAO,EAAE,GAAG,EAAE,EAAE,IAAI;AAAA,EACpD;AAEA,WAAS,QAAQ,SAAgC,MAAkC;AACjF,UAAM,OAAiB,CAAC;AACxB,UAAM,OAAO,oBAAI,IAAY;AAC7B,eAAW,UAAU,SAAS;AAC5B,YAAM,UAAU,MAAM,IAAI,MAAM;AAChC,UAAI,CAAC,QAAS;AACd,iBAAW,SAAS,SAAS;AAC3B,YAAI,MAAM,IAAI,KAAK,CAAC,KAAK,IAAI,MAAM,GAAG,GAAG;AACvC,eAAK,IAAI,MAAM,GAAG;AAClB,eAAK,KAAK,MAAM,GAAG;AAAA,QACrB;AAAA,MACF;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA,KAAK,CAAC,WAAW,MAAM,IAAI,MAAM;AAAA,IACjC,qBAAqB,CAAC,YAAY,QAAQ,SAAS,OAAO;AAAA,IAC1D,mBAAmB,CAAC,YAAY,QAAQ,SAAS,MAAM;AAAA,IACvD,QAAQ,CAAC,WAAW,MAAM,OAAO,MAAM;AAAA,IACvC,OAAO,MAAM,MAAM,MAAM;AAAA,EAC3B;AACF;AAEA,SAAS,OAAO,MAAuC;AACrD,QAAM,OAAO,oBAAI,IAAY;AAC7B,QAAM,MAAgB,CAAC;AACvB,aAAW,OAAO,MAAM;AACtB,QAAI,CAAC,KAAK,IAAI,GAAG,GAAG;AAClB,WAAK,IAAI,GAAG;AACZ,UAAI,KAAK,GAAG;AAAA,IACd;AAAA,EACF;AACA,SAAO;AACT;","names":[]}
package/dist/index.js CHANGED
@@ -8,7 +8,7 @@ import {
8
8
  isSetKind,
9
9
  listKindName,
10
10
  parseList
11
- } from "./chunk-2XYRP2S6.js";
11
+ } from "./chunk-NDOAX5PE.js";
12
12
  import {
13
13
  CacheStorageNappletArtifactCache,
14
14
  NAPPLET_ARTIFACT_CACHE_NAME,
@@ -26,23 +26,23 @@ import {
26
26
  resolveNapplet,
27
27
  verifyBlobHash,
28
28
  verifyManifestSignature
29
- } from "./chunk-CDBUCFLP.js";
29
+ } from "./chunk-NRUVARZL.js";
30
30
  import {
31
31
  aggregateTagValue,
32
32
  computeAggregateHash,
33
33
  pathEntriesFromTags,
34
34
  verifyAggregate
35
- } from "./chunk-2J3CSGKL.js";
35
+ } from "./chunk-JT5WCCCX.js";
36
36
  import {
37
37
  createNip65Registry,
38
38
  parseNip65RelayList,
39
39
  selectReadRelays,
40
40
  selectWriteRelays
41
- } from "./chunk-UUMBCHTA.js";
41
+ } from "./chunk-PSENX3MS.js";
42
42
  import {
43
43
  DEFAULT_RELAY_ATTRIBUTE_GROUPS,
44
44
  createNip66Aggregator
45
- } from "./chunk-DADZ6NUI.js";
45
+ } from "./chunk-PEKQVWZY.js";
46
46
  import {
47
47
  HANDLER_INFORMATION_KIND,
48
48
  HANDLER_PLATFORMS,
@@ -51,7 +51,7 @@ import {
51
51
  handlesKind,
52
52
  parseHandlerInformation,
53
53
  parseHandlerRecommendation
54
- } from "./chunk-BCTIL523.js";
54
+ } from "./chunk-7X33QYEY.js";
55
55
  export {
56
56
  CacheStorageNappletArtifactCache,
57
57
  DEFAULT_RELAY_ATTRIBUTE_GROUPS,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kehto/nip",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "description": "Tree-shakable bundle of unique Nostr NIP utilities for napplet runtimes — each NIP at its own subpath (e.g. @kehto/nip/66)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/5a/index.ts"],"sourcesContent":["import { sha256 } from '@noble/hashes/sha2.js';\nimport { bytesToHex } from '@noble/hashes/utils.js';\n\n/**\n * `@kehto/nip/5a` — NIP-5A \"Aggregate Hash\" computation and verification.\n *\n * NIP-5A (`nostr-protocol/nips` PR #2287) defines a content address for a set\n * of published files. Each file is declared by a `path` tag\n * (`[\"path\", \"<abs-path>\", \"<sha256>\"]`) and the set is summarized by a single\n * aggregate `x` tag (`[\"x\", \"<hex>\", \"aggregate\"]`).\n *\n * The aggregate is computed by:\n * 1. taking every `path` entry as the line `\"<sha256> <abs-path>\\n\"`,\n * 2. sorting the lines in ascending lexicographic order,\n * 3. concatenating them as UTF-8 bytes,\n * 4. hashing with SHA-256, encoded as lowercase hex.\n */\n\n// TextEncoder is available in all JS runtimes but not in the ES2022 lib types.\ndeclare class TextEncoder { encode(input: string): Uint8Array; }\n\n/**\n * One published file: an absolute path and the lowercase-hex SHA-256 of its\n * bytes. Parsed from a NIP-5A `[\"path\", \"<abs-path>\", \"<sha256>\"]` tag.\n *\n * @example\n * ```ts\n * const entry: PathEntry = { path: '/index.html', sha256: '186ea5…1c99' };\n * ```\n */\nexport interface PathEntry {\n /** Absolute path of the file (the `path` tag's second element). */\n path: string;\n /** Lowercase-hex SHA-256 of the file bytes (the `path` tag's third element). */\n sha256: string;\n}\n\n/**\n * Compute the NIP-5A aggregate hash over a set of {@link PathEntry} files.\n *\n * Lines (`\"<sha256> <abs-path>\\n\"`) are sorted before hashing, so the result is\n * independent of the input order.\n *\n * @param entries - The file path entries (any order)\n * @returns The aggregate hash as lowercase hex\n *\n * @example\n * ```ts\n * import { computeAggregateHash } from '@kehto/nip/5a';\n * const hex = computeAggregateHash([{ path: '/index.html', sha256: 'aa…' }]);\n * ```\n */\nexport function computeAggregateHash(entries: readonly PathEntry[]): string {\n const lines = entries.map((entry) => `${entry.sha256} ${entry.path}\\n`);\n lines.sort();\n return bytesToHex(sha256(new TextEncoder().encode(lines.join(''))));\n}\n\n/**\n * Extract {@link PathEntry} objects from an event's tags, keeping only\n * well-formed `[\"path\", \"<abs-path>\", \"<sha256>\"]` tags in tag order.\n *\n * @param tags - The manifest event's tags\n * @returns Parsed path entries (malformed `path` tags are skipped)\n */\nexport function pathEntriesFromTags(tags: readonly (readonly string[])[]): PathEntry[] {\n const entries: PathEntry[] = [];\n for (const tag of tags) {\n if (tag[0] === 'path' && typeof tag[1] === 'string' && typeof tag[2] === 'string'\n && tag[1].length > 0 && tag[2].length > 0) {\n entries.push({ path: tag[1], sha256: tag[2] });\n }\n }\n return entries;\n}\n\n/**\n * Read the aggregate hash declared by an event's `[\"x\", \"<hex>\", \"aggregate\"]`\n * tag.\n *\n * @param tags - The manifest event's tags\n * @returns The declared aggregate hex, or `undefined` if no aggregate `x` tag\n */\nexport function aggregateTagValue(tags: readonly (readonly string[])[]): string | undefined {\n for (const tag of tags) {\n if (tag[0] === 'x' && tag[2] === 'aggregate' && typeof tag[1] === 'string' && tag[1].length > 0) {\n return tag[1];\n }\n }\n return undefined;\n}\n\n/**\n * Verify that an event's declared aggregate `x` tag equals the aggregate\n * recomputed from its `path` tags.\n *\n * @param tags - The manifest event's tags\n * @returns `true` only when an aggregate `x` tag is present and matches the\n * recomputed value; `false` otherwise (mismatch or missing aggregate tag)\n *\n * @example\n * ```ts\n * import { verifyAggregate } from '@kehto/nip/5a';\n * if (!verifyAggregate(manifestEvent.tags)) throw new Error('aggregate mismatch');\n * ```\n */\nexport function verifyAggregate(tags: readonly (readonly string[])[]): boolean {\n const declared = aggregateTagValue(tags);\n if (!declared) return false;\n return computeAggregateHash(pathEntriesFromTags(tags)) === declared;\n}\n"],"mappings":";AAAA,SAAS,cAAc;AACvB,SAAS,kBAAkB;AAmDpB,SAAS,qBAAqB,SAAuC;AAC1E,QAAM,QAAQ,QAAQ,IAAI,CAAC,UAAU,GAAG,MAAM,MAAM,IAAI,MAAM,IAAI;AAAA,CAAI;AACtE,QAAM,KAAK;AACX,SAAO,WAAW,OAAO,IAAI,YAAY,EAAE,OAAO,MAAM,KAAK,EAAE,CAAC,CAAC,CAAC;AACpE;AASO,SAAS,oBAAoB,MAAmD;AACrF,QAAM,UAAuB,CAAC;AAC9B,aAAW,OAAO,MAAM;AACtB,QAAI,IAAI,CAAC,MAAM,UAAU,OAAO,IAAI,CAAC,MAAM,YAAY,OAAO,IAAI,CAAC,MAAM,YACpE,IAAI,CAAC,EAAE,SAAS,KAAK,IAAI,CAAC,EAAE,SAAS,GAAG;AAC3C,cAAQ,KAAK,EAAE,MAAM,IAAI,CAAC,GAAG,QAAQ,IAAI,CAAC,EAAE,CAAC;AAAA,IAC/C;AAAA,EACF;AACA,SAAO;AACT;AASO,SAAS,kBAAkB,MAA0D;AAC1F,aAAW,OAAO,MAAM;AACtB,QAAI,IAAI,CAAC,MAAM,OAAO,IAAI,CAAC,MAAM,eAAe,OAAO,IAAI,CAAC,MAAM,YAAY,IAAI,CAAC,EAAE,SAAS,GAAG;AAC/F,aAAO,IAAI,CAAC;AAAA,IACd;AAAA,EACF;AACA,SAAO;AACT;AAgBO,SAAS,gBAAgB,MAA+C;AAC7E,QAAM,WAAW,kBAAkB,IAAI;AACvC,MAAI,CAAC,SAAU,QAAO;AACtB,SAAO,qBAAqB,oBAAoB,IAAI,CAAC,MAAM;AAC7D;","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/51/index.ts"],"sourcesContent":["import type { NostrEvent } from 'nostr-tools';\n\n/**\n * `@kehto/nip/51` — NIP-51 list & set parsing.\n *\n * NIP-51 organizes user data into two families of events whose items live in\n * tags (public) and, optionally, in NIP-44-encrypted content (private):\n *\n * - **Standard lists** — replaceable, kinds `10000`–`10101`. One per user per\n * kind (e.g. mute list, bookmarks, blocked relays, DM relays).\n * - **Sets** — addressable / parameterized-replaceable, kinds `30000`–`30030`.\n * Many per user, distinguished by a `d` identifier (e.g. follow sets, relay\n * sets, emoji sets, curation sets).\n *\n * `nostr-tools` ships no NIP-51 helper. This module provides a single\n * widely-compatible parser that turns either family into a structured\n * {@link Nip51List}, plus item accessors and an injected-decryptor path for\n * private items (so the package stays crypto-free and framework-agnostic).\n */\n\n/** Canonical NIP-51 **standard list** kinds (replaceable, one per author). */\nexport const LIST_KINDS: Readonly<Record<number, string>> = {\n 10000: 'mute',\n 10001: 'pinned-notes',\n 10003: 'bookmarks',\n 10004: 'communities',\n 10005: 'public-chats',\n 10006: 'blocked-relays',\n 10007: 'search-relays',\n 10009: 'simple-groups',\n 10012: 'relay-feeds',\n 10015: 'interests',\n 10020: 'media-follows',\n 10030: 'emojis',\n 10050: 'dm-relays',\n 10101: 'good-wiki-authors',\n 10102: 'good-wiki-relays',\n};\n\n/** Canonical NIP-51 **set** kinds (addressable, many per author by `d`). */\nexport const SET_KINDS: Readonly<Record<number, string>> = {\n 30000: 'follow-sets',\n 30002: 'relay-sets',\n 30003: 'bookmark-sets',\n 30004: 'curation-sets',\n 30005: 'video-curation-sets',\n 30007: 'kind-mute-sets',\n 30015: 'interest-sets',\n 30019: 'release-sets',\n 30020: 'release-artifact-sets',\n 30030: 'emoji-sets',\n 30063: 'release-artifact-sets',\n 30267: 'app-curation-sets',\n};\n\n/**\n * Item-bearing NIP-51 tag names — the tags that carry list *content* (as\n * opposed to metadata tags like `d`, `title`, `image`, `description`).\n */\nexport const ITEM_TAGS = ['e', 'p', 'a', 't', 'r', 'word', 'emoji', 'relay', 'g', 'i', 'k'] as const;\n\n/** Whether `kind` is a NIP-51 standard list kind (`10000`–`10101`). */\nexport function isListKind(kind: number): boolean {\n return Object.prototype.hasOwnProperty.call(LIST_KINDS, kind);\n}\n\n/** Whether `kind` is a NIP-51 set kind (`30000`–`30030`). */\nexport function isSetKind(kind: number): boolean {\n return Object.prototype.hasOwnProperty.call(SET_KINDS, kind);\n}\n\n/**\n * Friendly name for a NIP-51 list/set kind (e.g. `10000` → `'mute'`,\n * `30030` → `'emoji-sets'`), or `undefined` if `kind` is not a known NIP-51 kind.\n */\nexport function listKindName(kind: number): string | undefined {\n return LIST_KINDS[kind] ?? SET_KINDS[kind];\n}\n\n/**\n * A parsed NIP-51 list or set.\n *\n * `publicItems` holds the item-bearing tags verbatim. `privateItems` is\n * populated only after {@link decryptPrivateItems} runs; until then it is\n * `undefined` (the encrypted payload is left untouched in {@link Nip51List.encryptedContent}).\n *\n * @example\n * ```ts\n * const list = parseList(kind10000MuteEvent);\n * list.kind; // 10000\n * list.type; // 'mute'\n * list.family; // 'list'\n * getTagValues(list.publicItems, 'p'); // muted pubkeys (public portion)\n * ```\n */\nexport interface Nip51List {\n /** Event kind. */\n kind: number;\n /** `'list'` for standard lists (10000–10101), `'set'` for sets (30000–30030), `'unknown'` otherwise. */\n family: 'list' | 'set' | 'unknown';\n /** Friendly kind name from {@link listKindName}, or `undefined` for unknown kinds. */\n type: string | undefined;\n /** Author pubkey. */\n pubkey: string;\n /** Set identifier from the `d` tag (sets only; `''` for standard lists / missing). */\n identifier: string;\n /** Addressable coordinate `kind:pubkey:d` for sets; `undefined` for standard lists. */\n address: string | undefined;\n /** Optional `title` tag (sets). */\n title: string | undefined;\n /** Optional `image` tag (sets). */\n image: string | undefined;\n /** Optional `description` tag (sets). */\n description: string | undefined;\n /** Item-bearing tags verbatim (the public portion of the list). */\n publicItems: string[][];\n /** Raw NIP-44-encrypted content, if any (the private portion). */\n encryptedContent: string | undefined;\n /** Decrypted private item tags — `undefined` until {@link decryptPrivateItems} succeeds. */\n privateItems: string[][] | undefined;\n}\n\nconst META_TAGS = new Set(['d', 'title', 'image', 'description', 'name', 'summary', 'alt']);\n\n/**\n * Parse a NIP-51 list or set event into a structured {@link Nip51List}.\n *\n * Works for any kind: known list/set kinds get a `type` and correct `family`;\n * unknown kinds still parse (`family: 'unknown'`) so forward-compatible callers\n * can handle new list kinds before this table is updated.\n *\n * Item-bearing tags (see {@link ITEM_TAGS}) become {@link Nip51List.publicItems};\n * metadata tags (`d`, `title`, `image`, `description`, …) are lifted into named\n * fields. The encrypted `content` is preserved verbatim in\n * {@link Nip51List.encryptedContent} — call {@link decryptPrivateItems} with a\n * NIP-44 decryptor to populate {@link Nip51List.privateItems}.\n *\n * @param event - A NIP-51 list or set event\n * @returns The structured list/set\n *\n * @example\n * ```ts\n * import { parseList, getTagValues } from '@kehto/nip/51';\n * const bookmarks = parseList(kind10003Event);\n * const notes = getTagValues(bookmarks.publicItems, 'e');\n * ```\n */\nexport function parseList(event: NostrEvent): Nip51List {\n const family: Nip51List['family'] = isListKind(event.kind)\n ? 'list'\n : isSetKind(event.kind)\n ? 'set'\n : 'unknown';\n\n const itemTagSet = new Set<string>(ITEM_TAGS);\n const publicItems: string[][] = [];\n let identifier = '';\n let title: string | undefined;\n let image: string | undefined;\n let description: string | undefined;\n\n for (const tag of event.tags) {\n const name = tag[0];\n if (name === 'd') identifier = tag[1] ?? '';\n else if (name === 'title') title = tag[1];\n else if (name === 'image') image = tag[1];\n else if (name === 'description') description = tag[1];\n else if (itemTagSet.has(name) && !META_TAGS.has(name)) publicItems.push([...tag]);\n }\n\n const isSet = family === 'set';\n const address = isSet ? `${event.kind}:${event.pubkey}:${identifier}` : undefined;\n const encryptedContent = event.content && event.content.length > 0 ? event.content : undefined;\n\n return {\n kind: event.kind,\n family,\n type: listKindName(event.kind),\n pubkey: event.pubkey,\n identifier,\n address,\n title,\n image,\n description,\n publicItems,\n encryptedContent,\n privateItems: undefined,\n };\n}\n\n/**\n * Collect the values (`tag[1]`) of every item tag with the given name.\n *\n * @param items - Item tags ({@link Nip51List.publicItems} or {@link Nip51List.privateItems})\n * @param tagName - Tag name to filter on (e.g. `'p'`, `'e'`, `'a'`, `'t'`, `'word'`)\n * @returns The second element of each matching tag (missing values skipped)\n *\n * @example\n * ```ts\n * const mutedPubkeys = getTagValues(list.publicItems, 'p');\n * const mutedWords = getTagValues(list.publicItems, 'word');\n * ```\n */\nexport function getTagValues(items: ReadonlyArray<ReadonlyArray<string>>, tagName: string): string[] {\n const out: string[] = [];\n for (const tag of items) {\n if (tag[0] === tagName && typeof tag[1] === 'string') out.push(tag[1]);\n }\n return out;\n}\n\n/**\n * A NIP-44 decryptor: takes the event's `content` and the author pubkey and\n * returns the plaintext (a JSON array of tags). Inject your signer's\n * `nip44.decrypt` (e.g. a NIP-07 `window.nostr.nip44`, a bunker, or\n * `nostr-tools/nip44`) so this package carries no crypto dependency.\n *\n * @example\n * ```ts\n * const decrypt: Nip44Decryptor = (content, author) =>\n * window.nostr!.nip44!.decrypt(author, content);\n * ```\n */\nexport type Nip44Decryptor = (content: string, authorPubkey: string) => string | Promise<string>;\n\n/**\n * Decrypt the private portion of a list/set and populate\n * {@link Nip51List.privateItems}.\n *\n * NIP-51 stores private items as a NIP-44-encrypted JSON array of tags in the\n * event `content`, encrypted to the author's own key. This helper runs the\n * injected {@link Nip44Decryptor}, parses the plaintext as `string[][]`, and\n * returns a **new** {@link Nip51List} with `privateItems` set (the input is not\n * mutated). Lists with no encrypted content return a copy with\n * `privateItems: []`.\n *\n * Throws if the decryptor throws or the plaintext is not a JSON tag array — so\n * callers can distinguish \"no private items\" from \"couldn't read private items\".\n *\n * @param list - A parsed list from {@link parseList}\n * @param decrypt - Injected NIP-44 decryptor\n * @returns A copy of `list` with `privateItems` populated\n *\n * @example\n * ```ts\n * const withPrivate = await decryptPrivateItems(list, (c, a) => signer.nip44.decrypt(a, c));\n * const privatelyMuted = getTagValues(withPrivate.privateItems!, 'p');\n * ```\n */\nexport async function decryptPrivateItems(list: Nip51List, decrypt: Nip44Decryptor): Promise<Nip51List> {\n if (!list.encryptedContent) {\n return { ...list, privateItems: [] };\n }\n const plaintext = await decrypt(list.encryptedContent, list.pubkey);\n const parsed: unknown = JSON.parse(plaintext);\n if (!Array.isArray(parsed) || !parsed.every((t) => Array.isArray(t) && t.every((s) => typeof s === 'string'))) {\n throw new Error('NIP-51: decrypted private items are not a string[][] tag array');\n }\n return { ...list, privateItems: parsed as string[][] };\n}\n"],"mappings":"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","names":[]}
@@ -1 +0,0 @@
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":[]}
@@ -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\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 * Archetype slugs this napplet fulfills, from `archetype` manifest tags; the\n * optional `nap` is the recommended default wire protocol (the 3rd tag element).\n */\n archetypes: Array<{ slug: string; nap?: 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; nap?: string }> {\n const out: Array<{ slug: string; nap?: string }> = [];\n for (const tag of tags) {\n if (tag[0] !== 'archetype') continue;\n if (typeof tag[1] !== 'string' || tag[1].length === 0) continue;\n out.push(\n typeof tag[2] === 'string' && tag[2].length > 0\n ? { slug: tag[1], nap: tag[2] }\n : { slug: tag[1] },\n );\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 return {\n kind: event.kind,\n pubkey: event.pubkey,\n dTag: firstTagValue(event.tags, 'd') ?? '',\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 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 message: string;\n cause?: unknown;\n}\n\nexport interface NappletBlobIndexEntry {\n size: number;\n lastAccessed: number;\n refCount: number;\n}\n\nexport interface NappletAggregateIndexEntry {\n dTag: string;\n aggregateHash: string;\n key: string;\n blobHashes: string[];\n size: number;\n lastAccessed: number;\n coordinateKey?: string;\n pinned?: boolean;\n}\n\nexport interface CachedCoordinate {\n key: string;\n kind: number;\n pubkey: string;\n dTag: string;\n aggregateHash: string;\n lastResolvedAt: number;\n foregroundTtlMs: number;\n backgroundTtlMs: number;\n}\n\nexport interface NappletArtifactCacheIndex {\n schemaVersion: number;\n updatedAt: number;\n blobs: Record<string, NappletBlobIndexEntry>;\n aggregates: Record<string, NappletAggregateIndexEntry>;\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 event: NostrEvent;\n manifest: NappletManifest;\n files: ReadonlyMap<string, Uint8Array>;\n indexHtml: string;\n now?: number;\n}\n\nexport interface CoordinateFreshnessOptions {\n now?: number;\n background?: boolean;\n}\n\nexport interface NappletArtifactCache {\n readBlob(sha256Hex: string): Promise<Uint8Array | undefined>;\n deleteBlob(sha256Hex: string): Promise<void>;\n writeVerifiedResolution(input: WriteVerifiedResolutionInput): Promise<void>;\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 getCoordinate(input: {\n kind: number;\n pubkey: string;\n dTag: string;\n }, options?: CoordinateFreshnessOptions): Promise<(CachedCoordinate & { fresh: boolean }) | undefined>;\n touchAggregate(dTag: string, aggregateHash: string, now?: number): Promise<void>;\n markAggregateActive(dTag: string, aggregateHash: string): void;\n releaseAggregateActive(dTag: string, aggregateHash: string): void;\n prune(now?: number): Promise<void>;\n}\n\nexport interface CacheStorageNappletArtifactCacheOptions {\n cacheStorage?: CacheStorage;\n storage?: StorageManager;\n cacheName?: string;\n baseUrl?: string;\n now?: () => number;\n onDiagnostic?: (diagnostic: NappletCacheDiagnostic) => void;\n unknownBudgetBytes?: number;\n softCeilingBytes?: number;\n hardCeilingBytes?: number;\n}\n\nexport interface OpenNappletArtifactCacheOptions extends CacheStorageNappletArtifactCacheOptions {\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\nexport function coordinateKey(kind: number, pubkey: string, dTag: string): string {\n return `${kind}:${pubkey}:${encodeURIComponent(dTag)}`;\n}\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\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 = 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 = 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\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":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/66/index.ts"],"sourcesContent":["import type { NostrEvent } from 'nostr-tools';\n\n/**\n * Minimal relay-pool contract `@kehto/nip/66` depends on. Consumers implement\n * this against their pool library of choice (nostr-tools SimplePool,\n * applesauce-relay, @snort/worker-relay, etc.). Intentionally smaller than\n * RxJS-style observable pools — one callback, one unsubscribe handle.\n *\n * @example\n * ```ts\n * import { SimplePool } from 'nostr-tools/pool';\n * const pool = new SimplePool();\n * const adapter: Nip66RelayPool = {\n * subscribe: (relays, filter, onEvent) => {\n * const sub = pool.subscribeMany(relays, [filter], { onevent: onEvent });\n * return () => sub.close();\n * },\n * };\n * ```\n */\nexport interface Nip66RelayPool {\n /**\n * Open a kind-30166 subscription against a set of monitor relays.\n *\n * @param relays - WebSocket URLs to subscribe against (the aggregator passes its bootstrap list)\n * @param filter - A kind-30166 filter, optionally narrowed with `#n` network tags\n * @param onEvent - Called synchronously with every matching event; the aggregator parses each one\n * @returns An unsubscribe handle the aggregator calls on `resync()` or teardown\n */\n subscribe(\n relays: ReadonlyArray<string>,\n filter: Nip66Filter,\n onEvent: (event: NostrEvent) => void,\n ): () => void;\n}\n\n/**\n * Minimal filter shape for kind-30166 subscriptions. `#n` optionally narrows\n * by network type (e.g. `['clearnet']`, `['clearnet', 'tor']`).\n *\n * @example\n * ```ts\n * const filter: Nip66Filter = { kinds: [30166], '#n': ['clearnet'] };\n * ```\n */\nexport type Nip66Filter = { kinds: [30166]; '#n'?: ReadonlyArray<string> };\n\n/**\n * Options for {@link createNip66Aggregator}.\n *\n * @example\n * ```ts\n * const options: Nip66AggregatorOptions = {\n * pool: myPoolAdapter,\n * bootstrap: ['wss://monitor1.example.com', 'wss://monitor2.example.com'],\n * networks: ['clearnet'],\n * };\n * ```\n */\nexport interface Nip66AggregatorOptions {\n /** A {@link Nip66RelayPool} adapter supplied by the consumer. */\n pool: Nip66RelayPool;\n /** Required non-empty list of monitor relay URLs to subscribe against. No default ships in the package. */\n bootstrap: ReadonlyArray<string>;\n /** Optional network-type narrowing passed to the filter as `#n` (e.g. `['clearnet', 'tor']`). Omit to receive every network. */\n networks?: ReadonlyArray<string>;\n /**\n * Optional named relay-attribute groups for interpreting ad hoc kind-30166 tags.\n * Omit to use {@link DEFAULT_RELAY_ATTRIBUTE_GROUPS}.\n */\n relayAttributeGroups?: RelayAttributeGroups;\n}\n\n/** A tag/value matcher for ad hoc relay attributes carried on kind-30166 events. */\nexport interface RelayAttributeMatcher {\n /** Tag name to match, for example `T`, `t`, or `k`. */\n tag: string;\n /** Optional accepted values. If omitted, any value for the tag matches. */\n values?: ReadonlyArray<string>;\n}\n\n/** Named groups of relay-attribute matchers. Matchers within a group are OR-ed. */\nexport type RelayAttributeGroups = Readonly<Record<string, ReadonlyArray<RelayAttributeMatcher>>>;\n\n/** Parsed attributes observed for one NIP-66 relay URL. */\nexport interface Nip66RelayAttributes {\n /** Raw tag copies from the latest observed kind-30166 event for the relay. */\n tags: ReadonlyArray<ReadonlyArray<string>>;\n /** Tag-value index, keyed by tag name. */\n values: ReadonlyMap<string, ReadonlySet<string>>;\n /** Parsed uppercase `N` tag support for convenience. */\n supportedNips: ReadonlySet<number>;\n}\n\nexport const DEFAULT_RELAY_ATTRIBUTE_GROUPS: RelayAttributeGroups = {\n Indexer: [\n { tag: 'T', values: ['Indexer', 'indexer'] },\n { tag: 't', values: ['Indexer', 'indexer'] },\n { tag: 'k', values: ['10002'] },\n ],\n RelayIndexer: [\n { tag: 'T', values: ['RelayIndexer', 'relay-indexer', 'relay_indexer'] },\n { tag: 't', values: ['RelayIndexer', 'relay-indexer', 'relay_indexer'] },\n { tag: 'k', values: ['30166', '10166'] },\n ],\n};\n\n/**\n * Aggregator handle returned by {@link createNip66Aggregator}. Each call\n * returns a fresh instance with its own closure-scoped state — multi-instance\n * safe (unlike a module-globals pattern).\n *\n * @example\n * ```ts\n * const aggregator = createNip66Aggregator({ pool, bootstrap: ['wss://monitor.example.com'] });\n * aggregator.start();\n * const relays = aggregator.getRelaySet();\n * ```\n */\nexport interface Nip66Aggregator {\n /** Begin subscribing against the bootstrap relay set. Idempotent — calling twice is a no-op. */\n start(): void;\n /** Tear down the current subscription, clear accumulated state, and re-subscribe. Call when `networks` semantics change upstream. */\n resync(): void;\n /**\n * Tear down the current subscription. Idempotent — calling without a prior\n * `start()` is a safe no-op. Preserves accumulated state (`getRelaySet`,\n * `relaySupportsNip`) — use `resync()` if you need to clear state AND\n * re-subscribe in one step. A subsequent `start()` re-subscribes against\n * the bootstrap relays (identical contract to the first start).\n *\n * Wire this to your consumer's teardown path (browser: `beforeunload`;\n * React: effect cleanup; Electron: `app.before-quit`) — the aggregator\n * holds a pool subscription that otherwise lives for the tab lifetime\n * (PITFALLS.md M-03).\n */\n stop(): void;\n /** The current set of relay URLs discovered from kind-30166 `d`-tags. Empty until `start()` fires at least one event. */\n getRelaySet(): ReadonlySet<string>;\n /**\n * Relay URLs whose `N`-tags declare support for the given NIP number.\n *\n * @param nip - NIP number to query support for (e.g. 44 for NIP-44 encrypted DMs)\n * @returns Array of relay URLs supporting that NIP\n */\n getRelaysSupportingNip(nip: number): string[];\n /**\n * Whether the relay at `url` has been seen with an `N`-tag declaring support for `nip`.\n *\n * @param url - Relay WebSocket URL to check\n * @param nip - NIP number to query support for\n * @returns `true` iff the relay has been observed with a matching `N`-tag\n */\n relaySupportsNip(url: string, nip: number): boolean;\n /**\n * Return parsed relay attributes for a relay URL, if that relay has been\n * observed in a kind-30166 event.\n */\n getRelayAttributes(url: string): Nip66RelayAttributes | undefined;\n /**\n * Return relays matching any supplied ad hoc attribute matcher.\n *\n * @param matchers - Tag/value matchers, OR semantics\n */\n getRelaysMatchingAttributes(matchers: ReadonlyArray<RelayAttributeMatcher>): string[];\n /**\n * Return relays in a named attribute group. Uses the group table passed to\n * `createNip66Aggregator`, or {@link DEFAULT_RELAY_ATTRIBUTE_GROUPS}.\n */\n getRelaysForAttributeGroup(groupName: string, groups?: RelayAttributeGroups): string[];\n}\n\n/**\n * Create a NIP-66 kind-30166 relay-discovery aggregator with closure-scoped state.\n *\n * Subscribes to kind-30166 events via the injected {@link Nip66RelayPool},\n * extracts relay URLs from `d`-tags, and parses `N`-tags to track which NIP\n * numbers each relay supports. Multi-instance safe — each factory call owns\n * its own `Set` + `Map`; multiple aggregators share nothing at module scope.\n *\n * The aggregator is framework-agnostic: it knows nothing about negentropy\n * (NIP-77), OPFS caching, worker relays, or network-type config stores.\n * Consumers inject those concerns through their {@link Nip66RelayPool}\n * implementation. `start()` is synchronous and idempotent — consumers\n * schedule their own cadence (delayed start, retry, etc.).\n *\n * @param options - Pool adapter + bootstrap monitor relays (+ optional network filter)\n * @returns A fresh {@link Nip66Aggregator} handle\n *\n * @example\n * ```ts\n * import { createNip66Aggregator } from '@kehto/nip/66';\n * import { SimplePool } from 'nostr-tools/pool';\n *\n * const pool = new SimplePool();\n * const aggregator = createNip66Aggregator({\n * pool: {\n * subscribe: (relays, filter, onEvent) => {\n * const sub = pool.subscribeMany(relays, [filter], { onevent: onEvent });\n * return () => sub.close();\n * },\n * },\n * bootstrap: ['wss://monitor1.example.com'],\n * });\n * aggregator.start();\n * // Later — inside a ShellAdapter hook:\n * const suggestions = Array.from(aggregator.getRelaySet());\n * ```\n */\nexport function createNip66Aggregator(options: Nip66AggregatorOptions): Nip66Aggregator {\n const relaySet = new Set<string>();\n const relaySupportedNips = new Map<string, Set<number>>();\n const relayAttributeTags = new Map<string, string[][]>();\n const relayAttributeValues = new Map<string, Map<string, Set<string>>>();\n const relayAttributeGroups = options.relayAttributeGroups ?? DEFAULT_RELAY_ATTRIBUTE_GROUPS;\n let unsubscribe: (() => void) | null = null;\n\n function buildFilter(): Nip66Filter {\n const filter: Nip66Filter = { kinds: [30166] };\n if (options.networks && options.networks.length > 0) {\n filter['#n'] = options.networks;\n }\n return filter;\n }\n\n function processEvent(event: NostrEvent): void {\n const dTag = event.tags.find((t) => t[0] === 'd');\n const relayUrl = dTag?.[1];\n if (!relayUrl) return;\n relaySet.add(relayUrl);\n captureRelayAttributes(relayUrl, event.tags);\n\n const nips = new Set<number>();\n for (const tag of event.tags) {\n if (tag[0] === 'N' && tag[1]) {\n const nipNum = parseInt(tag[1], 10);\n if (!Number.isNaN(nipNum)) nips.add(nipNum);\n }\n }\n if (nips.size > 0) relaySupportedNips.set(relayUrl, nips);\n }\n\n function captureRelayAttributes(relayUrl: string, tags: string[][]): void {\n const tagCopies = tags.map((tag) => [...tag]);\n const values = new Map<string, Set<string>>();\n for (const tag of tags) {\n const [name, value] = tag;\n if (!name || value === undefined) continue;\n let bucket = values.get(name);\n if (!bucket) {\n bucket = new Set<string>();\n values.set(name, bucket);\n }\n bucket.add(value);\n }\n relayAttributeTags.set(relayUrl, tagCopies);\n relayAttributeValues.set(relayUrl, values);\n }\n\n function start(): void {\n if (unsubscribe) return; // idempotent — already subscribed\n unsubscribe = options.pool.subscribe(options.bootstrap, buildFilter(), processEvent);\n }\n\n function resync(): void {\n if (unsubscribe) {\n unsubscribe();\n unsubscribe = null;\n }\n relaySet.clear();\n relaySupportedNips.clear();\n relayAttributeTags.clear();\n relayAttributeValues.clear();\n unsubscribe = options.pool.subscribe(options.bootstrap, buildFilter(), processEvent);\n }\n\n function stop(): void {\n if (unsubscribe) {\n unsubscribe();\n unsubscribe = null;\n }\n }\n\n function getRelaySet(): ReadonlySet<string> {\n return relaySet;\n }\n\n function getRelaysSupportingNip(nip: number): string[] {\n const result: string[] = [];\n for (const [url, nips] of relaySupportedNips) {\n if (nips.has(nip)) result.push(url);\n }\n return result;\n }\n\n function relaySupportsNip(url: string, nip: number): boolean {\n return relaySupportedNips.get(url)?.has(nip) ?? false;\n }\n\n function getRelayAttributes(url: string): Nip66RelayAttributes | undefined {\n const tagCopies = relayAttributeTags.get(url);\n const values = relayAttributeValues.get(url);\n if (!tagCopies || !values) return undefined;\n return {\n tags: tagCopies.map((tag) => [...tag]),\n values: new Map(Array.from(values, ([tag, tagValues]) => [tag, new Set(tagValues)])),\n supportedNips: new Set(relaySupportedNips.get(url) ?? []),\n };\n }\n\n function matcherMatches(url: string, matcher: RelayAttributeMatcher): boolean {\n const values = relayAttributeValues.get(url)?.get(matcher.tag);\n if (!values || values.size === 0) return false;\n if (!matcher.values || matcher.values.length === 0) return true;\n const accepted = new Set(matcher.values.map((value) => value.toLowerCase()));\n for (const value of values) {\n if (accepted.has(value.toLowerCase())) return true;\n }\n return false;\n }\n\n function getRelaysMatchingAttributes(matchers: ReadonlyArray<RelayAttributeMatcher>): string[] {\n const result: string[] = [];\n for (const url of relaySet) {\n if (matchers.some((matcher) => matcherMatches(url, matcher))) result.push(url);\n }\n return result;\n }\n\n function getRelaysForAttributeGroup(groupName: string, groups?: RelayAttributeGroups): string[] {\n const group = (groups ?? relayAttributeGroups)[groupName] ?? [];\n return getRelaysMatchingAttributes(group);\n }\n\n return {\n start,\n resync,\n stop,\n getRelaySet,\n getRelaysSupportingNip,\n relaySupportsNip,\n getRelayAttributes,\n getRelaysMatchingAttributes,\n getRelaysForAttributeGroup,\n };\n}\n"],"mappings":";AA8FO,IAAM,iCAAuD;AAAA,EAClE,SAAS;AAAA,IACP,EAAE,KAAK,KAAK,QAAQ,CAAC,WAAW,SAAS,EAAE;AAAA,IAC3C,EAAE,KAAK,KAAK,QAAQ,CAAC,WAAW,SAAS,EAAE;AAAA,IAC3C,EAAE,KAAK,KAAK,QAAQ,CAAC,OAAO,EAAE;AAAA,EAChC;AAAA,EACA,cAAc;AAAA,IACZ,EAAE,KAAK,KAAK,QAAQ,CAAC,gBAAgB,iBAAiB,eAAe,EAAE;AAAA,IACvE,EAAE,KAAK,KAAK,QAAQ,CAAC,gBAAgB,iBAAiB,eAAe,EAAE;AAAA,IACvE,EAAE,KAAK,KAAK,QAAQ,CAAC,SAAS,OAAO,EAAE;AAAA,EACzC;AACF;AAwGO,SAAS,sBAAsB,SAAkD;AACtF,QAAM,WAAW,oBAAI,IAAY;AACjC,QAAM,qBAAqB,oBAAI,IAAyB;AACxD,QAAM,qBAAqB,oBAAI,IAAwB;AACvD,QAAM,uBAAuB,oBAAI,IAAsC;AACvE,QAAM,uBAAuB,QAAQ,wBAAwB;AAC7D,MAAI,cAAmC;AAEvC,WAAS,cAA2B;AAClC,UAAM,SAAsB,EAAE,OAAO,CAAC,KAAK,EAAE;AAC7C,QAAI,QAAQ,YAAY,QAAQ,SAAS,SAAS,GAAG;AACnD,aAAO,IAAI,IAAI,QAAQ;AAAA,IACzB;AACA,WAAO;AAAA,EACT;AAEA,WAAS,aAAa,OAAyB;AAC7C,UAAM,OAAO,MAAM,KAAK,KAAK,CAAC,MAAM,EAAE,CAAC,MAAM,GAAG;AAChD,UAAM,WAAW,OAAO,CAAC;AACzB,QAAI,CAAC,SAAU;AACf,aAAS,IAAI,QAAQ;AACrB,2BAAuB,UAAU,MAAM,IAAI;AAE3C,UAAM,OAAO,oBAAI,IAAY;AAC7B,eAAW,OAAO,MAAM,MAAM;AAC5B,UAAI,IAAI,CAAC,MAAM,OAAO,IAAI,CAAC,GAAG;AAC5B,cAAM,SAAS,SAAS,IAAI,CAAC,GAAG,EAAE;AAClC,YAAI,CAAC,OAAO,MAAM,MAAM,EAAG,MAAK,IAAI,MAAM;AAAA,MAC5C;AAAA,IACF;AACA,QAAI,KAAK,OAAO,EAAG,oBAAmB,IAAI,UAAU,IAAI;AAAA,EAC1D;AAEA,WAAS,uBAAuB,UAAkB,MAAwB;AACxE,UAAM,YAAY,KAAK,IAAI,CAAC,QAAQ,CAAC,GAAG,GAAG,CAAC;AAC5C,UAAM,SAAS,oBAAI,IAAyB;AAC5C,eAAW,OAAO,MAAM;AACtB,YAAM,CAAC,MAAM,KAAK,IAAI;AACtB,UAAI,CAAC,QAAQ,UAAU,OAAW;AAClC,UAAI,SAAS,OAAO,IAAI,IAAI;AAC5B,UAAI,CAAC,QAAQ;AACX,iBAAS,oBAAI,IAAY;AACzB,eAAO,IAAI,MAAM,MAAM;AAAA,MACzB;AACA,aAAO,IAAI,KAAK;AAAA,IAClB;AACA,uBAAmB,IAAI,UAAU,SAAS;AAC1C,yBAAqB,IAAI,UAAU,MAAM;AAAA,EAC3C;AAEA,WAAS,QAAc;AACrB,QAAI,YAAa;AACjB,kBAAc,QAAQ,KAAK,UAAU,QAAQ,WAAW,YAAY,GAAG,YAAY;AAAA,EACrF;AAEA,WAAS,SAAe;AACtB,QAAI,aAAa;AACf,kBAAY;AACZ,oBAAc;AAAA,IAChB;AACA,aAAS,MAAM;AACf,uBAAmB,MAAM;AACzB,uBAAmB,MAAM;AACzB,yBAAqB,MAAM;AAC3B,kBAAc,QAAQ,KAAK,UAAU,QAAQ,WAAW,YAAY,GAAG,YAAY;AAAA,EACrF;AAEA,WAAS,OAAa;AACpB,QAAI,aAAa;AACf,kBAAY;AACZ,oBAAc;AAAA,IAChB;AAAA,EACF;AAEA,WAAS,cAAmC;AAC1C,WAAO;AAAA,EACT;AAEA,WAAS,uBAAuB,KAAuB;AACrD,UAAM,SAAmB,CAAC;AAC1B,eAAW,CAAC,KAAK,IAAI,KAAK,oBAAoB;AAC5C,UAAI,KAAK,IAAI,GAAG,EAAG,QAAO,KAAK,GAAG;AAAA,IACpC;AACA,WAAO;AAAA,EACT;AAEA,WAAS,iBAAiB,KAAa,KAAsB;AAC3D,WAAO,mBAAmB,IAAI,GAAG,GAAG,IAAI,GAAG,KAAK;AAAA,EAClD;AAEA,WAAS,mBAAmB,KAA+C;AACzE,UAAM,YAAY,mBAAmB,IAAI,GAAG;AAC5C,UAAM,SAAS,qBAAqB,IAAI,GAAG;AAC3C,QAAI,CAAC,aAAa,CAAC,OAAQ,QAAO;AAClC,WAAO;AAAA,MACL,MAAM,UAAU,IAAI,CAAC,QAAQ,CAAC,GAAG,GAAG,CAAC;AAAA,MACrC,QAAQ,IAAI,IAAI,MAAM,KAAK,QAAQ,CAAC,CAAC,KAAK,SAAS,MAAM,CAAC,KAAK,IAAI,IAAI,SAAS,CAAC,CAAC,CAAC;AAAA,MACnF,eAAe,IAAI,IAAI,mBAAmB,IAAI,GAAG,KAAK,CAAC,CAAC;AAAA,IAC1D;AAAA,EACF;AAEA,WAAS,eAAe,KAAa,SAAyC;AAC5E,UAAM,SAAS,qBAAqB,IAAI,GAAG,GAAG,IAAI,QAAQ,GAAG;AAC7D,QAAI,CAAC,UAAU,OAAO,SAAS,EAAG,QAAO;AACzC,QAAI,CAAC,QAAQ,UAAU,QAAQ,OAAO,WAAW,EAAG,QAAO;AAC3D,UAAM,WAAW,IAAI,IAAI,QAAQ,OAAO,IAAI,CAAC,UAAU,MAAM,YAAY,CAAC,CAAC;AAC3E,eAAW,SAAS,QAAQ;AAC1B,UAAI,SAAS,IAAI,MAAM,YAAY,CAAC,EAAG,QAAO;AAAA,IAChD;AACA,WAAO;AAAA,EACT;AAEA,WAAS,4BAA4B,UAA0D;AAC7F,UAAM,SAAmB,CAAC;AAC1B,eAAW,OAAO,UAAU;AAC1B,UAAI,SAAS,KAAK,CAAC,YAAY,eAAe,KAAK,OAAO,CAAC,EAAG,QAAO,KAAK,GAAG;AAAA,IAC/E;AACA,WAAO;AAAA,EACT;AAEA,WAAS,2BAA2B,WAAmB,QAAyC;AAC9F,UAAM,SAAS,UAAU,sBAAsB,SAAS,KAAK,CAAC;AAC9D,WAAO,4BAA4B,KAAK;AAAA,EAC1C;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/65/index.ts"],"sourcesContent":["import type { NostrEvent } from 'nostr-tools';\n\n/**\n * `@kehto/nip/65` — NIP-65 relay-list (kind 10002) parsing and outbox-model\n * relay resolution.\n *\n * NIP-65 defines kind-10002 replaceable events whose `r` tags declare which\n * relays an author reads from and writes to — the \"outbox model\" (a.k.a.\n * \"gossip\"). A client that wants to *reach* an author publishes to (or reads\n * from) that author's advertised relays instead of a fixed relay set.\n *\n * `nostr-tools` ships no NIP-65 helper, so this module fills the gap with a\n * pure parser plus a closure-scoped registry (no module-global state, so\n * multiple runtimes in one process never alias each other — same multi-instance\n * contract as `@kehto/nip/66`).\n *\n * Tag grammar:\n * ```text\n * [\"r\", \"wss://relay.example.com\"] → read AND write\n * [\"r\", \"wss://relay.example.com\", \"read\"] → read-only\n * [\"r\", \"wss://relay.example.com\", \"write\"] → write-only\n * ```\n */\n\n/**\n * A single relay entry parsed from a NIP-65 kind-10002 `r` tag.\n *\n * @example\n * ```ts\n * const entry: RelayEntry = { url: 'wss://relay.example.com', read: true, write: false };\n * ```\n */\nexport interface RelayEntry {\n /** Relay WebSocket URL from the `r` tag's second element. */\n url: string;\n /** Whether the author reads (receives events) from this relay. */\n read: boolean;\n /** Whether the author writes (publishes events) to this relay. */\n write: boolean;\n}\n\n/**\n * Parse a NIP-65 kind-10002 relay-list event into structured {@link RelayEntry}\n * objects.\n *\n * Keeps tags where `tag[0] === 'r'` and `tag[1]` is a string URL. The optional\n * third element is the marker (`'read'`, `'write'`, or absent for both). Any\n * marker other than the two canonical strings is treated as \"unmarked\" (read +\n * write), matching how lenient NIP-65 consumers behave in practice.\n *\n * No URL normalization or hostname de-duplication is performed — the relay URL\n * is preserved verbatim. Normalize with a NIP-66 dataset (`@kehto/nip/66`) or a\n * dedicated relay-URL normalizer if you need canonical hostnames.\n *\n * @param event - A kind-10002 relay-list event (kind is not re-validated; pass the right event)\n * @returns Array of relay entries in tag order; duplicate URLs are preserved\n *\n * @example\n * ```ts\n * import { parseNip65RelayList } from '@kehto/nip/65';\n * const entries = parseNip65RelayList(kind10002Event);\n * // [{ url: 'wss://a', read: true, write: true }, { url: 'wss://b', read: true, write: false }]\n * ```\n */\nexport function parseNip65RelayList(event: NostrEvent): RelayEntry[] {\n return event.tags.flatMap((tag) => {\n if (tag[0] !== 'r' || typeof tag[1] !== 'string' || tag[1].length === 0) return [];\n const marker = tag[2];\n // Canonical markers are 'read' and 'write'; anything else (absent or a\n // typo) is treated as unmarked → both, so a listed relay is never silently\n // dropped from the outbox/inbox set.\n const read = marker !== 'write';\n const write = marker !== 'read';\n return [{ url: tag[1], read, write }];\n });\n}\n\n/**\n * Select the write (outbox) relay URLs from one author's parsed relay list,\n * de-duplicated and in first-seen order.\n *\n * @param entries - Parsed entries from {@link parseNip65RelayList}\n * @returns De-duplicated write-relay URLs\n *\n * @example\n * ```ts\n * const outbox = selectWriteRelays(parseNip65RelayList(event));\n * ```\n */\nexport function selectWriteRelays(entries: ReadonlyArray<RelayEntry>): string[] {\n return dedupe(entries.filter((e) => e.write).map((e) => e.url));\n}\n\n/**\n * Select the read (inbox) relay URLs from one author's parsed relay list,\n * de-duplicated and in first-seen order.\n *\n * @param entries - Parsed entries from {@link parseNip65RelayList}\n * @returns De-duplicated read-relay URLs\n */\nexport function selectReadRelays(entries: ReadonlyArray<RelayEntry>): string[] {\n return dedupe(entries.filter((e) => e.read).map((e) => e.url));\n}\n\n/**\n * A closure-scoped registry of parsed NIP-65 relay lists keyed by author\n * pubkey. Returned by {@link createNip65Registry}. Each registry owns its own\n * `Map` — two registries never share state (unlike a module-globals pattern).\n *\n * @example\n * ```ts\n * const registry = createNip65Registry();\n * registry.ingest(event); // store by event.pubkey\n * const outbox = registry.resolveOutboxRelays([authorA, authorB]);\n * ```\n */\nexport interface Nip65Registry {\n /**\n * Parse and store a kind-10002 event under its own `pubkey`. Overwrites any\n * previously stored list for that author (kind-10002 is replaceable).\n *\n * @param event - A kind-10002 relay-list event\n * @returns The parsed entries that were stored\n */\n ingest(event: NostrEvent): RelayEntry[];\n /**\n * Store an already-parsed relay list for an explicit pubkey. Use when you\n * parsed elsewhere or want to seed the registry without an event object.\n *\n * @param pubkey - Author hex public key\n * @param entries - Parsed relay entries\n */\n setRelayList(pubkey: string, entries: ReadonlyArray<RelayEntry>): void;\n /**\n * Get the stored relay list for an author, or `undefined` if none seen.\n *\n * @param pubkey - Author hex public key\n */\n getRelayList(pubkey: string): RelayEntry[] | undefined;\n /** Whether a relay list has been stored for `pubkey`. */\n has(pubkey: string): boolean;\n /**\n * Resolve write (outbox) relay URLs across the given authors, de-duplicated.\n * Authors with no stored list contribute nothing. Empty input or all-unknown\n * authors yield `[]`.\n *\n * @param pubkeys - Author hex public keys\n * @returns De-duplicated outbox relay URLs in first-seen order\n */\n resolveOutboxRelays(pubkeys: ReadonlyArray<string>): string[];\n /**\n * Resolve read (inbox) relay URLs across the given authors, de-duplicated.\n *\n * @param pubkeys - Author hex public keys\n * @returns De-duplicated inbox relay URLs in first-seen order\n */\n resolveReadRelays(pubkeys: ReadonlyArray<string>): string[];\n /** Drop a single author's stored relay list. Returns `true` if one existed. */\n delete(pubkey: string): boolean;\n /** Clear every stored relay list. */\n clear(): void;\n}\n\n/**\n * Create a closure-scoped {@link Nip65Registry} for accumulating kind-10002\n * relay lists and resolving outbox/inbox relays.\n *\n * Framework-agnostic: it knows nothing about relay pools, subscriptions, or\n * storage. Feed it kind-10002 events (e.g. from your pool's `onevent`\n * callback) via {@link Nip65Registry.ingest} and query the outbox model with\n * {@link Nip65Registry.resolveOutboxRelays}.\n *\n * @returns A fresh registry with its own state\n *\n * @example\n * ```ts\n * import { createNip65Registry } from '@kehto/nip/65';\n * import { SimplePool } from 'nostr-tools/pool';\n *\n * const registry = createNip65Registry();\n * const pool = new SimplePool();\n * pool.subscribeMany(discoveryRelays, [{ kinds: [10002], authors }], {\n * onevent: (e) => registry.ingest(e),\n * });\n * // Later, before publishing on behalf of `author`:\n * const outbox = registry.resolveOutboxRelays([author]);\n * ```\n */\nexport function createNip65Registry(): Nip65Registry {\n const lists = new Map<string, RelayEntry[]>();\n\n function setRelayList(pubkey: string, entries: ReadonlyArray<RelayEntry>): void {\n lists.set(pubkey, entries.map((e) => ({ ...e })));\n }\n\n function ingest(event: NostrEvent): RelayEntry[] {\n const entries = parseNip65RelayList(event);\n setRelayList(event.pubkey, entries);\n return entries;\n }\n\n function getRelayList(pubkey: string): RelayEntry[] | undefined {\n const entries = lists.get(pubkey);\n return entries ? entries.map((e) => ({ ...e })) : undefined;\n }\n\n function resolve(pubkeys: ReadonlyArray<string>, want: 'read' | 'write'): string[] {\n const urls: string[] = [];\n const seen = new Set<string>();\n for (const pubkey of pubkeys) {\n const entries = lists.get(pubkey);\n if (!entries) continue;\n for (const entry of entries) {\n if (entry[want] && !seen.has(entry.url)) {\n seen.add(entry.url);\n urls.push(entry.url);\n }\n }\n }\n return urls;\n }\n\n return {\n ingest,\n setRelayList,\n getRelayList,\n has: (pubkey) => lists.has(pubkey),\n resolveOutboxRelays: (pubkeys) => resolve(pubkeys, 'write'),\n resolveReadRelays: (pubkeys) => resolve(pubkeys, 'read'),\n delete: (pubkey) => lists.delete(pubkey),\n clear: () => lists.clear(),\n };\n}\n\nfunction dedupe(urls: ReadonlyArray<string>): string[] {\n const seen = new Set<string>();\n const out: string[] = [];\n for (const url of urls) {\n if (!seen.has(url)) {\n seen.add(url);\n out.push(url);\n }\n }\n return out;\n}\n"],"mappings":";AAgEO,SAAS,oBAAoB,OAAiC;AACnE,SAAO,MAAM,KAAK,QAAQ,CAAC,QAAQ;AACjC,QAAI,IAAI,CAAC,MAAM,OAAO,OAAO,IAAI,CAAC,MAAM,YAAY,IAAI,CAAC,EAAE,WAAW,EAAG,QAAO,CAAC;AACjF,UAAM,SAAS,IAAI,CAAC;AAIpB,UAAM,OAAO,WAAW;AACxB,UAAM,QAAQ,WAAW;AACzB,WAAO,CAAC,EAAE,KAAK,IAAI,CAAC,GAAG,MAAM,MAAM,CAAC;AAAA,EACtC,CAAC;AACH;AAcO,SAAS,kBAAkB,SAA8C;AAC9E,SAAO,OAAO,QAAQ,OAAO,CAAC,MAAM,EAAE,KAAK,EAAE,IAAI,CAAC,MAAM,EAAE,GAAG,CAAC;AAChE;AASO,SAAS,iBAAiB,SAA8C;AAC7E,SAAO,OAAO,QAAQ,OAAO,CAAC,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,MAAM,EAAE,GAAG,CAAC;AAC/D;AAsFO,SAAS,sBAAqC;AACnD,QAAM,QAAQ,oBAAI,IAA0B;AAE5C,WAAS,aAAa,QAAgB,SAA0C;AAC9E,UAAM,IAAI,QAAQ,QAAQ,IAAI,CAAC,OAAO,EAAE,GAAG,EAAE,EAAE,CAAC;AAAA,EAClD;AAEA,WAAS,OAAO,OAAiC;AAC/C,UAAM,UAAU,oBAAoB,KAAK;AACzC,iBAAa,MAAM,QAAQ,OAAO;AAClC,WAAO;AAAA,EACT;AAEA,WAAS,aAAa,QAA0C;AAC9D,UAAM,UAAU,MAAM,IAAI,MAAM;AAChC,WAAO,UAAU,QAAQ,IAAI,CAAC,OAAO,EAAE,GAAG,EAAE,EAAE,IAAI;AAAA,EACpD;AAEA,WAAS,QAAQ,SAAgC,MAAkC;AACjF,UAAM,OAAiB,CAAC;AACxB,UAAM,OAAO,oBAAI,IAAY;AAC7B,eAAW,UAAU,SAAS;AAC5B,YAAM,UAAU,MAAM,IAAI,MAAM;AAChC,UAAI,CAAC,QAAS;AACd,iBAAW,SAAS,SAAS;AAC3B,YAAI,MAAM,IAAI,KAAK,CAAC,KAAK,IAAI,MAAM,GAAG,GAAG;AACvC,eAAK,IAAI,MAAM,GAAG;AAClB,eAAK,KAAK,MAAM,GAAG;AAAA,QACrB;AAAA,MACF;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA,KAAK,CAAC,WAAW,MAAM,IAAI,MAAM;AAAA,IACjC,qBAAqB,CAAC,YAAY,QAAQ,SAAS,OAAO;AAAA,IAC1D,mBAAmB,CAAC,YAAY,QAAQ,SAAS,MAAM;AAAA,IACvD,QAAQ,CAAC,WAAW,MAAM,OAAO,MAAM;AAAA,IACvC,OAAO,MAAM,MAAM,MAAM;AAAA,EAC3B;AACF;AAEA,SAAS,OAAO,MAAuC;AACrD,QAAM,OAAO,oBAAI,IAAY;AAC7B,QAAM,MAAgB,CAAC;AACvB,aAAW,OAAO,MAAM;AACtB,QAAI,CAAC,KAAK,IAAI,GAAG,GAAG;AAClB,WAAK,IAAI,GAAG;AACZ,UAAI,KAAK,GAAG;AAAA,IACd;AAAA,EACF;AACA,SAAO;AACT;","names":[]}