@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 +51 -15
- package/dist/51/index.d.ts +157 -0
- package/dist/51/index.js +23 -0
- package/dist/5a/index.d.ts +64 -0
- package/dist/5a/index.js +13 -0
- package/dist/5a/index.js.map +1 -0
- package/dist/5d/index.d.ts +320 -0
- package/dist/5d/index.js +38 -0
- package/dist/5d/index.js.map +1 -0
- package/dist/65/index.d.ts +170 -0
- package/dist/65/index.js +13 -0
- package/dist/65/index.js.map +1 -0
- package/dist/{66.js → 66/index.js} +2 -2
- package/dist/66/index.js.map +1 -0
- package/dist/89/index.d.ts +164 -0
- package/dist/89/index.js +19 -0
- package/dist/89/index.js.map +1 -0
- package/dist/chunk-2J3CSGKL.js +39 -0
- package/dist/chunk-2J3CSGKL.js.map +1 -0
- package/dist/chunk-2XYRP2S6.js +108 -0
- package/dist/chunk-2XYRP2S6.js.map +1 -0
- package/dist/chunk-BCTIL523.js +83 -0
- package/dist/chunk-BCTIL523.js.map +1 -0
- package/dist/chunk-CDBUCFLP.js +572 -0
- package/dist/chunk-CDBUCFLP.js.map +1 -0
- package/dist/{chunk-N4WPLKIJ.js → chunk-DADZ6NUI.js} +2 -2
- package/dist/chunk-DADZ6NUI.js.map +1 -0
- package/dist/chunk-UUMBCHTA.js +75 -0
- package/dist/chunk-UUMBCHTA.js.map +1 -0
- package/dist/index.d.ts +6 -1
- package/dist/index.js +92 -2
- package/package.json +38 -4
- package/dist/chunk-N4WPLKIJ.js.map +0 -1
- /package/dist/{66.js.map → 51/index.js.map} +0 -0
- /package/dist/{66.d.ts → 66/index.d.ts} +0 -0
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
|
|
4
|
-
|
|
5
|
-
|
|
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/
|
|
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 {
|
|
41
|
+
import { createNip65Registry } from '@kehto/nip/65';
|
|
20
42
|
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
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.
|
|
34
|
-
module-global state, inject relay/pool concerns via interfaces).
|
|
35
|
-
2. Add `src/<n
|
|
36
|
-
|
|
37
|
-
|
|
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 };
|
package/dist/51/index.js
ADDED
|
@@ -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 };
|
package/dist/5a/index.js
ADDED
|
@@ -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 };
|