@byoky/sdk 0.3.0 → 0.4.1

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/dist/index.d.cts CHANGED
@@ -23,16 +23,31 @@ interface ByokySession extends ConnectResponse {
23
23
  }
24
24
  interface ByokyOptions {
25
25
  timeout?: number;
26
+ /** Relay server URL for mobile wallet pairing. Default: wss://relay.byoky.com */
27
+ relayUrl?: string;
26
28
  }
27
29
  declare class Byoky {
28
30
  private timeout;
31
+ private relayUrl;
29
32
  constructor(options?: ByokyOptions);
30
- connect(request?: ConnectRequest): Promise<ByokySession>;
33
+ /**
34
+ * Connect to a Byoky wallet. Tries the browser extension first.
35
+ * If no extension is found and `onPairingReady` is provided,
36
+ * falls back to relay mode — pairing with a mobile wallet app.
37
+ */
38
+ connect(request?: ConnectRequest & {
39
+ /** Called with a pairing code when no extension is detected. Show as QR or text. */
40
+ onPairingReady?: (pairingCode: string) => void;
41
+ /** Skip extension detection and go directly to relay pairing. */
42
+ useRelay?: boolean;
43
+ }): Promise<ByokySession>;
31
44
  /**
32
45
  * Reconnect to an existing session using previously stored response data.
33
46
  * Returns null if the session is no longer valid.
34
47
  */
35
48
  reconnect(savedResponse: ConnectResponse): Promise<ByokySession | null>;
49
+ private connectViaRelay;
50
+ private buildRelaySession;
36
51
  private buildSession;
37
52
  private sendConnectRequest;
38
53
  private sendDisconnect;
package/dist/index.d.ts CHANGED
@@ -23,16 +23,31 @@ interface ByokySession extends ConnectResponse {
23
23
  }
24
24
  interface ByokyOptions {
25
25
  timeout?: number;
26
+ /** Relay server URL for mobile wallet pairing. Default: wss://relay.byoky.com */
27
+ relayUrl?: string;
26
28
  }
27
29
  declare class Byoky {
28
30
  private timeout;
31
+ private relayUrl;
29
32
  constructor(options?: ByokyOptions);
30
- connect(request?: ConnectRequest): Promise<ByokySession>;
33
+ /**
34
+ * Connect to a Byoky wallet. Tries the browser extension first.
35
+ * If no extension is found and `onPairingReady` is provided,
36
+ * falls back to relay mode — pairing with a mobile wallet app.
37
+ */
38
+ connect(request?: ConnectRequest & {
39
+ /** Called with a pairing code when no extension is detected. Show as QR or text. */
40
+ onPairingReady?: (pairingCode: string) => void;
41
+ /** Skip extension detection and go directly to relay pairing. */
42
+ useRelay?: boolean;
43
+ }): Promise<ByokySession>;
31
44
  /**
32
45
  * Reconnect to an existing session using previously stored response data.
33
46
  * Returns null if the session is no longer valid.
34
47
  */
35
48
  reconnect(savedResponse: ConnectResponse): Promise<ByokySession | null>;
49
+ private connectViaRelay;
50
+ private buildRelaySession;
36
51
  private buildSession;
37
52
  private sendConnectRequest;
38
53
  private sendDisconnect;
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/byoky.ts
2
- import { ByokyError as ByokyError2, ByokyErrorCode } from "@byoky/core";
2
+ import { ByokyError as ByokyError2, ByokyErrorCode, encodePairPayload } from "@byoky/core";
3
3
 
4
4
  // src/detect.ts
5
5
  import { BYOKY_PROVIDER_KEY } from "@byoky/core";
@@ -38,8 +38,9 @@ function createProxyFetch(providerId, sessionKey) {
38
38
  cleanup();
39
39
  reject(new Error("Proxy request timed out"));
40
40
  }, 12e4);
41
- function handleEvent(event) {
42
- const data = event.detail ?? event.data;
41
+ const channel = new MessageChannel();
42
+ channel.port1.onmessage = (event) => {
43
+ const data = event.data;
43
44
  if (data?.requestId !== requestId) return;
44
45
  switch (data.type) {
45
46
  case "BYOKY_PROXY_RESPONSE_META":
@@ -83,12 +84,11 @@ function createProxyFetch(providerId, sessionKey) {
83
84
  break;
84
85
  }
85
86
  }
86
- }
87
+ };
87
88
  function cleanup() {
88
89
  clearTimeout(timeout);
89
- document.removeEventListener("byoky-message", handleEvent);
90
+ channel.port1.close();
90
91
  }
91
- document.addEventListener("byoky-message", handleEvent);
92
92
  window.postMessage(
93
93
  {
94
94
  type: "BYOKY_PROXY_REQUEST",
@@ -100,7 +100,8 @@ function createProxyFetch(providerId, sessionKey) {
100
100
  headers,
101
101
  body
102
102
  },
103
- "*"
103
+ window.location.origin,
104
+ [channel.port2]
104
105
  );
105
106
  });
