@kehto/nip 0.1.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 ADDED
@@ -0,0 +1,37 @@
1
+ # @kehto/nip
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.
6
+
7
+ ## NIPs
8
+
9
+ | Subpath | NIP | What it provides |
10
+ |---------|-----|------------------|
11
+ | `@kehto/nip/66` | NIP-66 | kind-30166 relay-discovery aggregator (`createNip66Aggregator`) |
12
+
13
+ More unique NIPs are added as their own subpath (`@kehto/nip/<n>`) over time.
14
+
15
+ ## Usage
16
+
17
+ ```ts
18
+ // Import only the NIP you need — nothing else is bundled.
19
+ import { createNip66Aggregator } from '@kehto/nip/66';
20
+
21
+ const aggregator = createNip66Aggregator({
22
+ pool: myPoolAdapter,
23
+ bootstrap: ['wss://monitor.example.com'],
24
+ });
25
+ aggregator.start();
26
+ ```
27
+
28
+ A convenience barrel re-export is also available at the package root
29
+ (`@kehto/nip`), but prefer subpath imports for the smallest bundles.
30
+
31
+ ## Adding a NIP
32
+
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.
package/dist/66.d.ts ADDED
@@ -0,0 +1,192 @@
1
+ import { NostrEvent } from 'nostr-tools';
2
+
3
+ /**
4
+ * Minimal relay-pool contract `@kehto/nip/66` depends on. Consumers implement
5
+ * this against their pool library of choice (nostr-tools SimplePool,
6
+ * applesauce-relay, @snort/worker-relay, etc.). Intentionally smaller than
7
+ * RxJS-style observable pools — one callback, one unsubscribe handle.
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * import { SimplePool } from 'nostr-tools/pool';
12
+ * const pool = new SimplePool();
13
+ * const adapter: Nip66RelayPool = {
14
+ * subscribe: (relays, filter, onEvent) => {
15
+ * const sub = pool.subscribeMany(relays, [filter], { onevent: onEvent });
16
+ * return () => sub.close();
17
+ * },
18
+ * };
19
+ * ```
20
+ */
21
+ interface Nip66RelayPool {
22
+ /**
23
+ * Open a kind-30166 subscription against a set of monitor relays.
24
+ *
25
+ * @param relays - WebSocket URLs to subscribe against (the aggregator passes its bootstrap list)
26
+ * @param filter - A kind-30166 filter, optionally narrowed with `#n` network tags
27
+ * @param onEvent - Called synchronously with every matching event; the aggregator parses each one
28
+ * @returns An unsubscribe handle the aggregator calls on `resync()` or teardown
29
+ */
30
+ subscribe(relays: ReadonlyArray<string>, filter: Nip66Filter, onEvent: (event: NostrEvent) => void): () => void;
31
+ }
32
+ /**
33
+ * Minimal filter shape for kind-30166 subscriptions. `#n` optionally narrows
34
+ * by network type (e.g. `['clearnet']`, `['clearnet', 'tor']`).
35
+ *
36
+ * @example
37
+ * ```ts
38
+ * const filter: Nip66Filter = { kinds: [30166], '#n': ['clearnet'] };
39
+ * ```
40
+ */
41
+ type Nip66Filter = {
42
+ kinds: [30166];
43
+ '#n'?: ReadonlyArray<string>;
44
+ };
45
+ /**
46
+ * Options for {@link createNip66Aggregator}.
47
+ *
48
+ * @example
49
+ * ```ts
50
+ * const options: Nip66AggregatorOptions = {
51
+ * pool: myPoolAdapter,
52
+ * bootstrap: ['wss://monitor1.example.com', 'wss://monitor2.example.com'],
53
+ * networks: ['clearnet'],
54
+ * };
55
+ * ```
56
+ */
57
+ interface Nip66AggregatorOptions {
58
+ /** A {@link Nip66RelayPool} adapter supplied by the consumer. */
59
+ pool: Nip66RelayPool;
60
+ /** Required non-empty list of monitor relay URLs to subscribe against. No default ships in the package. */
61
+ bootstrap: ReadonlyArray<string>;
62
+ /** Optional network-type narrowing passed to the filter as `#n` (e.g. `['clearnet', 'tor']`). Omit to receive every network. */
63
+ networks?: ReadonlyArray<string>;
64
+ /**
65
+ * Optional named relay-attribute groups for interpreting ad hoc kind-30166 tags.
66
+ * Omit to use {@link DEFAULT_RELAY_ATTRIBUTE_GROUPS}.
67
+ */
68
+ relayAttributeGroups?: RelayAttributeGroups;
69
+ }
70
+ /** A tag/value matcher for ad hoc relay attributes carried on kind-30166 events. */
71
+ interface RelayAttributeMatcher {
72
+ /** Tag name to match, for example `T`, `t`, or `k`. */
73
+ tag: string;
74
+ /** Optional accepted values. If omitted, any value for the tag matches. */
75
+ values?: ReadonlyArray<string>;
76
+ }
77
+ /** Named groups of relay-attribute matchers. Matchers within a group are OR-ed. */
78
+ type RelayAttributeGroups = Readonly<Record<string, ReadonlyArray<RelayAttributeMatcher>>>;
79
+ /** Parsed attributes observed for one NIP-66 relay URL. */
80
+ interface Nip66RelayAttributes {
81
+ /** Raw tag copies from the latest observed kind-30166 event for the relay. */
82
+ tags: ReadonlyArray<ReadonlyArray<string>>;
83
+ /** Tag-value index, keyed by tag name. */
84
+ values: ReadonlyMap<string, ReadonlySet<string>>;
85
+ /** Parsed uppercase `N` tag support for convenience. */
86
+ supportedNips: ReadonlySet<number>;
87
+ }
88
+ declare const DEFAULT_RELAY_ATTRIBUTE_GROUPS: RelayAttributeGroups;
89
+ /**
90
+ * Aggregator handle returned by {@link createNip66Aggregator}. Each call
91
+ * returns a fresh instance with its own closure-scoped state — multi-instance
92
+ * safe (unlike a module-globals pattern).
93
+ *
94
+ * @example
95
+ * ```ts
96
+ * const aggregator = createNip66Aggregator({ pool, bootstrap: ['wss://monitor.example.com'] });
97
+ * aggregator.start();
98
+ * const relays = aggregator.getRelaySet();
99
+ * ```
100
+ */
101
+ interface Nip66Aggregator {
102
+ /** Begin subscribing against the bootstrap relay set. Idempotent — calling twice is a no-op. */
103
+ start(): void;
104
+ /** Tear down the current subscription, clear accumulated state, and re-subscribe. Call when `networks` semantics change upstream. */
105
+ resync(): void;
106
+ /**
107
+ * Tear down the current subscription. Idempotent — calling without a prior
108
+ * `start()` is a safe no-op. Preserves accumulated state (`getRelaySet`,
109
+ * `relaySupportsNip`) — use `resync()` if you need to clear state AND
110
+ * re-subscribe in one step. A subsequent `start()` re-subscribes against
111
+ * the bootstrap relays (identical contract to the first start).
112
+ *
113
+ * Wire this to your consumer's teardown path (browser: `beforeunload`;
114
+ * React: effect cleanup; Electron: `app.before-quit`) — the aggregator
115
+ * holds a pool subscription that otherwise lives for the tab lifetime
116
+ * (PITFALLS.md M-03).
117
+ */
118
+ stop(): void;
119
+ /** The current set of relay URLs discovered from kind-30166 `d`-tags. Empty until `start()` fires at least one event. */
120
+ getRelaySet(): ReadonlySet<string>;
121
+ /**
122
+ * Relay URLs whose `N`-tags declare support for the given NIP number.
123
+ *
124
+ * @param nip - NIP number to query support for (e.g. 44 for NIP-44 encrypted DMs)
125
+ * @returns Array of relay URLs supporting that NIP
126
+ */
127
+ getRelaysSupportingNip(nip: number): string[];
128
+ /**
129
+ * Whether the relay at `url` has been seen with an `N`-tag declaring support for `nip`.
130
+ *
131
+ * @param url - Relay WebSocket URL to check
132
+ * @param nip - NIP number to query support for
133
+ * @returns `true` iff the relay has been observed with a matching `N`-tag
134
+ */
135
+ relaySupportsNip(url: string, nip: number): boolean;
136
+ /**
137
+ * Return parsed relay attributes for a relay URL, if that relay has been
138
+ * observed in a kind-30166 event.
139
+ */
140
+ getRelayAttributes(url: string): Nip66RelayAttributes | undefined;
141
+ /**
142
+ * Return relays matching any supplied ad hoc attribute matcher.
143
+ *
144
+ * @param matchers - Tag/value matchers, OR semantics
145
+ */
146
+ getRelaysMatchingAttributes(matchers: ReadonlyArray<RelayAttributeMatcher>): string[];
147
+ /**
148
+ * Return relays in a named attribute group. Uses the group table passed to
149
+ * `createNip66Aggregator`, or {@link DEFAULT_RELAY_ATTRIBUTE_GROUPS}.
150
+ */
151
+ getRelaysForAttributeGroup(groupName: string, groups?: RelayAttributeGroups): string[];
152
+ }
153
+ /**
154
+ * Create a NIP-66 kind-30166 relay-discovery aggregator with closure-scoped state.
155
+ *
156
+ * Subscribes to kind-30166 events via the injected {@link Nip66RelayPool},
157
+ * extracts relay URLs from `d`-tags, and parses `N`-tags to track which NIP
158
+ * numbers each relay supports. Multi-instance safe — each factory call owns
159
+ * its own `Set` + `Map`; multiple aggregators share nothing at module scope.
160
+ *
161
+ * The aggregator is framework-agnostic: it knows nothing about negentropy
162
+ * (NIP-77), OPFS caching, worker relays, or network-type config stores.
163
+ * Consumers inject those concerns through their {@link Nip66RelayPool}
164
+ * implementation. `start()` is synchronous and idempotent — consumers
165
+ * schedule their own cadence (delayed start, retry, etc.).
166
+ *
167
+ * @param options - Pool adapter + bootstrap monitor relays (+ optional network filter)
168
+ * @returns A fresh {@link Nip66Aggregator} handle
169
+ *
170
+ * @example
171
+ * ```ts
172
+ * import { createNip66Aggregator } from '@kehto/nip/66';
173
+ * import { SimplePool } from 'nostr-tools/pool';
174
+ *
175
+ * const pool = new SimplePool();
176
+ * const aggregator = createNip66Aggregator({
177
+ * pool: {
178
+ * subscribe: (relays, filter, onEvent) => {
179
+ * const sub = pool.subscribeMany(relays, [filter], { onevent: onEvent });
180
+ * return () => sub.close();
181
+ * },
182
+ * },
183
+ * bootstrap: ['wss://monitor1.example.com'],
184
+ * });
185
+ * aggregator.start();
186
+ * // Later — inside a ShellAdapter hook:
187
+ * const suggestions = Array.from(aggregator.getRelaySet());
188
+ * ```
189
+ */
190
+ declare function createNip66Aggregator(options: Nip66AggregatorOptions): Nip66Aggregator;
191
+
192
+ export { DEFAULT_RELAY_ATTRIBUTE_GROUPS, type Nip66Aggregator, type Nip66AggregatorOptions, type Nip66Filter, type Nip66RelayAttributes, type Nip66RelayPool, type RelayAttributeGroups, type RelayAttributeMatcher, createNip66Aggregator };
package/dist/66.js ADDED
@@ -0,0 +1,9 @@
1
+ import {
2
+ DEFAULT_RELAY_ATTRIBUTE_GROUPS,
3
+ createNip66Aggregator
4
+ } from "./chunk-N4WPLKIJ.js";
5
+ export {
6
+ DEFAULT_RELAY_ATTRIBUTE_GROUPS,
7
+ createNip66Aggregator
8
+ };
9
+ //# sourceMappingURL=66.js.map
package/dist/66.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,141 @@
1
+ // src/66.ts
2
+ var DEFAULT_RELAY_ATTRIBUTE_GROUPS = {
3
+ Indexer: [
4
+ { tag: "T", values: ["Indexer", "indexer"] },
5
+ { tag: "t", values: ["Indexer", "indexer"] },
6
+ { tag: "k", values: ["10002"] }
7
+ ],
8
+ RelayIndexer: [
9
+ { tag: "T", values: ["RelayIndexer", "relay-indexer", "relay_indexer"] },
10
+ { tag: "t", values: ["RelayIndexer", "relay-indexer", "relay_indexer"] },
11
+ { tag: "k", values: ["30166", "10166"] }
12
+ ]
13
+ };
14
+ function createNip66Aggregator(options) {
15
+ const relaySet = /* @__PURE__ */ new Set();
16
+ const relaySupportedNips = /* @__PURE__ */ new Map();
17
+ const relayAttributeTags = /* @__PURE__ */ new Map();
18
+ const relayAttributeValues = /* @__PURE__ */ new Map();
19
+ const relayAttributeGroups = options.relayAttributeGroups ?? DEFAULT_RELAY_ATTRIBUTE_GROUPS;
20
+ let unsubscribe = null;
21
+ function buildFilter() {
22
+ const filter = { kinds: [30166] };
23
+ if (options.networks && options.networks.length > 0) {
24
+ filter["#n"] = options.networks;
25
+ }
26
+ return filter;
27
+ }
28
+ function processEvent(event) {
29
+ const dTag = event.tags.find((t) => t[0] === "d");
30
+ const relayUrl = dTag?.[1];
31
+ if (!relayUrl) return;
32
+ relaySet.add(relayUrl);
33
+ captureRelayAttributes(relayUrl, event.tags);
34
+ const nips = /* @__PURE__ */ new Set();
35
+ for (const tag of event.tags) {
36
+ if (tag[0] === "N" && tag[1]) {
37
+ const nipNum = parseInt(tag[1], 10);
38
+ if (!Number.isNaN(nipNum)) nips.add(nipNum);
39
+ }
40
+ }
41
+ if (nips.size > 0) relaySupportedNips.set(relayUrl, nips);
42
+ }
43
+ function captureRelayAttributes(relayUrl, tags) {
44
+ const tagCopies = tags.map((tag) => [...tag]);
45
+ const values = /* @__PURE__ */ new Map();
46
+ for (const tag of tags) {
47
+ const [name, value] = tag;
48
+ if (!name || value === void 0) continue;
49
+ let bucket = values.get(name);
50
+ if (!bucket) {
51
+ bucket = /* @__PURE__ */ new Set();
52
+ values.set(name, bucket);
53
+ }
54
+ bucket.add(value);
55
+ }
56
+ relayAttributeTags.set(relayUrl, tagCopies);
57
+ relayAttributeValues.set(relayUrl, values);
58
+ }
59
+ function start() {
60
+ if (unsubscribe) return;
61
+ unsubscribe = options.pool.subscribe(options.bootstrap, buildFilter(), processEvent);
62
+ }
63
+ function resync() {
64
+ if (unsubscribe) {
65
+ unsubscribe();
66
+ unsubscribe = null;
67
+ }
68
+ relaySet.clear();
69
+ relaySupportedNips.clear();
70
+ relayAttributeTags.clear();
71
+ relayAttributeValues.clear();
72
+ unsubscribe = options.pool.subscribe(options.bootstrap, buildFilter(), processEvent);
73
+ }
74
+ function stop() {
75
+ if (unsubscribe) {
76
+ unsubscribe();
77
+ unsubscribe = null;
78
+ }
79
+ }
80
+ function getRelaySet() {
81
+ return relaySet;
82
+ }
83
+ function getRelaysSupportingNip(nip) {
84
+ const result = [];
85
+ for (const [url, nips] of relaySupportedNips) {
86
+ if (nips.has(nip)) result.push(url);
87
+ }
88
+ return result;
89
+ }
90
+ function relaySupportsNip(url, nip) {
91
+ return relaySupportedNips.get(url)?.has(nip) ?? false;
92
+ }
93
+ function getRelayAttributes(url) {
94
+ const tagCopies = relayAttributeTags.get(url);
95
+ const values = relayAttributeValues.get(url);
96
+ if (!tagCopies || !values) return void 0;
97
+ return {
98
+ tags: tagCopies.map((tag) => [...tag]),
99
+ values: new Map(Array.from(values, ([tag, tagValues]) => [tag, new Set(tagValues)])),
100
+ supportedNips: new Set(relaySupportedNips.get(url) ?? [])
101
+ };
102
+ }
103
+ function matcherMatches(url, matcher) {
104
+ const values = relayAttributeValues.get(url)?.get(matcher.tag);
105
+ if (!values || values.size === 0) return false;
106
+ if (!matcher.values || matcher.values.length === 0) return true;
107
+ const accepted = new Set(matcher.values.map((value) => value.toLowerCase()));
108
+ for (const value of values) {
109
+ if (accepted.has(value.toLowerCase())) return true;
110
+ }
111
+ return false;
112
+ }
113
+ function getRelaysMatchingAttributes(matchers) {
114
+ const result = [];
115
+ for (const url of relaySet) {
116
+ if (matchers.some((matcher) => matcherMatches(url, matcher))) result.push(url);
117
+ }
118
+ return result;
119
+ }
120
+ function getRelaysForAttributeGroup(groupName, groups) {
121
+ const group = (groups ?? relayAttributeGroups)[groupName] ?? [];
122
+ return getRelaysMatchingAttributes(group);
123
+ }
124
+ return {
125
+ start,
126
+ resync,
127
+ stop,
128
+ getRelaySet,
129
+ getRelaysSupportingNip,
130
+ relaySupportsNip,
131
+ getRelayAttributes,
132
+ getRelaysMatchingAttributes,
133
+ getRelaysForAttributeGroup
134
+ };
135
+ }
136
+
137
+ export {
138
+ DEFAULT_RELAY_ATTRIBUTE_GROUPS,
139
+ createNip66Aggregator
140
+ };
141
+ //# sourceMappingURL=chunk-N4WPLKIJ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/66.ts"],"sourcesContent":["import type { NostrEvent } from 'nostr-tools';\n\n/**\n * Minimal relay-pool contract `@kehto/nip/66` depends on. Consumers implement\n * this against their pool library of choice (nostr-tools SimplePool,\n * applesauce-relay, @snort/worker-relay, etc.). Intentionally smaller than\n * RxJS-style observable pools — one callback, one unsubscribe handle.\n *\n * @example\n * ```ts\n * import { SimplePool } from 'nostr-tools/pool';\n * const pool = new SimplePool();\n * const adapter: Nip66RelayPool = {\n * subscribe: (relays, filter, onEvent) => {\n * const sub = pool.subscribeMany(relays, [filter], { onevent: onEvent });\n * return () => sub.close();\n * },\n * };\n * ```\n */\nexport interface Nip66RelayPool {\n /**\n * Open a kind-30166 subscription against a set of monitor relays.\n *\n * @param relays - WebSocket URLs to subscribe against (the aggregator passes its bootstrap list)\n * @param filter - A kind-30166 filter, optionally narrowed with `#n` network tags\n * @param onEvent - Called synchronously with every matching event; the aggregator parses each one\n * @returns An unsubscribe handle the aggregator calls on `resync()` or teardown\n */\n subscribe(\n relays: ReadonlyArray<string>,\n filter: Nip66Filter,\n onEvent: (event: NostrEvent) => void,\n ): () => void;\n}\n\n/**\n * Minimal filter shape for kind-30166 subscriptions. `#n` optionally narrows\n * by network type (e.g. `['clearnet']`, `['clearnet', 'tor']`).\n *\n * @example\n * ```ts\n * const filter: Nip66Filter = { kinds: [30166], '#n': ['clearnet'] };\n * ```\n */\nexport type Nip66Filter = { kinds: [30166]; '#n'?: ReadonlyArray<string> };\n\n/**\n * Options for {@link createNip66Aggregator}.\n *\n * @example\n * ```ts\n * const options: Nip66AggregatorOptions = {\n * pool: myPoolAdapter,\n * bootstrap: ['wss://monitor1.example.com', 'wss://monitor2.example.com'],\n * networks: ['clearnet'],\n * };\n * ```\n */\nexport interface Nip66AggregatorOptions {\n /** A {@link Nip66RelayPool} adapter supplied by the consumer. */\n pool: Nip66RelayPool;\n /** Required non-empty list of monitor relay URLs to subscribe against. No default ships in the package. */\n bootstrap: ReadonlyArray<string>;\n /** Optional network-type narrowing passed to the filter as `#n` (e.g. `['clearnet', 'tor']`). Omit to receive every network. */\n networks?: ReadonlyArray<string>;\n /**\n * Optional named relay-attribute groups for interpreting ad hoc kind-30166 tags.\n * Omit to use {@link DEFAULT_RELAY_ATTRIBUTE_GROUPS}.\n */\n relayAttributeGroups?: RelayAttributeGroups;\n}\n\n/** A tag/value matcher for ad hoc relay attributes carried on kind-30166 events. */\nexport interface RelayAttributeMatcher {\n /** Tag name to match, for example `T`, `t`, or `k`. */\n tag: string;\n /** Optional accepted values. If omitted, any value for the tag matches. */\n values?: ReadonlyArray<string>;\n}\n\n/** Named groups of relay-attribute matchers. Matchers within a group are OR-ed. */\nexport type RelayAttributeGroups = Readonly<Record<string, ReadonlyArray<RelayAttributeMatcher>>>;\n\n/** Parsed attributes observed for one NIP-66 relay URL. */\nexport interface Nip66RelayAttributes {\n /** Raw tag copies from the latest observed kind-30166 event for the relay. */\n tags: ReadonlyArray<ReadonlyArray<string>>;\n /** Tag-value index, keyed by tag name. */\n values: ReadonlyMap<string, ReadonlySet<string>>;\n /** Parsed uppercase `N` tag support for convenience. */\n supportedNips: ReadonlySet<number>;\n}\n\nexport const DEFAULT_RELAY_ATTRIBUTE_GROUPS: RelayAttributeGroups = {\n Indexer: [\n { tag: 'T', values: ['Indexer', 'indexer'] },\n { tag: 't', values: ['Indexer', 'indexer'] },\n { tag: 'k', values: ['10002'] },\n ],\n RelayIndexer: [\n { tag: 'T', values: ['RelayIndexer', 'relay-indexer', 'relay_indexer'] },\n { tag: 't', values: ['RelayIndexer', 'relay-indexer', 'relay_indexer'] },\n { tag: 'k', values: ['30166', '10166'] },\n ],\n};\n\n/**\n * Aggregator handle returned by {@link createNip66Aggregator}. Each call\n * returns a fresh instance with its own closure-scoped state — multi-instance\n * safe (unlike a module-globals pattern).\n *\n * @example\n * ```ts\n * const aggregator = createNip66Aggregator({ pool, bootstrap: ['wss://monitor.example.com'] });\n * aggregator.start();\n * const relays = aggregator.getRelaySet();\n * ```\n */\nexport interface Nip66Aggregator {\n /** Begin subscribing against the bootstrap relay set. Idempotent — calling twice is a no-op. */\n start(): void;\n /** Tear down the current subscription, clear accumulated state, and re-subscribe. Call when `networks` semantics change upstream. */\n resync(): void;\n /**\n * Tear down the current subscription. Idempotent — calling without a prior\n * `start()` is a safe no-op. Preserves accumulated state (`getRelaySet`,\n * `relaySupportsNip`) — use `resync()` if you need to clear state AND\n * re-subscribe in one step. A subsequent `start()` re-subscribes against\n * the bootstrap relays (identical contract to the first start).\n *\n * Wire this to your consumer's teardown path (browser: `beforeunload`;\n * React: effect cleanup; Electron: `app.before-quit`) — the aggregator\n * holds a pool subscription that otherwise lives for the tab lifetime\n * (PITFALLS.md M-03).\n */\n stop(): void;\n /** The current set of relay URLs discovered from kind-30166 `d`-tags. Empty until `start()` fires at least one event. */\n getRelaySet(): ReadonlySet<string>;\n /**\n * Relay URLs whose `N`-tags declare support for the given NIP number.\n *\n * @param nip - NIP number to query support for (e.g. 44 for NIP-44 encrypted DMs)\n * @returns Array of relay URLs supporting that NIP\n */\n getRelaysSupportingNip(nip: number): string[];\n /**\n * Whether the relay at `url` has been seen with an `N`-tag declaring support for `nip`.\n *\n * @param url - Relay WebSocket URL to check\n * @param nip - NIP number to query support for\n * @returns `true` iff the relay has been observed with a matching `N`-tag\n */\n relaySupportsNip(url: string, nip: number): boolean;\n /**\n * Return parsed relay attributes for a relay URL, if that relay has been\n * observed in a kind-30166 event.\n */\n getRelayAttributes(url: string): Nip66RelayAttributes | undefined;\n /**\n * Return relays matching any supplied ad hoc attribute matcher.\n *\n * @param matchers - Tag/value matchers, OR semantics\n */\n getRelaysMatchingAttributes(matchers: ReadonlyArray<RelayAttributeMatcher>): string[];\n /**\n * Return relays in a named attribute group. Uses the group table passed to\n * `createNip66Aggregator`, or {@link DEFAULT_RELAY_ATTRIBUTE_GROUPS}.\n */\n getRelaysForAttributeGroup(groupName: string, groups?: RelayAttributeGroups): string[];\n}\n\n/**\n * Create a NIP-66 kind-30166 relay-discovery aggregator with closure-scoped state.\n *\n * Subscribes to kind-30166 events via the injected {@link Nip66RelayPool},\n * extracts relay URLs from `d`-tags, and parses `N`-tags to track which NIP\n * numbers each relay supports. Multi-instance safe — each factory call owns\n * its own `Set` + `Map`; multiple aggregators share nothing at module scope.\n *\n * The aggregator is framework-agnostic: it knows nothing about negentropy\n * (NIP-77), OPFS caching, worker relays, or network-type config stores.\n * Consumers inject those concerns through their {@link Nip66RelayPool}\n * implementation. `start()` is synchronous and idempotent — consumers\n * schedule their own cadence (delayed start, retry, etc.).\n *\n * @param options - Pool adapter + bootstrap monitor relays (+ optional network filter)\n * @returns A fresh {@link Nip66Aggregator} handle\n *\n * @example\n * ```ts\n * import { createNip66Aggregator } from '@kehto/nip/66';\n * import { SimplePool } from 'nostr-tools/pool';\n *\n * const pool = new SimplePool();\n * const aggregator = createNip66Aggregator({\n * pool: {\n * subscribe: (relays, filter, onEvent) => {\n * const sub = pool.subscribeMany(relays, [filter], { onevent: onEvent });\n * return () => sub.close();\n * },\n * },\n * bootstrap: ['wss://monitor1.example.com'],\n * });\n * aggregator.start();\n * // Later — inside a ShellAdapter hook:\n * const suggestions = Array.from(aggregator.getRelaySet());\n * ```\n */\nexport function createNip66Aggregator(options: Nip66AggregatorOptions): Nip66Aggregator {\n const relaySet = new Set<string>();\n const relaySupportedNips = new Map<string, Set<number>>();\n const relayAttributeTags = new Map<string, string[][]>();\n const relayAttributeValues = new Map<string, Map<string, Set<string>>>();\n const relayAttributeGroups = options.relayAttributeGroups ?? DEFAULT_RELAY_ATTRIBUTE_GROUPS;\n let unsubscribe: (() => void) | null = null;\n\n function buildFilter(): Nip66Filter {\n const filter: Nip66Filter = { kinds: [30166] };\n if (options.networks && options.networks.length > 0) {\n filter['#n'] = options.networks;\n }\n return filter;\n }\n\n function processEvent(event: NostrEvent): void {\n const dTag = event.tags.find((t) => t[0] === 'd');\n const relayUrl = dTag?.[1];\n if (!relayUrl) return;\n relaySet.add(relayUrl);\n captureRelayAttributes(relayUrl, event.tags);\n\n const nips = new Set<number>();\n for (const tag of event.tags) {\n if (tag[0] === 'N' && tag[1]) {\n const nipNum = parseInt(tag[1], 10);\n if (!Number.isNaN(nipNum)) nips.add(nipNum);\n }\n }\n if (nips.size > 0) relaySupportedNips.set(relayUrl, nips);\n }\n\n function captureRelayAttributes(relayUrl: string, tags: string[][]): void {\n const tagCopies = tags.map((tag) => [...tag]);\n const values = new Map<string, Set<string>>();\n for (const tag of tags) {\n const [name, value] = tag;\n if (!name || value === undefined) continue;\n let bucket = values.get(name);\n if (!bucket) {\n bucket = new Set<string>();\n values.set(name, bucket);\n }\n bucket.add(value);\n }\n relayAttributeTags.set(relayUrl, tagCopies);\n relayAttributeValues.set(relayUrl, values);\n }\n\n function start(): void {\n if (unsubscribe) return; // idempotent — already subscribed\n unsubscribe = options.pool.subscribe(options.bootstrap, buildFilter(), processEvent);\n }\n\n function resync(): void {\n if (unsubscribe) {\n unsubscribe();\n unsubscribe = null;\n }\n relaySet.clear();\n relaySupportedNips.clear();\n relayAttributeTags.clear();\n relayAttributeValues.clear();\n unsubscribe = options.pool.subscribe(options.bootstrap, buildFilter(), processEvent);\n }\n\n function stop(): void {\n if (unsubscribe) {\n unsubscribe();\n unsubscribe = null;\n }\n }\n\n function getRelaySet(): ReadonlySet<string> {\n return relaySet;\n }\n\n function getRelaysSupportingNip(nip: number): string[] {\n const result: string[] = [];\n for (const [url, nips] of relaySupportedNips) {\n if (nips.has(nip)) result.push(url);\n }\n return result;\n }\n\n function relaySupportsNip(url: string, nip: number): boolean {\n return relaySupportedNips.get(url)?.has(nip) ?? false;\n }\n\n function getRelayAttributes(url: string): Nip66RelayAttributes | undefined {\n const tagCopies = relayAttributeTags.get(url);\n const values = relayAttributeValues.get(url);\n if (!tagCopies || !values) return undefined;\n return {\n tags: tagCopies.map((tag) => [...tag]),\n values: new Map(Array.from(values, ([tag, tagValues]) => [tag, new Set(tagValues)])),\n supportedNips: new Set(relaySupportedNips.get(url) ?? []),\n };\n }\n\n function matcherMatches(url: string, matcher: RelayAttributeMatcher): boolean {\n const values = relayAttributeValues.get(url)?.get(matcher.tag);\n if (!values || values.size === 0) return false;\n if (!matcher.values || matcher.values.length === 0) return true;\n const accepted = new Set(matcher.values.map((value) => value.toLowerCase()));\n for (const value of values) {\n if (accepted.has(value.toLowerCase())) return true;\n }\n return false;\n }\n\n function getRelaysMatchingAttributes(matchers: ReadonlyArray<RelayAttributeMatcher>): string[] {\n const result: string[] = [];\n for (const url of relaySet) {\n if (matchers.some((matcher) => matcherMatches(url, matcher))) result.push(url);\n }\n return result;\n }\n\n function getRelaysForAttributeGroup(groupName: string, groups?: RelayAttributeGroups): string[] {\n const group = (groups ?? relayAttributeGroups)[groupName] ?? [];\n return getRelaysMatchingAttributes(group);\n }\n\n return {\n start,\n resync,\n stop,\n getRelaySet,\n getRelaysSupportingNip,\n relaySupportsNip,\n getRelayAttributes,\n getRelaysMatchingAttributes,\n getRelaysForAttributeGroup,\n };\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,2 @@
1
+ export { DEFAULT_RELAY_ATTRIBUTE_GROUPS, Nip66Aggregator, Nip66AggregatorOptions, Nip66Filter, Nip66RelayAttributes, Nip66RelayPool, RelayAttributeGroups, RelayAttributeMatcher, createNip66Aggregator } from './66.js';
2
+ import 'nostr-tools';
package/dist/index.js ADDED
@@ -0,0 +1,9 @@
1
+ import {
2
+ DEFAULT_RELAY_ATTRIBUTE_GROUPS,
3
+ createNip66Aggregator
4
+ } from "./chunk-N4WPLKIJ.js";
5
+ export {
6
+ DEFAULT_RELAY_ATTRIBUTE_GROUPS,
7
+ createNip66Aggregator
8
+ };
9
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@kehto/nip",
3
+ "version": "0.1.0",
4
+ "description": "Tree-shakable bundle of unique Nostr NIP utilities for napplet runtimes — each NIP at its own subpath (e.g. @kehto/nip/66)",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js"
13
+ },
14
+ "./66": {
15
+ "types": "./dist/66.d.ts",
16
+ "import": "./dist/66.js"
17
+ }
18
+ },
19
+ "files": [
20
+ "dist"
21
+ ],
22
+ "sideEffects": false,
23
+ "publishConfig": {
24
+ "access": "public"
25
+ },
26
+ "peerDependencies": {
27
+ "nostr-tools": ">=2.23.3 <3.0.0"
28
+ },
29
+ "devDependencies": {
30
+ "nostr-tools": "^2.23.3",
31
+ "tsup": "^8.5.0",
32
+ "typescript": "^5.9.3",
33
+ "vitest": "^4.1.2"
34
+ },
35
+ "license": "MIT",
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "git+https://github.com/kehto/web.git",
39
+ "directory": "packages/nip"
40
+ },
41
+ "keywords": [
42
+ "nostr",
43
+ "nip",
44
+ "nip66",
45
+ "relay-discovery",
46
+ "kehto",
47
+ "napplet"
48
+ ],
49
+ "scripts": {
50
+ "build": "tsup",
51
+ "type-check": "tsc --noEmit",
52
+ "test:unit": "cd ../.. && vitest run --config vitest.config.ts packages/nip/src/66.test.ts",
53
+ "test": "pnpm test:unit"
54
+ }
55
+ }