@unicitylabs/sphere-sdk 0.6.3 → 0.6.5

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.
@@ -1,3 +1,107 @@
1
+ // core/logger.ts
2
+ var LOGGER_KEY = "__sphere_sdk_logger__";
3
+ function getState() {
4
+ const g = globalThis;
5
+ if (!g[LOGGER_KEY]) {
6
+ g[LOGGER_KEY] = { debug: false, tags: {}, handler: null };
7
+ }
8
+ return g[LOGGER_KEY];
9
+ }
10
+ function isEnabled(tag) {
11
+ const state = getState();
12
+ if (tag in state.tags) return state.tags[tag];
13
+ return state.debug;
14
+ }
15
+ var logger = {
16
+ /**
17
+ * Configure the logger. Can be called multiple times (last write wins).
18
+ * Typically called by createBrowserProviders(), createNodeProviders(), or Sphere.init().
19
+ */
20
+ configure(config) {
21
+ const state = getState();
22
+ if (config.debug !== void 0) state.debug = config.debug;
23
+ if (config.handler !== void 0) state.handler = config.handler;
24
+ },
25
+ /**
26
+ * Enable/disable debug logging for a specific tag.
27
+ * Per-tag setting overrides the global debug flag.
28
+ *
29
+ * @example
30
+ * ```ts
31
+ * logger.setTagDebug('Nostr', true); // enable only Nostr logs
32
+ * logger.setTagDebug('Nostr', false); // disable Nostr logs even if global debug=true
33
+ * ```
34
+ */
35
+ setTagDebug(tag, enabled) {
36
+ getState().tags[tag] = enabled;
37
+ },
38
+ /**
39
+ * Clear per-tag override, falling back to global debug flag.
40
+ */
41
+ clearTagDebug(tag) {
42
+ delete getState().tags[tag];
43
+ },
44
+ /** Returns true if debug mode is enabled for the given tag (or globally). */
45
+ isDebugEnabled(tag) {
46
+ if (tag) return isEnabled(tag);
47
+ return getState().debug;
48
+ },
49
+ /**
50
+ * Debug-level log. Only shown when debug is enabled (globally or for this tag).
51
+ * Use for detailed operational information.
52
+ */
53
+ debug(tag, message, ...args) {
54
+ if (!isEnabled(tag)) return;
55
+ const state = getState();
56
+ if (state.handler) {
57
+ state.handler("debug", tag, message, ...args);
58
+ } else {
59
+ console.log(`[${tag}]`, message, ...args);
60
+ }
61
+ },
62
+ /**
63
+ * Warning-level log. ALWAYS shown regardless of debug flag.
64
+ * Use for important but non-critical issues (timeouts, retries, degraded state).
65
+ */
66
+ warn(tag, message, ...args) {
67
+ const state = getState();
68
+ if (state.handler) {
69
+ state.handler("warn", tag, message, ...args);
70
+ } else {
71
+ console.warn(`[${tag}]`, message, ...args);
72
+ }
73
+ },
74
+ /**
75
+ * Error-level log. ALWAYS shown regardless of debug flag.
76
+ * Use for critical failures that should never be silenced.
77
+ */
78
+ error(tag, message, ...args) {
79
+ const state = getState();
80
+ if (state.handler) {
81
+ state.handler("error", tag, message, ...args);
82
+ } else {
83
+ console.error(`[${tag}]`, message, ...args);
84
+ }
85
+ },
86
+ /** Reset all logger state (debug flag, tags, handler). Primarily for tests. */
87
+ reset() {
88
+ const g = globalThis;
89
+ delete g[LOGGER_KEY];
90
+ }
91
+ };
92
+
93
+ // core/errors.ts
94
+ var SphereError = class extends Error {
95
+ code;
96
+ cause;
97
+ constructor(message, code, cause) {
98
+ super(message);
99
+ this.name = "SphereError";
100
+ this.code = code;
101
+ this.cause = cause;
102
+ }
103
+ };
104
+
1
105
  // constants.ts
