@kehto/nip 0.1.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,38 @@
1
+ import {
2
+ CacheStorageNappletArtifactCache,
3
+ NAPPLET_ARTIFACT_CACHE_NAME,
4
+ NAPPLET_KINDS,
5
+ NAPPLET_KIND_NAMED,
6
+ NAPPLET_KIND_ROOT,
7
+ NAPPLET_KIND_SNAPSHOT,
8
+ NappletResolutionError,
9
+ coordinateKey,
10
+ fetchBlob,
11
+ isCoordinateFresh,
12
+ isNappletManifestKind,
13
+ openNappletArtifactCache,
14
+ parseNappletManifest,
15
+ resolveNapplet,
16
+ verifyBlobHash,
17
+ verifyManifestSignature
18
+ } from "../chunk-CDBUCFLP.js";
19
+ import "../chunk-2J3CSGKL.js";
20
+ export {
21
+ CacheStorageNappletArtifactCache,
22
+ NAPPLET_ARTIFACT_CACHE_NAME,
23
+ NAPPLET_KINDS,
24
+ NAPPLET_KIND_NAMED,
25
+ NAPPLET_KIND_ROOT,
26
+ NAPPLET_KIND_SNAPSHOT,
27
+ NappletResolutionError,
28
+ coordinateKey,
29
+ fetchBlob,
30
+ isCoordinateFresh,
31
+ isNappletManifestKind,
32
+ openNappletArtifactCache,
33
+ parseNappletManifest,
34
+ resolveNapplet,
35
+ verifyBlobHash,
36
+ verifyManifestSignature
37
+ };
38
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,170 @@
1
+ import { NostrEvent } from 'nostr-tools';
2
+
3
+ /**
4
+ * `@kehto/nip/65` — NIP-65 relay-list (kind 10002) parsing and outbox-model
5
+ * relay resolution.
6
+ *
7
+ * NIP-65 defines kind-10002 replaceable events whose `r` tags declare which
8
+ * relays an author reads from and writes to — the "outbox model" (a.k.a.
9
+ * "gossip"). A client that wants to *reach* an author publishes to (or reads
10
+ * from) that author's advertised relays instead of a fixed relay set.
11
+ *
12
+ * `nostr-tools` ships no NIP-65 helper, so this module fills the gap with a
13
+ * pure parser plus a closure-scoped registry (no module-global state, so
14
+ * multiple runtimes in one process never alias each other — same multi-instance
15
+ * contract as `@kehto/nip/66`).
16
+ *
17
+ * Tag grammar:
18
+ * ```text
19
+ * ["r", "wss://relay.example.com"] → read AND write
20
+ * ["r", "wss://relay.example.com", "read"] → read-only
21
+ * ["r", "wss://relay.example.com", "write"] → write-only
22
+ * ```
23
+ */
24
+ /**
25
+ * A single relay entry parsed from a NIP-65 kind-10002 `r` tag.
26
+ *
27
+ * @example
28
+ * ```ts
29
+ * const entry: RelayEntry = { url: 'wss://relay.example.com', read: true, write: false };
30
+ * ```
31
+ */
32
+ interface RelayEntry {
33
+ /** Relay WebSocket URL from the `r` tag's second element. */
34
+ url: string;
35
+ /** Whether the author reads (receives events) from this relay. */
36
+ read: boolean;
37
+ /** Whether the author writes (publishes events) to this relay. */
38
+ write: boolean;
39
+ }
40
+ /**
41
+ * Parse a NIP-65 kind-10002 relay-list event into structured {@link RelayEntry}
42
+ * objects.
43
+ *
44
+ * Keeps tags where `tag[0] === 'r'` and `tag[1]` is a string URL. The optional
45
+ * third element is the marker (`'read'`, `'write'`, or absent for both). Any
46
+ * marker other than the two canonical strings is treated as "unmarked" (read +
47
+ * write), matching how lenient NIP-65 consumers behave in practice.
48
+ *
49
+ * No URL normalization or hostname de-duplication is performed — the relay URL
50
+ * is preserved verbatim. Normalize with a NIP-66 dataset (`@kehto/nip/66`) or a
51
+ * dedicated relay-URL normalizer if you need canonical hostnames.
52
+ *
53
+ * @param event - A kind-10002 relay-list event (kind is not re-validated; pass the right event)
54
+ * @returns Array of relay entries in tag order; duplicate URLs are preserved
55
+ *
56
+ * @example
57
+ * ```ts
58
+ * import { parseNip65RelayList } from '@kehto/nip/65';
59
+ * const entries = parseNip65RelayList(kind10002Event);
60
+ * // [{ url: 'wss://a', read: true, write: true }, { url: 'wss://b', read: true, write: false }]
61
+ * ```
62
+ */
63
+ declare function parseNip65RelayList(event: NostrEvent): RelayEntry[];
64
+ /**
65
+ * Select the write (outbox) relay URLs from one author's parsed relay list,
66
+ * de-duplicated and in first-seen order.
67
+ *
68
+ * @param entries - Parsed entries from {@link parseNip65RelayList}
69
+ * @returns De-duplicated write-relay URLs
70
+ *
71
+ * @example
72
+ * ```ts
73
+ * const outbox = selectWriteRelays(parseNip65RelayList(event));
74
+ * ```
75
+ */
76
+ declare function selectWriteRelays(entries: ReadonlyArray<RelayEntry>): string[];
77
+ /**
78
+ * Select the read (inbox) relay URLs from one author's parsed relay list,
79
+ * de-duplicated and in first-seen order.
80
+ *
81
+ * @param entries - Parsed entries from {@link parseNip65RelayList}
82
+ * @returns De-duplicated read-relay URLs
83
+ */
84
+ declare function selectReadRelays(entries: ReadonlyArray<RelayEntry>): string[];
85
+ /**
86
+ * A closure-scoped registry of parsed NIP-65 relay lists keyed by author
87
+ * pubkey. Returned by {@link createNip65Registry}. Each registry owns its own
88
+ * `Map` — two registries never share state (unlike a module-globals pattern).
89
+ *
90
+ * @example
91
+ * ```ts
92
+ * const registry = createNip65Registry();
93
+ * registry.ingest(event); // store by event.pubkey
94
+ * const outbox = registry.resolveOutboxRelays([authorA, authorB]);
95
+ * ```
96
+ */
97
+ interface Nip65Registry {
98
+ /**
99
+ * Parse and store a kind-10002 event under its own `pubkey`. Overwrites any
100
+ * previously stored list for that author (kind-10002 is replaceable).
101
+ *
102
+ * @param event - A kind-10002 relay-list event
103
+ * @returns The parsed entries that were stored
104
+ */
105
+ ingest(event: NostrEvent): RelayEntry[];
106
+ /**
107
+ * Store an already-parsed relay list for an explicit pubkey. Use when you
108
+ * parsed elsewhere or want to seed the registry without an event object.
109
+ *
110
+ * @param pubkey - Author hex public key
111
+ * @param entries - Parsed relay entries
112
+ */
113
+ setRelayList(pubkey: string, entries: ReadonlyArray<RelayEntry>): void;
114
+ /**
115
+ * Get the stored relay list for an author, or `undefined` if none seen.
116
+ *
117
+ * @param pubkey - Author hex public key
118
+ */
119
+ getRelayList(pubkey: string): RelayEntry[] | undefined;
120
+ /** Whether a relay list has been stored for `pubkey`. */
121
+ has(pubkey: string): boolean;
122
+ /**
123
+ * Resolve write (outbox) relay URLs across the given authors, de-duplicated.
124
+ * Authors with no stored list contribute nothing. Empty input or all-unknown
125
+ * authors yield `[]`.
126
+ *
127
+ * @param pubkeys - Author hex public keys
128
+ * @returns De-duplicated outbox relay URLs in first-seen order
129
+ */
130
+ resolveOutboxRelays(pubkeys: ReadonlyArray<string>): string[];
131
+ /**
132
+ * Resolve read (inbox) relay URLs across the given authors, de-duplicated.
133
+ *
134
+ * @param pubkeys - Author hex public keys
135
+ * @returns De-duplicated inbox relay URLs in first-seen order
136
+ */
137
+ resolveReadRelays(pubkeys: ReadonlyArray<string>): string[];
138
+ /** Drop a single author's stored relay list. Returns `true` if one existed. */
139
+ delete(pubkey: string): boolean;
140
+ /** Clear every stored relay list. */
141
+ clear(): void;
142
+ }
143
+ /**
144
+ * Create a closure-scoped {@link Nip65Registry} for accumulating kind-10002
145
+ * relay lists and resolving outbox/inbox relays.
146
+ *
147
+ * Framework-agnostic: it knows nothing about relay pools, subscriptions, or
148
+ * storage. Feed it kind-10002 events (e.g. from your pool's `onevent`
149
+ * callback) via {@link Nip65Registry.ingest} and query the outbox model with
150
+ * {@link Nip65Registry.resolveOutboxRelays}.
151
+ *
152
+ * @returns A fresh registry with its own state
153
+ *
154
+ * @example
155
+ * ```ts
156
+ * import { createNip65Registry } from '@kehto/nip/65';
157
+ * import { SimplePool } from 'nostr-tools/pool';
158
+ *
159
+ * const registry = createNip65Registry();
160
+ * const pool = new SimplePool();
161
+ * pool.subscribeMany(discoveryRelays, [{ kinds: [10002], authors }], {
162
+ * onevent: (e) => registry.ingest(e),
163
+ * });
164
+ * // Later, before publishing on behalf of `author`:
165
+ * const outbox = registry.resolveOutboxRelays([author]);
166
+ * ```
167
+ */
168
+ declare function createNip65Registry(): Nip65Registry;
169
+
170
+ export { type Nip65Registry, type RelayEntry, createNip65Registry, parseNip65RelayList, selectReadRelays, selectWriteRelays };
@@ -0,0 +1,13 @@
1
+ import {
2
+ createNip65Registry,
3
+ parseNip65RelayList,
4
+ selectReadRelays,
5
+ selectWriteRelays
6
+ } from "../chunk-UUMBCHTA.js";
7
+ export {
8
+ createNip65Registry,
9
+ parseNip65RelayList,
10
+ selectReadRelays,
11
+ selectWriteRelays
12
+ };
13
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  DEFAULT_RELAY_ATTRIBUTE_GROUPS,
3
3
  createNip66Aggregator
