@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 +37 -0
- package/dist/66.d.ts +192 -0
- package/dist/66.js +9 -0
- package/dist/66.js.map +1 -0
- package/dist/chunk-N4WPLKIJ.js +141 -0
- package/dist/chunk-N4WPLKIJ.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/package.json +55 -0
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
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":[]}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -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
|
+
}
|