nostr-wot-sdk 0.6.0 → 0.7.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/CHANGELOG.md CHANGED
@@ -5,6 +5,46 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.7.0] - 2026-02-23
9
+
10
+ ### Added
11
+
12
+ - **Relay utilities subpackage** (`nostr-wot-sdk/relay`) — Reusable relay infrastructure extracted from nostr-wot-feed
13
+ - `QueryBatcher` — Debounced query batching with progressive relay queries and automatic filter merging
14
+ - `RelayPool` — Pool lifecycle management, subscriptions, publishing, status tracking, and NIP-65 relay list fetching
15
+ - `RelayStats` — Per-relay performance tracking (latency, success rate, exponential backoff) with optional persistence adapter
16
+ - All classes are pool-agnostic via a `PoolLike` interface that duck-types against `SimplePool` from nostr-tools
17
+ - All timing constants are configurable via options with sensible defaults
18
+
19
+ - **Relay React integration** (`nostr-wot-sdk/relay/react`)
20
+ - `RelayProvider` — Context provider that manages RelayPool, QueryBatcher, and RelayStats lifecycle
21
+ - `useRelayPool()` — Access the RelayPool instance
22
+ - `useQueryBatcher()` — Access the QueryBatcher instance
23
+ - `useRelayStats()` — Access the RelayStats instance
24
+ - `useRelayStatuses()` — Reactive relay connection statuses and connected count
25
+
26
+ - `nostr-tools` as optional peer dependency (`>=2.0.0`)
27
+
28
+ - New types: `PoolLike`, `SubCloser`, `NostrEvent`, `NostrFilter`, `QueryBatcherOptions`, `QueryOptions`, `RelayPoolOptions`, `RelayStatus`, `RelayStatsOptions`, `RelayMetrics`, `RelayStatsData`, `RelayStatsPersistence`
29
+
30
+ ## [0.6.2] - 2026-02-22
31
+
32
+ ### Fixed
33
+
34
+ - **CI build failure** — Added `esbuild-plugin-solid` to `devDependencies` so it's available in CI environments
35
+
36
+ ## [0.6.1] - 2026-02-22
37
+
38
+ ### Changed
39
+
40
+ - **Default oracle URL** changed from `nostr-wot.com` to `wot-oracle.mappingbitcoin.com`
41
+ - **Oracle URL validation** now accepts both HTTP and HTTPS URLs
42
+
43
+ ### Fixed
44
+
45
+ - **SolidJS primitives** - Added `{ defer: false }` to `createEffect` calls in `createWoT`, `createIsInWoT`, `createTrustScore`, and `createBatchWoT` to ensure effects run immediately on mount
46
+ - **SolidJS build** - Added `esbuild-plugin-solid` to tsup config for proper JSX compilation
47
+
8
48
  ## [0.6.0] - 2026-02-22
9
49
 
10
50
  ### Added