4
- } from "./chunk-N4WPLKIJ.js";
4
+ } from "../chunk-DADZ6NUI.js";
5
5
  export {
6
6
  DEFAULT_RELAY_ATTRIBUTE_GROUPS,
7
7
  createNip66Aggregator
8
8
  };
9
- //# sourceMappingURL=66.js.map
9
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,164 @@
1
+ import { NostrEvent } from 'nostr-tools';
2
+
3
+ /**
4
+ * `@kehto/nip/89` — NIP-89 application-handler discovery.
5
+ *
6
+ * NIP-89 lets clients answer "who can open an event of kind N?" without
7
+ * hardcoding app knowledge — essential for any runtime that hosts or launches
8
+ * other apps (the napplet model, "open with…" menus, unknown-kind fallbacks).
9
+ *
10
+ * Two event kinds:
11
+ * - **kind 31990 — handler information.** An app advertises which kinds it
12
+ * handles (`k` tags) and per-platform URL templates to open an entity
13
+ * (`["web", "https://app/e/<bech32>", "nevent"]`).
14
+ * - **kind 31989 — handler recommendation.** A user recommends a handler for a
15
+ * given kind via `a` tags pointing at 31990 addresses.
16
+ *
17
+ * `nostr-tools` ships no NIP-89 helper. This module provides pure parsers for
18
+ * both kinds plus URL-template expansion — no relay, signer, or framework
19
+ * coupling.
20
+ */
21
+ /** Kind 31990 — handler information (an app describing what it can open). */
22
+ declare const HANDLER_INFORMATION_KIND = 31990;
23
+ /** Kind 31989 — handler recommendation (a user endorsing a handler for a kind). */
24
+ declare const HANDLER_RECOMMENDATION_KIND = 31989;
25
+ /** Platform tag names defined by NIP-89 for handler URL templates. */
26
+ declare const HANDLER_PLATFORMS: readonly ["web", "ios", "android", "macos", "windows", "linux"];
27
+ /** One of the NIP-89 platform identifiers. */
28
+ type HandlerPlatform = (typeof HANDLER_PLATFORMS)[number];
29
+ /**
30
+ * One platform-specific way to open an entity, from a 31990 platform tag.
31
+ *
32
+ * @example
33
+ * ```ts
34
+ * // tag ["web", "https://app.example/e/<bech32>", "nevent"]
35
+ * const target: HandlerTarget = { url: 'https://app.example/e/<bech32>', entity: 'nevent' };
36
+ * ```
37
+ */
38
+ interface HandlerTarget {
39
+ /** URL template containing a `<bech32>` and/or `<raw>` placeholder. */
40
+ url: string;
41
+ /** Optional NIP-19 entity type this template is for (e.g. `'nevent'`, `'naddr'`, `'nprofile'`). */
42
+ entity: string | undefined;
43
+ }
44
+ /**
45
+ * A parsed kind-31990 handler-information event.
46
+ *
47
+ * @example
48
+ * ```ts
49
+ * const info = parseHandlerInformation(event);
50
+ * info.kinds; // [1, 30023]
51
+ * info.metadata.name; // 'My Reader'
52
+ * info.platforms.web; // [{ url: 'https://app/<bech32>', entity: 'nevent' }]
53
+ * ```
54
+ */
55
+ interface HandlerInformation {
56
+ /** Handler author pubkey. */
57
+ pubkey: string;
58
+ /** `d`-tag identifier (handler instance id). */
59
+ identifier: string;
60
+ /** Addressable coordinate `31990:pubkey:d`. */
61
+ address: string;
62
+ /** Event kinds the handler declares support for (from `k` tags, de-duplicated). */
63
+ kinds: number[];
64
+ /** Parsed kind-0-style metadata from `content` (name, display_name, picture, about, …). `{}` if absent/invalid. */
65
+ metadata: Record<string, unknown>;
66
+ /** Per-platform URL templates, keyed by {@link HandlerPlatform}. Only platforms present in the event appear. */
67
+ platforms: Partial<Record<HandlerPlatform, HandlerTarget[]>>;
68
+ }
69
+ /**
70
+ * Parse a kind-31990 handler-information event.
71
+ *
72
+ * @param event - A kind-31990 event (kind is not re-validated)
73
+ * @returns The structured handler information
74
+ *
75
+ * @example
76
+ * ```ts
77
+ * import { parseHandlerInformation, handlesKind } from '@kehto/nip/89';
78
+ * const info = parseHandlerInformation(event);
79
+ * if (handlesKind(info, 1)) openWith(info);
80
+ * ```
81
+ */
82
+ declare function parseHandlerInformation(event: NostrEvent): HandlerInformation;
83
+ /**
84
+ * Whether a handler declares support for `kind`.
85
+ *
86
+ * @param info - Parsed handler information
87
+ * @param kind - Event kind to check
88
+ */
89
+ declare function handlesKind(info: HandlerInformation, kind: number): boolean;
90
+ /**
91
+ * Substitution values for {@link buildHandlerUrl}.
92
+ *
93
+ * NIP-89 templates use `<bech32>` (NIP-19 encoded entity) and/or `<raw>`
94
+ * (hex id / address coordinate). Supply whichever the template needs.
95
+ */
96
+ interface HandlerEntityValues {
97
+ /** NIP-19 bech32 entity (e.g. an `nevent…`, `naddr…`, `nprofile…`). */
98
+ bech32?: string;
99
+ /** Raw value (hex event id, or `kind:pubkey:d` coordinate). */
100
+ raw?: string;
101
+ }
102
+ /**
103
+ * Expand a handler URL template by substituting its `<bech32>` / `<raw>`
104
+ * placeholders.
105
+ *
106
+ * Returns `undefined` when the template requires a placeholder you didn't
107
+ * supply (e.g. it contains `<bech32>` but `values.bech32` is missing) — so a
108
+ * caller can fall through to the next target instead of producing a broken URL.
109
+ *
110
+ * @param target - A {@link HandlerTarget} or a raw template string
111
+ * @param values - The `<bech32>` / `<raw>` substitutions
112
+ * @returns The expanded URL, or `undefined` if a needed placeholder is absent
113
+ *
114
+ * @example
115
+ * ```ts
116
+ * buildHandlerUrl({ url: 'https://app/e/<bech32>', entity: 'nevent' }, { bech32: 'nevent1…' });
117
+ * // 'https://app/e/nevent1…'
118
+ * ```
119
+ */
120
+ declare function buildHandlerUrl(target: HandlerTarget | string, values: HandlerEntityValues): string | undefined;
121
+ /** One handler endorsement parsed from a 31989 `a` tag. */
122
+ interface HandlerRecommendationEntry {
123
+ /** The recommended handler's 31990 coordinate `31990:pubkey:d`. */
124
+ address: string;
125
+ /** Handler author pubkey extracted from the coordinate, if well-formed. */
126
+ handlerPubkey: string | undefined;
127
+ /** Handler `d`-tag identifier extracted from the coordinate, if present. */
128
+ identifier: string | undefined;
129
+ /** Optional relay-hint where the 31990 event can be found (`a`-tag 3rd element). */
130
+ relay: string | undefined;
131
+ /** Optional platform marker the handler is recommended for (`a`-tag 4th element). */
132
+ platform: string | undefined;
133
+ }
134
+ /**
135
+ * A parsed kind-31989 handler-recommendation event.
136
+ *
137
+ * @example
138
+ * ```ts
139
+ * const rec = parseHandlerRecommendation(event);
140
+ * rec.recommendedKind; // 1
141
+ * rec.recommendations[0].address; // '31990:pubkey:d'
142
+ * ```
143
+ */
144
+ interface HandlerRecommendation {
145
+ /** Recommendation author pubkey. */
146
+ pubkey: string;
147
+ /** The kind being recommended for, parsed from the `d` tag; `undefined` if non-numeric/missing. */
148
+ recommendedKind: number | undefined;
149
+ /** Endorsed handlers, in tag order. */
150
+ recommendations: HandlerRecommendationEntry[];
151
+ }
152
+ /**
153
+ * Parse a kind-31989 handler-recommendation event.
154
+ *
155
+ * The `d` tag carries the kind being recommended for; each `a` tag (whose
156
+ * coordinate starts with `31990:`) endorses one handler, optionally with a
157
+ * relay hint and platform marker.
158
+ *
159
+ * @param event - A kind-31989 event (kind is not re-validated)
160
+ * @returns The structured recommendation
161
+ */
162
+ declare function parseHandlerRecommendation(event: NostrEvent): HandlerRecommendation;
163
+
164
+ export { HANDLER_INFORMATION_KIND, HANDLER_PLATFORMS, HANDLER_RECOMMENDATION_KIND, type HandlerEntityValues, type HandlerInformation, type HandlerPlatform, type HandlerRecommendation, type HandlerRecommendationEntry, type HandlerTarget, buildHandlerUrl, handlesKind, parseHandlerInformation, parseHandlerRecommendation };
@@ -0,0 +1,19 @@
1
+ import {
2
+ HANDLER_INFORMATION_KIND,
3
+ HANDLER_PLATFORMS,
4
+ HANDLER_RECOMMENDATION_KIND,
5
+ buildHandlerUrl,
6
+ handlesKind,
7
+ parseHandlerInformation,
8
+ parseHandlerRecommendation
9
+ } from "../chunk-BCTIL523.js";
10
+ export {
11
+ HANDLER_INFORMATION_KIND,
12
+ HANDLER_PLATFORMS,
13
+ HANDLER_RECOMMENDATION_KIND,
14
+ buildHandlerUrl,
15
+ handlesKind,
16
+ parseHandlerInformation,
17
+ parseHandlerRecommendation
18
+ };
19
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,39 @@
1
+ // src/5a/index.ts
2
+ import { sha256 } from "@noble/hashes/sha2.js";
3
+ import { bytesToHex } from "@noble/hashes/utils.js";
4
+ function computeAggregateHash(entries) {
5
+ const lines = entries.map((entry) => `${entry.sha256} ${entry.path}
6
+ `);
7
+ lines.sort();
8
+ return bytesToHex(sha256(new TextEncoder().encode(lines.join(""))));
9
+ }
10
+ function pathEntriesFromTags(tags) {
11
+ const entries = [];
12
+ for (const tag of tags) {
13
+ if (tag[0] === "path" && typeof tag[1] === "string" && typeof tag[2] === "string" && tag[1].length > 0 && tag[2].length > 0) {
14
+ entries.push({ path: tag[1], sha256: tag[2] });
15
+ }
16
+ }
17
+ return entries;
18
+ }
19
+ function aggregateTagValue(tags) {
20
+ for (const tag of tags) {
21
+ if (tag[0] === "x" && tag[2] === "aggregate" && typeof tag[1] === "string" && tag[1].length > 0) {
22
+ return tag[1];
23
+ }
24
+ }
25
+ return void 0;
26
+ }
27
+ function verifyAggregate(tags) {
28
+ const declared = aggregateTagValue(tags);
29
+ if (!declared) return false;
30
+ return computeAggregateHash(pathEntriesFromTags(tags)) === declared;
31
+ }
32
+
33
+ export {
34
+ computeAggregateHash,
35
+ pathEntriesFromTags,
36
+ aggregateTagValue,
37
+ verifyAggregate
38
+ };
39
+ //# sourceMappingURL=chunk-2J3CSGKL.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\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":[]}
@@ -0,0 +1,108 @@
1
+ // src/51/index.ts
2
+ var LIST_KINDS = {
3
+ 1e4: "mute",
4
+ 10001: "pinned-notes",
5
+ 10003: "bookmarks",
6
+ 10004: "communities",
7
+ 10005: "public-chats",
8
+ 10006: "blocked-relays",
9
+ 10007: "search-relays",
10
+ 10009: "simple-groups",
11
+ 10012: "relay-feeds",
12
+ 10015: "interests",
13
+ 10020: "media-follows",
14
+ 10030: "emojis",
15
+ 10050: "dm-relays",
16
+ 10101: "good-wiki-authors",
17
+ 10102: "good-wiki-relays"
18
+ };
19
+ var SET_KINDS = {
20
+ 3e4: "follow-sets",
21
+ 30002: "relay-sets",
22
+ 30003: "bookmark-sets",
23
+ 30004: "curation-sets",
24
+ 30005: "video-curation-sets",
25
+ 30007: "kind-mute-sets",
26
+ 30015: "interest-sets",
27
+ 30019: "release-sets",
28
+ 30020: "release-artifact-sets",
29
+ 30030: "emoji-sets",
30
+ 30063: "release-artifact-sets",
31
+ 30267: "app-curation-sets"
32
+ };
33
+ var ITEM_TAGS = ["e", "p", "a", "t", "r", "word", "emoji", "relay", "g", "i", "k"];
34
+ function isListKind(kind) {
35
+ return Object.prototype.hasOwnProperty.call(LIST_KINDS, kind);
36
+ }
37
+ function isSetKind(kind) {
38
+ return Object.prototype.hasOwnProperty.call(SET_KINDS, kind);
39
+ }
40
+ function listKindName(kind) {
41
+ return LIST_KINDS[kind] ?? SET_KINDS[kind];
42
+ }
43
+ var META_TAGS = /* @__PURE__ */ new Set(["d", "title", "image", "description", "name", "summary", "alt"]);
44
+ function parseList(event) {
45
+ const family = isListKind(event.kind) ? "list" : isSetKind(event.kind) ? "set" : "unknown";
46
+ const itemTagSet = new Set(ITEM_TAGS);
47
+ const publicItems = [];
48
+ let identifier = "";
49
+ let title;
50
+ let image;
51
+ let description;
52
+ for (const tag of event.tags) {
53
+ const name = tag[0];
54
+ if (name === "d") identifier = tag[1] ?? "";
55
+ else if (name === "title") title = tag[1];
56
+ else if (name === "image") image = tag[1];
57
+ else if (name === "description") description = tag[1];
58
+ else if (itemTagSet.has(name) && !META_TAGS.has(name)) publicItems.push([...tag]);
59
+ }
60
+ const isSet = family === "set";
61
+ const address = isSet ? `${event.kind}:${event.pubkey}:${identifier}` : void 0;
62
+ const encryptedContent = event.content && event.content.length > 0 ? event.content : void 0;
63
+ return {
64
+ kind: event.kind,
65
+ family,
66
+ type: listKindName(event.kind),
67
+ pubkey: event.pubkey,
68
+ identifier,
69
+ address,
70
+ title,
71
+ image,
72
+ description,
73
+ publicItems,
74
+ encryptedContent,
75
+ privateItems: void 0
76
+ };
77
+ }
78
+ function getTagValues(items, tagName) {
79
+ const out = [];
80
+ for (const tag of items) {
81
+ if (tag[0] === tagName && typeof tag[1] === "string") out.push(tag[1]);
82
+ }
83
+ return out;
84
+ }
85
+ async function decryptPrivateItems(list, decrypt) {
86
+ if (!list.encryptedContent) {
87
+ return { ...list, privateItems: [] };
88
+ }
89
+ const plaintext = await decrypt(list.encryptedContent, list.pubkey);
90
+ const parsed = JSON.parse(plaintext);
91
+ if (!Array.isArray(parsed) || !parsed.every((t) => Array.isArray(t) && t.every((s) => typeof s === "string"))) {
92
+ throw new Error("NIP-51: decrypted private items are not a string[][] tag array");
93
+ }
94
+ return { ...list, privateItems: parsed };
95
+ }
96
+
97
+ export {
98
+ LIST_KINDS,
99
+ SET_KINDS,
100
+ ITEM_TAGS,
101
+ isListKind,
102
+ isSetKind,
103
+ listKindName,
104
+ parseList,
105
+ getTagValues,
106
+ decryptPrivateItems
107
+ };
108
+ //# sourceMappingURL=chunk-2XYRP2S6.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\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":[]}