@kehto/nip 0.1.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,37 +1,73 @@
1
1
  # @kehto/nip
2
2
 
3
- A tree-shakable bundle of unique Nostr NIP utilities for napplet runtimes. Each
4
- NIP lives at its own subpath so consumers import only what they use; the package
5
- is `sideEffects: false`, so bundlers drop any NIP that isn't referenced.
3
+ A tree-shakable bundle of **unique** Nostr NIP utilities for napplet runtimes —
4
+ the NIPs that `nostr-tools` does *not* ship but that relay-aware runtimes
5
+ commonly need. Each NIP lives in its own folder at its own subpath, so consumers
6
+ import only what they use; the package is `sideEffects: false`, so bundlers drop
7
+ any NIP that isn't referenced.
6
8
 
7
9
  ## NIPs
8
10
 
9
11
  | Subpath | NIP | What it provides |
10
12
  |---------|-----|------------------|
11
- | `@kehto/nip/66` | NIP-66 | kind-30166 relay-discovery aggregator (`createNip66Aggregator`) |
13
+ | [`@kehto/nip/5a`](./src/5a/README.md) | NIP-5A | aggregate hash compute/verify over `path` tags (`computeAggregateHash`, `verifyAggregate`) |
14
+ | [`@kehto/nip/5d`](./src/5d/README.md) | NIP-5D | content-addressed napplet manifest resolution — parse, verify sig + aggregate + Blossom blobs (`resolveNapplet`) with optional Cache Storage artifact caching |
15
+ | [`@kehto/nip/51`](./src/51/README.md) | NIP-51 | lists & sets parser — mute/bookmarks/relay-sets/emoji-sets/… (`parseList`) |
16
+ | [`@kehto/nip/65`](./src/65/README.md) | NIP-65 | relay-list (kind 10002) parsing + outbox/inbox resolution (`createNip65Registry`) |
17
+ | [`@kehto/nip/66`](./src/66/README.md) | NIP-66 | kind-30166 relay-discovery aggregator (`createNip66Aggregator`) |
18
+ | [`@kehto/nip/89`](./src/89/README.md) | NIP-89 | app-handler discovery (kind 31989/31990) (`parseHandlerInformation`) |
12
19
 
20
+ Each subpath has its own README with full API docs and examples (linked above).
13
21
  More unique NIPs are added as their own subpath (`@kehto/nip/<n>`) over time.
14
22
 
