@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.
@@ -818,7 +818,7 @@ var STORE_TOKENS = "tokens";
818
818
  var STORE_META = "meta";
819
819
  var STORE_HISTORY = "history";
820
820
  var connectionSeq2 = 0;
821
- var IndexedDBTokenStorageProvider = class {
821
+ var IndexedDBTokenStorageProvider = class _IndexedDBTokenStorageProvider {
822
822
  id = "indexeddb-token-storage";
823
823
  name = "IndexedDB Token Storage";
824
824
  type = "local";
@@ -1266,6 +1266,16 @@ var IndexedDBTokenStorageProvider = class {
1266
1266
  db.close();
1267
1267
  }
1268
1268
  }
1269
+ /**
1270
+ * Create an independent instance for a different address.
1271
+ * The new instance shares the same config but has its own IDB connection.
1272
+ */
1273
+ createForAddress() {
1274
+ return new _IndexedDBTokenStorageProvider({
1275
+ dbNamePrefix: this.dbNamePrefix,
1276
+ debug: this.debug
1277
+ });
1278
+ }
1269
1279
  };
1270
1280
  function createIndexedDBTokenStorageProvider(config) {
1271
1281
  return new IndexedDBTokenStorageProvider(config);
@@ -1673,7 +1683,7 @@ function defaultUUIDGenerator() {
1673
1683
  var COMPOSING_INDICATOR_KIND = 25050;
1674
1684
  var TIMESTAMP_RANDOMIZATION = 2 * 24 * 60 * 60;
1675
1685
  var EVENT_KINDS = NOSTR_EVENT_KINDS;
1676
- var NostrTransportProvider = class {
1686
+ var NostrTransportProvider = class _NostrTransportProvider {
1677
1687
  id = "nostr";
1678
1688
  name = "Nostr Transport";
1679
1689
  type = "p2p";
@@ -1682,6 +1692,8 @@ var NostrTransportProvider = class {
1682
1692
  storage = null;
1683
1693
  /** In-memory max event timestamp to avoid read-before-write races in updateLastEventTimestamp. */
1684
1694
  lastEventTs = 0;
1695
+ /** Fallback 'since' timestamp for first-time address subscriptions (consumed once). */
1696
+ fallbackSince = null;
1685
1697
  identity = null;
1686
1698
  keyManager = null;
1687
1699
  status = "disconnected";
@@ -1714,6 +1726,48 @@ var NostrTransportProvider = class {
1714
1726
  };
1715
1727
  this.storage = config.storage ?? null;
1716
1728
  }
1729
+ /**
1730
+ * Get the WebSocket factory (used by MultiAddressTransportMux to share the same factory).
1731
+ */
1732
+ getWebSocketFactory() {
1733
+ return this.config.createWebSocket;
1734
+ }
1735
+ /**
1736
+ * Get the configured relay URLs.
1737
+ */
1738
+ getConfiguredRelays() {
1739
+ return [...this.config.relays];
1740
+ }
1741
+ /**
1742
+ * Get the storage adapter.
1743
+ */
1744
+ getStorageAdapter() {
1745
+ return this.storage;
1746
+ }
1747
+ /**
1748
+ * Suppress event subscriptions — unsubscribe wallet/chat filters
1749
+ * but keep the connection alive for resolve/identity-binding operations.
1750
+ * Used when MultiAddressTransportMux takes over event handling.
1751
+ */
1752
+ suppressSubscriptions() {
1753
+ if (!this.nostrClient) return;
1754
+ if (this.walletSubscriptionId) {
1755
+ this.nostrClient.unsubscribe(this.walletSubscriptionId);
1756
+ this.walletSubscriptionId = null;
1757
+ }
1758
+ if (this.chatSubscriptionId) {
1759
+ this.nostrClient.unsubscribe(this.chatSubscriptionId);
1760
+ this.chatSubscriptionId = null;
1761
+ }
1762
+ if (this.mainSubscriptionId) {
1763
+ this.nostrClient.unsubscribe(this.mainSubscriptionId);
1764
+ this.mainSubscriptionId = null;
1765
+ }
1766
+ this._subscriptionsSuppressed = true;
1767
+ logger.debug("Nostr", "Subscriptions suppressed \u2014 mux handles event routing");
1768
+ }
1769
+ // Flag to prevent re-subscription after suppressSubscriptions()
1770
+ _subscriptionsSuppressed = false;
1717
1771
  // ===========================================================================
1718
1772
  // BaseProvider Implementation
1719
1773
  // ===========================================================================
@@ -1892,6 +1946,8 @@ var NostrTransportProvider = class {
1892
1946
  // ===========================================================================
1893
1947
  async setIdentity(identity) {
1894
1948
  this.identity = identity;
1949
+ this.processedEventIds.clear();
1950
+ this.lastEventTs = 0;
1895
1951
  const secretKey = import_buffer.Buffer.from(identity.privateKey, "hex");
1896
1952
  this.keyManager = import_nostr_js_sdk.NostrKeyManager.fromPrivateKey(secretKey);
1897
1953
  const nostrPubkey = this.keyManager.getPublicKeyHex();
@@ -1934,6 +1990,9 @@ var NostrTransportProvider = class {
1934
1990
  await this.subscribeToEvents();
1935
1991
  }
1936
1992
  }
1993
+ setFallbackSince(sinceSeconds) {
1994
+ this.fallbackSince = sinceSeconds;
1995
+ }
1937
1996
  /**
1938
1997
  * Get the Nostr-format public key (32 bytes / 64 hex chars)
1939
1998
  * This is the x-coordinate only, without the 02/03 prefix.
@@ -2155,11 +2214,11 @@ var NostrTransportProvider = class {
2155
2214
  return this.resolveNametagInfo(identifier);
2156
2215
  }
2157
2216
  async resolveNametag(nametag) {
2158
- this.ensureConnected();
2217
+ await this.ensureConnectedForResolve();
2159
2218
  return this.nostrClient.queryPubkeyByNametag(nametag);
2160
2219
  }
2161
2220
  async resolveNametagInfo(nametag) {
2162
- this.ensureConnected();
2221
+ await this.ensureConnectedForResolve();
2163
2222
  const binding = await this.nostrClient.queryBindingByNametag(nametag);
2164
2223
  if (!binding) {
2165
2224
  logger.debug("Nostr", `resolveNametagInfo: no binding events found for Unicity ID "${nametag}"`);
@@ -2172,7 +2231,7 @@ var NostrTransportProvider = class {
2172
2231
  * Performs reverse lookup via nostr-js-sdk with first-seen-wins anti-hijacking.
2173
2232
  */
2174
2233
  async resolveAddressInfo(address) {
2175
- this.ensureConnected();
2234
+ await this.ensureConnectedForResolve();
2176
2235
  const binding = await this.nostrClient.queryBindingByAddress(address);
2177
2236
  if (!binding) return null;
2178
2237
  return this.bindingInfoToPeerInfo(binding);
@@ -2207,7 +2266,7 @@ var NostrTransportProvider = class {
2207
2266
  * Queries binding events authored by the given pubkey.
2208
2267
  */
2209
2268
  async resolveTransportPubkeyInfo(transportPubkey) {
2210
- this.ensureConnected();
2269
+ await this.ensureConnectedForResolve();
2211
2270
  const events = await this.queryEvents({
2212
2271
  kinds: [EVENT_KINDS.NAMETAG_BINDING],
2213
2272
  authors: [transportPubkey],
@@ -2242,7 +2301,7 @@ var NostrTransportProvider = class {
2242
2301
  * Used for HD address discovery — single relay query with multi-author filter.
2243
2302
  */
2244
2303
  async discoverAddresses(transportPubkeys) {
2245
- this.ensureConnected();
2304
+ await this.ensureConnectedForResolve();
2246
2305
  if (transportPubkeys.length === 0) return [];
2247
2306
  const events = await this.queryEvents({
2248
2307
  kinds: [EVENT_KINDS.NAMETAG_BINDING],
@@ -2281,7 +2340,10 @@ var NostrTransportProvider = class {
2281
2340
  * @returns Decrypted nametag or null if none found
2282
2341
  */
2283
2342
  async recoverNametag() {
2284
- this.ensureReady();
2343
+ await this.ensureConnectedForResolve();
2344
+ if (!this.identity) {
2345
+ throw new SphereError("Identity not set", "NOT_INITIALIZED");
2346
+ }
2285
2347
  if (!this.identity || !this.keyManager) {
2286
2348
  throw new SphereError("Identity not set", "NOT_INITIALIZED");
2287
2349
  }
@@ -2841,6 +2903,10 @@ var NostrTransportProvider = class {
2841
2903
  chatEoseFired = false;
2842
2904
  async subscribeToEvents() {
2843
2905
  logger.debug("Nostr", "subscribeToEvents called, identity:", !!this.identity, "keyManager:", !!this.keyManager, "nostrClient:", !!this.nostrClient);
2906
+ if (this._subscriptionsSuppressed) {
2907
+ logger.debug("Nostr", "subscribeToEvents: suppressed \u2014 mux handles event routing");
2908
+ return;
2909
+ }
2844
2910
  if (!this.identity || !this.keyManager || !this.nostrClient) {
2845
2911
  logger.debug("Nostr", "subscribeToEvents: skipped - no identity, keyManager, or nostrClient");
2846
2912
  return;
@@ -2867,7 +2933,13 @@ var NostrTransportProvider = class {
2867
2933
  if (stored) {
2868
2934
  since = parseInt(stored, 10);
2869
2935
  this.lastEventTs = since;
2936
+ this.fallbackSince = null;
2870
2937
  logger.debug("Nostr", "Resuming from stored event timestamp:", since);
2938
+ } else if (this.fallbackSince !== null) {
2939
+ since = this.fallbackSince;
2940
+ this.lastEventTs = since;
2941
+ this.fallbackSince = null;
2942
+ logger.debug("Nostr", "Using fallback since timestamp:", since);
2871
2943
  } else {
2872
2944
  since = Math.floor(Date.now() / 1e3);
2873
2945
  logger.debug("Nostr", "No stored timestamp, starting from now:", since);
@@ -2875,6 +2947,7 @@ var NostrTransportProvider = class {
2875
2947
  } catch (err) {
2876
2948
  logger.debug("Nostr", "Failed to read last event timestamp, falling back to now:", err);
2877
2949
  since = Math.floor(Date.now() / 1e3);
2950
+ this.fallbackSince = null;
2878
2951
  }
2879
2952
  } else {
2880
2953
  since = Math.floor(Date.now() / 1e3) - 86400;
@@ -3006,6 +3079,31 @@ var NostrTransportProvider = class {
3006
3079
  throw new SphereError("NostrTransportProvider not connected", "TRANSPORT_ERROR");
3007
3080
  }
3008
3081
  }
3082
+ /**
3083
+ * Async version of ensureConnected — reconnects if the original transport
3084
+ * lost its WebSocket while subscriptions are suppressed (mux handles events).
3085
+ * Used by resolve methods which are always async.
3086
+ */
3087
+ async ensureConnectedForResolve() {
3088
+ if (this.isConnected()) return;
3089
+ if (this._subscriptionsSuppressed && this.nostrClient) {
3090
+ logger.debug("Nostr", "Suppressed transport disconnected \u2014 reconnecting for resolve");
3091
+ try {
3092
+ await Promise.race([
3093
+ this.nostrClient.connect(...this.config.relays),
3094
+ new Promise(
3095
+ (_, reject) => setTimeout(() => reject(new Error("reconnect timeout")), 5e3)
3096
+ )
3097
+ ]);
3098
+ if (this.nostrClient.isConnected()) {
3099
+ this.status = "connected";
3100
+ return;
3101
+ }
3102
+ } catch {
3103
+ }
3104
+ }
3105
+ throw new SphereError("NostrTransportProvider not connected", "TRANSPORT_ERROR");
3106
+ }
3009
3107
  ensureReady() {
3010
3108
  this.ensureConnected();
3011
3109
  if (!this.identity) {
@@ -3027,16 +3125,23 @@ var NostrTransportProvider = class {
3027
3125
  * because NIP17.createGiftWrap hardcodes kind 14 for the inner rumor.
3028
3126
  */
3029
3127
  createCustomKindGiftWrap(recipientPubkeyHex, content, rumorKind) {
3030
- const senderPubkey = this.keyManager.getPublicKeyHex();
3128
+ return _NostrTransportProvider.createCustomKindGiftWrap(this.keyManager, recipientPubkeyHex, content, rumorKind);
3129
+ }
3130
+ /**
3131
+ * Create a NIP-17 gift wrap with a custom rumor kind.
3132
+ * Shared between NostrTransportProvider and MultiAddressTransportMux.
3133
+ */
3134
+ static createCustomKindGiftWrap(keyManager, recipientPubkeyHex, content, rumorKind) {
3135
+ const senderPubkey = keyManager.getPublicKeyHex();
3031
3136
  const now = Math.floor(Date.now() / 1e3);
3032
3137
  const rumorTags = [["p", recipientPubkeyHex]];
3033
3138
  const rumorSerialized = JSON.stringify([0, senderPubkey, now, rumorKind, rumorTags, content]);
3034
3139
  const rumorId = bytesToHex(sha256(new TextEncoder().encode(rumorSerialized)));
3035
3140
  const rumor = { id: rumorId, pubkey: senderPubkey, created_at: now, kind: rumorKind, tags: rumorTags, content };
3036
3141
  const recipientPubkeyBytes = hexToBytes(recipientPubkeyHex);
3037
- const encryptedRumor = import_nostr_js_sdk.NIP44.encrypt(JSON.stringify(rumor), this.keyManager.getPrivateKey(), recipientPubkeyBytes);
3142
+ const encryptedRumor = import_nostr_js_sdk.NIP44.encrypt(JSON.stringify(rumor), keyManager.getPrivateKey(), recipientPubkeyBytes);
3038
3143
  const sealTimestamp = now + Math.floor(Math.random() * 2 * TIMESTAMP_RANDOMIZATION) - TIMESTAMP_RANDOMIZATION;
3039
- const seal = import_nostr_js_sdk.Event.create(this.keyManager, {
3144
+ const seal = import_nostr_js_sdk.Event.create(keyManager, {
3040
3145
  kind: import_nostr_js_sdk.EventKinds.SEAL,
3041
3146
  tags: [],
3042
3147
  content: encryptedRumor,
@@ -4895,11 +5000,17 @@ var AsyncSerialQueue = class {
4895
5000
  var WriteBuffer = class {
4896
5001
  /** Full TXF data from save() calls — latest wins */
4897
5002
  txfData = null;
5003
+ /** IPNS context captured at save() time — ensures flush writes to the correct
5004
+ * IPNS record even if identity changes between save() and flush(). */
5005
+ capturedIpnsKeyPair = null;
5006
+ capturedIpnsName = null;
4898
5007
  get isEmpty() {
4899
5008
  return this.txfData === null;
4900
5009
  }
4901
5010
  clear() {
4902
5011
  this.txfData = null;
5012
+ this.capturedIpnsKeyPair = null;
5013
+ this.capturedIpnsName = null;
4903
5014
  }
4904
5015
  /**
4905
5016
  * Merge another buffer's contents into this one (for rollback).
@@ -4908,12 +5019,14 @@ var WriteBuffer = class {
4908
5019
  mergeFrom(other) {
4909
5020
  if (other.txfData && !this.txfData) {
4910
5021
  this.txfData = other.txfData;
5022
+ this.capturedIpnsKeyPair = other.capturedIpnsKeyPair;
5023
+ this.capturedIpnsName = other.capturedIpnsName;
4911
5024
  }
4912
5025
  }
4913
5026
  };
4914
5027
 
4915
5028
  // impl/shared/ipfs/ipfs-storage-provider.ts
4916
- var IpfsStorageProvider = class {
5029
+ var IpfsStorageProvider = class _IpfsStorageProvider {
4917
5030
  id = "ipfs";
4918
5031
  name = "IPFS Storage";
4919
5032
  type = "p2p";
@@ -4958,7 +5071,12 @@ var IpfsStorageProvider = class {
4958
5071
  flushDebounceMs;
4959
5072
  /** Set to true during shutdown to prevent new flushes */
4960
5073
  isShuttingDown = false;
5074
+ /** Stored config for createForAddress() cloning */
5075
+ _config;
5076
+ _statePersistenceCtor;
4961
5077
  constructor(config, statePersistence) {
5078
+ this._config = config;
5079
+ this._statePersistenceCtor = statePersistence;
4962
5080
  const gateways = config?.gateways ?? getIpfsGatewayUrls();
4963
5081
  this.debug = config?.debug ?? false;
4964
5082
  this.ipnsLifetimeMs = config?.ipnsLifetimeMs ?? 99 * 365 * 24 * 60 * 60 * 1e3;
@@ -5078,6 +5196,7 @@ var IpfsStorageProvider = class {
5078
5196
  }
5079
5197
  async shutdown() {
5080
5198
  this.isShuttingDown = true;
5199
+ logger.debug("IPFS-Storage", `shutdown: ipnsName=${this.ipnsName?.slice(0, 20)}..., pendingEmpty=${this.pendingBuffer.isEmpty}, capturedIpns=${this.pendingBuffer.capturedIpnsName?.slice(0, 20) ?? "none"}`);
5081
5200
  if (this.flushTimer) {
5082
5201
  clearTimeout(this.flushTimer);
5083
5202
  this.flushTimer = null;
@@ -5110,6 +5229,8 @@ var IpfsStorageProvider = class {
5110
5229
  return { success: false, error: "Not initialized", timestamp: Date.now() };
5111
5230
  }
5112
5231
  this.pendingBuffer.txfData = data;
5232
+ this.pendingBuffer.capturedIpnsKeyPair = this.ipnsKeyPair;
5233
+ this.pendingBuffer.capturedIpnsName = this.ipnsName;
5113
5234
  this.scheduleFlush();
5114
5235
  return { success: true, timestamp: Date.now() };
5115
5236
  }
@@ -5120,8 +5241,12 @@ var IpfsStorageProvider = class {
5120
5241
  * Perform the actual upload + IPNS publish synchronously.
5121
5242
  * Called by executeFlush() and sync() — never by public save().
5122
5243
  */
5123
- async _doSave(data) {
5124
- if (!this.ipnsKeyPair || !this.ipnsName) {
5244
+ async _doSave(data, overrideIpns) {
5245
+ const ipnsKeyPair = overrideIpns?.keyPair ?? this.ipnsKeyPair;
5246
+ const ipnsName = overrideIpns?.name ?? this.ipnsName;
5247
+ const metaAddr = data?._meta?.address;
5248
+ logger.debug("IPFS-Storage", `_doSave: ipnsName=${ipnsName?.slice(0, 20)}..., override=${!!overrideIpns}, meta.address=${metaAddr?.slice(0, 20) ?? "none"}`);
5249
+ if (!ipnsKeyPair || !ipnsName) {
5125
5250
  return { success: false, error: "Not initialized", timestamp: Date.now() };
5126
5251
  }
5127
5252
  this.emitEvent({ type: "storage:saving", timestamp: Date.now() });
@@ -5130,7 +5255,7 @@ var IpfsStorageProvider = class {
5130
5255
  const metaUpdate = {
5131
5256
  ...data._meta,
5132
5257
  version: this.dataVersion,
5133
- ipnsName: this.ipnsName,
5258
+ ipnsName,
5134
5259
  updatedAt: Date.now()
5135
5260
  };
5136
5261
  if (this.remoteCid) {
@@ -5142,13 +5267,13 @@ var IpfsStorageProvider = class {
5142
5267
  const baseSeq = this.ipnsSequenceNumber > this.lastKnownRemoteSequence ? this.ipnsSequenceNumber : this.lastKnownRemoteSequence;
5143
5268
  const newSeq = baseSeq + 1n;
5144
5269
  const marshalledRecord = await createSignedRecord(
5145
- this.ipnsKeyPair,
5270
+ ipnsKeyPair,
5146
5271
  cid,
5147
5272
  newSeq,
5148
5273
  this.ipnsLifetimeMs
5149
5274
  );
5150
5275
  const publishResult = await this.httpClient.publishIpns(
5151
- this.ipnsName,
5276
+ ipnsName,
5152
5277
  marshalledRecord
5153
5278
  );
5154
5279
  if (!publishResult.success) {
@@ -5163,14 +5288,14 @@ var IpfsStorageProvider = class {
5163
5288
  this.ipnsSequenceNumber = newSeq;
5164
5289
  this.lastCid = cid;
5165
5290
  this.remoteCid = cid;
5166
- this.cache.setIpnsRecord(this.ipnsName, {
5291
+ this.cache.setIpnsRecord(ipnsName, {
5167
5292
  cid,
5168
5293
  sequence: newSeq,
5169
5294
  gateway: "local"
5170
5295
  });
5171
5296
  this.cache.setContent(cid, updatedData);
5172
- this.cache.markIpnsFresh(this.ipnsName);
5173
- await this.statePersistence.save(this.ipnsName, {
5297
+ this.cache.markIpnsFresh(ipnsName);
5298
+ await this.statePersistence.save(ipnsName, {
5174
5299
  sequenceNumber: newSeq.toString(),
5175
5300
  lastCid: cid,
5176
5301
  version: this.dataVersion
@@ -5222,7 +5347,8 @@ var IpfsStorageProvider = class {
5222
5347
  const baseData = active.txfData ?? {
5223
5348
  _meta: { version: 0, address: this.identity?.directAddress ?? "", formatVersion: "2.0", updatedAt: 0 }
5224
5349
  };
5225
- const result = await this._doSave(baseData);
5350
+ const overrideIpns = active.capturedIpnsKeyPair && active.capturedIpnsName ? { keyPair: active.capturedIpnsKeyPair, name: active.capturedIpnsName } : void 0;
5351
+ const result = await this._doSave(baseData, overrideIpns);
5226
5352
  if (!result.success) {
5227
5353
  throw new SphereError(result.error ?? "Save failed", "STORAGE_ERROR");
5228
5354
  }
@@ -5525,6 +5651,13 @@ var IpfsStorageProvider = class {
5525
5651
  log(message) {
5526
5652
  logger.debug("IPFS-Storage", message);
5527
5653
  }
5654
+ /**
5655
+ * Create an independent instance for a different address.
5656
+ * Shares the same gateway/timeout config but has fresh IPNS state.
5657
+ */
5658
+ createForAddress() {
5659
+ return new _IpfsStorageProvider(this._config, this._statePersistenceCtor);
5660
+ }
5528
5661
  };
5529
5662
 
5530
5663
  // impl/browser/ipfs/browser-ipfs-state-persistence.ts