@xtr-dev/rondevu-client 0.21.6 → 0.21.8

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.
@@ -20,3 +20,4 @@ export { ICE_SERVER_PRESETS } from './ice-config.js';
20
20
  export type { RondevuOptions, OfferOptions, OfferHandle, DiscoverOptions, DiscoverResult, } from './rondevu.js';
21
21
  export type { PeerState, PeerOptions } from './peer.js';
22
22
  export type { IceServerPreset } from './ice-config.js';
23
+ export type { KeyPair, CryptoAdapter } from '../crypto/adapter.js';
@@ -20,6 +20,12 @@ export interface OfferPoolOptions {
20
20
  webrtcAdapter: WebRTCAdapter;
21
21
  connectionConfig?: Partial<ConnectionConfig>;
22
22
  debugEnabled?: boolean;
23
+ /**
24
+ * Delay in milliseconds between creating each offer during pool filling.
25
+ * Helps avoid rate limiting when creating multiple offers.
26
+ * Default: 100ms
27
+ */
28
+ offerCreationThrottleMs?: number;
23
29
  }
24
30
  interface OfferPoolEvents {
25
31
  'connection:opened': (offerId: string, connection: OffererConnection, matchedTags?: string[]) => void;
@@ -44,6 +50,7 @@ export declare class OfferPool extends EventEmitter<OfferPoolEvents> {
44
50
  private readonly webrtcAdapter;
45
51
  private readonly connectionConfig?;
46
52
  private readonly debugEnabled;
53
+ private readonly offerCreationThrottleMs;
47
54
  private readonly activeConnections;
48
55
  private readonly matchedTagsByOffer;
49
56
  private readonly fillLock;
@@ -25,6 +25,7 @@ export class OfferPool extends EventEmitter {
25
25
  this.iceTransportPolicy = options.iceTransportPolicy;
26
26
  this.connectionConfig = options.connectionConfig;
27
27
  this.debugEnabled = options.debugEnabled || false;
28
+ this.offerCreationThrottleMs = options.offerCreationThrottleMs ?? 100;
28
29
  }
29
30
  /**
30
31
  * Start filling offers
@@ -116,6 +117,10 @@ export class OfferPool extends EventEmitter {
116
117
  for (let i = 0; i < needed; i++) {
117
118
  try {
118
119
  await this.createOffer();
120
+ // Throttle between offer creations to avoid rate limiting
121
+ if (i < needed - 1 && this.offerCreationThrottleMs > 0) {
122
+ await new Promise(resolve => setTimeout(resolve, this.offerCreationThrottleMs));
123
+ }
119
124
  }
120
125
  catch (err) {
121
126
  console.error('[OfferPool] Failed to create offer:', err);
@@ -59,6 +59,8 @@ export interface OfferOptions {
59
59
  connectionConfig?: Partial<ConnectionConfig>;
60
60
  /** Auto-start filling offers (default: true). Set to false to manually call startFilling() */
61
61
  autoStart?: boolean;
62
+ /** Delay in ms between creating each offer during pool filling (default: 100). Helps avoid rate limiting. */
63
+ offerCreationThrottleMs?: number;
62
64
  }
63
65
  /**
64
66
  * Handle returned by rondevu.offer() for controlling the offer lifecycle
@@ -1,4 +1,5 @@
1
1
  import { KeyPair, IceCandidate } from '../api/client.js';
2
+ import { CryptoAdapter } from '../crypto/adapter.js';
2
3
  import { WebRTCAdapter } from '../webrtc/adapter.js';
3
4
  import { EventEmitter } from 'eventemitter3';
4
5
  import { OffererConnection } from '../connections/offerer.js';
@@ -111,6 +112,11 @@ export declare class Rondevu extends EventEmitter {
111
112
  * Used internally by offer pool and connections
112
113
  */
113
114
  getWebRTCAdapter(): WebRTCAdapter;
115
+ /**
116
+ * Get the crypto adapter for signing/verification operations
117
+ * Used for meta.json signing and other crypto operations
118
+ */
119
+ getCryptoAdapter(): CryptoAdapter;
114
120
  /**
115
121
  * Default offer factory - creates a simple data channel connection
116
122
  * The RTCPeerConnection is created by Rondevu and passed in
@@ -1,4 +1,5 @@
1
1
  import { RondevuAPI } from '../api/client.js';
2
+ import { WebCryptoAdapter } from '../crypto/web.js';
2
3
  import { BrowserWebRTCAdapter } from '../webrtc/browser.js';
3
4
  import { EventEmitter } from 'eventemitter3';
4
5
  import { OfferPool } from './offer-pool.js';
@@ -122,12 +123,14 @@ export class Rondevu extends EventEmitter {
122
123
  iceTransportPolicy: iceConfig.iceTransportPolicy || 'all',
123
124
  });
124
125
  }
126
+ // Ensure crypto adapter is available (default to WebCryptoAdapter for browser)
127
+ const cryptoAdapter = options.cryptoAdapter || new WebCryptoAdapter();
125
128
  // Generate keypair if not provided (purely client-side, no server registration)
126
129
  let keyPair = options.keyPair;
127
130
  if (!keyPair) {
128
131
  if (options.debug)
129
132
  console.log('[Rondevu] Generating new keypair...');
130
- keyPair = await RondevuAPI.generateKeyPair(options.cryptoAdapter);
133
+ keyPair = await cryptoAdapter.generateKeyPair();
131
134
  if (options.debug)
132
135
  console.log('[Rondevu] Generated keypair, public key:', keyPair.publicKey);
133
136
  }
@@ -136,10 +139,10 @@ export class Rondevu extends EventEmitter {
136
139
  console.log('[Rondevu] Using existing keypair, public key:', keyPair.publicKey);
137
140
  }
138
141
  // Create API instance
139
- const api = new RondevuAPI(apiUrl, keyPair, options.cryptoAdapter);
142
+ const api = new RondevuAPI(apiUrl, keyPair, cryptoAdapter);
140
143
  if (options.debug)
141
144
  console.log('[Rondevu] Created API instance');
142
- return new Rondevu(apiUrl, keyPair, api, iceConfig.iceServers || [], iceConfig.iceTransportPolicy, webrtcAdapter, options.cryptoAdapter, options.debug || false);
145
+ return new Rondevu(apiUrl, keyPair, api, iceConfig.iceServers || [], iceConfig.iceTransportPolicy, webrtcAdapter, cryptoAdapter, options.debug || false);
143
146
  }
144
147
  // ============================================
145
148
  // Identity Access
@@ -170,6 +173,16 @@ export class Rondevu extends EventEmitter {
170
173
  getWebRTCAdapter() {
171
174
  return this.webrtcAdapter;
172
175
  }
176
+ /**
177
+ * Get the crypto adapter for signing/verification operations
178
+ * Used for meta.json signing and other crypto operations
179
+ */
180
+ getCryptoAdapter() {
181
+ if (!this.cryptoAdapter) {
182
+ throw new Error('Crypto adapter not available');
183
+ }
184
+ return this.cryptoAdapter;
185
+ }
173
186
  // ============================================
174
187
  // Service Publishing
175
188
  // ============================================
@@ -202,7 +215,7 @@ export class Rondevu extends EventEmitter {
202
215
  * ```
203
216
  */
204
217
  async offer(options) {
205
- const { tags, maxOffers, offerFactory, ttl, connectionConfig, autoStart = true } = options;
218
+ const { tags, maxOffers, offerFactory, ttl, connectionConfig, autoStart = true, offerCreationThrottleMs, } = options;
206
219
  this.currentTags = tags;
207
220
  this.connectionConfig = connectionConfig;
208
221
  this.debug(`Creating offers with tags: ${tags.join(', ')} with maxOffers: ${maxOffers}`);
@@ -219,6 +232,7 @@ export class Rondevu extends EventEmitter {
219
232
  webrtcAdapter: this.webrtcAdapter,
220
233
  connectionConfig,
221
234
  debugEnabled: this.debugEnabled,
235
+ offerCreationThrottleMs,
222
236
  });
223
237
  // Forward events from OfferPool
224
238
  this.offerPool.on('connection:opened', (offerId, connection, matchedTags) => {
package/dist/meta.d.ts ADDED
@@ -0,0 +1,221 @@
1
+ /**
2
+ * meta.json format for authenticated torrent metadata
3
+ *
4
+ * Provides cryptographic proof of authorship using Ed25519 signatures.
5
+ * Supports extensible metadata for audio, video, image, and document files.
6
+ */
7
+ import { CryptoAdapter, KeyPair } from './crypto/adapter.js';
8
+ /**
9
+ * Root meta.json structure
10
+ */
11
+ export interface TorrentMeta {
12
+ version: string;
13
+ infohash?: string;
14
+ created: number;
15
+ title: string;
16
+ description?: string;
17
+ files: MetaFile[];
18
+ authors: MetaAuthor[];
19
+ extensions?: MetaExtensions;
20
+ }
21
+ /**
22
+ * File entry with optional per-file metadata
23
+ */
24
+ export interface MetaFile {
25
+ path: string;
26
+ size: number;
27
+ type?: string;
28
+ sha256?: string;
29
+ role?: FileRole;
30
+ meta?: FileMetaExtensions;
31
+ }
32
+ /**
33
+ * Special file roles for cover art, thumbnails, etc.
34
+ */
35
+ export type FileRole = 'cover' | 'thumbnail' | 'preview' | 'lyrics' | 'subtitles';
36
+ /**
37
+ * Author entry with signature
38
+ */
39
+ export interface MetaAuthor {
40
+ publicKey: string;
41
+ role?: AuthorRole;
42
+ signedAt: number;
43
+ signature: string;
44
+ }
45
+ /**
46
+ * Author roles
47
+ */
48
+ export type AuthorRole = 'creator' | 'co-author' | 'endorser';
49
+ /**
50
+ * Torrent-level metadata extensions
51
+ */
52
+ export interface MetaExtensions {
53
+ audio?: AudioMeta;
54
+ video?: VideoMeta;
55
+ image?: ImageMeta;
56
+ document?: DocumentMeta;
57
+ }
58
+ /**
59
+ * Per-file metadata extensions
60
+ */
61
+ export interface FileMetaExtensions {
62
+ audio?: AudioFileMeta;
63
+ video?: VideoMeta;
64
+ image?: ImageMeta;
65
+ document?: DocumentMeta;
66
+ }
67
+ /**
68
+ * Audio metadata (torrent-level, e.g., album info)
69
+ */
70
+ export interface AudioMeta {
71
+ artist?: string;
72
+ album?: string;
73
+ genre?: string;
74
+ year?: number;
75
+ totalTracks?: number;
76
+ }
77
+ /**
78
+ * Audio metadata (file-level, e.g., track info)
79
+ */
80
+ export interface AudioFileMeta {
81
+ title?: string;
82
+ track?: number;
83
+ duration?: number;
84
+ artist?: string;
85
+ bpm?: number;
86
+ }
87
+ /**
88
+ * Video metadata
89
+ */
90
+ export interface VideoMeta {
91
+ duration?: number;
92
+ width?: number;
93
+ height?: number;
94
+ codec?: string;
95
+ framerate?: number;
96
+ bitrate?: number;
97
+ }
98
+ /**
99
+ * Image metadata
100
+ */
101
+ export interface ImageMeta {
102
+ width?: number;
103
+ height?: number;
104
+ format?: string;
105
+ camera?: string;
106
+ takenAt?: number;
107
+ gps?: {
108
+ lat: number;
109
+ lon: number;
110
+ };
111
+ }
112
+ /**
113
+ * Document metadata
114
+ */
115
+ export interface DocumentMeta {
116
+ pages?: number;
117
+ author?: string;
118
+ language?: string;
119
+ format?: string;
120
+ }
121
+ /**
122
+ * The canonical payload that gets signed
123
+ * Includes author public keys to prevent fake co-author additions
124
+ * Note: infohash is optional because we may not know it before signing
125
+ * (torrent infohash includes meta.json, creating a chicken-egg problem)
126
+ * Content authenticity is ensured by sha256 hashes in the files array
127
+ */
128
+ export interface SigningPayload {
129
+ created: number;
130
+ title: string;
131
+ files: MetaFile[];
132
+ authors: string[];
133
+ signedAt: number;
134
+ }
135
+ /**
136
+ * Canonicalize an object to JSON with sorted keys (deterministic)
137
+ * This ensures the same object always produces the same string for signing
138
+ */
139
+ export declare function canonicalize(obj: unknown): string;
140
+ /**
141
+ * Build the signing payload from meta.json data
142
+ */
143
+ export declare function buildSigningPayload(meta: Pick<TorrentMeta, 'created' | 'title' | 'files'>, authorPublicKeys: string[], signedAt: number): SigningPayload;
144
+ /**
145
+ * Sign a torrent meta.json
146
+ *
147
+ * @param crypto - Crypto adapter for signing
148
+ * @param keyPair - The signer's key pair
149
+ * @param meta - The meta.json data (without this author's signature)
150
+ * @param authorPublicKeys - All author public keys (including this signer)
151
+ * @param role - This author's role
152
+ * @returns MetaAuthor entry with signature
153
+ */
154
+ export declare function signMeta(crypto: CryptoAdapter, keyPair: KeyPair, meta: Pick<TorrentMeta, 'created' | 'title' | 'files'>, authorPublicKeys: string[], role?: AuthorRole): Promise<MetaAuthor>;
155
+ /**
156
+ * Verify a single author's signature
157
+ *
158
+ * @param crypto - Crypto adapter for verification
159
+ * @param meta - The meta.json data
160
+ * @param author - The author entry to verify
161
+ * @returns True if signature is valid
162
+ */
163
+ export declare function verifyAuthorSignature(crypto: CryptoAdapter, meta: Pick<TorrentMeta, 'created' | 'title' | 'files' | 'authors'>, author: MetaAuthor): Promise<boolean>;
164
+ /**
165
+ * Verify all author signatures in a meta.json
166
+ *
167
+ * @param crypto - Crypto adapter for verification
168
+ * @param meta - The complete meta.json
169
+ * @returns Object with verification results per author
170
+ */
171
+ export declare function verifyMeta(crypto: CryptoAdapter, meta: TorrentMeta): Promise<{
172
+ valid: boolean;
173
+ results: Map<string, boolean>;
174
+ }>;
175
+ /**
176
+ * Create a complete meta.json with signatures from all authors
177
+ *
178
+ * @param crypto - Crypto adapter for signing
179
+ * @param options - Meta creation options
180
+ * @returns Complete TorrentMeta with all signatures
181
+ */
182
+ export declare function createMeta(crypto: CryptoAdapter, options: {
183
+ title: string;
184
+ description?: string;
185
+ files: MetaFile[];
186
+ authors: Array<{
187
+ keyPair: KeyPair;
188
+ role?: AuthorRole;
189
+ }>;
190
+ extensions?: MetaExtensions;
191
+ infohash?: string;
192
+ }): Promise<TorrentMeta>;
193
+ /**
194
+ * Parse and validate a meta.json string
195
+ * Does NOT verify signatures - use verifyMeta for that
196
+ *
197
+ * @param json - JSON string to parse
198
+ * @returns Parsed TorrentMeta or null if invalid
199
+ */
200
+ export declare function parseMeta(json: string): TorrentMeta | null;
201
+ /**
202
+ * Find a file with a specific role (e.g., cover art)
203
+ */
204
+ export declare function findFileByRole(meta: TorrentMeta, role: FileRole): MetaFile | undefined;
205
+ /**
206
+ * Get the cover art file if present
207
+ */
208
+ export declare function getCoverFile(meta: TorrentMeta): MetaFile | undefined;
209
+ /**
210
+ * Compute SHA256 hash of a file (browser-compatible)
211
+ */
212
+ export declare function computeFileSha256(file: File | Blob | ArrayBuffer): Promise<string>;
213
+ /**
214
+ * Build MetaFile entries from File objects with SHA256 hashes
215
+ */
216
+ export declare function buildMetaFiles(files: File[]): Promise<MetaFile[]>;
217
+ /**
218
+ * Set the infohash on a TorrentMeta after torrent creation
219
+ * Note: This does NOT re-sign - infohash is informational only
220
+ */
221
+ export declare function setInfohash(meta: TorrentMeta, infohash: string): TorrentMeta;
package/dist/meta.js ADDED
@@ -0,0 +1,213 @@
1
+ /**
2
+ * meta.json format for authenticated torrent metadata
3
+ *
4
+ * Provides cryptographic proof of authorship using Ed25519 signatures.
5
+ * Supports extensible metadata for audio, video, image, and document files.
6
+ */
7
+ // =============================================================================
8
+ // Utility Functions
9
+ // =============================================================================
10
+ /**
11
+ * Canonicalize an object to JSON with sorted keys (deterministic)
12
+ * This ensures the same object always produces the same string for signing
13
+ */
14
+ export function canonicalize(obj) {
15
+ return JSON.stringify(obj, (_, value) => {
16
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
17
+ return Object.keys(value)
18
+ .sort()
19
+ .reduce((sorted, key) => {
20
+ sorted[key] = value[key];
21
+ return sorted;
22
+ }, {});
23
+ }
24
+ return value;
25
+ });
26
+ }
27
+ /**
28
+ * Build the signing payload from meta.json data
29
+ */
30
+ export function buildSigningPayload(meta, authorPublicKeys, signedAt) {
31
+ return {
32
+ created: meta.created,
33
+ title: meta.title,
34
+ files: meta.files,
35
+ authors: authorPublicKeys,
36
+ signedAt
37
+ };
38
+ }
39
+ /**
40
+ * Sign a torrent meta.json
41
+ *
42
+ * @param crypto - Crypto adapter for signing
43
+ * @param keyPair - The signer's key pair
44
+ * @param meta - The meta.json data (without this author's signature)
45
+ * @param authorPublicKeys - All author public keys (including this signer)
46
+ * @param role - This author's role
47
+ * @returns MetaAuthor entry with signature
48
+ */
49
+ export async function signMeta(crypto, keyPair, meta, authorPublicKeys, role) {
50
+ const signedAt = Date.now();
51
+ const payload = buildSigningPayload(meta, authorPublicKeys, signedAt);
52
+ const message = canonicalize(payload);
53
+ const signature = await crypto.signMessage(keyPair.privateKey, message);
54
+ return {
55
+ publicKey: keyPair.publicKey,
56
+ role,
57
+ signedAt,
58
+ signature
59
+ };
60
+ }
61
+ /**
62
+ * Verify a single author's signature
63
+ *
64
+ * @param crypto - Crypto adapter for verification
65
+ * @param meta - The meta.json data
66
+ * @param author - The author entry to verify
67
+ * @returns True if signature is valid
68
+ */
69
+ export async function verifyAuthorSignature(crypto, meta, author) {
70
+ const authorPublicKeys = meta.authors.map(a => a.publicKey);
71
+ const payload = buildSigningPayload(meta, authorPublicKeys, author.signedAt);
72
+ const message = canonicalize(payload);
73
+ return crypto.verifySignature(author.publicKey, message, author.signature);
74
+ }
75
+ /**
76
+ * Verify all author signatures in a meta.json
77
+ *
78
+ * @param crypto - Crypto adapter for verification
79
+ * @param meta - The complete meta.json
80
+ * @returns Object with verification results per author
81
+ */
82
+ export async function verifyMeta(crypto, meta) {
83
+ const results = new Map();
84
+ let allValid = true;
85
+ for (const author of meta.authors) {
86
+ const isValid = await verifyAuthorSignature(crypto, meta, author);
87
+ results.set(author.publicKey, isValid);
88
+ if (!isValid) {
89
+ allValid = false;
90
+ }
91
+ }
92
+ return { valid: allValid, results };
93
+ }
94
+ /**
95
+ * Create a complete meta.json with signatures from all authors
96
+ *
97
+ * @param crypto - Crypto adapter for signing
98
+ * @param options - Meta creation options
99
+ * @returns Complete TorrentMeta with all signatures
100
+ */
101
+ export async function createMeta(crypto, options) {
102
+ const created = Date.now();
103
+ const authorPublicKeys = options.authors.map(a => a.keyPair.publicKey);
104
+ const baseMeta = {
105
+ created,
106
+ title: options.title,
107
+ files: options.files
108
+ };
109
+ // Sign with each author
110
+ const signedAuthors = [];
111
+ for (const { keyPair, role } of options.authors) {
112
+ const author = await signMeta(crypto, keyPair, baseMeta, authorPublicKeys, role);
113
+ signedAuthors.push(author);
114
+ }
115
+ const meta = {
116
+ version: '1',
117
+ created,
118
+ title: options.title,
119
+ files: options.files,
120
+ authors: signedAuthors,
121
+ };
122
+ if (options.infohash) {
123
+ meta.infohash = options.infohash;
124
+ }
125
+ if (options.description) {
126
+ meta.description = options.description;
127
+ }
128
+ if (options.extensions) {
129
+ meta.extensions = options.extensions;
130
+ }
131
+ return meta;
132
+ }
133
+ /**
134
+ * Parse and validate a meta.json string
135
+ * Does NOT verify signatures - use verifyMeta for that
136
+ *
137
+ * @param json - JSON string to parse
138
+ * @returns Parsed TorrentMeta or null if invalid
139
+ */
140
+ export function parseMeta(json) {
141
+ try {
142
+ const data = JSON.parse(json);
143
+ // Basic validation
144
+ if (typeof data.version !== 'string')
145
+ return null;
146
+ // infohash is optional
147
+ if (data.infohash !== undefined && typeof data.infohash !== 'string')
148
+ return null;
149
+ if (typeof data.created !== 'number')
150
+ return null;
151
+ if (typeof data.title !== 'string')
152
+ return null;
153
+ if (!Array.isArray(data.files))
154
+ return null;
155
+ if (!Array.isArray(data.authors))
156
+ return null;
157
+ return data;
158
+ }
159
+ catch {
160
+ return null;
161
+ }
162
+ }
163
+ /**
164
+ * Find a file with a specific role (e.g., cover art)
165
+ */
166
+ export function findFileByRole(meta, role) {
167
+ return meta.files.find(f => f.role === role);
168
+ }
169
+ /**
170
+ * Get the cover art file if present
171
+ */
172
+ export function getCoverFile(meta) {
173
+ return findFileByRole(meta, 'cover');
174
+ }
175
+ /**
176
+ * Compute SHA256 hash of a file (browser-compatible)
177
+ */
178
+ export async function computeFileSha256(file) {
179
+ let buffer;
180
+ if (file instanceof ArrayBuffer) {
181
+ buffer = file;
182
+ }
183
+ else {
184
+ buffer = await file.arrayBuffer();
185
+ }
186
+ const hashBuffer = await crypto.subtle.digest('SHA-256', buffer);
187
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
188
+ return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
189
+ }
190
+ /**
191
+ * Build MetaFile entries from File objects with SHA256 hashes
192
+ */
193
+ export async function buildMetaFiles(files) {
194
+ const metaFiles = [];
195
+ for (const file of files) {
196
+ const sha256 = await computeFileSha256(file);
197
+ const metaFile = {
198
+ path: file.name,
199
+ size: file.size,
200
+ type: file.type || undefined,
201
+ sha256
202
+ };
203
+ metaFiles.push(metaFile);
204
+ }
205
+ return metaFiles;
206
+ }
207
+ /**
208
+ * Set the infohash on a TorrentMeta after torrent creation
209
+ * Note: This does NOT re-sign - infohash is informational only
210
+ */
211
+ export function setInfohash(meta, infohash) {
212
+ return { ...meta, infohash };
213
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xtr-dev/rondevu-client",
3
- "version": "0.21.6",
3
+ "version": "0.21.8",
4
4
  "description": "TypeScript client for Rondevu with durable WebRTC connections, automatic reconnection, and message queuing",
5
5
  "type": "module",
6
6
  "main": "dist/core/index.js",