23
+ For browser hosts that want repeated NIP-5D loads to reuse verified artifact
24
+ bytes, see the repo guide:
25
+ [Implement a napplet artifact cache](https://kehto.github.io/web/docs/how-tos/implement-napplet-artifact-cache).
26
+
27
+ ## Selection criteria
28
+
29
+ A NIP earns a place here only if it is:
30
+
31
+ 1. **Unique** — not already provided by `nostr-tools` (re-wrapping `nip04`,
32
+ `nip19`, `nip25`, `nip44`, `nip57`, … would be a no-op package).
33
+ 2. **Broadly needed** — likely required by many runtimes, not just one app.
34
+ 3. **Substantive** — handles a real common case in a widely-compatible way, with
35
+ no framework coupling and no module-global state.
36
+
15
37
  ## Usage
16
38
 
17
39
  ```ts
18
40
  // Import only the NIP you need — nothing else is bundled.
19
- import { createNip66Aggregator } from '@kehto/nip/66';
41
+ import { createNip65Registry } from '@kehto/nip/65';
20
42
 
21
- const aggregator = createNip66Aggregator({
22
- pool: myPoolAdapter,
23
- bootstrap: ['wss://monitor.example.com'],
24
- });
25
- aggregator.start();
43
+ const registry = createNip65Registry();
44
+ registry.ingest(kind10002Event);
45
+ const outbox = registry.resolveOutboxRelays([authorPubkey]);
26
46
  ```
27
47
 
28
48
  A convenience barrel re-export is also available at the package root
29
49
  (`@kehto/nip`), but prefer subpath imports for the smallest bundles.
30
50
 
51
+ ## Conventions
52
+
53
+ Every NIP module is:
54
+
55
+ - **Pure / framework-agnostic** — no Svelte/React, no DOM assumptions, no relay
56
+ library baked in. Relay pools, signers, and decryptors are injected via small
57
+ interfaces.
58
+ - **Multi-instance safe** — stateful helpers are closure-scoped factories
59
+ (`create*`), never module globals, so multiple runtimes in one process never
60
+ alias each other.
61
+ - **Fully documented** — all public exports carry JSDoc with `@param`,
62
+ `@returns`, and `@example`.
63
+
31
64
  ## Adding a NIP
32
65
 
33
- 1. Add `src/<n>.ts` implementing the NIP utility (framework-agnostic, no
34
- module-global state, inject relay/pool concerns via interfaces).
35
- 2. Add `src/<n>.ts` to the `tsup` `entry` array and the `./<n>` `exports` entry
36
- in `package.json`.
37
- 3. Re-export it from `src/index.ts` for the barrel.
66
+ 1. Create `src/<n>/index.ts` implementing the NIP utility (framework-agnostic, no
67
+ module-global state, inject relay/pool/crypto concerns via interfaces).
68
+ 2. Add `src/<n>/index.test.ts` with unit tests and `src/<n>/README.md` with API docs.
69
+ 3. Add `src/<n>/index.ts` to the `tsup` `entry` array and a `./<n>` entry to
70
+ `exports` in `package.json`.
71
+ 4. Re-export it from `src/index.ts` for the barrel, and add the
72
+ `@kehto/nip/<n>` alias to the repo `vitest.config.ts`.
73
+ 5. Add a row to the table above.
@@ -0,0 +1,157 @@
1
+ import { NostrEvent } from 'nostr-tools';
2
+
3
+ /**
4
+ * `@kehto/nip/51` — NIP-51 list & set parsing.
5
+ *
6
+ * NIP-51 organizes user data into two families of events whose items live in
7
+ * tags (public) and, optionally, in NIP-44-encrypted content (private):
8
+ *
9
+ * - **Standard lists** — replaceable, kinds `10000`–`10101`. One per user per
10
+ * kind (e.g. mute list, bookmarks, blocked relays, DM relays).
11
+ * - **Sets** — addressable / parameterized-replaceable, kinds `30000`–`30030`.
12
+ * Many per user, distinguished by a `d` identifier (e.g. follow sets, relay
13
+ * sets, emoji sets, curation sets).
14
+ *
15
+ * `nostr-tools` ships no NIP-51 helper. This module provides a single
16
+ * widely-compatible parser that turns either family into a structured
17
+ * {@link Nip51List}, plus item accessors and an injected-decryptor path for
18
+ * private items (so the package stays crypto-free and framework-agnostic).
19
+ */
20
+ /** Canonical NIP-51 **standard list** kinds (replaceable, one per author). */
21
+ declare const LIST_KINDS: Readonly<Record<number, string>>;
22
+ /** Canonical NIP-51 **set** kinds (addressable, many per author by `d`). */
23
+ declare const SET_KINDS: Readonly<Record<number, string>>;
24
+ /**
25
+ * Item-bearing NIP-51 tag names — the tags that carry list *content* (as
26
+ * opposed to metadata tags like `d`, `title`, `image`, `description`).
27
+ */
28
+ declare const ITEM_TAGS: readonly ["e", "p", "a", "t", "r", "word", "emoji", "relay", "g", "i", "k"];
29
+ /** Whether `kind` is a NIP-51 standard list kind (`10000`–`10101`). */
30
+ declare function isListKind(kind: number): boolean;
31
+ /** Whether `kind` is a NIP-51 set kind (`30000`–`30030`). */
32
+ declare function isSetKind(kind: number): boolean;
33
+ /**
34
+ * Friendly name for a NIP-51 list/set kind (e.g. `10000` → `'mute'`,
35
+ * `30030` → `'emoji-sets'`), or `undefined` if `kind` is not a known NIP-51 kind.
36
+ */
37
+ declare function listKindName(kind: number): string | undefined;
38
+ /**
39
+ * A parsed NIP-51 list or set.
40
+ *
41
+ * `publicItems` holds the item-bearing tags verbatim. `privateItems` is
42
+ * populated only after {@link decryptPrivateItems} runs; until then it is
43
+ * `undefined` (the encrypted payload is left untouched in {@link Nip51List.encryptedContent}).
44
+ *
45
+ * @example
46
+ * ```ts
47
+ * const list = parseList(kind10000MuteEvent);
48
+ * list.kind; // 10000
49
+ * list.type; // 'mute'
50
+ * list.family; // 'list'
51
+ * getTagValues(list.publicItems, 'p'); // muted pubkeys (public portion)
52
+ * ```
53
+ */
54
+ interface Nip51List {
55
+ /** Event kind. */
56
+ kind: number;
57
+ /** `'list'` for standard lists (10000–10101), `'set'` for sets (30000–30030), `'unknown'` otherwise. */
58
+ family: 'list' | 'set' | 'unknown';
59
+ /** Friendly kind name from {@link listKindName}, or `undefined` for unknown kinds. */
60
+ type: string | undefined;
61
+ /** Author pubkey. */
62
+ pubkey: string;
63
+ /** Set identifier from the `d` tag (sets only; `''` for standard lists / missing). */
64
+ identifier: string;
65
+ /** Addressable coordinate `kind:pubkey:d` for sets; `undefined` for standard lists. */
66
+ address: string | undefined;
67
+ /** Optional `title` tag (sets). */
68
+ title: string | undefined;
69
+ /** Optional `image` tag (sets). */
70
+ image: string | undefined;
71
+ /** Optional `description` tag (sets). */
72
+ description: string | undefined;
73
+ /** Item-bearing tags verbatim (the public portion of the list). */
74
+ publicItems: string[][];
75
+ /** Raw NIP-44-encrypted content, if any (the private portion). */
76
+ encryptedContent: string | undefined;
77
+ /** Decrypted private item tags — `undefined` until {@link decryptPrivateItems} succeeds. */
78
+ privateItems: string[][] | undefined;
79
+ }
80
+ /**
81
+ * Parse a NIP-51 list or set event into a structured {@link Nip51List}.
82
+ *
83
+ * Works for any kind: known list/set kinds get a `type` and correct `family`;
84
+ * unknown kinds still parse (`family: 'unknown'`) so forward-compatible callers
85
+ * can handle new list kinds before this table is updated.
86
+ *
87
+ * Item-bearing tags (see {@link ITEM_TAGS}) become {@link Nip51List.publicItems};
88
+ * metadata tags (`d`, `title`, `image`, `description`, …) are lifted into named
89
+ * fields. The encrypted `content` is preserved verbatim in
90
+ * {@link Nip51List.encryptedContent} — call {@link decryptPrivateItems} with a
91
+ * NIP-44 decryptor to populate {@link Nip51List.privateItems}.
92
+ *
93
+ * @param event - A NIP-51 list or set event
94
+ * @returns The structured list/set
95
+ *
96
+ * @example
97
+ * ```ts
98
+ * import { parseList, getTagValues } from '@kehto/nip/51';
99
+ * const bookmarks = parseList(kind10003Event);
100
+ * const notes = getTagValues(bookmarks.publicItems, 'e');
101
+ * ```
102
+ */
103
+ declare function parseList(event: NostrEvent): Nip51List;
104
+ /**
105
+ * Collect the values (`tag[1]`) of every item tag with the given name.
106
+ *
107
+ * @param items - Item tags ({@link Nip51List.publicItems} or {@link Nip51List.privateItems})
108
+ * @param tagName - Tag name to filter on (e.g. `'p'`, `'e'`, `'a'`, `'t'`, `'word'`)
109
+ * @returns The second element of each matching tag (missing values skipped)
110
+ *
111
+ * @example
112
+ * ```ts
113
+ * const mutedPubkeys = getTagValues(list.publicItems, 'p');
114
+ * const mutedWords = getTagValues(list.publicItems, 'word');
115
+ * ```
116
+ */
117
+ declare function getTagValues(items: ReadonlyArray<ReadonlyArray<string>>, tagName: string): string[];
118
+ /**
119
+ * A NIP-44 decryptor: takes the event's `content` and the author pubkey and
120
+ * returns the plaintext (a JSON array of tags). Inject your signer's
121
+ * `nip44.decrypt` (e.g. a NIP-07 `window.nostr.nip44`, a bunker, or
122
+ * `nostr-tools/nip44`) so this package carries no crypto dependency.
123
+ *
124
+ * @example
125
+ * ```ts
126
+ * const decrypt: Nip44Decryptor = (content, author) =>
127
+ * window.nostr!.nip44!.decrypt(author, content);
128
+ * ```
129
+ */
130
+ type Nip44Decryptor = (content: string, authorPubkey: string) => string | Promise<string>;
131
+ /**
132
+ * Decrypt the private portion of a list/set and populate
133
+ * {@link Nip51List.privateItems}.
134
+ *
135
+ * NIP-51 stores private items as a NIP-44-encrypted JSON array of tags in the
136
+ * event `content`, encrypted to the author's own key. This helper runs the
137
+ * injected {@link Nip44Decryptor}, parses the plaintext as `string[][]`, and
138
+ * returns a **new** {@link Nip51List} with `privateItems` set (the input is not
139
+ * mutated). Lists with no encrypted content return a copy with
140
+ * `privateItems: []`.
141
+ *
142
+ * Throws if the decryptor throws or the plaintext is not a JSON tag array — so
143
+ * callers can distinguish "no private items" from "couldn't read private items".
144
+ *
145
+ * @param list - A parsed list from {@link parseList}
146
+ * @param decrypt - Injected NIP-44 decryptor
147
+ * @returns A copy of `list` with `privateItems` populated
148
+ *
149
+ * @example
150
+ * ```ts
151
+ * const withPrivate = await decryptPrivateItems(list, (c, a) => signer.nip44.decrypt(a, c));
152
+ * const privatelyMuted = getTagValues(withPrivate.privateItems!, 'p');
153
+ * ```
154
+ */
155
+ declare function decryptPrivateItems(list: Nip51List, decrypt: Nip44Decryptor): Promise<Nip51List>;
156
+
157
+ export { ITEM_TAGS, LIST_KINDS, type Nip44Decryptor, type Nip51List, SET_KINDS, decryptPrivateItems, getTagValues, isListKind, isSetKind, listKindName, parseList };
@@ -0,0 +1,23 @@
1
+ import {
2
+ ITEM_TAGS,
3
+ LIST_KINDS,
4
+ SET_KINDS,
5
+ decryptPrivateItems,
6
+ getTagValues,
7
+ isListKind,
8
+ isSetKind,
9
+ listKindName,
10
+ parseList
11
+ } from "../chunk-2XYRP2S6.js";
12
+ export {
13
+ ITEM_TAGS,
14
+ LIST_KINDS,
15
+ SET_KINDS,
16
+ decryptPrivateItems,
17
+ getTagValues,
18
+ isListKind,
19
+ isSetKind,
20
+ listKindName,
21
+ parseList
22
+ };
23
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,64 @@
1
+ /**
2
+ * One published file: an absolute path and the lowercase-hex SHA-256 of its
3
+ * bytes. Parsed from a NIP-5A `["path", "<abs-path>", "<sha256>"]` tag.
4
+ *
5
+ * @example
6
+ * ```ts
7
+ * const entry: PathEntry = { path: '/index.html', sha256: '186ea5…1c99' };
8
+ * ```
9
+ */
10
+ interface PathEntry {
11
+ /** Absolute path of the file (the `path` tag's second element). */
12
+ path: string;
13
+ /** Lowercase-hex SHA-256 of the file bytes (the `path` tag's third element). */
14
+ sha256: string;
15
+ }
16
+ /**
17
+ * Compute the NIP-5A aggregate hash over a set of {@link PathEntry} files.
18
+ *
19
+ * Lines (`"<sha256> <abs-path>\n"`) are sorted before hashing, so the result is
20
+ * independent of the input order.
21
+ *
22
+ * @param entries - The file path entries (any order)
23
+ * @returns The aggregate hash as lowercase hex
24
+ *
25
+ * @example
26
+ * ```ts
27
+ * import { computeAggregateHash } from '@kehto/nip/5a';
28
+ * const hex = computeAggregateHash([{ path: '/index.html', sha256: 'aa…' }]);
29
+ * ```
30
+ */
31
+ declare function computeAggregateHash(entries: readonly PathEntry[]): string;
32
+ /**
33
+ * Extract {@link PathEntry} objects from an event's tags, keeping only
34
+ * well-formed `["path", "<abs-path>", "<sha256>"]` tags in tag order.
35
+ *
36
+ * @param tags - The manifest event's tags
37
+ * @returns Parsed path entries (malformed `path` tags are skipped)
38
+ */
39
+ declare function pathEntriesFromTags(tags: readonly (readonly string[])[]): PathEntry[];
40
+ /**
41
+ * Read the aggregate hash declared by an event's `["x", "<hex>", "aggregate"]`
42
+ * tag.
43
+ *
44
+ * @param tags - The manifest event's tags
45
+ * @returns The declared aggregate hex, or `undefined` if no aggregate `x` tag
46
+ */
47
+ declare function aggregateTagValue(tags: readonly (readonly string[])[]): string | undefined;
48
+ /**
49
+ * Verify that an event's declared aggregate `x` tag equals the aggregate
50
+ * recomputed from its `path` tags.
51
+ *
52
+ * @param tags - The manifest event's tags
53
+ * @returns `true` only when an aggregate `x` tag is present and matches the
54
+ * recomputed value; `false` otherwise (mismatch or missing aggregate tag)
55
+ *
56
+ * @example
57
+ * ```ts
58
+ * import { verifyAggregate } from '@kehto/nip/5a';
59
+ * if (!verifyAggregate(manifestEvent.tags)) throw new Error('aggregate mismatch');
60
+ * ```
61
+ */
62
+ declare function verifyAggregate(tags: readonly (readonly string[])[]): boolean;
63
+
64
+ export { type PathEntry, aggregateTagValue, computeAggregateHash, pathEntriesFromTags, verifyAggregate };
@@ -0,0 +1,13 @@
1
+ import {
2
+ aggregateTagValue,
3
+ computeAggregateHash,
4
+ pathEntriesFromTags,
5
+ verifyAggregate
6
+ } from "../chunk-2J3CSGKL.js";
7
+ export {
8
+ aggregateTagValue,
9
+ computeAggregateHash,
10
+ pathEntriesFromTags,
11
+ verifyAggregate
12
+ };
13
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,320 @@
1
+ import { NostrEvent } from 'nostr-tools';
2
+ import { PathEntry } from '../5a/index.js';
3
+
4
+ /** Versioned Cache Storage namespace for verified napplet artifacts. */
5
+ declare const NAPPLET_ARTIFACT_CACHE_NAME = "kehto:napplet-artifacts:v1";
6
+ interface NappletCacheDiagnostic {
7
+ type: 'cache-open-failed' | 'cache-read-failed' | 'cache-write-failed' | 'cache-delete-failed' | 'cache-prune-failed' | 'storage-estimate-unavailable';
8
+ message: string;
9
+ cause?: unknown;
10
+ }
11
+ interface NappletBlobIndexEntry {
12
+ size: number;
13
+ lastAccessed: number;
14
+ refCount: number;
15
+ }
16
+ interface NappletAggregateIndexEntry {
17
+ dTag: string;
18
+ aggregateHash: string;
19
+ key: string;
20
+ blobHashes: string[];
21
+ size: number;
22
+ lastAccessed: number;
23
+ coordinateKey?: string;
24
+ pinned?: boolean;
25
+ }
26
+ interface CachedCoordinate {
27
+ key: string;
28
+ kind: number;
29
+ pubkey: string;
30
+ dTag: string;
31
+ aggregateHash: string;
32
+ lastResolvedAt: number;
33
+ foregroundTtlMs: number;
34
+ backgroundTtlMs: number;
35
+ }
36
+ interface NappletArtifactCacheIndex {
37
+ schemaVersion: number;
38
+ updatedAt: number;
39
+ blobs: Record<string, NappletBlobIndexEntry>;
40
+ aggregates: Record<string, NappletAggregateIndexEntry>;
41
+ coordinates: Record<string, CachedCoordinate>;
42
+ }
43
+ interface WriteVerifiedResolutionInput {
44
+ event: NostrEvent;
45
+ manifest: NappletManifest;
46
+ files: ReadonlyMap<string, Uint8Array>;
47
+ indexHtml: string;
48
+ now?: number;
49
+ }
50
+ interface CoordinateFreshnessOptions {
51
+ now?: number;
52
+ background?: boolean;
53
+ }
54
+ interface NappletArtifactCache {
55
+ readBlob(sha256Hex: string): Promise<Uint8Array | undefined>;
56
+ deleteBlob(sha256Hex: string): Promise<void>;
57
+ writeVerifiedResolution(input: WriteVerifiedResolutionInput): Promise<void>;
58
+ writeCoordinate(input: {
59
+ kind: number;
60
+ pubkey: string;
61
+ dTag: string;
62
+ aggregateHash: string;
63
+ lastResolvedAt?: number;
64
+ foregroundTtlMs?: number;
65
+ backgroundTtlMs?: number;
66
+ }): Promise<void>;
67
+ getCoordinate(input: {
68
+ kind: number;
69
+ pubkey: string;
70
+ dTag: string;
71
+ }, options?: CoordinateFreshnessOptions): Promise<(CachedCoordinate & {
72
+ fresh: boolean;
73
+ }) | undefined>;
74
+ touchAggregate(dTag: string, aggregateHash: string, now?: number): Promise<void>;
75
+ markAggregateActive(dTag: string, aggregateHash: string): void;
76
+ releaseAggregateActive(dTag: string, aggregateHash: string): void;
77
+ prune(now?: number): Promise<void>;
78
+ }
79
+ interface CacheStorageNappletArtifactCacheOptions {
80
+ cacheStorage?: CacheStorage;
81
+ storage?: StorageManager;
82
+ cacheName?: string;
83
+ baseUrl?: string;
84
+ now?: () => number;
85
+ onDiagnostic?: (diagnostic: NappletCacheDiagnostic) => void;
86
+ unknownBudgetBytes?: number;
87
+ softCeilingBytes?: number;
88
+ hardCeilingBytes?: number;
89
+ }
90
+ interface OpenNappletArtifactCacheOptions extends CacheStorageNappletArtifactCacheOptions {
91
+ requireStorageEstimate?: boolean;
92
+ }
93
+ declare function coordinateKey(kind: number, pubkey: string, dTag: string): string;
94
+ declare function isCoordinateFresh(coordinate: CachedCoordinate, options?: CoordinateFreshnessOptions): boolean;
95
+ declare class CacheStorageNappletArtifactCache implements NappletArtifactCache {
96
+ private readonly cacheStorage;
97
+ private readonly storage?;
98
+ private readonly cacheName;
99
+ private readonly baseUrl;
100
+ private readonly now;
101
+ private readonly activeAggregates;
102
+ private readonly onDiagnostic?;
103
+ private readonly unknownBudgetBytes;
104
+ private readonly softCeilingBytes;
105
+ private readonly hardCeilingBytes;
106
+ private cachePromise?;
107
+ constructor(options?: CacheStorageNappletArtifactCacheOptions);
108
+ readBlob(sha256Hex: string): Promise<Uint8Array | undefined>;
109
+ deleteBlob(sha256Hex: string): Promise<void>;
110
+ writeVerifiedResolution(input: WriteVerifiedResolutionInput): Promise<void>;
111
+ writeCoordinate(input: {
112
+ kind: number;
113
+ pubkey: string;
114
+ dTag: string;
115
+ aggregateHash: string;
116
+ lastResolvedAt?: number;
117
+ foregroundTtlMs?: number;
118
+ backgroundTtlMs?: number;
119
+ }): Promise<void>;
120
+ getCoordinate(input: {
121
+ kind: number;
122
+ pubkey: string;
123
+ dTag: string;
124
+ }, options?: CoordinateFreshnessOptions): Promise<(CachedCoordinate & {
125
+ fresh: boolean;
126
+ }) | undefined>;
127
+ touchAggregate(dTag: string, aggregateHash: string, now?: number): Promise<void>;
128
+ markAggregateActive(dTag: string, aggregateHash: string): void;
129
+ releaseAggregateActive(dTag: string, aggregateHash: string): void;
130
+ prune(now?: number): Promise<void>;
131
+ snapshotIndex(): Promise<NappletArtifactCacheIndex>;
132
+ private writeVerifiedResolutionOnce;
133
+ private open;
134
+ private indexUrl;
135
+ private blobUrl;
136
+ private aggregateUrl;
137
+ private readIndex;
138
+ private writeIndex;
139
+ private rewriteRefCounts;
140
+ private budgetBytes;
141
+ private hardBudgetBytes;
142
+ private originPressureHigh;
143
+ private estimateStorage;
144
+ private diagnostic;
145
+ }
146
+ declare function openNappletArtifactCache(options?: OpenNappletArtifactCacheOptions): Promise<CacheStorageNappletArtifactCache | undefined>;
147
+
148
+ /**
149
+ * `@kehto/nip/5d` — NIP-5D napplet manifest resolution.
150
+ *
151
+ * NIP-5D (`dskvr/nips` branch `nip/5d`) defines content-addressed "napplet"
152
+ * web applets published as Nostr events that reference their files by hash
153
+ * (NIP-5A `path` tags + an aggregate `x` tag). A runtime resolves a napplet by:
154
+ *
155
+ * 1. verifying the manifest event's signature,
156
+ * 2. recomputing the NIP-5A aggregate from its `path` tags and checking the
157
+ * `["x","<hex>","aggregate"]` tag,
158
+ * 3. fetching each referenced blob (e.g. from Blossom) and verifying its hash,
159
+ * 4. assembling the verified `/index.html`.
160
+ *
161
+ * The napplet's identity is the `(dTag, aggregateHash)` tuple **computed** from
162
+ * these verified bytes — it is never accepted from a host or gateway.
163
+ */
164
+ /** Snapshot manifest — regular event, immutable point-in-time release. */
165
+ declare const NAPPLET_KIND_SNAPSHOT = 5129;
166
+ /** Root manifest — replaceable event, an author's latest unnamed napplet. */
167
+ declare const NAPPLET_KIND_ROOT = 15129;
168
+ /** Named manifest — addressable event (carries a `d` tag identifier). */
169
+ declare const NAPPLET_KIND_NAMED = 35129;
170
+ /** All three NIP-5D napplet manifest kinds. */
171
+ declare const NAPPLET_KINDS: readonly number[];
172
+ /**
173
+ * Whether `kind` is one of the three NIP-5D napplet manifest kinds.
174
+ *
175
+ * @param kind - A Nostr event kind
176
+ * @returns `true` for `5129` / `15129` / `35129`
177
+ */
178
+ declare function isNappletManifestKind(kind: number): boolean;
179
+ /**
180
+ * A parsed NIP-5D napplet manifest. All fields are derived from the manifest
181
+ * event's tags; the aggregate is the declared `x` tag (verify it separately
182
+ * with {@link resolveNapplet} or `@kehto/nip/5a` `verifyAggregate`).
183
+ */
184
+ interface NappletManifest {
185
+ /** Manifest event kind (`5129` / `15129` / `35129`). */
186
+ kind: number;
187
+ /** Author hex public key. */
188
+ pubkey: string;
189
+ /** Named-napplet `d` identifier, or `''` for root/snapshot manifests. */
190
+ dTag: string;
191
+ /** File path entries from `path` tags. */
192
+ paths: PathEntry[];
193
+ /** Declared aggregate hash from the `["x","<hex>","aggregate"]` tag. */
194
+ aggregateHash: string;
195
+ /** Blossom server URL hints from `server` tags. */
196
+ servers: string[];
197
+ /** Short NAP capability names from `requires` tags. */
198
+ requires: string[];
199
+ /**
200
+ * Archetype slugs this napplet fulfills, from `archetype` manifest tags; the
201
+ * optional `nap` is the recommended default wire protocol (the 3rd tag element).
202
+ */
203
+ archetypes: Array<{
204
+ slug: string;
205
+ nap?: string;
206
+ }>;
207
+ /** Optional human title. */
208
+ title?: string;
209
+ /** Optional human description. */
210
+ description?: string;
211
+ /** Optional upstream source URL from the `source` tag. */
212
+ source?: string;
213
+ }
214
+ /** Error codes for every napplet resolution failure path. */
215
+ type NappletResolutionErrorCode = 'invalid-signature' | 'invalid-manifest' | 'aggregate-mismatch' | 'blob-hash-mismatch' | 'blob-unavailable' | 'missing-index';
216
+ /**
217
+ * Thrown on any napplet resolution/verification failure. The `code` field
218
+ * identifies which guard rejected, so callers can fail closed without parsing
219
+ * the message.
220
+ */
221
+ declare class NappletResolutionError extends Error {
222
+ readonly code: NappletResolutionErrorCode;
223
+ constructor(code: NappletResolutionErrorCode, message: string);
224
+ }
225
+ /**
226
+ * Parse a NIP-5D manifest event into a {@link NappletManifest}.
227
+ *
228
+ * Does not verify the signature, aggregate, or blobs — use {@link resolveNapplet}
229
+ * for full verification.
230
+ *
231
+ * @param event - A NIP-5D manifest event (`5129` / `15129` / `35129`)
232
+ * @returns The parsed manifest
233
+ * @throws {@link NappletResolutionError} `invalid-manifest` for a non-napplet
234
+ * kind, missing `path` tags, or a missing aggregate `x` tag
235
+ */
236
+ declare function parseNappletManifest(event: NostrEvent): NappletManifest;
237
+ /**
238
+ * Verify a manifest event's Nostr signature (id + schnorr sig).
239
+ *
240
+ * @param event - A manifest event
241
+ * @returns `true` if the event is internally consistent and validly signed
242
+ */
243
+ declare function verifyManifestSignature(event: NostrEvent): boolean;
244
+ /**
245
+ * Whether `bytes` hash to the expected lowercase-hex SHA-256.
246
+ *
247
+ * @param bytes - The blob bytes
248
+ * @param sha256Hex - Expected lowercase-hex SHA-256
249
+ * @returns `true` only on an exact match
250
+ */
251
+ declare function verifyBlobHash(bytes: Uint8Array, sha256Hex: string): boolean;
252
+ /**
253
+ * Fetch a blob by SHA-256 from a list of Blossom servers, returning the first
254
+ * server's bytes whose hash matches. The returned bytes are always re-verified
255
+ * against `sha256Hex` — servers (and gateways) are never trusted.
256
+ *
257
+ * @param servers - Candidate Blossom server base URLs, tried in order
258
+ * @param sha256Hex - The blob's lowercase-hex SHA-256
259
+ * @param fetchBytes - Fetches raw bytes for a URL (`<server>/<sha256>`)
260
+ * @returns The verified blob bytes
261
+ * @throws {@link NappletResolutionError} `blob-unavailable` if no server serves
262
+ * a hash-matching blob
263
+ */
264
+ declare function fetchBlob(servers: readonly string[], sha256Hex: string, fetchBytes: (url: string) => Promise<Uint8Array>): Promise<Uint8Array>;
265
+ /** A fully verified napplet, ready to inject via `iframe.srcdoc`. */
266
+ interface ResolvedNapplet {
267
+ /** Computed `d` identifier (`''` for root/snapshot). */
268
+ dTag: string;
269
+ /** Computed (and verified) aggregate hash — the content address. */
270
+ aggregateHash: string;
271
+ /** Verified file bytes keyed by manifest path. */
272
+ files: Map<string, Uint8Array>;
273
+ /** The verified `/index.html` decoded to text. */
274
+ indexHtml: string;
275
+ /** The parsed manifest. */
276
+ manifest: NappletManifest;
277
+ }
278
+ /** Options for {@link resolveNapplet}. */
279
+ interface ResolveNappletOptions {
280
+ /** The candidate manifest event (resolved from relays by the caller). */
281
+ event: NostrEvent;
282
+ /**
283
+ * Fetch raw blob bytes by SHA-256. Backed by Blossom (or a gateway) by the
284
+ * caller. The bytes are re-verified against the hash here, so the fetcher is
285
+ * untrusted.
286
+ */
287
+ fetchBlob: (sha256Hex: string, servers: readonly string[]) => Promise<Uint8Array>;
288
+ /** Decode blob bytes to text for `indexHtml` (default UTF-8). */
289
+ textDecode?: (bytes: Uint8Array) => string;
290
+ /**
291
+ * Optional verified artifact cache. Cache hits are still re-verified against
292
+ * the manifest hash before use; cache writes happen only after the signature,
293
+ * aggregate, and every blob hash have been verified.
294
+ */
295
+ cache?: NappletArtifactCache;
296
+ }
297
+ /**
298
+ * Resolve a napplet end-to-end from a candidate manifest event: verify the
299
+ * signature, parse the manifest, verify the NIP-5A aggregate, fetch and verify
300
+ * every blob, then assemble the verified `/index.html`.
301
+ *
302
+ * The returned `(dTag, aggregateHash)` is computed from the verified bytes and
303
+ * is the napplet's identity. Any failure throws a {@link NappletResolutionError}
304
+ * — the caller must fail closed (never render unverified bytes).
305
+ *
306
+ * @param options - {@link ResolveNappletOptions}
307
+ * @returns The {@link ResolvedNapplet}
308
+ * @throws {@link NappletResolutionError} on signature, manifest, aggregate,
309
+ * blob-hash, blob-availability, or missing-index failures
310
+ *
311
+ * @example
312
+ * ```ts
313
+ * import { resolveNapplet } from '@kehto/nip/5d';
314
+ * const napplet = await resolveNapplet({ event, fetchBlob });
315
+ * iframe.srcdoc = napplet.indexHtml; // sandbox="allow-scripts", opaque origin
316
+ * ```
317
+ */
318
+ declare function resolveNapplet(options: ResolveNappletOptions): Promise<ResolvedNapplet>;
319
+
320
+ export { CacheStorageNappletArtifactCache, type CacheStorageNappletArtifactCacheOptions, type CachedCoordinate, type CoordinateFreshnessOptions, NAPPLET_ARTIFACT_CACHE_NAME, NAPPLET_KINDS, NAPPLET_KIND_NAMED, NAPPLET_KIND_ROOT, NAPPLET_KIND_SNAPSHOT, type NappletAggregateIndexEntry, type NappletArtifactCache, type NappletArtifactCacheIndex, type NappletBlobIndexEntry, type NappletCacheDiagnostic, type NappletManifest, NappletResolutionError, type NappletResolutionErrorCode, type OpenNappletArtifactCacheOptions, type ResolveNappletOptions, type ResolvedNapplet, type WriteVerifiedResolutionInput, coordinateKey, fetchBlob, isCoordinateFresh, isNappletManifestKind, openNappletArtifactCache, parseNappletManifest, resolveNapplet, verifyBlobHash, verifyManifestSignature };