@unicitylabs/sphere-sdk 0.6.2 → 0.6.3

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.
@@ -195,6 +195,16 @@ interface TransportProvider extends BaseProvider {
195
195
  * @returns Unsubscribe function
196
196
  */
197
197
  onInstantSplitReceived?(handler: InstantSplitBundleHandler): () => void;
198
+ /**
199
+ * Set fallback 'since' timestamp for event subscriptions.
200
+ * Used when switching to an address that has never subscribed before.
201
+ * The transport uses this instead of 'now' as the initial since filter,
202
+ * ensuring events sent while the address was inactive are not missed.
203
+ * Consumed once by the next subscription setup, then cleared.
204
+ *
205
+ * @param sinceSeconds - Unix timestamp in seconds
206
+ */
207
+ setFallbackSince?(sinceSeconds: number): void;
198
208
  /**
199
209
  * Fetch pending events from transport (one-shot query).
200
210
  * Creates a temporary subscription, processes events through normal handlers,
@@ -476,6 +486,8 @@ declare class NostrTransportProvider implements TransportProvider {
476
486
  private storage;
477
487
  /** In-memory max event timestamp to avoid read-before-write races in updateLastEventTimestamp. */
478
488
  private lastEventTs;
489
+ /** Fallback 'since' timestamp for first-time address subscriptions (consumed once). */
490
+ private fallbackSince;
479
491
  private identity;
480
492
  private keyManager;
481
493
  private status;
@@ -493,6 +505,25 @@ declare class NostrTransportProvider implements TransportProvider {
493
505
  private broadcastHandlers;
494
506
  private eventCallbacks;
495
507
  constructor(config: NostrTransportProviderConfig);
508
+ /**
509
+ * Get the WebSocket factory (used by MultiAddressTransportMux to share the same factory).
510
+ */
511
+ getWebSocketFactory(): WebSocketFactory;
512
+ /**
513
+ * Get the configured relay URLs.
514
+ */
515
+ getConfiguredRelays(): string[];
516
+ /**
517
+ * Get the storage adapter.
518
+ */
519
+ getStorageAdapter(): TransportStorageAdapter | null;
520
+ /**
521
+ * Suppress event subscriptions — unsubscribe wallet/chat filters
522
+ * but keep the connection alive for resolve/identity-binding operations.
523
+ * Used when MultiAddressTransportMux takes over event handling.
524
+ */
525
+ suppressSubscriptions(): void;
526
+ private _subscriptionsSuppressed;
496
527
  connect(): Promise<void>;
497
528
  disconnect(): Promise<void>;
498
529
  isConnected(): boolean;
@@ -526,6 +557,7 @@ declare class NostrTransportProvider implements TransportProvider {
526
557
  */
527
558
  isRelayConnected(relayUrl: string): boolean;
528
559
  setIdentity(identity: FullIdentity): Promise<void>;
560
+ setFallbackSince(sinceSeconds: number): void;
529
561
  /**
530
562
  * Get the Nostr-format public key (32 bytes / 64 hex chars)
531
563
  * This is the x-coordinate only, without the 02/03 prefix.
@@ -1133,6 +1165,13 @@ interface TokenStorageProvider<TData = unknown> extends BaseProvider {
1133
1165
  * Clear all data
1134
1166
  */
1135
1167
  clear?(): Promise<boolean>;
1168
+ /**
1169
+ * Create a new independent instance of this provider for a different address.
1170
+ * Used by per-address module architecture — each address gets its own
1171
+ * TokenStorageProvider instance to avoid cross-address data contamination.
1172
+ * If not implemented, the provider cannot be used in multi-address mode.
1173
+ */
1174
+ createForAddress?(): TokenStorageProvider<TData>;
1136
1175
  /**
1137
1176
  * Subscribe to storage events
1138
1177
  */
@@ -1282,6 +1321,10 @@ declare class FileTokenStorageProvider implements TokenStorageProvider<TxfStorag
1282
1321
  hasHistoryEntry(dedupKey: string): Promise<boolean>;
1283
1322
  clearHistory(): Promise<void>;
1284
1323
  importHistoryEntries(entries: HistoryRecord[]): Promise<number>;
1324
+ /**
1325
+ * Create an independent instance for a different address.
1326
+ */
1327
+ createForAddress(): FileTokenStorageProvider;
1285
1328
  }
1286
1329
  declare function createFileTokenStorageProvider(config: FileTokenStorageConfig | string): FileTokenStorageProvider;
1287
1330
 
@@ -195,6 +195,16 @@ interface TransportProvider extends BaseProvider {
195
195
  * @returns Unsubscribe function
196
196
  */
197
197
  onInstantSplitReceived?(handler: InstantSplitBundleHandler): () => void;
198
+ /**
199
+ * Set fallback 'since' timestamp for event subscriptions.
200
+ * Used when switching to an address that has never subscribed before.
201
+ * The transport uses this instead of 'now' as the initial since filter,
202
+ * ensuring events sent while the address was inactive are not missed.
203
+ * Consumed once by the next subscription setup, then cleared.
204
+ *
205
+ * @param sinceSeconds - Unix timestamp in seconds
206
+ */
207
+ setFallbackSince?(sinceSeconds: number): void;
198
208
  /**
199
209
  * Fetch pending events from transport (one-shot query).
200
210
  * Creates a temporary subscription, processes events through normal handlers,
@@ -476,6 +486,8 @@ declare class NostrTransportProvider implements TransportProvider {
476
486
  private storage;
477
487
  /** In-memory max event timestamp to avoid read-before-write races in updateLastEventTimestamp. */
478
488
  private lastEventTs;
489
+ /** Fallback 'since' timestamp for first-time address subscriptions (consumed once). */
490
+ private fallbackSince;
479
491
  private identity;
480
492
  private keyManager;
481
493
  private status;
@@ -493,6 +505,25 @@ declare class NostrTransportProvider implements TransportProvider {
493
505
  private broadcastHandlers;
494
506
  private eventCallbacks;
495
507
  constructor(config: NostrTransportProviderConfig);
508
+ /**
509
+ * Get the WebSocket factory (used by MultiAddressTransportMux to share the same factory).
510
+ */
511
+ getWebSocketFactory(): WebSocketFactory;
512
+ /**
513
+ * Get the configured relay URLs.
514
+ */
515
+ getConfiguredRelays(): string[];
516
+ /**
517
+ * Get the storage adapter.
518
+ */
519
+ getStorageAdapter(): TransportStorageAdapter | null;
520
+ /**
521
+ * Suppress event subscriptions — unsubscribe wallet/chat filters
522
+ * but keep the connection alive for resolve/identity-binding operations.
523
+ * Used when MultiAddressTransportMux takes over event handling.
524
+ */
525
+ suppressSubscriptions(): void;
526
+ private _subscriptionsSuppressed;
496
527
  connect(): Promise<void>;
497
528
  disconnect(): Promise<void>;
498
529
  isConnected(): boolean;
@@ -526,6 +557,7 @@ declare class NostrTransportProvider implements TransportProvider {
526
557
  */
527
558
  isRelayConnected(relayUrl: string): boolean;
528
559
  setIdentity(identity: FullIdentity): Promise<void>;
560
+ setFallbackSince(sinceSeconds: number): void;
529
561
  /**
530
562
  * Get the Nostr-format public key (32 bytes / 64 hex chars)
531
563
  * This is the x-coordinate only, without the 02/03 prefix.
@@ -1133,6 +1165,13 @@ interface TokenStorageProvider<TData = unknown> extends BaseProvider {
1133
1165
  * Clear all data
1134
1166
  */
1135
1167
  clear?(): Promise<boolean>;
1168
+ /**
1169
+ * Create a new independent instance of this provider for a different address.
1170
+ * Used by per-address module architecture — each address gets its own
1171
+ * TokenStorageProvider instance to avoid cross-address data contamination.
1172
+ * If not implemented, the provider cannot be used in multi-address mode.
1173
+ */
1174
+ createForAddress?(): TokenStorageProvider<TData>;
1136
1175
  /**
1137
1176
  * Subscribe to storage events
1138
1177
  */
@@ -1282,6 +1321,10 @@ declare class FileTokenStorageProvider implements TokenStorageProvider<TxfStorag
1282
1321
  hasHistoryEntry(dedupKey: string): Promise<boolean>;
1283
1322
  clearHistory(): Promise<void>;
1284
1323
  importHistoryEntries(entries: HistoryRecord[]): Promise<number>;
1324
+ /**
1325
+ * Create an independent instance for a different address.
1326
+ */
1327
+ createForAddress(): FileTokenStorageProvider;
1285
1328
  }
1286
1329
  declare function createFileTokenStorageProvider(config: FileTokenStorageConfig | string): FileTokenStorageProvider;
1287
1330
 
@@ -319,7 +319,7 @@ import * as path2 from "path";
319
319
  var META_FILE = "_meta.json";
320
320
  var TOMBSTONES_FILE = "_tombstones.json";
321
321
  var HISTORY_FILE = "_history.json";
322
- var FileTokenStorageProvider = class {
322
+ var FileTokenStorageProvider = class _FileTokenStorageProvider {
323
323
  id = "file-token-storage";
324
324
  name = "File Token Storage";
325
325
  type = "local";
@@ -546,6 +546,12 @@ var FileTokenStorageProvider = class {
546
546
  }
547
547
  return imported;
548
548
  }
549
+ /**
550
+ * Create an independent instance for a different address.
551
+ */
552
+ createForAddress() {
553
+ return new _FileTokenStorageProvider({ tokensDir: this.baseTokensDir });
554
+ }
549
555
  };
550
556
  function createFileTokenStorageProvider(config) {
551
557
  return new FileTokenStorageProvider(config);
@@ -1081,6 +1087,8 @@ var NostrTransportProvider = class {
1081
1087
  storage = null;
1082
1088
  /** In-memory max event timestamp to avoid read-before-write races in updateLastEventTimestamp. */
1083
1089
  lastEventTs = 0;
1090
+ /** Fallback 'since' timestamp for first-time address subscriptions (consumed once). */
1091
+ fallbackSince = null;
1084
1092
  identity = null;
1085
1093
  keyManager = null;
1086
1094
  status = "disconnected";
@@ -1113,6 +1121,48 @@ var NostrTransportProvider = class {
1113
1121
  };
1114
1122
  this.storage = config.storage ?? null;
1115
1123
  }
1124
+ /**
1125
+ * Get the WebSocket factory (used by MultiAddressTransportMux to share the same factory).
1126
+ */
1127
+ getWebSocketFactory() {
1128
+ return this.config.createWebSocket;
1129
+ }
1130
+ /**
1131
+ * Get the configured relay URLs.
1132
+ */
1133
+ getConfiguredRelays() {
1134
+ return [...this.config.relays];
1135
+ }
1136
+ /**
1137
+ * Get the storage adapter.
1138
+ */
1139
+ getStorageAdapter() {
1140
+ return this.storage;
1141
+ }
1142
+ /**
1143
+ * Suppress event subscriptions — unsubscribe wallet/chat filters
1144
+ * but keep the connection alive for resolve/identity-binding operations.
1145
+ * Used when MultiAddressTransportMux takes over event handling.
1146
+ */
1147
+ suppressSubscriptions() {
1148
+ if (!this.nostrClient) return;
1149
+ if (this.walletSubscriptionId) {
1150
+ this.nostrClient.unsubscribe(this.walletSubscriptionId);
1151
+ this.walletSubscriptionId = null;
1152
+ }
1153
+ if (this.chatSubscriptionId) {
1154
+ this.nostrClient.unsubscribe(this.chatSubscriptionId);
1155
+ this.chatSubscriptionId = null;
1156
+ }
1157
+ if (this.mainSubscriptionId) {
1158
+ this.nostrClient.unsubscribe(this.mainSubscriptionId);
1159
+ this.mainSubscriptionId = null;
1160
+ }
1161
+ this._subscriptionsSuppressed = true;
1162
+ logger.debug("Nostr", "Subscriptions suppressed \u2014 mux handles event routing");
1163
+ }
1164
+ // Flag to prevent re-subscription after suppressSubscriptions()
1165
+ _subscriptionsSuppressed = false;
1116
1166
  // ===========================================================================
1117
1167
  // BaseProvider Implementation
1118
1168
  // ===========================================================================
@@ -1291,6 +1341,8 @@ var NostrTransportProvider = class {
1291
1341
  // ===========================================================================
1292
1342
  async setIdentity(identity) {
1293
1343
  this.identity = identity;
1344
+ this.processedEventIds.clear();
1345
+ this.lastEventTs = 0;
1294
1346
  const secretKey = Buffer2.from(identity.privateKey, "hex");
1295
1347
  this.keyManager = NostrKeyManager.fromPrivateKey(secretKey);
1296
1348
  const nostrPubkey = this.keyManager.getPublicKeyHex();
@@ -1333,6 +1385,9 @@ var NostrTransportProvider = class {
1333
1385
  await this.subscribeToEvents();
1334
1386
  }
1335
1387
  }
1388
+ setFallbackSince(sinceSeconds) {
1389
+ this.fallbackSince = sinceSeconds;
1390
+ }
1336
1391
  /**
1337
1392
  * Get the Nostr-format public key (32 bytes / 64 hex chars)
1338
1393
  * This is the x-coordinate only, without the 02/03 prefix.
@@ -2240,6 +2295,10 @@ var NostrTransportProvider = class {
2240
2295
  chatEoseFired = false;
2241
2296
  async subscribeToEvents() {
2242
2297
  logger.debug("Nostr", "subscribeToEvents called, identity:", !!this.identity, "keyManager:", !!this.keyManager, "nostrClient:", !!this.nostrClient);
2298
+ if (this._subscriptionsSuppressed) {
2299
+ logger.debug("Nostr", "subscribeToEvents: suppressed \u2014 mux handles event routing");
2300
+ return;
2301
+ }
2243
2302
  if (!this.identity || !this.keyManager || !this.nostrClient) {
2244
2303
  logger.debug("Nostr", "subscribeToEvents: skipped - no identity, keyManager, or nostrClient");
2245
2304
  return;
@@ -2266,7 +2325,13 @@ var NostrTransportProvider = class {
2266
2325
  if (stored) {
2267
2326
  since = parseInt(stored, 10);
2268
2327
  this.lastEventTs = since;
2328
+ this.fallbackSince = null;
2269
2329
  logger.debug("Nostr", "Resuming from stored event timestamp:", since);
2330
+ } else if (this.fallbackSince !== null) {
2331
+ since = this.fallbackSince;
2332
+ this.lastEventTs = since;
2333
+ this.fallbackSince = null;
2334
+ logger.debug("Nostr", "Using fallback since timestamp:", since);
2270
2335
  } else {
2271
2336
  since = Math.floor(Date.now() / 1e3);
2272
2337
  logger.debug("Nostr", "No stored timestamp, starting from now:", since);
@@ -2274,6 +2339,7 @@ var NostrTransportProvider = class {
2274
2339
  } catch (err) {
2275
2340
  logger.debug("Nostr", "Failed to read last event timestamp, falling back to now:", err);
2276
2341
  since = Math.floor(Date.now() / 1e3);
2342
+ this.fallbackSince = null;
2277
2343
  }
2278
2344
  } else {
2279
2345
  since = Math.floor(Date.now() / 1e3) - 86400;
@@ -4241,11 +4307,17 @@ var AsyncSerialQueue = class {
4241
4307
  var WriteBuffer = class {
4242
4308
  /** Full TXF data from save() calls — latest wins */
4243
4309
  txfData = null;
4310
+ /** IPNS context captured at save() time — ensures flush writes to the correct
4311
+ * IPNS record even if identity changes between save() and flush(). */
4312
+ capturedIpnsKeyPair = null;
4313
+ capturedIpnsName = null;
4244
4314
  get isEmpty() {
4245
4315
  return this.txfData === null;
4246
4316
  }
4247
4317
  clear() {
4248
4318
  this.txfData = null;
4319
+ this.capturedIpnsKeyPair = null;
4320
+ this.capturedIpnsName = null;
4249
4321
  }
4250
4322
  /**
4251
4323
  * Merge another buffer's contents into this one (for rollback).
@@ -4254,12 +4326,14 @@ var WriteBuffer = class {
4254
4326
  mergeFrom(other) {
4255
4327
  if (other.txfData && !this.txfData) {
4256
4328
  this.txfData = other.txfData;
4329
+ this.capturedIpnsKeyPair = other.capturedIpnsKeyPair;
4330
+ this.capturedIpnsName = other.capturedIpnsName;
4257
4331
  }
4258
4332
  }
4259
4333
  };
4260
4334
 
4261
4335
  // impl/shared/ipfs/ipfs-storage-provider.ts
4262
- var IpfsStorageProvider = class {
4336
+ var IpfsStorageProvider = class _IpfsStorageProvider {
4263
4337
  id = "ipfs";
4264
4338
  name = "IPFS Storage";
4265
4339
  type = "p2p";
@@ -4304,7 +4378,12 @@ var IpfsStorageProvider = class {
4304
4378
  flushDebounceMs;
4305
4379
  /** Set to true during shutdown to prevent new flushes */
4306
4380
  isShuttingDown = false;
4381
+ /** Stored config for createForAddress() cloning */
4382
+ _config;
4383
+ _statePersistenceCtor;
4307
4384
  constructor(config, statePersistence) {
4385
+ this._config = config;
4386
+ this._statePersistenceCtor = statePersistence;
4308
4387
  const gateways = config?.gateways ?? getIpfsGatewayUrls();
4309
4388
  this.debug = config?.debug ?? false;
4310
4389
  this.ipnsLifetimeMs = config?.ipnsLifetimeMs ?? 99 * 365 * 24 * 60 * 60 * 1e3;
@@ -4424,6 +4503,7 @@ var IpfsStorageProvider = class {
4424
4503
  }
4425
4504
  async shutdown() {
4426
4505
  this.isShuttingDown = true;
4506
+ logger.debug("IPFS-Storage", `shutdown: ipnsName=${this.ipnsName?.slice(0, 20)}..., pendingEmpty=${this.pendingBuffer.isEmpty}, capturedIpns=${this.pendingBuffer.capturedIpnsName?.slice(0, 20) ?? "none"}`);
4427
4507
  if (this.flushTimer) {
4428
4508
  clearTimeout(this.flushTimer);
4429
4509
  this.flushTimer = null;
@@ -4456,6 +4536,8 @@ var IpfsStorageProvider = class {
4456
4536
  return { success: false, error: "Not initialized", timestamp: Date.now() };
4457
4537
  }
4458
4538
  this.pendingBuffer.txfData = data;
4539
+ this.pendingBuffer.capturedIpnsKeyPair = this.ipnsKeyPair;
4540
+ this.pendingBuffer.capturedIpnsName = this.ipnsName;
4459
4541
  this.scheduleFlush();
4460
4542
  return { success: true, timestamp: Date.now() };
4461
4543
  }
@@ -4466,8 +4548,12 @@ var IpfsStorageProvider = class {
4466
4548
  * Perform the actual upload + IPNS publish synchronously.
4467
4549
  * Called by executeFlush() and sync() — never by public save().
4468
4550
  */
4469
- async _doSave(data) {
4470
- if (!this.ipnsKeyPair || !this.ipnsName) {
4551
+ async _doSave(data, overrideIpns) {
4552
+ const ipnsKeyPair = overrideIpns?.keyPair ?? this.ipnsKeyPair;
4553
+ const ipnsName = overrideIpns?.name ?? this.ipnsName;
4554
+ const metaAddr = data?._meta?.address;
4555
+ logger.debug("IPFS-Storage", `_doSave: ipnsName=${ipnsName?.slice(0, 20)}..., override=${!!overrideIpns}, meta.address=${metaAddr?.slice(0, 20) ?? "none"}`);
4556
+ if (!ipnsKeyPair || !ipnsName) {
4471
4557
  return { success: false, error: "Not initialized", timestamp: Date.now() };
4472
4558
  }
4473
4559
  this.emitEvent({ type: "storage:saving", timestamp: Date.now() });
@@ -4476,7 +4562,7 @@ var IpfsStorageProvider = class {
4476
4562
  const metaUpdate = {
4477
4563
  ...data._meta,
4478
4564
  version: this.dataVersion,
4479
- ipnsName: this.ipnsName,
4565
+ ipnsName,
4480
4566
  updatedAt: Date.now()
4481
4567
  };
4482
4568
  if (this.remoteCid) {
@@ -4488,13 +4574,13 @@ var IpfsStorageProvider = class {
4488
4574
  const baseSeq = this.ipnsSequenceNumber > this.lastKnownRemoteSequence ? this.ipnsSequenceNumber : this.lastKnownRemoteSequence;
4489
4575
  const newSeq = baseSeq + 1n;
4490
4576
  const marshalledRecord = await createSignedRecord(
4491
- this.ipnsKeyPair,
4577
+ ipnsKeyPair,
4492
4578
  cid,
4493
4579
  newSeq,
4494
4580
  this.ipnsLifetimeMs
4495
4581
  );
4496
4582
  const publishResult = await this.httpClient.publishIpns(
4497
- this.ipnsName,
4583
+ ipnsName,
4498
4584
  marshalledRecord
4499
4585
  );
4500
4586
  if (!publishResult.success) {
@@ -4509,14 +4595,14 @@ var IpfsStorageProvider = class {
4509
4595
  this.ipnsSequenceNumber = newSeq;
4510
4596
  this.lastCid = cid;
4511
4597
  this.remoteCid = cid;
4512
- this.cache.setIpnsRecord(this.ipnsName, {
4598
+ this.cache.setIpnsRecord(ipnsName, {
4513
4599
  cid,
4514
4600
  sequence: newSeq,
4515
4601
  gateway: "local"
4516
4602
  });
4517
4603
  this.cache.setContent(cid, updatedData);
4518
- this.cache.markIpnsFresh(this.ipnsName);
4519
- await this.statePersistence.save(this.ipnsName, {
4604
+ this.cache.markIpnsFresh(ipnsName);
4605
+ await this.statePersistence.save(ipnsName, {
4520
4606
  sequenceNumber: newSeq.toString(),
4521
4607
  lastCid: cid,
4522
4608
  version: this.dataVersion
@@ -4568,7 +4654,8 @@ var IpfsStorageProvider = class {
4568
4654
  const baseData = active.txfData ?? {
4569
4655
  _meta: { version: 0, address: this.identity?.directAddress ?? "", formatVersion: "2.0", updatedAt: 0 }
4570
4656
  };
4571
- const result = await this._doSave(baseData);
4657
+ const overrideIpns = active.capturedIpnsKeyPair && active.capturedIpnsName ? { keyPair: active.capturedIpnsKeyPair, name: active.capturedIpnsName } : void 0;
4658
+ const result = await this._doSave(baseData, overrideIpns);
4572
4659
  if (!result.success) {
4573
4660
  throw new SphereError(result.error ?? "Save failed", "STORAGE_ERROR");
4574
4661
  }
@@ -4871,6 +4958,13 @@ var IpfsStorageProvider = class {
4871
4958
  log(message) {
4872
4959
  logger.debug("IPFS-Storage", message);
4873
4960
  }
4961
+ /**
4962
+ * Create an independent instance for a different address.
4963
+ * Shares the same gateway/timeout config but has fresh IPNS state.
4964
+ */
4965
+ createForAddress() {
4966
+ return new _IpfsStorageProvider(this._config, this._statePersistenceCtor);
4967
+ }
4874
4968
  };
4875
4969
 
4876
4970
  // impl/nodejs/ipfs/nodejs-ipfs-state-persistence.ts