@unicitylabs/sphere-sdk 0.6.2 → 0.6.4

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.
@@ -758,7 +758,7 @@ var STORE_TOKENS = "tokens";
758
758
  var STORE_META = "meta";
759
759
  var STORE_HISTORY = "history";
760
760
  var connectionSeq2 = 0;
761
- var IndexedDBTokenStorageProvider = class {
761
+ var IndexedDBTokenStorageProvider = class _IndexedDBTokenStorageProvider {
762
762
  id = "indexeddb-token-storage";
763
763
  name = "IndexedDB Token Storage";
764
764
  type = "local";
@@ -1206,6 +1206,16 @@ var IndexedDBTokenStorageProvider = class {
1206
1206
  db.close();
1207
1207
  }
1208
1208
  }
1209
+ /**
1210
+ * Create an independent instance for a different address.
1211
+ * The new instance shares the same config but has its own IDB connection.
1212
+ */
1213
+ createForAddress() {
1214
+ return new _IndexedDBTokenStorageProvider({
1215
+ dbNamePrefix: this.dbNamePrefix,
1216
+ debug: this.debug
1217
+ });
1218
+ }
1209
1219
  };
1210
1220
  function createIndexedDBTokenStorageProvider(config) {
1211
1221
  return new IndexedDBTokenStorageProvider(config);
@@ -1625,7 +1635,7 @@ function defaultUUIDGenerator() {
1625
1635
  var COMPOSING_INDICATOR_KIND = 25050;
1626
1636
  var TIMESTAMP_RANDOMIZATION = 2 * 24 * 60 * 60;
1627
1637
  var EVENT_KINDS = NOSTR_EVENT_KINDS;
1628
- var NostrTransportProvider = class {
1638
+ var NostrTransportProvider = class _NostrTransportProvider {
1629
1639
  id = "nostr";
1630
1640
  name = "Nostr Transport";
1631
1641
  type = "p2p";
@@ -1634,6 +1644,8 @@ var NostrTransportProvider = class {
1634
1644
  storage = null;
1635
1645
  /** In-memory max event timestamp to avoid read-before-write races in updateLastEventTimestamp. */
1636
1646
  lastEventTs = 0;
1647
+ /** Fallback 'since' timestamp for first-time address subscriptions (consumed once). */
1648
+ fallbackSince = null;
1637
1649
  identity = null;
1638
1650
  keyManager = null;
1639
1651
  status = "disconnected";
@@ -1666,6 +1678,48 @@ var NostrTransportProvider = class {
1666
1678
  };
1667
1679
  this.storage = config.storage ?? null;
1668
1680
  }
1681
+ /**
1682
+ * Get the WebSocket factory (used by MultiAddressTransportMux to share the same factory).
1683
+ */
1684
+ getWebSocketFactory() {
1685
+ return this.config.createWebSocket;
1686
+ }
1687
+ /**
1688
+ * Get the configured relay URLs.
1689
+ */
1690
+ getConfiguredRelays() {
1691
+ return [...this.config.relays];
1692
+ }
1693
+ /**
1694
+ * Get the storage adapter.
1695
+ */
1696
+ getStorageAdapter() {
1697
+ return this.storage;
1698
+ }
1699
+ /**
1700
+ * Suppress event subscriptions — unsubscribe wallet/chat filters
1701
+ * but keep the connection alive for resolve/identity-binding operations.
1702
+ * Used when MultiAddressTransportMux takes over event handling.
1703
+ */
1704
+ suppressSubscriptions() {
1705
+ if (!this.nostrClient) return;
1706
+ if (this.walletSubscriptionId) {
1707
+ this.nostrClient.unsubscribe(this.walletSubscriptionId);
1708
+ this.walletSubscriptionId = null;
1709
+ }
1710
+ if (this.chatSubscriptionId) {
1711
+ this.nostrClient.unsubscribe(this.chatSubscriptionId);
1712
+ this.chatSubscriptionId = null;
1713
+ }
1714
+ if (this.mainSubscriptionId) {
1715
+ this.nostrClient.unsubscribe(this.mainSubscriptionId);
1716
+ this.mainSubscriptionId = null;
1717
+ }
1718
+ this._subscriptionsSuppressed = true;
1719
+ logger.debug("Nostr", "Subscriptions suppressed \u2014 mux handles event routing");
1720
+ }
1721
+ // Flag to prevent re-subscription after suppressSubscriptions()
1722
+ _subscriptionsSuppressed = false;
1669
1723
  // ===========================================================================
1670
1724
  // BaseProvider Implementation
1671
1725
  // ===========================================================================
@@ -1844,6 +1898,8 @@ var NostrTransportProvider = class {
1844
1898
  // ===========================================================================
1845
1899
  async setIdentity(identity) {
1846
1900
  this.identity = identity;
1901
+ this.processedEventIds.clear();
1902
+ this.lastEventTs = 0;
1847
1903
  const secretKey = Buffer2.from(identity.privateKey, "hex");
1848
1904
  this.keyManager = NostrKeyManager.fromPrivateKey(secretKey);
1849
1905
  const nostrPubkey = this.keyManager.getPublicKeyHex();
@@ -1886,6 +1942,9 @@ var NostrTransportProvider = class {
1886
1942
  await this.subscribeToEvents();
1887
1943
  }
1888
1944
  }
1945
+ setFallbackSince(sinceSeconds) {
1946
+ this.fallbackSince = sinceSeconds;
1947
+ }
1889
1948
  /**
1890
1949
  * Get the Nostr-format public key (32 bytes / 64 hex chars)
1891
1950
  * This is the x-coordinate only, without the 02/03 prefix.
@@ -2107,11 +2166,11 @@ var NostrTransportProvider = class {
2107
2166
  return this.resolveNametagInfo(identifier);
2108
2167
  }
2109
2168
  async resolveNametag(nametag) {
2110
- this.ensureConnected();
2169
+ await this.ensureConnectedForResolve();
2111
2170
  return this.nostrClient.queryPubkeyByNametag(nametag);
2112
2171
  }
2113
2172
  async resolveNametagInfo(nametag) {
2114
- this.ensureConnected();
2173
+ await this.ensureConnectedForResolve();
2115
2174
  const binding = await this.nostrClient.queryBindingByNametag(nametag);
2116
2175
  if (!binding) {
2117
2176
  logger.debug("Nostr", `resolveNametagInfo: no binding events found for Unicity ID "${nametag}"`);
@@ -2124,7 +2183,7 @@ var NostrTransportProvider = class {
2124
2183
  * Performs reverse lookup via nostr-js-sdk with first-seen-wins anti-hijacking.
2125
2184
  */
2126
2185
  async resolveAddressInfo(address) {
2127
- this.ensureConnected();
2186
+ await this.ensureConnectedForResolve();
2128
2187
  const binding = await this.nostrClient.queryBindingByAddress(address);
2129
2188
  if (!binding) return null;
2130
2189
  return this.bindingInfoToPeerInfo(binding);
@@ -2159,7 +2218,7 @@ var NostrTransportProvider = class {
2159
2218
  * Queries binding events authored by the given pubkey.
2160
2219
  */
2161
2220
  async resolveTransportPubkeyInfo(transportPubkey) {
2162
- this.ensureConnected();
2221
+ await this.ensureConnectedForResolve();
2163
2222
  const events = await this.queryEvents({
2164
2223
  kinds: [EVENT_KINDS.NAMETAG_BINDING],
2165
2224
  authors: [transportPubkey],
@@ -2194,7 +2253,7 @@ var NostrTransportProvider = class {
2194
2253
  * Used for HD address discovery — single relay query with multi-author filter.
2195
2254
  */
2196
2255
  async discoverAddresses(transportPubkeys) {
2197
- this.ensureConnected();
2256
+ await this.ensureConnectedForResolve();
2198
2257
  if (transportPubkeys.length === 0) return [];
2199
2258
  const events = await this.queryEvents({
2200
2259
  kinds: [EVENT_KINDS.NAMETAG_BINDING],
@@ -2233,7 +2292,10 @@ var NostrTransportProvider = class {
2233
2292
  * @returns Decrypted nametag or null if none found
2234
2293
  */
2235
2294
  async recoverNametag() {
2236
- this.ensureReady();
2295
+ await this.ensureConnectedForResolve();
2296
+ if (!this.identity) {
2297
+ throw new SphereError("Identity not set", "NOT_INITIALIZED");
2298
+ }
2237
2299
  if (!this.identity || !this.keyManager) {
2238
2300
  throw new SphereError("Identity not set", "NOT_INITIALIZED");
2239
2301
  }
@@ -2793,6 +2855,10 @@ var NostrTransportProvider = class {
2793
2855
  chatEoseFired = false;
2794
2856
  async subscribeToEvents() {
2795
2857
  logger.debug("Nostr", "subscribeToEvents called, identity:", !!this.identity, "keyManager:", !!this.keyManager, "nostrClient:", !!this.nostrClient);
2858
+ if (this._subscriptionsSuppressed) {
2859
+ logger.debug("Nostr", "subscribeToEvents: suppressed \u2014 mux handles event routing");
2860
+ return;
2861
+ }
2796
2862
  if (!this.identity || !this.keyManager || !this.nostrClient) {
2797
2863
  logger.debug("Nostr", "subscribeToEvents: skipped - no identity, keyManager, or nostrClient");
2798
2864
  return;
@@ -2819,7 +2885,13 @@ var NostrTransportProvider = class {
2819
2885
  if (stored) {
2820
2886
  since = parseInt(stored, 10);
2821
2887
  this.lastEventTs = since;
2888
+ this.fallbackSince = null;
2822
2889
  logger.debug("Nostr", "Resuming from stored event timestamp:", since);
2890
+ } else if (this.fallbackSince !== null) {
2891
+ since = this.fallbackSince;
2892
+ this.lastEventTs = since;
2893
+ this.fallbackSince = null;
2894
+ logger.debug("Nostr", "Using fallback since timestamp:", since);
2823
2895
  } else {
2824
2896
  since = Math.floor(Date.now() / 1e3);
2825
2897
  logger.debug("Nostr", "No stored timestamp, starting from now:", since);
@@ -2827,6 +2899,7 @@ var NostrTransportProvider = class {
2827
2899
  } catch (err) {
2828
2900
  logger.debug("Nostr", "Failed to read last event timestamp, falling back to now:", err);
2829
2901
  since = Math.floor(Date.now() / 1e3);
2902
+ this.fallbackSince = null;
2830
2903
  }
2831
2904
  } else {
2832
2905
  since = Math.floor(Date.now() / 1e3) - 86400;
@@ -2958,6 +3031,31 @@ var NostrTransportProvider = class {
2958
3031
  throw new SphereError("NostrTransportProvider not connected", "TRANSPORT_ERROR");
2959
3032
  }
2960
3033
  }
3034
+ /**
3035
+ * Async version of ensureConnected — reconnects if the original transport
3036
+ * lost its WebSocket while subscriptions are suppressed (mux handles events).
3037
+ * Used by resolve methods which are always async.
3038
+ */
3039
+ async ensureConnectedForResolve() {
3040
+ if (this.isConnected()) return;
3041
+ if (this._subscriptionsSuppressed && this.nostrClient) {
3042
+ logger.debug("Nostr", "Suppressed transport disconnected \u2014 reconnecting for resolve");
3043
+ try {
3044
+ await Promise.race([
3045
+ this.nostrClient.connect(...this.config.relays),
3046
+ new Promise(
3047
+ (_, reject) => setTimeout(() => reject(new Error("reconnect timeout")), 5e3)
3048
+ )
3049
+ ]);
3050
+ if (this.nostrClient.isConnected()) {
3051
+ this.status = "connected";
3052
+ return;
3053
+ }
3054
+ } catch {
3055
+ }
3056
+ }
3057
+ throw new SphereError("NostrTransportProvider not connected", "TRANSPORT_ERROR");
3058
+ }
2961
3059
  ensureReady() {
2962
3060
  this.ensureConnected();
2963
3061
  if (!this.identity) {
@@ -2979,16 +3077,23 @@ var NostrTransportProvider = class {
2979
3077
  * because NIP17.createGiftWrap hardcodes kind 14 for the inner rumor.
2980
3078
  */
2981
3079
  createCustomKindGiftWrap(recipientPubkeyHex, content, rumorKind) {
2982
- const senderPubkey = this.keyManager.getPublicKeyHex();
3080
+ return _NostrTransportProvider.createCustomKindGiftWrap(this.keyManager, recipientPubkeyHex, content, rumorKind);
3081
+ }
3082
+ /**
3083
+ * Create a NIP-17 gift wrap with a custom rumor kind.
3084
+ * Shared between NostrTransportProvider and MultiAddressTransportMux.
3085
+ */
3086
+ static createCustomKindGiftWrap(keyManager, recipientPubkeyHex, content, rumorKind) {
3087
+ const senderPubkey = keyManager.getPublicKeyHex();
2983
3088
  const now = Math.floor(Date.now() / 1e3);
2984
3089
  const rumorTags = [["p", recipientPubkeyHex]];
2985
3090
  const rumorSerialized = JSON.stringify([0, senderPubkey, now, rumorKind, rumorTags, content]);
2986
3091
  const rumorId = bytesToHex(sha256(new TextEncoder().encode(rumorSerialized)));
2987
3092
  const rumor = { id: rumorId, pubkey: senderPubkey, created_at: now, kind: rumorKind, tags: rumorTags, content };
2988
3093
  const recipientPubkeyBytes = hexToBytes(recipientPubkeyHex);
2989
- const encryptedRumor = NIP44.encrypt(JSON.stringify(rumor), this.keyManager.getPrivateKey(), recipientPubkeyBytes);
3094
+ const encryptedRumor = NIP44.encrypt(JSON.stringify(rumor), keyManager.getPrivateKey(), recipientPubkeyBytes);
2990
3095
  const sealTimestamp = now + Math.floor(Math.random() * 2 * TIMESTAMP_RANDOMIZATION) - TIMESTAMP_RANDOMIZATION;
2991
- const seal = NostrEventClass.create(this.keyManager, {
3096
+ const seal = NostrEventClass.create(keyManager, {
2992
3097
  kind: EventKinds.SEAL,
2993
3098
  tags: [],
2994
3099
  content: encryptedRumor,
@@ -4847,11 +4952,17 @@ var AsyncSerialQueue = class {
4847
4952
  var WriteBuffer = class {
4848
4953
  /** Full TXF data from save() calls — latest wins */
4849
4954
  txfData = null;
4955
+ /** IPNS context captured at save() time — ensures flush writes to the correct
4956
+ * IPNS record even if identity changes between save() and flush(). */
4957
+ capturedIpnsKeyPair = null;
4958
+ capturedIpnsName = null;
4850
4959
  get isEmpty() {
4851
4960
  return this.txfData === null;
4852
4961
  }
4853
4962
  clear() {
4854
4963
  this.txfData = null;
4964
+ this.capturedIpnsKeyPair = null;
4965
+ this.capturedIpnsName = null;
4855
4966
  }
4856
4967
  /**
4857
4968
  * Merge another buffer's contents into this one (for rollback).
@@ -4860,12 +4971,14 @@ var WriteBuffer = class {
4860
4971
  mergeFrom(other) {
4861
4972
  if (other.txfData && !this.txfData) {
4862
4973
  this.txfData = other.txfData;
4974
+ this.capturedIpnsKeyPair = other.capturedIpnsKeyPair;
4975
+ this.capturedIpnsName = other.capturedIpnsName;
4863
4976
  }
4864
4977
  }
4865
4978
  };
4866
4979
 
4867
4980
  // impl/shared/ipfs/ipfs-storage-provider.ts
4868
- var IpfsStorageProvider = class {
4981
+ var IpfsStorageProvider = class _IpfsStorageProvider {
4869
4982
  id = "ipfs";
4870
4983
  name = "IPFS Storage";
4871
4984
  type = "p2p";
@@ -4910,7 +5023,12 @@ var IpfsStorageProvider = class {
4910
5023
  flushDebounceMs;
4911
5024
  /** Set to true during shutdown to prevent new flushes */
4912
5025
  isShuttingDown = false;
5026
+ /** Stored config for createForAddress() cloning */
5027
+ _config;
5028
+ _statePersistenceCtor;
4913
5029
  constructor(config, statePersistence) {
5030
+ this._config = config;
5031
+ this._statePersistenceCtor = statePersistence;
4914
5032
  const gateways = config?.gateways ?? getIpfsGatewayUrls();
4915
5033
  this.debug = config?.debug ?? false;
4916
5034
  this.ipnsLifetimeMs = config?.ipnsLifetimeMs ?? 99 * 365 * 24 * 60 * 60 * 1e3;
@@ -5030,6 +5148,7 @@ var IpfsStorageProvider = class {
5030
5148
  }
5031
5149
  async shutdown() {
5032
5150
  this.isShuttingDown = true;
5151
+ logger.debug("IPFS-Storage", `shutdown: ipnsName=${this.ipnsName?.slice(0, 20)}..., pendingEmpty=${this.pendingBuffer.isEmpty}, capturedIpns=${this.pendingBuffer.capturedIpnsName?.slice(0, 20) ?? "none"}`);
5033
5152
  if (this.flushTimer) {
5034
5153
  clearTimeout(this.flushTimer);
5035
5154
  this.flushTimer = null;
@@ -5062,6 +5181,8 @@ var IpfsStorageProvider = class {
5062
5181
  return { success: false, error: "Not initialized", timestamp: Date.now() };
5063
5182
  }
5064
5183
  this.pendingBuffer.txfData = data;
5184
+ this.pendingBuffer.capturedIpnsKeyPair = this.ipnsKeyPair;
5185
+ this.pendingBuffer.capturedIpnsName = this.ipnsName;
5065
5186
  this.scheduleFlush();
5066
5187
  return { success: true, timestamp: Date.now() };
5067
5188
  }
@@ -5072,8 +5193,12 @@ var IpfsStorageProvider = class {
5072
5193
  * Perform the actual upload + IPNS publish synchronously.
5073
5194
  * Called by executeFlush() and sync() — never by public save().
5074
5195
  */
5075
- async _doSave(data) {
5076
- if (!this.ipnsKeyPair || !this.ipnsName) {
5196
+ async _doSave(data, overrideIpns) {
5197
+ const ipnsKeyPair = overrideIpns?.keyPair ?? this.ipnsKeyPair;
5198
+ const ipnsName = overrideIpns?.name ?? this.ipnsName;
5199
+ const metaAddr = data?._meta?.address;
5200
+ logger.debug("IPFS-Storage", `_doSave: ipnsName=${ipnsName?.slice(0, 20)}..., override=${!!overrideIpns}, meta.address=${metaAddr?.slice(0, 20) ?? "none"}`);
5201
+ if (!ipnsKeyPair || !ipnsName) {
5077
5202
  return { success: false, error: "Not initialized", timestamp: Date.now() };
5078
5203
  }
5079
5204
  this.emitEvent({ type: "storage:saving", timestamp: Date.now() });
@@ -5082,7 +5207,7 @@ var IpfsStorageProvider = class {
5082
5207
  const metaUpdate = {
5083
5208
  ...data._meta,
5084
5209
  version: this.dataVersion,
5085
- ipnsName: this.ipnsName,
5210
+ ipnsName,
5086
5211
  updatedAt: Date.now()
5087
5212
  };
5088
5213
  if (this.remoteCid) {
@@ -5094,13 +5219,13 @@ var IpfsStorageProvider = class {
5094
5219
  const baseSeq = this.ipnsSequenceNumber > this.lastKnownRemoteSequence ? this.ipnsSequenceNumber : this.lastKnownRemoteSequence;
5095
5220
  const newSeq = baseSeq + 1n;
5096
5221
  const marshalledRecord = await createSignedRecord(
5097
- this.ipnsKeyPair,
5222
+ ipnsKeyPair,
5098
5223
  cid,
5099
5224
  newSeq,
5100
5225
  this.ipnsLifetimeMs
5101
5226
  );
5102
5227
  const publishResult = await this.httpClient.publishIpns(
5103
- this.ipnsName,
5228
+ ipnsName,
5104
5229
  marshalledRecord
5105
5230
  );
5106
5231
  if (!publishResult.success) {
@@ -5115,14 +5240,14 @@ var IpfsStorageProvider = class {
5115
5240
  this.ipnsSequenceNumber = newSeq;
5116
5241
  this.lastCid = cid;
5117
5242
  this.remoteCid = cid;
5118
- this.cache.setIpnsRecord(this.ipnsName, {
5243
+ this.cache.setIpnsRecord(ipnsName, {
5119
5244
  cid,
5120
5245
  sequence: newSeq,
5121
5246
  gateway: "local"
5122
5247
  });
5123
5248
  this.cache.setContent(cid, updatedData);
5124
- this.cache.markIpnsFresh(this.ipnsName);
5125
- await this.statePersistence.save(this.ipnsName, {
5249
+ this.cache.markIpnsFresh(ipnsName);
5250
+ await this.statePersistence.save(ipnsName, {
5126
5251
  sequenceNumber: newSeq.toString(),
5127
5252
  lastCid: cid,
5128
5253
  version: this.dataVersion
@@ -5174,7 +5299,8 @@ var IpfsStorageProvider = class {
5174
5299
  const baseData = active.txfData ?? {
5175
5300
  _meta: { version: 0, address: this.identity?.directAddress ?? "", formatVersion: "2.0", updatedAt: 0 }
5176
5301
  };
5177
- const result = await this._doSave(baseData);
5302
+ const overrideIpns = active.capturedIpnsKeyPair && active.capturedIpnsName ? { keyPair: active.capturedIpnsKeyPair, name: active.capturedIpnsName } : void 0;
5303
+ const result = await this._doSave(baseData, overrideIpns);
5178
5304
  if (!result.success) {
5179
5305
  throw new SphereError(result.error ?? "Save failed", "STORAGE_ERROR");
5180
5306
  }
@@ -5477,6 +5603,13 @@ var IpfsStorageProvider = class {
5477
5603
  log(message) {
5478
5604
  logger.debug("IPFS-Storage", message);
5479
5605
  }
5606
+ /**
5607
+ * Create an independent instance for a different address.
5608
+ * Shares the same gateway/timeout config but has fresh IPNS state.
5609
+ */
5610
+ createForAddress() {
5611
+ return new _IpfsStorageProvider(this._config, this._statePersistenceCtor);
5612
+ }
5480
5613
  };
5481
5614
 
5482
5615
  // impl/browser/ipfs/browser-ipfs-state-persistence.ts