@unicitylabs/sphere-sdk 0.6.1 → 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,12 +195,30 @@ 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,
201
211
  * and resolves after EOSE (End Of Stored Events).
202
212
  */
203
213
  fetchPendingEvents?(): Promise<void>;
214
+ /**
215
+ * Register a handler to be called when the chat subscription receives EOSE
216
+ * (End Of Stored Events), indicating that historical DMs have been delivered.
217
+ * The handler fires at most once per subscription lifecycle.
218
+ *
219
+ * @returns Unsubscribe function
220
+ */
221
+ onChatReady?(handler: () => void): () => void;
204
222
  }
205
223
  /**
206
224
  * Payload for sending instant split bundles
@@ -468,6 +486,8 @@ declare class NostrTransportProvider implements TransportProvider {
468
486
  private storage;
469
487
  /** In-memory max event timestamp to avoid read-before-write races in updateLastEventTimestamp. */
470
488
  private lastEventTs;
489
+ /** Fallback 'since' timestamp for first-time address subscriptions (consumed once). */
490
+ private fallbackSince;
471
491
  private identity;
472
492
  private keyManager;
473
493
  private status;
@@ -485,6 +505,25 @@ declare class NostrTransportProvider implements TransportProvider {
485
505
  private broadcastHandlers;
486
506
  private eventCallbacks;
487
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;
488
527
  connect(): Promise<void>;
489
528
  disconnect(): Promise<void>;
490
529
  isConnected(): boolean;
@@ -518,6 +557,7 @@ declare class NostrTransportProvider implements TransportProvider {
518
557
  */
519
558
  isRelayConnected(relayUrl: string): boolean;
520
559
  setIdentity(identity: FullIdentity): Promise<void>;
560
+ setFallbackSince(sinceSeconds: number): void;
521
561
  /**
522
562
  * Get the Nostr-format public key (32 bytes / 64 hex chars)
523
563
  * This is the x-coordinate only, without the 02/03 prefix.
@@ -535,6 +575,7 @@ declare class NostrTransportProvider implements TransportProvider {
535
575
  onReadReceipt(handler: ReadReceiptHandler): () => void;
536
576
  sendTypingIndicator(recipientTransportPubkey: string): Promise<void>;
537
577
  onTypingIndicator(handler: TypingIndicatorHandler): () => void;
578
+ onChatReady(handler: () => void): () => void;
538
579
  onComposing(handler: ComposingHandler): () => void;
539
580
  sendComposingIndicator(recipientPubkey: string, content: string): Promise<void>;
540
581
  /**
@@ -604,6 +645,8 @@ declare class NostrTransportProvider implements TransportProvider {
604
645
  private queryEvents;
605
646
  private walletSubscriptionId;
606
647
  private chatSubscriptionId;
648
+ private chatEoseHandlers;
649
+ private chatEoseFired;
607
650
  private subscribeToEvents;
608
651
  private subscribeToTags;
609
652
  private decryptContent;
@@ -1122,6 +1165,13 @@ interface TokenStorageProvider<TData = unknown> extends BaseProvider {
1122
1165
  * Clear all data
1123
1166
  */
1124
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>;
1125
1175
  /**
1126
1176
  * Subscribe to storage events
1127
1177
  */
@@ -1271,6 +1321,10 @@ declare class FileTokenStorageProvider implements TokenStorageProvider<TxfStorag
1271
1321
  hasHistoryEntry(dedupKey: string): Promise<boolean>;
1272
1322
  clearHistory(): Promise<void>;
1273
1323
  importHistoryEntries(entries: HistoryRecord[]): Promise<number>;
1324
+ /**
1325
+ * Create an independent instance for a different address.
1326
+ */
1327
+ createForAddress(): FileTokenStorageProvider;
1274
1328
  }
1275
1329
  declare function createFileTokenStorageProvider(config: FileTokenStorageConfig | string): FileTokenStorageProvider;
1276
1330
 
@@ -195,12 +195,30 @@ 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,
201
211
  * and resolves after EOSE (End Of Stored Events).
202
212
  */
203
213
  fetchPendingEvents?(): Promise<void>;
214
+ /**
215
+ * Register a handler to be called when the chat subscription receives EOSE
216
+ * (End Of Stored Events), indicating that historical DMs have been delivered.
217
+ * The handler fires at most once per subscription lifecycle.
218
+ *
219
+ * @returns Unsubscribe function
220
+ */
221
+ onChatReady?(handler: () => void): () => void;
204
222
  }
205
223
  /**
206
224
  * Payload for sending instant split bundles
@@ -468,6 +486,8 @@ declare class NostrTransportProvider implements TransportProvider {
468
486
  private storage;
469
487
  /** In-memory max event timestamp to avoid read-before-write races in updateLastEventTimestamp. */
470
488
  private lastEventTs;