@@ -252,6 +292,9 @@ function Profile({ pubkey }) {
252
292
  - TypeScript support with full type definitions
253
293
  - Error classes: `WoTError`, `NetworkError`, `NotFoundError`, `TimeoutError`, `ValidationError`
254
294
 
295
+ [0.7.0]: https://github.com/nostr-wot/nostr-wot-sdk/compare/v0.6.2...v0.7.0
296
+ [0.6.2]: https://github.com/nostr-wot/nostr-wot-sdk/compare/v0.6.1...v0.6.2
297
+ [0.6.1]: https://github.com/nostr-wot/nostr-wot-sdk/compare/v0.6.0...v0.6.1
255
298
  [0.6.0]: https://github.com/nostr-wot/nostr-wot-sdk/compare/v0.5.3...v0.6.0
256
299
  [0.5.3]: https://github.com/nostr-wot/nostr-wot-sdk/compare/v0.5.2...v0.5.3
257
300
  [0.5.2]: https://github.com/nostr-wot/nostr-wot-sdk/compare/v0.5.1...v0.5.2
package/README.md CHANGED
@@ -59,6 +59,7 @@ const hops = await wot.getDistance('def456...');
59
59
  - **Cross-Site Trust** — Extension provides same WoT data on all websites
60
60
  - **Offline Support** — Extension caches data locally for offline queries
61
61
  - **Batch Queries** — Check multiple pubkeys efficiently
62
+ - **Relay Utilities** — Reusable relay pool, query batching, and stats tracking
62
63
  - **TypeScript** — Full type definitions included
63
64
 
64
65
  ## API Reference
@@ -443,6 +444,202 @@ ext.isChecked() // Check complete
443
444
  ext.refresh() // Function to re-check extension availability
444
445
  ```
445
446
 
447
+ ## Relay Utilities
448
+
449
+ The SDK includes a standalone relay subpackage for managing Nostr relay connections, batching queries, and tracking relay performance. These utilities are pool-agnostic and work with any `PoolLike` implementation (e.g. `SimplePool` from nostr-tools).
450
+
451
+ ```bash
452
+ npm install nostr-wot-sdk nostr-tools
453
+ ```
454
+
455
+ ### Basic Usage
456
+
457
+ ```javascript
458
+ import { QueryBatcher, RelayPool, RelayStats } from 'nostr-wot-sdk/relay';
459
+ import { SimplePool } from 'nostr-tools';
460
+
461
+ // Optional: track relay performance
462
+ const stats = new RelayStats();
463
+ await stats.init(); // works in-memory, or pass a persistence adapter
464
+
465
+ // Create a relay pool
466
+ const pool = new RelayPool({
467
+ urls: ['wss://relay.damus.io', 'wss://nos.lol'],
468
+ prioritizeUrls: (urls) => stats.getPrioritizedUrls(urls),
469
+ onStatusChange: (statuses) => console.log('Relay statuses:', statuses),
470
+ });
471
+
472
+ // Initialize with SimplePool
473
+ pool.ensurePool(() => new SimplePool());
474
+
475
+ // Query with automatic batching + progressive results
476
+ const events = await pool.query(
477
+ { kinds: [1], limit: 50 },
478
+ { onUpdate: (partial) => renderNotes(partial) }
479
+ );
480
+
481
+ // Subscribe to live events
482
+ const sub = pool.subscribe({ kinds: [1], since: Math.floor(Date.now() / 1000) }, {
483
+ onEvent: (event) => console.log('New note:', event.content),
484
+ onEose: () => console.log('Caught up'),
485
+ });
486
+
487
+ // Clean up
488
+ sub.close();
489
+ pool.destroy();
490
+ stats.destroy();
491
+ ```
492
+
493
+ ### QueryBatcher
494
+
495
+ Debounces and merges concurrent relay queries for efficiency. Queries made within the debounce window are batched together, compatible filters are merged, and results stream progressively.
496
+
497
+ ```javascript
498
+ import { QueryBatcher } from 'nostr-wot-sdk/relay';
499
+ import { SimplePool } from 'nostr-tools';
500
+
501
+ const batcher = new QueryBatcher(new SimplePool(), {
502
+ debounceMs: 100, // batch window (default: 100)
503
+ collectionWindowMs: 200, // wait after first event (default: 200)
504
+ maxWaitMs: 5000, // hard timeout (default: 5000)
505
+ });
506
+
507
+ // These concurrent queries get merged into fewer relay requests
508
+ const [profiles, contacts] = await Promise.all([
509
+ batcher.query(['wss://relay.damus.io'], { kinds: [0], authors: pubkeys }),
510
+ batcher.query(['wss://relay.damus.io'], { kinds: [3], authors: pubkeys }),
511
+ ]);
512
+
513
+ // For user-initiated actions, bypass the debounce
514
+ const notes = await batcher.queryImmediate(urls, { kinds: [1], limit: 20 });
515
+
516
+ batcher.destroy();
517
+ ```
518
+
519
+ ### RelayStats
520
+
521
+ Tracks per-relay latency, success rate, and applies exponential backoff to failing relays.
522
+
523
+ ```javascript
524
+ import { RelayStats } from 'nostr-wot-sdk/relay';
525
+
526
+ const stats = new RelayStats({ maxBackoffMs: 30000 });
527
+
528
+ // Optional: load persisted stats (e.g. from IndexedDB)
529
+ await stats.init({
530
+ load: async () => db.getAll('relayStats'),
531
+ save: async (data) => db.putAll('relayStats', data),
532
+ });
533
+
534
+ // Record relay performance
535
+ stats.recordSuccess('wss://relay.damus.io', 150); // 150ms latency
536
+ stats.recordFailure('wss://slow.relay.com', 'timeout');
537
+
538
+ // Get URLs sorted by reliability + speed
539
+ const prioritized = stats.getPrioritizedUrls([
540
+ 'wss://relay.damus.io',
541
+ 'wss://slow.relay.com',
542
+ 'wss://nos.lol',
543
+ ]);
544
+
545
+ // Check backoff status
546
+ stats.isBackedOff('wss://slow.relay.com'); // true
547
+
548
+ stats.destroy();
549
+ ```
550
+
551
+ ### RelayPool
552
+
553
+ Manages pool lifecycle, subscriptions, publishing, and NIP-65 relay list fetching.
554
+
555
+ ```javascript
556
+ import { RelayPool } from 'nostr-wot-sdk/relay';
557
+ import { SimplePool } from 'nostr-tools';
558
+
559
+ const pool = new RelayPool({
560
+ urls: ['wss://relay.damus.io', 'wss://nos.lol'],
561
+ authorChunkSize: 150, // chunk large author lists
562
+ onRelaysChanged: (urls) => saveToSettings(urls),
563
+ });
564
+
565
+ pool.ensurePool(() => new SimplePool());
566
+
567
+ // Subscribe to notes from followed authors (auto-chunks large lists)
568
+ const sub = pool.subscribeAuthors(followedPubkeys, { kinds: [1], since, limit: 200 },
569
+ (event) => addToFeed(event),
570
+ () => console.log('Caught up')
571
+ );
572
+
573
+ // Publish events
574
+ await pool.publish(signedEvent);
575
+
576
+ // Fetch a user's NIP-65 relay list
577
+ const userRelays = await pool.fetchUserRelays(pubkey);
578
+
579
+ // Manage relays
580
+ pool.addRelay('wss://new.relay.com');
581
+ pool.removeRelay('wss://old.relay.com');
582
+
583
+ pool.destroy();
584
+ ```
585
+
586
+ ### React Integration
587
+
588
+ ```javascript
589
+ import { RelayProvider, useRelayPool, useRelayStatuses } from 'nostr-wot-sdk/relay/react';
590
+ import { SimplePool } from 'nostr-tools';
591
+
592
+ function App() {
593
+ return (
594
+ <RelayProvider
595
+ urls={['wss://relay.damus.io', 'wss://nos.lol']}
596
+ createPool={() => new SimplePool()}
597
+ enableStats={true}
598
+ >
599
+ <Feed />
600
+ </RelayProvider>
601
+ );
602
+ }
603
+
604
+ function Feed() {
605
+ const pool = useRelayPool();
606
+ const { statuses, connectedCount } = useRelayStatuses();
607
+
608
+ useEffect(() => {
609
+ pool.query({ kinds: [1], limit: 30 }).then(setNotes);
610
+ }, [pool]);
611
+
612
+ return (
613
+ <div>
614
+ <span>{connectedCount} relays connected</span>
615
+ {/* render notes */}
616
+ </div>
617
+ );
618
+ }
619
+ ```
620
+
621
+ #### Provider Props
622
+
623
+ | Prop | Type | Default | Description |
624
+ |------|------|---------|-------------|
625
+ | `urls` | `string[]` | required | Relay WebSocket URLs |
626
+ | `createPool` | `() => PoolLike` | required | Factory to create pool instance |
627
+ | `poolOptions` | `Partial<RelayPoolOptions>` | — | Options for RelayPool |
628
+ | `batcherOptions` | `QueryBatcherOptions` | — | Options for QueryBatcher |
629
+ | `statsPersistence` | `RelayStatsPersistence` | — | Persistence adapter for stats |
630
+ | `statsOptions` | `RelayStatsOptions` | — | Options for RelayStats |
631
+ | `enableStats` | `boolean` | `true` | Enable relay performance tracking |
632
+
633
+ #### Available Hooks
634
+
635
+ | Hook | Returns | Description |
636
+ |------|---------|-------------|
637
+ | `useRelayPool()` | `RelayPool` | Access the RelayPool instance |
638
+ | `useQueryBatcher()` | `QueryBatcher` | Access the QueryBatcher instance |
639
+ | `useRelayStats()` | `RelayStats \| null` | Access RelayStats (null if disabled) |
640
+ | `useRelayStatuses()` | `{ statuses, connectedCount }` | Reactive connection statuses |
641
+ | `useRelayContext()` | `RelayContextValue` | Full context with all instances |
642
+
446
643
  ## TypeScript
447
644
 
448
645
  Full type definitions included:
package/dist/index.cjs CHANGED
@@ -46,7 +46,7 @@ var ValidationError = class extends WoTError {
46
46
  };
47
47
 
48
48
  // src/utils.ts
49
- var DEFAULT_ORACLE = "https://nostr-wot.com";
49
+ var DEFAULT_ORACLE = "https://wot-oracle.mappingbitcoin.com";
50
50
  var DEFAULT_MAX_HOPS = 3;
51
51
  var DEFAULT_TIMEOUT = 5e3;
52
52
  function isValidPubkey(pubkey) {
@@ -55,7 +55,7 @@ function isValidPubkey(pubkey) {
55
55
  function isValidOracleUrl(url) {
56
56
  try {
57
57
  const parsed = new URL(url);
58
- return parsed.protocol === "https:";
58
+ return parsed.protocol === "https:" || parsed.protocol === "http:";
59
59
  } catch (e) {
60
60
  return false;
61
61
  }
package/dist/index.d.cts CHANGED
@@ -30,7 +30,7 @@ interface ScoringConfig {
30
30
  interface WoTFallbackOptions {
31
31
  /**
32
32
  * Oracle API URL
33
- * @default 'https://nostr-wot.com'
33
+ * @default 'https://wot-oracle.mappingbitcoin.com'
34
34
  */
35
35
  oracle?: string;
36
36
  /**
@@ -54,7 +54,7 @@ interface WoTFallbackOptions {
54
54
  interface WoTOptions {
55
55
  /**
56
56
  * Oracle API URL (used when extension is not available)
57
- * @default 'https://nostr-wot.com'
57
+ * @default 'https://wot-oracle.mappingbitcoin.com'
58
58
  */
59
59
  oracle?: string;
60
60
  /**
@@ -594,7 +594,7 @@ declare class ValidationError extends WoTError {
594
594
  /**
595
595
  * Default oracle URL
596
596
  */
597
- declare const DEFAULT_ORACLE = "https://nostr-wot.com";
597
+ declare const DEFAULT_ORACLE = "https://wot-oracle.mappingbitcoin.com";
598
598
  /**
599
599
  * Default max hops for WoT queries
600
600
  */
@@ -608,7 +608,7 @@ declare const DEFAULT_TIMEOUT = 5000;
608
608
  */
609
609
  declare function isValidPubkey(pubkey: string): boolean;
610
610
  /**
611
- * Validates an oracle URL (must be HTTPS)
611
+ * Validates an oracle URL (HTTP or HTTPS)
612
612
  */
613
613
  declare function isValidOracleUrl(url: string): boolean;
614
614
  /**
package/dist/index.d.ts CHANGED
@@ -30,7 +30,7 @@ interface ScoringConfig {
30
30
  interface WoTFallbackOptions {
31
31
  /**
32
32
  * Oracle API URL
33
- * @default 'https://nostr-wot.com'
33
+ * @default 'https://wot-oracle.mappingbitcoin.com'
34
34
  */
35
35
  oracle?: string;
36
36
  /**
@@ -54,7 +54,7 @@ interface WoTFallbackOptions {
54
54
  interface WoTOptions {
55
55
  /**
56
56
  * Oracle API URL (used when extension is not available)
57
- * @default 'https://nostr-wot.com'
57
+ * @default 'https://wot-oracle.mappingbitcoin.com'
58
58
  */
59
59
  oracle?: string;
60
60
  /**
@@ -594,7 +594,7 @@ declare class ValidationError extends WoTError {
594
594
  /**
595
595
  * Default oracle URL
596
596
  */
597
- declare const DEFAULT_ORACLE = "https://nostr-wot.com";
597
+ declare const DEFAULT_ORACLE = "https://wot-oracle.mappingbitcoin.com";
598
598
  /**
599
599
  * Default max hops for WoT queries
600
600
  */
@@ -608,7 +608,7 @@ declare const DEFAULT_TIMEOUT = 5000;
608
608
  */
609
609
  declare function isValidPubkey(pubkey: string): boolean;
610
610
  /**
611
- * Validates an oracle URL (must be HTTPS)
611
+ * Validates an oracle URL (HTTP or HTTPS)
612
612
  */
613
613
  declare function isValidOracleUrl(url: string): boolean;
614
614
  /**
package/dist/index.js CHANGED
@@ -44,7 +44,7 @@ var ValidationError = class extends WoTError {
44
44
  };
45
45
 
46
46
  // src/utils.ts
47
- var DEFAULT_ORACLE = "https://nostr-wot.com";
47
+ var DEFAULT_ORACLE = "https://wot-oracle.mappingbitcoin.com";
48
48
  var DEFAULT_MAX_HOPS = 3;
49
49
  var DEFAULT_TIMEOUT = 5e3;
50
50
  function isValidPubkey(pubkey) {
@@ -53,7 +53,7 @@ function isValidPubkey(pubkey) {
53
53
  function isValidOracleUrl(url) {
54
54
  try {
55
55
  const parsed = new URL(url);
56
- return parsed.protocol === "https:";
56
+ return parsed.protocol === "https:" || parsed.protocol === "http:";
57
57
  } catch (e) {
58
58
  return false;
59
59
  }
@@ -51,7 +51,7 @@ var ValidationError = class extends WoTError {
51
51
  };
52
52
 
53
53
  // src/utils.ts
54
- var DEFAULT_ORACLE = "https://nostr-wot.com";
54
+ var DEFAULT_ORACLE = "https://wot-oracle.mappingbitcoin.com";
55
55
  var DEFAULT_MAX_HOPS = 3;
56
56
  var DEFAULT_TIMEOUT = 5e3;
57
57
  function isValidPubkey(pubkey) {
@@ -60,7 +60,7 @@ function isValidPubkey(pubkey) {
60
60
  function isValidOracleUrl(url) {
61
61
  try {
62
62
  const parsed = new URL(url);
63
- return parsed.protocol === "https:";
63
+ return parsed.protocol === "https:" || parsed.protocol === "http:";
64
64
  } catch (e) {
65
65
  return false;
66
66
  }