106
107
  };
@@ -130,6 +131,115 @@ async function readBody(body) {
130
131
  return void 0;
131
132
  }
132
133
 
134
+ // src/relay-fetch.ts
135
+ function createRelayFetch(ws, providerId) {
136
+ return async (input, init) => {
137
+ const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
138
+ const method = init?.method ?? "GET";
139
+ const headers = init?.headers ? Object.fromEntries(new Headers(init.headers).entries()) : {};
140
+ const body = init?.body ? await readBody2(init.body) : void 0;
141
+ const requestId = crypto.randomUUID();
142
+ return new Promise((resolve, reject) => {
143
+ const { readable, writable } = new TransformStream();
144
+ const writer = writable.getWriter();
145
+ const encoder = new TextEncoder();
146
+ let resolved = false;
147
+ const timeout = setTimeout(() => {
148
+ cleanup();
149
+ reject(new Error("Relay proxy request timed out"));
150
+ }, 12e4);
151
+ function handleMessage(event) {
152
+ let data;
153
+ try {
154
+ data = JSON.parse(event.data);
155
+ } catch {
156
+ return;
157
+ }
158
+ if (data.requestId !== requestId) return;
159
+ switch (data.type) {
160
+ case "relay:response:meta":
161
+ if (!resolved) {
162
+ resolved = true;
163
+ clearTimeout(timeout);
164
+ resolve(
165
+ new Response(readable, {
166
+ status: data.status,
167
+ statusText: data.statusText ?? "",
168
+ headers: new Headers(data.headers)
169
+ })
170
+ );
171
+ }
172
+ break;
173
+ case "relay:response:chunk":
174
+ writer.write(encoder.encode(data.chunk)).catch(() => {
175
+ });
176
+ break;
177
+ case "relay:response:done":
178
+ writer.close().catch(() => {
179
+ });
180
+ cleanup();
181
+ break;
182
+ case "relay:response:error": {
183
+ const err = data.error;
184
+ const message = typeof err === "string" ? err : err?.message ?? "Relay proxy error";
185
+ const errResponse = new Response(
186
+ JSON.stringify({ error: { message, code: typeof err === "object" ? err?.code : "RELAY_ERROR" } }),
187
+ { status: 500, headers: { "content-type": "application/json" } }
188
+ );
189
+ if (!resolved) {
190
+ resolved = true;
191
+ clearTimeout(timeout);
192
+ resolve(errResponse);
193
+ }
194
+ writer.close().catch(() => {
195
+ });
196
+ cleanup();
197
+ break;
198
+ }
199
+ }
200
+ }
201
+ function cleanup() {
202
+ clearTimeout(timeout);
203
+ ws.removeEventListener("message", handleMessage);
204
+ }
205
+ ws.addEventListener("message", handleMessage);
206
+ ws.send(JSON.stringify({
207
+ type: "relay:request",
208
+ requestId,
209
+ providerId,
210
+ url,
211
+ method,
212
+ headers,
213
+ body
214
+ }));
215
+ });
216
+ };
217
+ }
218
+ async function readBody2(body) {
219
+ if (typeof body === "string") return body;
220
+ if (body instanceof ArrayBuffer) return new TextDecoder().decode(body);
221
+ if (body instanceof Blob) return body.text();
222
+ if (body instanceof URLSearchParams) return body.toString();
223
+ if (body instanceof ReadableStream) {
224
+ const reader = body.getReader();
225
+ const chunks = [];
226
+ for (; ; ) {
227
+ const { done, value } = await reader.read();
228
+ if (done) break;
229
+ chunks.push(value);
230
+ }
231
+ const total = chunks.reduce((acc, c) => acc + c.length, 0);
232
+ const combined = new Uint8Array(total);
233
+ let offset = 0;
234
+ for (const chunk of chunks) {
235
+ combined.set(chunk, offset);
236
+ offset += chunk.length;
237
+ }
238
+ return new TextDecoder().decode(combined);
239
+ }
240
+ return void 0;
241
+ }
242
+
133
243
  // src/relay-client.ts