489
+ /** Fallback 'since' timestamp for first-time address subscriptions (consumed once). */
490
+ private fallbackSince;
471
491
  private identity;
472
492
  private keyManager;
473
493
  private status;
@@ -485,6 +505,25 @@ declare class NostrTransportProvider implements TransportProvider {
485
505
  private broadcastHandlers;
486
506
  private eventCallbacks;
487
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;
488
527
  connect(): Promise<void>;
489
528
  disconnect(): Promise<void>;
490
529
  isConnected(): boolean;
@@ -518,6 +557,7 @@ declare class NostrTransportProvider implements TransportProvider {
518
557
  */
519
558
  isRelayConnected(relayUrl: string): boolean;
520
559
  setIdentity(identity: FullIdentity): Promise<void>;
560
+ setFallbackSince(sinceSeconds: number): void;
521
561
  /**
522
562
  * Get the Nostr-format public key (32 bytes / 64 hex chars)
523
563
  * This is the x-coordinate only, without the 02/03 prefix.
@@ -535,6 +575,7 @@ declare class NostrTransportProvider implements TransportProvider {
535
575
  onReadReceipt(handler: ReadReceiptHandler): () => void;
536
576
  sendTypingIndicator(recipientTransportPubkey: string): Promise<void>;
537
577
  onTypingIndicator(handler: TypingIndicatorHandler): () => void;
578
+ onChatReady(handler: () => void): () => void;
538
579
  onComposing(handler: ComposingHandler): () => void;
539
580
  sendComposingIndicator(recipientPubkey: string, content: string): Promise<void>;
540
581
  /**
@@ -604,6 +645,8 @@ declare class NostrTransportProvider implements TransportProvider {
604
645
  private queryEvents;
605
646
  private walletSubscriptionId;
606
647
  private chatSubscriptionId;
648
+ private chatEoseHandlers;
649
+ private chatEoseFired;
607
650
  private subscribeToEvents;
608
651
  private subscribeToTags;
609
652
  private decryptContent;
@@ -1122,6 +1165,13 @@ interface TokenStorageProvider<TData = unknown> extends BaseProvider {
1122
1165
  * Clear all data
1123
1166
  */
1124
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>;
1125
1175
  /**
1126
1176
  * Subscribe to storage events
1127
1177
  */
@@ -1271,6 +1321,10 @@ declare class FileTokenStorageProvider implements TokenStorageProvider<TxfStorag
1271
1321
  hasHistoryEntry(dedupKey: string): Promise<boolean>;
1272
1322
  clearHistory(): Promise<void>;
1273
1323
  importHistoryEntries(entries: HistoryRecord[]): Promise<number>;
1324
+ /**
1325
+ * Create an independent instance for a different address.
1326
+ */
1327
+ createForAddress(): FileTokenStorageProvider;
1274
1328
  }
1275
1329
  declare function createFileTokenStorageProvider(config: FileTokenStorageConfig | string): FileTokenStorageProvider;
1276
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
  // ===========================================================================
@@ -1180,6 +1230,7 @@ var NostrTransportProvider = class {
1180
1230
  this.mainSubscriptionId = null;
1181
1231
  this.walletSubscriptionId = null;
1182
1232
  this.chatSubscriptionId = null;
1233
+ this.chatEoseFired = false;
1183
1234
  this.status = "disconnected";
1184
1235
  this.emitEvent({ type: "transport:disconnected", timestamp: Date.now() });
1185
1236
  logger.debug("Nostr", "Disconnected from all relays");
@@ -1290,6 +1341,8 @@ var NostrTransportProvider = class {
1290
1341
  // ===========================================================================
1291
1342
  async setIdentity(identity) {
1292
1343
  this.identity = identity;
1344
+ this.processedEventIds.clear();
1345
+ this.lastEventTs = 0;
1293
1346
  const secretKey = Buffer2.from(identity.privateKey, "hex");
1294
1347
  this.keyManager = NostrKeyManager.fromPrivateKey(secretKey);
1295
1348
  const nostrPubkey = this.keyManager.getPublicKeyHex();
@@ -1332,6 +1385,9 @@ var NostrTransportProvider = class {
1332
1385
  await this.subscribeToEvents();
1333
1386
  }
1334
1387
  }
1388
+ setFallbackSince(sinceSeconds) {
1389
+ this.fallbackSince = sinceSeconds;
1390
+ }
1335
1391
  /**
1336
1392
  * Get the Nostr-format public key (32 bytes / 64 hex chars)
1337
1393
  * This is the x-coordinate only, without the 02/03 prefix.
@@ -1503,6 +1559,20 @@ var NostrTransportProvider = class {
1503
1559
  this.typingIndicatorHandlers.add(handler);
1504
1560
  return () => this.typingIndicatorHandlers.delete(handler);
1505
1561
  }
1562
+ onChatReady(handler) {
1563
+ if (this.chatEoseFired) {
1564
+ try {
1565
+ handler();
1566
+ } catch {
1567
+ }
1568
+ return () => {
1569
+ };
1570
+ }
1571
+ this.chatEoseHandlers.push(handler);
1572
+ return () => {
1573
+ this.chatEoseHandlers = this.chatEoseHandlers.filter((h) => h !== handler);
1574
+ };
1575
+ }
1506
1576
  // ===========================================================================
1507
1577
  // Composing Indicators (NIP-59 kind 25050)
1508
1578
  // ===========================================================================
@@ -2220,8 +2290,15 @@ var NostrTransportProvider = class {
2220
2290
  // Track subscription IDs for cleanup
2221
2291
  walletSubscriptionId = null;
2222
2292
  chatSubscriptionId = null;
2293
+ // Chat EOSE handlers — fired once when relay finishes delivering stored DMs
2294
+ chatEoseHandlers = [];
2295
+ chatEoseFired = false;
2223
2296
  async subscribeToEvents() {
2224
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
+ }
2225
2302
  if (!this.identity || !this.keyManager || !this.nostrClient) {
2226
2303
  logger.debug("Nostr", "subscribeToEvents: skipped - no identity, keyManager, or nostrClient");
2227
2304
  return;
@@ -2248,7 +2325,13 @@ var NostrTransportProvider = class {
2248
2325
  if (stored) {
2249
2326
  since = parseInt(stored, 10);
2250
2327
  this.lastEventTs = since;
2328
+ this.fallbackSince = null;
2251
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);
2252
2335
  } else {
2253
2336
  since = Math.floor(Date.now() / 1e3);
2254
2337
  logger.debug("Nostr", "No stored timestamp, starting from now:", since);
@@ -2256,6 +2339,7 @@ var NostrTransportProvider = class {
2256
2339
  } catch (err) {
2257
2340
  logger.debug("Nostr", "Failed to read last event timestamp, falling back to now:", err);
2258
2341
  since = Math.floor(Date.now() / 1e3);
2342
+ this.fallbackSince = null;
2259
2343
  }
2260
2344
  } else {
2261
2345
  since = Math.floor(Date.now() / 1e3) - 86400;
@@ -2309,6 +2393,16 @@ var NostrTransportProvider = class {
2309
2393
  },
2310
2394
  onEndOfStoredEvents: () => {
2311
2395
  logger.debug("Nostr", "Chat subscription ready (EOSE)");
2396
+ if (!this.chatEoseFired) {
2397
+ this.chatEoseFired = true;
2398
+ for (const handler of this.chatEoseHandlers) {
2399
+ try {
2400
+ handler();
2401
+ } catch {
2402
+ }
2403
+ }
2404
+ this.chatEoseHandlers = [];
2405
+ }
2312
2406
  },
2313
2407
  onError: (_subId, error) => {
2314
2408
  logger.debug("Nostr", "Chat subscription error:", error);
@@ -4213,11 +4307,17 @@ var AsyncSerialQueue = class {
4213
4307
  var WriteBuffer = class {
4214
4308
  /** Full TXF data from save() calls — latest wins */
4215
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;
4216
4314
  get isEmpty() {
4217
4315
  return this.txfData === null;
4218
4316
  }
4219
4317
  clear() {
4220
4318
  this.txfData = null;
4319
+ this.capturedIpnsKeyPair = null;
4320
+ this.capturedIpnsName = null;
4221
4321
  }
4222
4322
  /**
4223
4323
  * Merge another buffer's contents into this one (for rollback).
@@ -4226,12 +4326,14 @@ var WriteBuffer = class {
4226
4326
  mergeFrom(other) {
4227
4327
  if (other.txfData && !this.txfData) {
4228
4328
  this.txfData = other.txfData;
4329
+ this.capturedIpnsKeyPair = other.capturedIpnsKeyPair;
4330
+ this.capturedIpnsName = other.capturedIpnsName;
4229
4331
  }
4230
4332
  }
4231
4333
  };
4232
4334
 
4233
4335
  // impl/shared/ipfs/ipfs-storage-provider.ts
4234
- var IpfsStorageProvider = class {
4336
+ var IpfsStorageProvider = class _IpfsStorageProvider {
4235
4337
  id = "ipfs";
4236
4338
  name = "IPFS Storage";
4237
4339
  type = "p2p";
@@ -4276,7 +4378,12 @@ var IpfsStorageProvider = class {
4276
4378
  flushDebounceMs;
4277
4379
  /** Set to true during shutdown to prevent new flushes */
4278
4380
  isShuttingDown = false;
4381
+ /** Stored config for createForAddress() cloning */
4382
+ _config;
4383
+ _statePersistenceCtor;
4279
4384
  constructor(config, statePersistence) {
4385
+ this._config = config;
4386
+ this._statePersistenceCtor = statePersistence;
4280
4387
  const gateways = config?.gateways ?? getIpfsGatewayUrls();
4281
4388
  this.debug = config?.debug ?? false;
4282
4389
  this.ipnsLifetimeMs = config?.ipnsLifetimeMs ?? 99 * 365 * 24 * 60 * 60 * 1e3;
@@ -4396,6 +4503,7 @@ var IpfsStorageProvider = class {
4396
4503
  }
4397
4504
  async shutdown() {
4398
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"}`);
4399
4507
  if (this.flushTimer) {
4400
4508
  clearTimeout(this.flushTimer);
4401
4509
  this.flushTimer = null;
@@ -4428,6 +4536,8 @@ var IpfsStorageProvider = class {
4428
4536
  return { success: false, error: "Not initialized", timestamp: Date.now() };
4429
4537
  }
4430
4538
  this.pendingBuffer.txfData = data;
4539
+ this.pendingBuffer.capturedIpnsKeyPair = this.ipnsKeyPair;
4540
+ this.pendingBuffer.capturedIpnsName = this.ipnsName;
4431
4541
  this.scheduleFlush();
4432
4542
  return { success: true, timestamp: Date.now() };
4433
4543
  }
@@ -4438,8 +4548,12 @@ var IpfsStorageProvider = class {
4438
4548
  * Perform the actual upload + IPNS publish synchronously.
4439
4549
  * Called by executeFlush() and sync() — never by public save().
4440
4550
  */
4441
- async _doSave(data) {
4442
- 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) {
4443
4557
  return { success: false, error: "Not initialized", timestamp: Date.now() };
4444
4558
  }
4445
4559
  this.emitEvent({ type: "storage:saving", timestamp: Date.now() });
@@ -4448,7 +4562,7 @@ var IpfsStorageProvider = class {
4448
4562
  const metaUpdate = {
4449
4563
  ...data._meta,
4450
4564
  version: this.dataVersion,
4451
- ipnsName: this.ipnsName,
4565
+ ipnsName,
4452
4566
  updatedAt: Date.now()
4453
4567
  };
4454
4568
  if (this.remoteCid) {
@@ -4460,13 +4574,13 @@ var IpfsStorageProvider = class {
4460
4574
  const baseSeq = this.ipnsSequenceNumber > this.lastKnownRemoteSequence ? this.ipnsSequenceNumber : this.lastKnownRemoteSequence;
4461
4575
  const newSeq = baseSeq + 1n;
4462
4576
  const marshalledRecord = await createSignedRecord(
4463
- this.ipnsKeyPair,
4577
+ ipnsKeyPair,
4464
4578
  cid,
4465
4579
  newSeq,
4466
4580
  this.ipnsLifetimeMs
4467
4581
  );
4468
4582
  const publishResult = await this.httpClient.publishIpns(
4469
- this.ipnsName,
4583
+ ipnsName,
4470
4584
  marshalledRecord
4471
4585
  );
4472
4586
  if (!publishResult.success) {
@@ -4481,14 +4595,14 @@ var IpfsStorageProvider = class {
4481
4595
  this.ipnsSequenceNumber = newSeq;
4482
4596
  this.lastCid = cid;
4483
4597
  this.remoteCid = cid;
4484
- this.cache.setIpnsRecord(this.ipnsName, {
4598
+ this.cache.setIpnsRecord(ipnsName, {
4485
4599
  cid,
4486
4600
  sequence: newSeq,
4487
4601
  gateway: "local"
4488
4602
  });
4489
4603
  this.cache.setContent(cid, updatedData);
4490
- this.cache.markIpnsFresh(this.ipnsName);
4491
- await this.statePersistence.save(this.ipnsName, {
4604
+ this.cache.markIpnsFresh(ipnsName);
4605
+ await this.statePersistence.save(ipnsName, {
4492
4606
  sequenceNumber: newSeq.toString(),
4493
4607
  lastCid: cid,
4494
4608
  version: this.dataVersion
@@ -4540,7 +4654,8 @@ var IpfsStorageProvider = class {
4540
4654
  const baseData = active.txfData ?? {
4541
4655
  _meta: { version: 0, address: this.identity?.directAddress ?? "", formatVersion: "2.0", updatedAt: 0 }
4542
4656
  };
4543
- 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);
4544
4659
  if (!result.success) {
4545
4660
  throw new SphereError(result.error ?? "Save failed", "STORAGE_ERROR");
4546
4661
  }
@@ -4843,6 +4958,13 @@ var IpfsStorageProvider = class {
4843
4958
  log(message) {
4844
4959
  logger.debug("IPFS-Storage", message);
4845
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
+ }
4846
4968
  };
4847
4969
 
4848
4970
  // impl/nodejs/ipfs/nodejs-ipfs-state-persistence.ts