@unicitylabs/sphere-sdk 0.6.4 → 0.6.6

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.
@@ -24,10 +24,118 @@ __export(connect_exports, {
24
24
  EXT_MSG_TO_HOST: () => EXT_MSG_TO_HOST,
25
25
  ExtensionTransport: () => ExtensionTransport,
26
26
  PostMessageTransport: () => PostMessageTransport,
27
- isExtensionConnectEnvelope: () => isExtensionConnectEnvelope
27
+ autoConnect: () => autoConnect,
28
+ detectTransport: () => detectTransport,
29
+ hasExtension: () => hasExtension,
30
+ isExtensionConnectEnvelope: () => isExtensionConnectEnvelope,
31
+ isInIframe: () => isInIframe
28
32
  });
29
33
  module.exports = __toCommonJS(connect_exports);
30
34
 
35
+ // core/logger.ts
36
+ var LOGGER_KEY = "__sphere_sdk_logger__";
37
+ function getState() {
38
+ const g = globalThis;
39
+ if (!g[LOGGER_KEY]) {
40
+ g[LOGGER_KEY] = { debug: false, tags: {}, handler: null };
41
+ }
42
+ return g[LOGGER_KEY];
43
+ }
44
+ function isEnabled(tag) {
45
+ const state = getState();
46
+ if (tag in state.tags) return state.tags[tag];
47
+ return state.debug;
48
+ }
49
+ var logger = {
50
+ /**
51
+ * Configure the logger. Can be called multiple times (last write wins).
52
+ * Typically called by createBrowserProviders(), createNodeProviders(), or Sphere.init().
53
+ */
54
+ configure(config) {
55
+ const state = getState();
56
+ if (config.debug !== void 0) state.debug = config.debug;
57
+ if (config.handler !== void 0) state.handler = config.handler;
58
+ },
59
+ /**
60
+ * Enable/disable debug logging for a specific tag.
61
+ * Per-tag setting overrides the global debug flag.
62
+ *
63
+ * @example
64
+ * ```ts
65
+ * logger.setTagDebug('Nostr', true); // enable only Nostr logs
66
+ * logger.setTagDebug('Nostr', false); // disable Nostr logs even if global debug=true
67
+ * ```
68
+ */
69
+ setTagDebug(tag, enabled) {
70
+ getState().tags[tag] = enabled;
71
+ },
72
+ /**
73
+ * Clear per-tag override, falling back to global debug flag.
74
+ */
75
+ clearTagDebug(tag) {
76
+ delete getState().tags[tag];
77
+ },
78
+ /** Returns true if debug mode is enabled for the given tag (or globally). */
79
+ isDebugEnabled(tag) {
80
+ if (tag) return isEnabled(tag);
81
+ return getState().debug;
82
+ },
83
+ /**
84
+ * Debug-level log. Only shown when debug is enabled (globally or for this tag).
85
+ * Use for detailed operational information.
86
+ */
87
+ debug(tag, message, ...args) {
88
+ if (!isEnabled(tag)) return;
89
+ const state = getState();
90
+ if (state.handler) {
91
+ state.handler("debug", tag, message, ...args);
92
+ } else {
93
+ console.log(`[${tag}]`, message, ...args);
94
+ }
95
+ },
96
+ /**
97
+ * Warning-level log. ALWAYS shown regardless of debug flag.
98
+ * Use for important but non-critical issues (timeouts, retries, degraded state).
99
+ */
100
+ warn(tag, message, ...args) {
101
+ const state = getState();
102
+ if (state.handler) {
103
+ state.handler("warn", tag, message, ...args);
104
+ } else {
105
+ console.warn(`[${tag}]`, message, ...args);
106
+ }
107
+ },
108
+ /**
109
+ * Error-level log. ALWAYS shown regardless of debug flag.
110
+ * Use for critical failures that should never be silenced.
111
+ */
112
+ error(tag, message, ...args) {
113
+ const state = getState();
114
+ if (state.handler) {
115
+ state.handler("error", tag, message, ...args);
116
+ } else {
117
+ console.error(`[${tag}]`, message, ...args);
118
+ }
119
+ },
120
+ /** Reset all logger state (debug flag, tags, handler). Primarily for tests. */
121
+ reset() {
122
+ const g = globalThis;
123
+ delete g[LOGGER_KEY];
124
+ }
125
+ };
126
+
127
+ // core/errors.ts
128
+ var SphereError = class extends Error {
129
+ code;
130
+ cause;
131
+ constructor(message, code, cause) {
132
+ super(message);
133
+ this.name = "SphereError";
134
+ this.code = code;
135
+ this.cause = cause;
136
+ }
137
+ };
138
+
31
139
  // constants.ts