2
106
  var STORAGE_KEYS_GLOBAL = {
3
107
  /** Encrypted BIP39 mnemonic */
@@ -67,6 +171,8 @@ var STORAGE_KEYS = {
67
171
  };
68
172
  var DEFAULT_BASE_PATH = "m/44'/0'/0'";
69
173
  var DEFAULT_DERIVATION_PATH = `${DEFAULT_BASE_PATH}/0/0`;
174
+ var HOST_READY_TYPE = "sphere-connect:host-ready";
175
+ var HOST_READY_TIMEOUT = 3e4;
70
176
 
71
177
  // connect/protocol.ts
72
178
  var SPHERE_CONNECT_NAMESPACE = "sphere-connect";
@@ -102,6 +208,12 @@ function isSphereConnectMessage(msg) {
102
208
  const m = msg;
103
209
  return m.ns === SPHERE_CONNECT_NAMESPACE && m.v === SPHERE_CONNECT_VERSION;
104
210
  }
211
+ function createRequestId() {
212
+ if (typeof crypto !== "undefined" && crypto.randomUUID) {
213
+ return crypto.randomUUID();
214
+ }
215
+ return `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
216
+ }
105
217
 
106
218
  // connect/permissions.ts
107
219
  var PERMISSION_SCOPES = {
@@ -149,6 +261,246 @@ var INTENT_PERMISSIONS = {
149
261
  [INTENT_ACTIONS.SIGN_MESSAGE]: PERMISSION_SCOPES.SIGN_REQUEST
150
262
  };
151
263
 
264
+ // connect/client/ConnectClient.ts
265
+ var DEFAULT_TIMEOUT = 3e4;
266
+ var DEFAULT_INTENT_TIMEOUT = 12e4;
267
+ var ConnectClient = class {
268
+ transport;
269
+ dapp;
270
+ requestedPermissions;
271
+ timeout;
272
+ intentTimeout;
273
+ resumeSessionId;
274
+ silent;
275
+ sessionId = null;
276
+ grantedPermissions = [];
277
+ identity = null;
278
+ connected = false;
279
+ pendingRequests = /* @__PURE__ */ new Map();
280
+ eventHandlers = /* @__PURE__ */ new Map();
281
+ unsubscribeTransport = null;
282
+ // Handshake resolver (one-shot)
283
+ handshakeResolver = null;
284
+ constructor(config) {
285
+ this.transport = config.transport;
286
+ this.dapp = config.dapp;
287
+ this.requestedPermissions = config.permissions ?? [...ALL_PERMISSIONS];
288
+ this.timeout = config.timeout ?? DEFAULT_TIMEOUT;
289
+ this.intentTimeout = config.intentTimeout ?? DEFAULT_INTENT_TIMEOUT;
290
+ this.resumeSessionId = config.resumeSessionId ?? null;
291
+ this.silent = config.silent ?? false;
292
+ }
293
+ // ===========================================================================
294
+ // Connection
295
+ // ===========================================================================
296
+ /** Connect to the wallet. Returns session info and public identity. */
297
+ async connect() {
298
+ this.unsubscribeTransport = this.transport.onMessage(this.handleMessage.bind(this));
299
+ return new Promise((resolve, reject) => {
300
+ const timer = setTimeout(() => {
301
+ this.handshakeResolver = null;
302
+ reject(new Error("Connection timeout"));
303
+ }, this.timeout);
304
+ this.handshakeResolver = { resolve, reject, timer };
305
+ this.transport.send({
306
+ ns: SPHERE_CONNECT_NAMESPACE,
307
+ v: SPHERE_CONNECT_VERSION,
308
+ type: "handshake",
309
+ direction: "request",
310
+ permissions: this.requestedPermissions,
311
+ dapp: this.dapp,
312
+ ...this.resumeSessionId ? { sessionId: this.resumeSessionId } : {},
313
+ ...this.silent ? { silent: true } : {}
314
+ });
315
+ });
316
+ }
317
+ /** Disconnect from the wallet */
318
+ async disconnect() {
319
+ if (this.connected) {
320
+ try {
321
+ await this.query(RPC_METHODS.DISCONNECT);
322
+ } catch {
323
+ }
324
+ }
325
+ this.cleanup();
326
+ }
327
+ /** Whether currently connected */
328
+ get isConnected() {
329
+ return this.connected;
330
+ }
331
+ /** Granted permission scopes */
332
+ get permissions() {
333
+ return this.grantedPermissions;
334
+ }
335
+ /** Current session ID */
336
+ get session() {
337
+ return this.sessionId;
338
+ }
339
+ /** Public identity received during handshake */
340
+ get walletIdentity() {
341
+ return this.identity;
342
+ }
343
+ // ===========================================================================
344
+ // Query (read data)
345
+ // ===========================================================================
346
+ /** Send a query request and return the result */
347
+ async query(method, params) {
348
+ if (!this.connected) throw new SphereError("Not connected", "NOT_INITIALIZED");
349
+ const id = createRequestId();
350
+ return new Promise((resolve, reject) => {
351
+ const timer = setTimeout(() => {
352
+ this.pendingRequests.delete(id);
353
+ reject(new Error(`Query timeout: ${method}`));
354
+ }, this.timeout);
355
+ this.pendingRequests.set(id, {
356
+ resolve,
357
+ reject,
358
+ timer
359
+ });
360
+ this.transport.send({
361
+ ns: SPHERE_CONNECT_NAMESPACE,
362
+ v: SPHERE_CONNECT_VERSION,
363
+ type: "request",
364
+ id,
365
+ method,
366
+ params
367
+ });
368
+ });
369
+ }
370
+ // ===========================================================================
371
+ // Intent (trigger wallet UI)
372
+ // ===========================================================================
373
+ /** Send an intent request. The wallet will open its UI for user confirmation. */
374
+ async intent(action, params) {
375
+ if (!this.connected) throw new SphereError("Not connected", "NOT_INITIALIZED");
376
+ const id = createRequestId();
377
+ return new Promise((resolve, reject) => {
378
+ const timer = setTimeout(() => {
379
+ this.pendingRequests.delete(id);
380
+ reject(new Error(`Intent timeout: ${action}`));
381
+ }, this.intentTimeout);
382
+ this.pendingRequests.set(id, {
383
+ resolve,
384
+ reject,
385
+ timer
386
+ });
387
+ this.transport.send({
388
+ ns: SPHERE_CONNECT_NAMESPACE,
389
+ v: SPHERE_CONNECT_VERSION,
390
+ type: "intent",
391
+ id,
392
+ action,
393
+ params
394
+ });
395
+ });
396
+ }
397
+ // ===========================================================================
398
+ // Events
399
+ // ===========================================================================
400
+ /** Subscribe to a wallet event. Returns unsubscribe function. */
401
+ on(event, handler) {
402
+ if (!this.eventHandlers.has(event)) {
403
+ this.eventHandlers.set(event, /* @__PURE__ */ new Set());
404
+ if (this.connected) {
405
+ this.query(RPC_METHODS.SUBSCRIBE, { event }).catch((err) => logger.debug("Connect", "Event subscription failed", err));
406
+ }
407
+ }
408
+ this.eventHandlers.get(event).add(handler);
409
+ return () => {
410
+ const handlers = this.eventHandlers.get(event);
411
+ if (handlers) {
412
+ handlers.delete(handler);
413
+ if (handlers.size === 0) {
414
+ this.eventHandlers.delete(event);
415
+ if (this.connected) {
416
+ this.query(RPC_METHODS.UNSUBSCRIBE, { event }).catch((err) => logger.debug("Connect", "Event unsubscription failed", err));
417
+ }
418
+ }
419
+ }
420
+ };
421
+ }
422
+ // ===========================================================================
423
+ // Message Handling
424
+ // ===========================================================================
425
+ handleMessage(msg) {
426
+ if (msg.type === "handshake" && msg.direction === "response") {
427
+ this.handleHandshakeResponse(msg);
428
+ return;
429
+ }
430
+ if (msg.type === "response") {
431
+ this.handlePendingResponse(msg.id, msg.result, msg.error);
432
+ return;
433
+ }
434
+ if (msg.type === "intent_result") {
435
+ this.handlePendingResponse(msg.id, msg.result, msg.error);
436
+ return;
437
+ }
438
+ if (msg.type === "event") {
439
+ const handlers = this.eventHandlers.get(msg.event);
440
+ if (handlers) {
441
+ for (const handler of handlers) {
442
+ try {
443
+ handler(msg.data);
444
+ } catch (err) {
445
+ logger.debug("Connect", "Event handler error", err);
446
+ }
447
+ }
448
+ }
449
+ }
450
+ }
451
+ handleHandshakeResponse(msg) {
452
+ if (!this.handshakeResolver) return;
453
+ clearTimeout(this.handshakeResolver.timer);
454
+ if (msg.sessionId && msg.identity) {
455
+ this.sessionId = msg.sessionId;
456
+ this.grantedPermissions = msg.permissions;
457
+ this.identity = msg.identity;
458
+ this.connected = true;
459
+ this.handshakeResolver.resolve({
460
+ sessionId: msg.sessionId,
461
+ permissions: this.grantedPermissions,
462
+ identity: msg.identity
463
+ });
464
+ } else {
465
+ this.handshakeResolver.reject(new Error("Connection rejected by wallet"));
466
+ }
467
+ this.handshakeResolver = null;
468
+ }
469
+ handlePendingResponse(id, result, error) {
470
+ const pending = this.pendingRequests.get(id);
471
+ if (!pending) return;
472
+ clearTimeout(pending.timer);
473
+ this.pendingRequests.delete(id);
474
+ if (error) {
475
+ const err = new Error(error.message);
476
+ err.code = error.code;
477
+ err.data = error.data;
478
+ pending.reject(err);
479
+ } else {
480
+ pending.resolve(result);
481
+ }
482
+ }
483
+ // ===========================================================================
484
+ // Cleanup
485
+ // ===========================================================================
486
+ cleanup() {
487
+ if (this.unsubscribeTransport) {
488
+ this.unsubscribeTransport();
489
+ this.unsubscribeTransport = null;
490
+ }
491
+ for (const [, pending] of this.pendingRequests) {
492
+ clearTimeout(pending.timer);
493
+ pending.reject(new Error("Disconnected"));
494
+ }
495
+ this.pendingRequests.clear();
496
+ this.eventHandlers.clear();
497
+ this.connected = false;
498
+ this.sessionId = null;
499
+ this.grantedPermissions = [];
500
+ this.identity = null;
501
+ }
502
+ };
503
+
152
504
  // impl/browser/connect/PostMessageTransport.ts
153
505
  var POPUP_CLOSE_CHECK_INTERVAL = 1e3;
154
506
  var PostMessageTransport = class _PostMessageTransport {
@@ -369,11 +721,160 @@ var ExtensionTransport = {
369
721
  return new ExtensionHostTransport(chromeApi);
370
722
  }
371
723
  };
724
+
725
+ // impl/browser/connect/autoConnect.ts
726
+ function isInIframe() {
727
+ try {
728
+ return window.parent !== window && window.self !== window.top;
729
+ } catch {
730
+ return true;
731
+ }
732
+ }
733
+ function hasExtension() {
734
+ try {
735
+ const sphere = window.sphere;
736
+ if (!sphere || typeof sphere !== "object") return false;
737
+ const isInstalled = sphere.isInstalled;
738
+ if (typeof isInstalled !== "function") return false;
739
+ return isInstalled() === true;
740
+ } catch {
741
+ return false;
742
+ }
743
+ }
744
+ function detectTransport() {
745
+ if (isInIframe()) return "iframe";
746
+ if (hasExtension()) return "extension";
747
+ return "popup";
748
+ }
749
+ var DEFAULT_POPUP_FEATURES = "width=420,height=720,scrollbars=yes,resizable=yes";
750
+ async function autoConnect(config) {
751
+ const transportType = config.forceTransport ?? detectTransport();
752
+ switch (transportType) {
753
+ case "iframe":
754
+ return connectViaIframe(config);
755
+ case "extension":
756
+ return connectViaExtension(config);
757
+ case "popup":
758
+ return connectViaPopup(config);
759
+ }
760
+ }
761
+ async function connectViaIframe(config) {
762
+ const transport = PostMessageTransport.forClient();
763
+ const { client, connection, cleanup } = await createAndConnect(transport, config);
764
+ return {
765
+ client,
766
+ connection,
767
+ transport: "iframe",
768
+ disconnect: async () => {
769
+ await client.disconnect();
770
+ cleanup();
771
+ }
772
+ };
773
+ }
774
+ async function connectViaExtension(config) {
775
+ const transport = ExtensionTransport.forClient();
776
+ const { client, connection, cleanup } = await createAndConnect(transport, config);
777
+ return {
778
+ client,
779
+ connection,
780
+ transport: "extension",
781
+ disconnect: async () => {
782
+ await client.disconnect();
783
+ cleanup();
784
+ }
785
+ };
786
+ }
787
+ async function connectViaPopup(config) {
788
+ if (!config.walletUrl) {
789
+ throw new Error("autoConnect: walletUrl is required when no extension or iframe is available");
790
+ }
791
+ const origin = encodeURIComponent(window.location.origin);
792
+ const popupUrl = `${config.walletUrl}/connect?origin=${origin}`;
793
+ const features = config.popupFeatures ?? DEFAULT_POPUP_FEATURES;
794
+ const popup = window.open(popupUrl, "sphere-wallet", features);
795
+ if (!popup) {
796
+ throw new Error("autoConnect: Failed to open wallet popup \u2014 check popup blocker settings");
797
+ }
798
+ await waitForHostReady(popup, config.walletUrl);
799
+ const transport = PostMessageTransport.forClient({
800
+ target: popup,
801
+ targetOrigin: config.walletUrl
802
+ });
803
+ const { client, connection, cleanup } = await createAndConnect(transport, config);
804
+ const closeCheckInterval = setInterval(() => {
805
+ if (popup.closed) {
806
+ clearInterval(closeCheckInterval);
807
+ cleanup();
808
+ }
809
+ }, 1e3);
810
+ return {
811
+ client,
812
+ connection,
813
+ transport: "popup",
814
+ disconnect: async () => {
815
+ clearInterval(closeCheckInterval);
816
+ await client.disconnect();
817
+ cleanup();
818
+ if (!popup.closed) popup.close();
819
+ }
820
+ };
821
+ }
822
+ function waitForHostReady(popup, walletOrigin) {
823
+ return new Promise((resolve, reject) => {
824
+ const timer = setTimeout(() => {
825
+ window.removeEventListener("message", listener);
826
+ reject(new Error("autoConnect: Wallet popup did not respond in time"));
827
+ }, HOST_READY_TIMEOUT);
828
+ function listener(event) {
829
+ if (event.data?.type === HOST_READY_TYPE) {
830
+ clearTimeout(timer);
831
+ window.removeEventListener("message", listener);
832
+ resolve();
833
+ }
834
+ }
835
+ window.addEventListener("message", listener);
836
+ const closeCheck = setInterval(() => {
837
+ if (popup.closed) {
838
+ clearInterval(closeCheck);
839
+ clearTimeout(timer);
840
+ window.removeEventListener("message", listener);
841
+ reject(new Error("autoConnect: Wallet popup was closed before connecting"));
842
+ }
843
+ }, 500);
844
+ });
845
+ }
846
+ async function createAndConnect(transport, config) {
847
+ const clientConfig = {
848
+ transport,
849
+ dapp: config.dapp,
850
+ permissions: config.permissions,
851
+ timeout: config.timeout,
852
+ intentTimeout: config.intentTimeout,
853
+ resumeSessionId: config.resumeSessionId,
854
+ silent: config.silent
855
+ };
856
+ const client = new ConnectClient(clientConfig);
857
+ try {
858
+ const connection = await client.connect();
859
+ return {
860
+ client,
861
+ connection,
862
+ cleanup: () => transport.destroy()
863
+ };
864
+ } catch (err) {
865
+ transport.destroy();
866
+ throw err;
867
+ }
868
+ }
372
869
  export {
373
870
  EXT_MSG_TO_CLIENT,
374
871
  EXT_MSG_TO_HOST,
375
872
  ExtensionTransport,
376
873
  PostMessageTransport,
377
- isExtensionConnectEnvelope
874
+ autoConnect,
875
+ detectTransport,
876
+ hasExtension,
877
+ isExtensionConnectEnvelope,
878
+ isInIframe
378
879
  };
379
880
  //# sourceMappingURL=index.js.map