134
244
  import {
135
245
  ByokyError,
@@ -145,7 +255,7 @@ function createRelayClient(wsUrl, sessionKey, providers) {
145
255
  try {
146
256
  const parsed = new URL(wsUrl);
147
257
  const isSecure = parsed.protocol === "wss:";
148
- const isLocalWs = parsed.protocol === "ws:" && (parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1");
258
+ const isLocalWs = parsed.protocol === "ws:" && (parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1" || parsed.hostname === "[::1]");
149
259
  if (!isSecure && !isLocalWs) {
150
260
  status = "disconnected";
151
261
  return {
@@ -316,9 +426,16 @@ function createRelayClient(wsUrl, sessionKey, providers) {
316
426
  // src/byoky.ts
317
427
  var Byoky = class {
318
428
  timeout;
429
+ relayUrl;
319
430
  constructor(options = {}) {
320
431
  this.timeout = options.timeout ?? 6e4;
432
+ this.relayUrl = options.relayUrl ?? "wss://relay.byoky.com";
321
433
  }
434
+ /**
435
+ * Connect to a Byoky wallet. Tries the browser extension first.
436
+ * If no extension is found and `onPairingReady` is provided,
437
+ * falls back to relay mode — pairing with a mobile wallet app.
438
+ */
322
439
  async connect(request = {}) {
323
440
  if (typeof window === "undefined") {
324
441
  throw new ByokyError2(
@@ -326,15 +443,22 @@ var Byoky = class {
326
443
  "Byoky SDK requires a browser environment. On the server, use your API key directly."
327
444
  );
328
445
  }
329
- if (!isExtensionInstalled()) {
330
- const storeUrl = getStoreUrl();
331
- if (storeUrl) {
332
- window.open(storeUrl, "_blank");
333
- }
334
- throw ByokyError2.walletNotInstalled();
446
+ const { onPairingReady, useRelay, ...connectRequest } = request;
447
+ if (useRelay && onPairingReady) {
448
+ return this.connectViaRelay(connectRequest, onPairingReady);
449
+ }
450
+ if (isExtensionInstalled()) {
451
+ const response = await this.sendConnectRequest(connectRequest);
452
+ return this.buildSession(response);
453
+ }
454
+ if (onPairingReady) {
455
+ return this.connectViaRelay(connectRequest, onPairingReady);
335
456
  }
336
- const response = await this.sendConnectRequest(request);
337
- return this.buildSession(response);
457
+ const storeUrl = getStoreUrl();
458
+ if (storeUrl) {
459
+ window.open(storeUrl, "_blank");
460
+ }
461
+ throw ByokyError2.walletNotInstalled();
338
462
  }
339
463
  /**
340
464
  * Reconnect to an existing session using previously stored response data.
@@ -347,24 +471,139 @@ var Byoky = class {
347
471
  if (!connected) return null;
348
472
  return this.buildSession(savedResponse);
349
473
  }
474
+ // --- Relay pairing ---
475
+ connectViaRelay(request, onPairingReady) {
476
+ return new Promise((resolve, reject) => {
477
+ const roomId = crypto.randomUUID();
478
+ const authToken = Array.from(crypto.getRandomValues(new Uint8Array(32))).map((b) => b.toString(16).padStart(2, "0")).join("");
479
+ const pairingCode = encodePairPayload({
480
+ v: 1,
481
+ r: this.relayUrl,
482
+ id: roomId,
483
+ t: authToken,
484
+ o: window.location.origin
485
+ });
486
+ const ws = new WebSocket(this.relayUrl);
487
+ const timeout = setTimeout(() => {
488
+ ws.close();
489
+ reject(new ByokyError2(ByokyErrorCode.UNKNOWN, "Pairing timed out. No wallet connected."));
490
+ }, this.timeout);
491
+ ws.onopen = () => {
492
+ ws.send(JSON.stringify({
493
+ type: "relay:auth",
494
+ roomId,
495
+ authToken,
496
+ role: "recipient"
497
+ }));
498
+ };
499
+ ws.onmessage = (event) => {
500
+ let msg;
501
+ try {
502
+ msg = JSON.parse(event.data);
503
+ } catch {
504
+ return;
505
+ }
506
+ switch (msg.type) {
507
+ case "relay:auth:result":
508
+ if (msg.success) {
509
+ onPairingReady(pairingCode);
510
+ } else {
511
+ clearTimeout(timeout);
512
+ ws.close();
513
+ reject(new ByokyError2(ByokyErrorCode.UNKNOWN, `Relay auth failed: ${msg.error}`));
514
+ }
515
+ break;
516
+ case "relay:pair:hello": {
517
+ clearTimeout(timeout);
518
+ const providers = msg.providers;
519
+ ws.send(JSON.stringify({ type: "relay:pair:ack" }));
520
+ resolve(this.buildRelaySession(ws, roomId, providers));
521
+ break;
522
+ }
523
+ case "relay:peer:status":
524
+ break;
525
+ }
526
+ };
527
+ ws.onerror = () => {
528
+ clearTimeout(timeout);
529
+ reject(new ByokyError2(ByokyErrorCode.UNKNOWN, "Failed to connect to relay server"));
530
+ };
531
+ ws.onclose = () => {
532
+ clearTimeout(timeout);
533
+ };
534
+ });
535
+ }
536
+ buildRelaySession(ws, roomId, providers) {
537
+ const sessionKey = `relay_${roomId}`;
538
+ const disconnectCallbacks = /* @__PURE__ */ new Set();
539
+ const pingInterval = setInterval(() => {
540
+ if (ws.readyState === WebSocket.OPEN) {
541
+ ws.send(JSON.stringify({ type: "relay:ping", ts: Date.now() }));
542
+ }
543
+ }, 3e4);
544
+ ws.addEventListener("close", () => {
545
+ clearInterval(pingInterval);
546
+ for (const cb of disconnectCallbacks) cb();
547
+ disconnectCallbacks.clear();
548
+ });
549
+ ws.addEventListener("message", (event) => {
550
+ try {
551
+ const msg = JSON.parse(event.data);
552
+ if (msg.type === "relay:peer:status" && msg.online === false) {
553
+ for (const cb of disconnectCallbacks) cb();
554
+ disconnectCallbacks.clear();
555
+ clearInterval(pingInterval);
556
+ ws.close(1e3, "Phone disconnected");
557
+ }
558
+ } catch {
559
+ }
560
+ });
561
+ return {
562
+ sessionKey,
563
+ proxyUrl: "",
564
+ providers,
565
+ createFetch: (providerId) => createRelayFetch(ws, providerId),
566
+ createRelay: () => {
567
+ throw new Error("Relay-in-relay not supported");
568
+ },
569
+ disconnect: () => {
570
+ clearInterval(pingInterval);
571
+ ws.close(1e3, "Client disconnected");
572
+ },
573
+ isConnected: async () => ws.readyState === WebSocket.OPEN,
574
+ getUsage: async () => ({ requests: 0, inputTokens: 0, outputTokens: 0, byProvider: {} }),
575
+ onDisconnect: (callback) => {
576
+ disconnectCallbacks.add(callback);
577
+ return () => {
578
+ disconnectCallbacks.delete(callback);
579
+ };
580
+ }
581
+ };
582
+ }
583
+ // --- Extension-based session ---
350
584
  buildSession(response) {
351
585
  const sessionKey = response.sessionKey;
352
586
  const disconnectCallbacks = /* @__PURE__ */ new Set();
353
- function handleRevocation(event) {
354
- const msg = event.detail;
587
+ const notifyChannel = new MessageChannel();
588
+ notifyChannel.port1.onmessage = (event) => {
589
+ const msg = event.data;
355
590
  if (msg?.type === "BYOKY_SESSION_REVOKED" && msg.payload?.sessionKey === sessionKey) {
356
591
  for (const cb of disconnectCallbacks) cb();
357
592
  disconnectCallbacks.clear();
358
- document.removeEventListener("byoky-message", handleRevocation);
593
+ notifyChannel.port1.close();
359
594
  }
360
- }
361
- document.addEventListener("byoky-message", handleRevocation);
595
+ };
596
+ window.postMessage(
597
+ { type: "BYOKY_REGISTER_NOTIFY" },
598
+ window.location.origin,
599
+ [notifyChannel.port2]
600
+ );
362
601
  return {
363
602
  ...response,
364
603
  createFetch: (providerId) => createProxyFetch(providerId, sessionKey),
365
604
  createRelay: (wsUrl) => createRelayClient(wsUrl, sessionKey, response.providers),
366
605
  disconnect: () => {
367
- document.removeEventListener("byoky-message", handleRevocation);
606
+ notifyChannel.port1.close();
368
607
  this.sendDisconnect(sessionKey);
369
608
  },
370
609
  isConnected: () => this.querySessionStatus(sessionKey),
@@ -386,8 +625,9 @@ var Byoky = class {
386
625
  new ByokyError2(ByokyErrorCode.UNKNOWN, "Connection request timed out")
387
626
  );
388
627
  }, this.timeout);
389
- function handleEvent(event) {
390
- const msg = event.detail;
628
+ const channel = new MessageChannel();
629
+ channel.port1.onmessage = (event) => {
630
+ const msg = event.data;
391
631
  if (typeof msg?.type !== "string" || !msg.type.startsWith("BYOKY_")) return;
392
632
  if (msg.requestId !== requestId) return;
393
633
  cleanup();
@@ -397,12 +637,11 @@ var Byoky = class {
397
637
  const { code, message } = msg.payload;
398
638
  reject(new ByokyError2(code, message));
399
639
  }
400
- }
640
+ };
401
641
  function cleanup() {
402
642
  clearTimeout(timeoutId);
403
- document.removeEventListener("byoky-message", handleEvent);
643
+ channel.port1.close();
404
644
  }
405
- document.addEventListener("byoky-message", handleEvent);
406
645
  window.postMessage(
407
646
  {
408
647
  type: "BYOKY_CONNECT_REQUEST",
@@ -410,14 +649,15 @@ var Byoky = class {
410
649
  requestId,
411
650
  payload: request
412
651
  },
413
- "*"
652
+ window.location.origin,
653
+ [channel.port2]
414
654
  );
415
655
  });
416
656
  }
417
657
  sendDisconnect(sessionKey) {
418
658
  window.postMessage(
419
659
  { type: "BYOKY_DISCONNECT", payload: { sessionKey } },
420
- "*"
660
+ window.location.origin
421
661
  );
422
662
  }
423
663
  querySessionStatus(sessionKey) {
@@ -427,24 +667,24 @@ var Byoky = class {
427
667
  cleanup();
428
668
  resolve(false);
429
669
  }, 5e3);
430
- function handleEvent(event) {
431
- const msg = event.detail;
670
+ const channel = new MessageChannel();
671
+ channel.port1.onmessage = (event) => {
672
+ const msg = event.data;
432
673
  if (msg?.requestId !== requestId) return;
433
674
  if (msg.type === "BYOKY_SESSION_STATUS_RESPONSE") {
434
675
  cleanup();
435
676
  resolve(!!msg.payload?.connected);
436
677
  }
437
- }
678
+ };
438
679
  function cleanup() {
439
680
  clearTimeout(timeout);
440
- document.removeEventListener("byoky-message", handleEvent);
681
+ channel.port1.close();
441
682
  }
442
- document.addEventListener("byoky-message", handleEvent);
443
683
  window.postMessage({
444
684
  type: "BYOKY_SESSION_STATUS",
445
685
  requestId,
446
686
  payload: { sessionKey }
447
- }, "*");
687
+ }, window.location.origin, [channel.port2]);
448
688
  });
449
689
  }
450
690
  querySessionUsage(sessionKey) {
@@ -454,8 +694,9 @@ var Byoky = class {
454
694
  cleanup();
455
695
  reject(new Error("Usage query timed out"));
456
696
  }, 5e3);
457
- function handleEvent(event) {
458
- const msg = event.detail;
697
+ const channel = new MessageChannel();
698
+ channel.port1.onmessage = (event) => {
699
+ const msg = event.data;
459
700
  if (msg?.requestId !== requestId) return;
460
701
  if (msg.type === "BYOKY_SESSION_USAGE_RESPONSE") {
461
702
  cleanup();
@@ -465,17 +706,16 @@ var Byoky = class {
465
706
  reject(new Error("Session not found"));
466
707
  }
467
708
  }
468
- }
709
+ };
469
710
  function cleanup() {
470
711
  clearTimeout(timeout);
471
- document.removeEventListener("byoky-message", handleEvent);
712
+ channel.port1.close();
472
713
  }
473
- document.addEventListener("byoky-message", handleEvent);
474
714
  window.postMessage({
475
715
  type: "BYOKY_SESSION_USAGE",
476
716
  requestId,
477
717
  payload: { sessionKey }
478
- }, "*");
718
+ }, window.location.origin, [channel.port2]);
479
719
  });
480
720
  }
481
721
  };