32
140
  var STORAGE_KEYS_GLOBAL = {
33
141
  /** Encrypted BIP39 mnemonic */
@@ -97,6 +205,8 @@ var STORAGE_KEYS = {
97
205
  };
98
206
  var DEFAULT_BASE_PATH = "m/44'/0'/0'";
99
207
  var DEFAULT_DERIVATION_PATH = `${DEFAULT_BASE_PATH}/0/0`;
208
+ var HOST_READY_TYPE = "sphere-connect:host-ready";
209
+ var HOST_READY_TIMEOUT = 3e4;
100
210
 
101
211
  // connect/protocol.ts
102
212
  var SPHERE_CONNECT_NAMESPACE = "sphere-connect";
@@ -132,6 +242,12 @@ function isSphereConnectMessage(msg) {
132
242
  const m = msg;
133
243
  return m.ns === SPHERE_CONNECT_NAMESPACE && m.v === SPHERE_CONNECT_VERSION;
134
244
  }
245
+ function createRequestId() {
246
+ if (typeof crypto !== "undefined" && crypto.randomUUID) {
247
+ return crypto.randomUUID();
248
+ }
249
+ return `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
250
+ }
135
251
 
136
252
  // connect/permissions.ts
137
253
  var PERMISSION_SCOPES = {
@@ -179,6 +295,246 @@ var INTENT_PERMISSIONS = {
179
295
  [INTENT_ACTIONS.SIGN_MESSAGE]: PERMISSION_SCOPES.SIGN_REQUEST
180
296
  };
181
297
 
298
+ // connect/client/ConnectClient.ts
299
+ var DEFAULT_TIMEOUT = 3e4;
300
+ var DEFAULT_INTENT_TIMEOUT = 12e4;
301
+ var ConnectClient = class {
302
+ transport;
303
+ dapp;
304
+ requestedPermissions;
305
+ timeout;
306
+ intentTimeout;
307
+ resumeSessionId;
308
+ silent;
309
+ sessionId = null;
310
+ grantedPermissions = [];
311
+ identity = null;
312
+ connected = false;
313
+ pendingRequests = /* @__PURE__ */ new Map();
314
+ eventHandlers = /* @__PURE__ */ new Map();
315
+ unsubscribeTransport = null;
316
+ // Handshake resolver (one-shot)
317
+ handshakeResolver = null;
318
+ constructor(config) {
319
+ this.transport = config.transport;
320
+ this.dapp = config.dapp;
321
+ this.requestedPermissions = config.permissions ?? [...ALL_PERMISSIONS];
322
+ this.timeout = config.timeout ?? DEFAULT_TIMEOUT;
323
+ this.intentTimeout = config.intentTimeout ?? DEFAULT_INTENT_TIMEOUT;
324
+ this.resumeSessionId = config.resumeSessionId ?? null;
325
+ this.silent = config.silent ?? false;
326
+ }
327
+ // ===========================================================================
328
+ // Connection
329
+ // ===========================================================================
330
+ /** Connect to the wallet. Returns session info and public identity. */
331
+ async connect() {
332
+ this.unsubscribeTransport = this.transport.onMessage(this.handleMessage.bind(this));
333
+ return new Promise((resolve, reject) => {
334
+ const timer = setTimeout(() => {
335
+ this.handshakeResolver = null;
336
+ reject(new Error("Connection timeout"));
337
+ }, this.timeout);
338
+ this.handshakeResolver = { resolve, reject, timer };
339
+ this.transport.send({
340
+ ns: SPHERE_CONNECT_NAMESPACE,
341
+ v: SPHERE_CONNECT_VERSION,
342
+ type: "handshake",
343
+ direction: "request",
344
+ permissions: this.requestedPermissions,
345
+ dapp: this.dapp,
346
+ ...this.resumeSessionId ? { sessionId: this.resumeSessionId } : {},
347
+ ...this.silent ? { silent: true } : {}
348
+ });
349
+ });
350
+ }
351
+ /** Disconnect from the wallet */
352
+ async disconnect() {
353
+ if (this.connected) {
354
+ try {
355
+ await this.query(RPC_METHODS.DISCONNECT);
356
+ } catch {
357
+ }
358
+ }
359
+ this.cleanup();
360
+ }
361
+ /** Whether currently connected */
362
+ get isConnected() {
363
+ return this.connected;
364
+ }
365
+ /** Granted permission scopes */
366
+ get permissions() {
367
+ return this.grantedPermissions;
368
+ }
369
+ /** Current session ID */
370
+ get session() {
371
+ return this.sessionId;
372
+ }
373
+ /** Public identity received during handshake */
374
+ get walletIdentity() {
375
+ return this.identity;
376
+ }
377
+ // ===========================================================================
378
+ // Query (read data)
379
+ // ===========================================================================
380
+ /** Send a query request and return the result */
381
+ async query(method, params) {
382
+ if (!this.connected) throw new SphereError("Not connected", "NOT_INITIALIZED");
383
+ const id = createRequestId();
384
+ return new Promise((resolve, reject) => {
385
+ const timer = setTimeout(() => {
386
+ this.pendingRequests.delete(id);
387
+ reject(new Error(`Query timeout: ${method}`));
388
+ }, this.timeout);
389
+ this.pendingRequests.set(id, {
390
+ resolve,
391
+ reject,
392
+ timer
393
+ });
394
+ this.transport.send({
395
+ ns: SPHERE_CONNECT_NAMESPACE,
396
+ v: SPHERE_CONNECT_VERSION,
397
+ type: "request",
398
+ id,
399
+ method,
400
+ params
401
+ });
402
+ });
403
+ }
404
+ // ===========================================================================
405
+ // Intent (trigger wallet UI)
406
+ // ===========================================================================
407
+ /** Send an intent request. The wallet will open its UI for user confirmation. */
408
+ async intent(action, params) {
409
+ if (!this.connected) throw new SphereError("Not connected", "NOT_INITIALIZED");
410
+ const id = createRequestId();
411
+ return new Promise((resolve, reject) => {
412
+ const timer = setTimeout(() => {
413
+ this.pendingRequests.delete(id);
414
+ reject(new Error(`Intent timeout: ${action}`));
415
+ }, this.intentTimeout);
416
+ this.pendingRequests.set(id, {
417
+ resolve,
418
+ reject,
419
+ timer
420
+ });
421
+ this.transport.send({
422
+ ns: SPHERE_CONNECT_NAMESPACE,
423
+ v: SPHERE_CONNECT_VERSION,
424
+ type: "intent",
425
+ id,
426
+ action,
427
+ params
428
+ });
429
+ });
430
+ }
431
+ // ===========================================================================
432
+ // Events
433
+ // ===========================================================================
434
+ /** Subscribe to a wallet event. Returns unsubscribe function. */
435
+ on(event, handler) {
436
+ if (!this.eventHandlers.has(event)) {
437
+ this.eventHandlers.set(event, /* @__PURE__ */ new Set());
438
+ if (this.connected) {
439
+ this.query(RPC_METHODS.SUBSCRIBE, { event }).catch((err) => logger.debug("Connect", "Event subscription failed", err));
440
+ }
441
+ }
442
+ this.eventHandlers.get(event).add(handler);
443
+ return () => {
444
+ const handlers = this.eventHandlers.get(event);
445
+ if (handlers) {
446
+ handlers.delete(handler);
447
+ if (handlers.size === 0) {
448
+ this.eventHandlers.delete(event);
449
+ if (this.connected) {
450
+ this.query(RPC_METHODS.UNSUBSCRIBE, { event }).catch((err) => logger.debug("Connect", "Event unsubscription failed", err));
451
+ }
452
+ }
453
+ }
454
+ };
455
+ }
456
+ // ===========================================================================
457
+ // Message Handling
458
+ // ===========================================================================
459
+ handleMessage(msg) {
460
+ if (msg.type === "handshake" && msg.direction === "response") {
461
+ this.handleHandshakeResponse(msg);
462
+ return;
463
+ }
464
+ if (msg.type === "response") {
465
+ this.handlePendingResponse(msg.id, msg.result, msg.error);
466
+ return;
467
+ }
468
+ if (msg.type === "intent_result") {
469
+ this.handlePendingResponse(msg.id, msg.result, msg.error);
470
+ return;
471
+ }
472
+ if (msg.type === "event") {
473
+ const handlers = this.eventHandlers.get(msg.event);
474
+ if (handlers) {
475
+ for (const handler of handlers) {
476
+ try {
477
+ handler(msg.data);
478
+ } catch (err) {
479
+ logger.debug("Connect", "Event handler error", err);
480
+ }
481
+ }
482
+ }
483
+ }
484
+ }
485
+ handleHandshakeResponse(msg) {
486
+ if (!this.handshakeResolver) return;
487
+ clearTimeout(this.handshakeResolver.timer);
488
+ if (msg.sessionId && msg.identity) {
489
+ this.sessionId = msg.sessionId;
490
+ this.grantedPermissions = msg.permissions;
491
+ this.identity = msg.identity;
492
+ this.connected = true;
493
+ this.handshakeResolver.resolve({
494
+ sessionId: msg.sessionId,
495
+ permissions: this.grantedPermissions,
496
+ identity: msg.identity
497
+ });
498
+ } else {
499
+ this.handshakeResolver.reject(new Error("Connection rejected by wallet"));
500
+ }
501
+ this.handshakeResolver = null;
502
+ }
503
+ handlePendingResponse(id, result, error) {
504
+ const pending = this.pendingRequests.get(id);
505
+ if (!pending) return;
506
+ clearTimeout(pending.timer);
507
+ this.pendingRequests.delete(id);
508
+ if (error) {
509
+ const err = new Error(error.message);
510
+ err.code = error.code;
511
+ err.data = error.data;
512
+ pending.reject(err);
513
+ } else {
514
+ pending.resolve(result);
515
+ }
516
+ }
517
+ // ===========================================================================
518
+ // Cleanup
519
+ // ===========================================================================
520
+ cleanup() {
521
+ if (this.unsubscribeTransport) {
522
+ this.unsubscribeTransport();
523
+ this.unsubscribeTransport = null;
524
+ }
525
+ for (const [, pending] of this.pendingRequests) {
526
+ clearTimeout(pending.timer);
527
+ pending.reject(new Error("Disconnected"));
528
+ }
529
+ this.pendingRequests.clear();
530
+ this.eventHandlers.clear();
531
+ this.connected = false;
532
+ this.sessionId = null;
533
+ this.grantedPermissions = [];
534
+ this.identity = null;
535
+ }
536
+ };
537
+
182
538
  // impl/browser/connect/PostMessageTransport.ts
183
539
  var POPUP_CLOSE_CHECK_INTERVAL = 1e3;
184
540
  var PostMessageTransport = class _PostMessageTransport {
@@ -399,4 +755,149 @@ var ExtensionTransport = {
399
755
  return new ExtensionHostTransport(chromeApi);
400
756
  }
401
757
  };
758
+
759
+ // impl/browser/connect/autoConnect.ts
760
+ function isInIframe() {
761
+ try {
762
+ return window.parent !== window && window.self !== window.top;
763
+ } catch {
764
+ return true;
765
+ }
766
+ }
767
+ function hasExtension() {
768
+ try {
769
+ const sphere = window.sphere;
770
+ if (!sphere || typeof sphere !== "object") return false;
771
+ const isInstalled = sphere.isInstalled;
772
+ if (typeof isInstalled !== "function") return false;
773
+ return isInstalled() === true;
774
+ } catch {
775
+ return false;
776
+ }
777
+ }
778
+ function detectTransport() {
779
+ if (isInIframe()) return "iframe";
780
+ if (hasExtension()) return "extension";
781
+ return "popup";
782
+ }
783
+ var DEFAULT_POPUP_FEATURES = "width=420,height=720,scrollbars=yes,resizable=yes";
784
+ async function autoConnect(config) {
785
+ const transportType = config.forceTransport ?? detectTransport();
786
+ switch (transportType) {
787
+ case "iframe":
788
+ return connectViaIframe(config);
789
+ case "extension":
790
+ return connectViaExtension(config);
791
+ case "popup":
792
+ return connectViaPopup(config);
793
+ }
794
+ }
795
+ async function connectViaIframe(config) {
796
+ const transport = PostMessageTransport.forClient();
797
+ const { client, connection, cleanup } = await createAndConnect(transport, config);
798
+ return {
799
+ client,
800
+ connection,
801
+ transport: "iframe",
802
+ disconnect: async () => {
803
+ await client.disconnect();
804
+ cleanup();
805
+ }
806
+ };
807
+ }
808
+ async function connectViaExtension(config) {
809
+ const transport = ExtensionTransport.forClient();
810
+ const { client, connection, cleanup } = await createAndConnect(transport, config);
811
+ return {
812
+ client,
813
+ connection,
814
+ transport: "extension",
815
+ disconnect: async () => {
816
+ await client.disconnect();
817
+ cleanup();
818
+ }
819
+ };
820
+ }
821
+ async function connectViaPopup(config) {
822
+ if (!config.walletUrl) {
823
+ throw new Error("autoConnect: walletUrl is required when no extension or iframe is available");
824
+ }
825
+ const origin = encodeURIComponent(window.location.origin);
826
+ const popupUrl = `${config.walletUrl}/connect?origin=${origin}`;
827
+ const features = config.popupFeatures ?? DEFAULT_POPUP_FEATURES;
828
+ const popup = window.open(popupUrl, "sphere-wallet", features);
829
+ if (!popup) {
830
+ throw new Error("autoConnect: Failed to open wallet popup \u2014 check popup blocker settings");
831
+ }
832
+ await waitForHostReady(popup, config.walletUrl);
833
+ const transport = PostMessageTransport.forClient({
834
+ target: popup,
835
+ targetOrigin: config.walletUrl
836
+ });
837
+ const { client, connection, cleanup } = await createAndConnect(transport, config);
838
+ const closeCheckInterval = setInterval(() => {
839
+ if (popup.closed) {
840
+ clearInterval(closeCheckInterval);
841
+ cleanup();
842
+ }
843
+ }, 1e3);
844
+ return {
845
+ client,
846
+ connection,
847
+ transport: "popup",
848
+ disconnect: async () => {
849
+ clearInterval(closeCheckInterval);
850
+ await client.disconnect();
851
+ cleanup();
852
+ if (!popup.closed) popup.close();
853
+ }
854
+ };
855
+ }
856
+ function waitForHostReady(popup, walletOrigin) {
857
+ return new Promise((resolve, reject) => {
858
+ const timer = setTimeout(() => {
859
+ window.removeEventListener("message", listener);
860
+ reject(new Error("autoConnect: Wallet popup did not respond in time"));
861
+ }, HOST_READY_TIMEOUT);
862
+ function listener(event) {
863
+ if (event.data?.type === HOST_READY_TYPE) {
864
+ clearTimeout(timer);
865
+ window.removeEventListener("message", listener);
866
+ resolve();
867
+ }
868
+ }
869
+ window.addEventListener("message", listener);
870
+ const closeCheck = setInterval(() => {
871
+ if (popup.closed) {
872
+ clearInterval(closeCheck);
873
+ clearTimeout(timer);
874
+ window.removeEventListener("message", listener);
875
+ reject(new Error("autoConnect: Wallet popup was closed before connecting"));
876
+ }
877
+ }, 500);
878
+ });
879
+ }
880
+ async function createAndConnect(transport, config) {
881
+ const clientConfig = {
882
+ transport,
883
+ dapp: config.dapp,
884
+ permissions: config.permissions,
885
+ timeout: config.timeout,
886
+ intentTimeout: config.intentTimeout,
887
+ resumeSessionId: config.resumeSessionId,
888
+ silent: config.silent
889
+ };
890
+ const client = new ConnectClient(clientConfig);
891
+ try {
892
+ const connection = await client.connect();
893
+ return {
894
+ client,
895
+ connection,
896
+ cleanup: () => transport.destroy()
897
+ };
898
+ } catch (err) {
899
+ transport.destroy();
900
+ throw err;
901
+ }
902
+ }
402
903
  //# sourceMappingURL=index.cjs.map