@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.
@@ -1,3 +1,4 @@
1
+ import { NostrKeyManager, Event } from '@unicitylabs/nostr-js-sdk';
1
2
  import { StateTransitionClient } from '@unicitylabs/state-transition-sdk/lib/StateTransitionClient';
2
3
  import { AggregatorClient } from '@unicitylabs/state-transition-sdk/lib/api/AggregatorClient';
3
4
  import { RootTrustBase } from '@unicitylabs/state-transition-sdk/lib/bft/RootTrustBase';
@@ -195,6 +196,16 @@ interface TransportProvider extends BaseProvider {
195
196
  * @returns Unsubscribe function
196
197
  */
197
198
  onInstantSplitReceived?(handler: InstantSplitBundleHandler): () => void;
199
+ /**
200
+ * Set fallback 'since' timestamp for event subscriptions.
201
+ * Used when switching to an address that has never subscribed before.
202
+ * The transport uses this instead of 'now' as the initial since filter,
203
+ * ensuring events sent while the address was inactive are not missed.
204
+ * Consumed once by the next subscription setup, then cleared.
205
+ *
206
+ * @param sinceSeconds - Unix timestamp in seconds
207
+ */
208
+ setFallbackSince?(sinceSeconds: number): void;
198
209
  /**
199
210
  * Fetch pending events from transport (one-shot query).
200
211
  * Creates a temporary subscription, processes events through normal handlers,
@@ -476,6 +487,8 @@ declare class NostrTransportProvider implements TransportProvider {
476
487
  private storage;
477
488
  /** In-memory max event timestamp to avoid read-before-write races in updateLastEventTimestamp. */
478
489
  private lastEventTs;
490
+ /** Fallback 'since' timestamp for first-time address subscriptions (consumed once). */
491
+ private fallbackSince;
479
492
  private identity;
480
493
  private keyManager;
481
494
  private status;
@@ -493,6 +506,25 @@ declare class NostrTransportProvider implements TransportProvider {
493
506
  private broadcastHandlers;
494
507
  private eventCallbacks;
495
508
  constructor(config: NostrTransportProviderConfig);
509
+ /**
510
+ * Get the WebSocket factory (used by MultiAddressTransportMux to share the same factory).
511
+ */
512
+ getWebSocketFactory(): WebSocketFactory;
513
+ /**
514
+ * Get the configured relay URLs.
515
+ */
516
+ getConfiguredRelays(): string[];
517
+ /**
518
+ * Get the storage adapter.
519
+ */
520
+ getStorageAdapter(): TransportStorageAdapter | null;
521
+ /**
522
+ * Suppress event subscriptions — unsubscribe wallet/chat filters
523
+ * but keep the connection alive for resolve/identity-binding operations.
524
+ * Used when MultiAddressTransportMux takes over event handling.
525
+ */
526
+ suppressSubscriptions(): void;
527
+ private _subscriptionsSuppressed;
496
528
  connect(): Promise<void>;
497
529
  disconnect(): Promise<void>;
498
530
  isConnected(): boolean;
@@ -526,6 +558,7 @@ declare class NostrTransportProvider implements TransportProvider {
526
558
  */
527
559
  isRelayConnected(relayUrl: string): boolean;
528
560
  setIdentity(identity: FullIdentity): Promise<void>;
561
+ setFallbackSince(sinceSeconds: number): void;
529
562
  /**
530
563
  * Get the Nostr-format public key (32 bytes / 64 hex chars)
531
564
  * This is the x-coordinate only, without the 02/03 prefix.
@@ -624,6 +657,12 @@ declare class NostrTransportProvider implements TransportProvider {
624
657
  */
625
658
  private stripContentPrefix;
626
659
  private ensureConnected;
660
+ /**
661
+ * Async version of ensureConnected — reconnects if the original transport
662
+ * lost its WebSocket while subscriptions are suppressed (mux handles events).
663
+ * Used by resolve methods which are always async.
664
+ */
665
+ private ensureConnectedForResolve;
627
666
  private ensureReady;
628
667
  private emitEvent;
629
668
  /**
@@ -632,6 +671,11 @@ declare class NostrTransportProvider implements TransportProvider {
632
671
  * because NIP17.createGiftWrap hardcodes kind 14 for the inner rumor.
633
672
  */
634
673
  private createCustomKindGiftWrap;
674
+ /**
675
+ * Create a NIP-17 gift wrap with a custom rumor kind.
676
+ * Shared between NostrTransportProvider and MultiAddressTransportMux.
677
+ */
678
+ static createCustomKindGiftWrap(keyManager: NostrKeyManager, recipientPubkeyHex: string, content: string, rumorKind: number): Event;
635
679
  }
636
680
 
637
681
  /**
@@ -1133,6 +1177,13 @@ interface TokenStorageProvider<TData = unknown> extends BaseProvider {
1133
1177
  * Clear all data
1134
1178
  */
1135
1179
  clear?(): Promise<boolean>;
1180
+ /**
1181
+ * Create a new independent instance of this provider for a different address.
1182
+ * Used by per-address module architecture — each address gets its own
1183
+ * TokenStorageProvider instance to avoid cross-address data contamination.
1184
+ * If not implemented, the provider cannot be used in multi-address mode.
1185
+ */
1186
+ createForAddress?(): TokenStorageProvider<TData>;
1136
1187
  /**
1137
1188
  * Subscribe to storage events
1138
1189
  */
@@ -1282,6 +1333,10 @@ declare class FileTokenStorageProvider implements TokenStorageProvider<TxfStorag
1282
1333
  hasHistoryEntry(dedupKey: string): Promise<boolean>;
1283
1334
  clearHistory(): Promise<void>;
1284
1335
  importHistoryEntries(entries: HistoryRecord[]): Promise<number>;
1336
+ /**
1337
+ * Create an independent instance for a different address.
1338
+ */
1339
+ createForAddress(): FileTokenStorageProvider;
1285
1340
  }
1286
1341
  declare function createFileTokenStorageProvider(config: FileTokenStorageConfig | string): FileTokenStorageProvider;
1287
1342
 
@@ -1,3 +1,4 @@
1
+ import { NostrKeyManager, Event } from '@unicitylabs/nostr-js-sdk';
1
2
  import { StateTransitionClient } from '@unicitylabs/state-transition-sdk/lib/StateTransitionClient';
2
3
  import { AggregatorClient } from '@unicitylabs/state-transition-sdk/lib/api/AggregatorClient';
3
4
  import { RootTrustBase } from '@unicitylabs/state-transition-sdk/lib/bft/RootTrustBase';
@@ -195,6 +196,16 @@ interface TransportProvider extends BaseProvider {
195
196
  * @returns Unsubscribe function
196
197
  */
197
198
  onInstantSplitReceived?(handler: InstantSplitBundleHandler): () => void;
199
+ /**
200
+ * Set fallback 'since' timestamp for event subscriptions.
201
+ * Used when switching to an address that has never subscribed before.
202
+ * The transport uses this instead of 'now' as the initial since filter,
203
+ * ensuring events sent while the address was inactive are not missed.
204
+ * Consumed once by the next subscription setup, then cleared.
205
+ *
206
+ * @param sinceSeconds - Unix timestamp in seconds
207
+ */
208
+ setFallbackSince?(sinceSeconds: number): void;
198
209
  /**
199
210
  * Fetch pending events from transport (one-shot query).
200
211
  * Creates a temporary subscription, processes events through normal handlers,
@@ -476,6 +487,8 @@ declare class NostrTransportProvider implements TransportProvider {
476
487
  private storage;
477
488
  /** In-memory max event timestamp to avoid read-before-write races in updateLastEventTimestamp. */
478
489
  private lastEventTs;
490
+ /** Fallback 'since' timestamp for first-time address subscriptions (consumed once). */
491
+ private fallbackSince;
479
492
  private identity;
480
493
  private keyManager;
481
494
  private status;
@@ -493,6 +506,25 @@ declare class NostrTransportProvider implements TransportProvider {
493
506
  private broadcastHandlers;
494
507
  private eventCallbacks;
495
508
  constructor(config: NostrTransportProviderConfig);
509
+ /**
510
+ * Get the WebSocket factory (used by MultiAddressTransportMux to share the same factory).
511
+ */
512
+ getWebSocketFactory(): WebSocketFactory;
513
+ /**
514
+ * Get the configured relay URLs.
515
+ */
516
+ getConfiguredRelays(): string[];
517
+ /**
518
+ * Get the storage adapter.
519
+ */
520
+ getStorageAdapter(): TransportStorageAdapter | null;
521
+ /**
522
+ * Suppress event subscriptions — unsubscribe wallet/chat filters
523
+ * but keep the connection alive for resolve/identity-binding operations.
524
+ * Used when MultiAddressTransportMux takes over event handling.
525
+ */
526
+ suppressSubscriptions(): void;
527
+ private _subscriptionsSuppressed;
496
528
  connect(): Promise<void>;
497
529
  disconnect(): Promise<void>;
498
530
  isConnected(): boolean;
@@ -526,6 +558,7 @@ declare class NostrTransportProvider implements TransportProvider {
526
558
  */
527
559
  isRelayConnected(relayUrl: string): boolean;
528
560
  setIdentity(identity: FullIdentity): Promise<void>;
561
+ setFallbackSince(sinceSeconds: number): void;
529
562
  /**
530
563
  * Get the Nostr-format public key (32 bytes / 64 hex chars)
531
564
  * This is the x-coordinate only, without the 02/03 prefix.
@@ -624,6 +657,12 @@ declare class NostrTransportProvider implements TransportProvider {
624
657
  */
625
658
  private stripContentPrefix;
626
659
  private ensureConnected;
660
+ /**
661
+ * Async version of ensureConnected — reconnects if the original transport
662
+ * lost its WebSocket while subscriptions are suppressed (mux handles events).
663
+ * Used by resolve methods which are always async.
664
+ */
665
+ private ensureConnectedForResolve;
627
666
  private ensureReady;
628
667
  private emitEvent;
629
668
  /**
@@ -632,6 +671,11 @@ declare class NostrTransportProvider implements TransportProvider {
632
671
  * because NIP17.createGiftWrap hardcodes kind 14 for the inner rumor.
633
672
  */
634
673
  private createCustomKindGiftWrap;
674
+ /**
675
+ * Create a NIP-17 gift wrap with a custom rumor kind.
676
+ * Shared between NostrTransportProvider and MultiAddressTransportMux.
677
+ */
678
+ static createCustomKindGiftWrap(keyManager: NostrKeyManager, recipientPubkeyHex: string, content: string, rumorKind: number): Event;
635
679
  }
636
680
 
637
681
  /**
@@ -1133,6 +1177,13 @@ interface TokenStorageProvider<TData = unknown> extends BaseProvider {
1133
1177
  * Clear all data
1134
1178
  */
1135
1179
  clear?(): Promise<boolean>;
1180
+ /**
1181
+ * Create a new independent instance of this provider for a different address.
1182
+ * Used by per-address module architecture — each address gets its own
1183
+ * TokenStorageProvider instance to avoid cross-address data contamination.
1184
+ * If not implemented, the provider cannot be used in multi-address mode.
1185
+ */
1186
+ createForAddress?(): TokenStorageProvider<TData>;
1136
1187
  /**
1137
1188
  * Subscribe to storage events
1138
1189
  */
@@ -1282,6 +1333,10 @@ declare class FileTokenStorageProvider implements TokenStorageProvider<TxfStorag
1282
1333
  hasHistoryEntry(dedupKey: string): Promise<boolean>;
1283
1334
  clearHistory(): Promise<void>;
1284
1335
  importHistoryEntries(entries: HistoryRecord[]): Promise<number>;
1336
+ /**
1337
+ * Create an independent instance for a different address.
1338
+ */
1339
+ createForAddress(): FileTokenStorageProvider;
1285
1340
  }
1286
1341
  declare function createFileTokenStorageProvider(config: FileTokenStorageConfig | string): FileTokenStorageProvider;
1287
1342
 
@@ -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);
@@ -1072,7 +1078,7 @@ function defaultUUIDGenerator() {
1072
1078
  var COMPOSING_INDICATOR_KIND = 25050;
1073
1079
  var TIMESTAMP_RANDOMIZATION = 2 * 24 * 60 * 60;
1074
1080
  var EVENT_KINDS = NOSTR_EVENT_KINDS;
1075
- var NostrTransportProvider = class {
1081
+ var NostrTransportProvider = class _NostrTransportProvider {
1076
1082
  id = "nostr";
1077
1083
  name = "Nostr Transport";
1078
1084
  type = "p2p";
@@ -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.
@@ -1554,11 +1609,11 @@ var NostrTransportProvider = class {
1554
1609
  return this.resolveNametagInfo(identifier);
1555
1610
  }
1556
1611
  async resolveNametag(nametag) {
1557
- this.ensureConnected();
1612
+ await this.ensureConnectedForResolve();
1558
1613
  return this.nostrClient.queryPubkeyByNametag(nametag);
1559
1614
  }
1560
1615
  async resolveNametagInfo(nametag) {
1561
- this.ensureConnected();
1616
+ await this.ensureConnectedForResolve();
1562
1617
  const binding = await this.nostrClient.queryBindingByNametag(nametag);
1563
1618
  if (!binding) {
1564
1619
  logger.debug("Nostr", `resolveNametagInfo: no binding events found for Unicity ID "${nametag}"`);
@@ -1571,7 +1626,7 @@ var NostrTransportProvider = class {
1571
1626
  * Performs reverse lookup via nostr-js-sdk with first-seen-wins anti-hijacking.
1572
1627
  */
1573
1628
  async resolveAddressInfo(address) {
1574
- this.ensureConnected();
1629
+ await this.ensureConnectedForResolve();
1575
1630
  const binding = await this.nostrClient.queryBindingByAddress(address);
1576
1631
  if (!binding) return null;
1577
1632
  return this.bindingInfoToPeerInfo(binding);
@@ -1606,7 +1661,7 @@ var NostrTransportProvider = class {
1606
1661
  * Queries binding events authored by the given pubkey.
1607
1662
  */
1608
1663
  async resolveTransportPubkeyInfo(transportPubkey) {
1609
- this.ensureConnected();
1664
+ await this.ensureConnectedForResolve();
1610
1665
  const events = await this.queryEvents({
1611
1666
  kinds: [EVENT_KINDS.NAMETAG_BINDING],
1612
1667
  authors: [transportPubkey],
@@ -1641,7 +1696,7 @@ var NostrTransportProvider = class {
1641
1696
  * Used for HD address discovery — single relay query with multi-author filter.
1642
1697
  */
1643
1698
  async discoverAddresses(transportPubkeys) {
1644
- this.ensureConnected();
1699
+ await this.ensureConnectedForResolve();
1645
1700
  if (transportPubkeys.length === 0) return [];
1646
1701
  const events = await this.queryEvents({
1647
1702
  kinds: [EVENT_KINDS.NAMETAG_BINDING],
@@ -1680,7 +1735,10 @@ var NostrTransportProvider = class {
1680
1735
  * @returns Decrypted nametag or null if none found
1681
1736
  */
1682
1737
  async recoverNametag() {
1683
- this.ensureReady();
1738
+ await this.ensureConnectedForResolve();
1739
+ if (!this.identity) {
1740
+ throw new SphereError("Identity not set", "NOT_INITIALIZED");
1741
+ }
1684
1742
  if (!this.identity || !this.keyManager) {
1685
1743
  throw new SphereError("Identity not set", "NOT_INITIALIZED");
1686
1744
  }
@@ -2240,6 +2298,10 @@ var NostrTransportProvider = class {
2240
2298
  chatEoseFired = false;
2241
2299
  async subscribeToEvents() {
2242
2300
  logger.debug("Nostr", "subscribeToEvents called, identity:", !!this.identity, "keyManager:", !!this.keyManager, "nostrClient:", !!this.nostrClient);
2301
+ if (this._subscriptionsSuppressed) {
2302
+ logger.debug("Nostr", "subscribeToEvents: suppressed \u2014 mux handles event routing");
2303
+ return;
2304
+ }
2243
2305
  if (!this.identity || !this.keyManager || !this.nostrClient) {
2244
2306
  logger.debug("Nostr", "subscribeToEvents: skipped - no identity, keyManager, or nostrClient");
2245
2307
  return;
@@ -2266,7 +2328,13 @@ var NostrTransportProvider = class {
2266
2328
  if (stored) {
2267
2329
  since = parseInt(stored, 10);
2268
2330
  this.lastEventTs = since;
2331
+ this.fallbackSince = null;
2269
2332
  logger.debug("Nostr", "Resuming from stored event timestamp:", since);
2333
+ } else if (this.fallbackSince !== null) {
2334
+ since = this.fallbackSince;
2335
+ this.lastEventTs = since;
2336
+ this.fallbackSince = null;
2337
+ logger.debug("Nostr", "Using fallback since timestamp:", since);
2270
2338
  } else {
2271
2339
  since = Math.floor(Date.now() / 1e3);
2272
2340
  logger.debug("Nostr", "No stored timestamp, starting from now:", since);
@@ -2274,6 +2342,7 @@ var NostrTransportProvider = class {
2274
2342
  } catch (err) {
2275
2343
  logger.debug("Nostr", "Failed to read last event timestamp, falling back to now:", err);
2276
2344
  since = Math.floor(Date.now() / 1e3);
2345
+ this.fallbackSince = null;
2277
2346
  }
2278
2347
  } else {
2279
2348
  since = Math.floor(Date.now() / 1e3) - 86400;
@@ -2405,6 +2474,31 @@ var NostrTransportProvider = class {
2405
2474
  throw new SphereError("NostrTransportProvider not connected", "TRANSPORT_ERROR");
2406
2475
  }
2407
2476
  }
2477
+ /**
2478
+ * Async version of ensureConnected — reconnects if the original transport
2479
+ * lost its WebSocket while subscriptions are suppressed (mux handles events).
2480
+ * Used by resolve methods which are always async.
2481
+ */
2482
+ async ensureConnectedForResolve() {
2483
+ if (this.isConnected()) return;
2484
+ if (this._subscriptionsSuppressed && this.nostrClient) {
2485
+ logger.debug("Nostr", "Suppressed transport disconnected \u2014 reconnecting for resolve");
2486
+ try {
2487
+ await Promise.race([
2488
+ this.nostrClient.connect(...this.config.relays),
2489
+ new Promise(
2490
+ (_, reject) => setTimeout(() => reject(new Error("reconnect timeout")), 5e3)
2491
+ )
2492
+ ]);
2493
+ if (this.nostrClient.isConnected()) {
2494
+ this.status = "connected";
2495
+ return;
2496
+ }
2497
+ } catch {
2498
+ }
2499
+ }
2500
+ throw new SphereError("NostrTransportProvider not connected", "TRANSPORT_ERROR");
2501
+ }
2408
2502
  ensureReady() {
2409
2503
  this.ensureConnected();
2410
2504
  if (!this.identity) {
@@ -2426,16 +2520,23 @@ var NostrTransportProvider = class {
2426
2520
  * because NIP17.createGiftWrap hardcodes kind 14 for the inner rumor.
2427
2521
  */
2428
2522
  createCustomKindGiftWrap(recipientPubkeyHex, content, rumorKind) {
2429
- const senderPubkey = this.keyManager.getPublicKeyHex();
2523
+ return _NostrTransportProvider.createCustomKindGiftWrap(this.keyManager, recipientPubkeyHex, content, rumorKind);
2524
+ }
2525
+ /**
2526
+ * Create a NIP-17 gift wrap with a custom rumor kind.
2527
+ * Shared between NostrTransportProvider and MultiAddressTransportMux.
2528
+ */
2529
+ static createCustomKindGiftWrap(keyManager, recipientPubkeyHex, content, rumorKind) {
2530
+ const senderPubkey = keyManager.getPublicKeyHex();
2430
2531
  const now = Math.floor(Date.now() / 1e3);
2431
2532
  const rumorTags = [["p", recipientPubkeyHex]];
2432
2533
  const rumorSerialized = JSON.stringify([0, senderPubkey, now, rumorKind, rumorTags, content]);
2433
2534
  const rumorId = bytesToHex(sha256(new TextEncoder().encode(rumorSerialized)));
2434
2535
  const rumor = { id: rumorId, pubkey: senderPubkey, created_at: now, kind: rumorKind, tags: rumorTags, content };
2435
2536
  const recipientPubkeyBytes = hexToBytes(recipientPubkeyHex);
2436
- const encryptedRumor = NIP44.encrypt(JSON.stringify(rumor), this.keyManager.getPrivateKey(), recipientPubkeyBytes);
2537
+ const encryptedRumor = NIP44.encrypt(JSON.stringify(rumor), keyManager.getPrivateKey(), recipientPubkeyBytes);
2437
2538
  const sealTimestamp = now + Math.floor(Math.random() * 2 * TIMESTAMP_RANDOMIZATION) - TIMESTAMP_RANDOMIZATION;
2438
- const seal = NostrEventClass.create(this.keyManager, {
2539
+ const seal = NostrEventClass.create(keyManager, {
2439
2540
  kind: EventKinds.SEAL,
2440
2541
  tags: [],
2441
2542
  content: encryptedRumor,
@@ -4241,11 +4342,17 @@ var AsyncSerialQueue = class {
4241
4342
  var WriteBuffer = class {
4242
4343
  /** Full TXF data from save() calls — latest wins */
4243
4344
  txfData = null;
4345
+ /** IPNS context captured at save() time — ensures flush writes to the correct
4346
+ * IPNS record even if identity changes between save() and flush(). */
4347
+ capturedIpnsKeyPair = null;
4348
+ capturedIpnsName = null;
4244
4349
  get isEmpty() {
4245
4350
  return this.txfData === null;
4246
4351
  }
4247
4352
  clear() {
4248
4353
  this.txfData = null;
4354
+ this.capturedIpnsKeyPair = null;
4355
+ this.capturedIpnsName = null;
4249
4356
  }
4250
4357
  /**
4251
4358
  * Merge another buffer's contents into this one (for rollback).
@@ -4254,12 +4361,14 @@ var WriteBuffer = class {
4254
4361
  mergeFrom(other) {
4255
4362
  if (other.txfData && !this.txfData) {
4256
4363
  this.txfData = other.txfData;
4364
+ this.capturedIpnsKeyPair = other.capturedIpnsKeyPair;
4365
+ this.capturedIpnsName = other.capturedIpnsName;
4257
4366
  }
4258
4367
  }
4259
4368
  };
4260
4369
 
4261
4370
  // impl/shared/ipfs/ipfs-storage-provider.ts
4262
- var IpfsStorageProvider = class {
4371
+ var IpfsStorageProvider = class _IpfsStorageProvider {
4263
4372
  id = "ipfs";
4264
4373
  name = "IPFS Storage";
4265
4374
  type = "p2p";
@@ -4304,7 +4413,12 @@ var IpfsStorageProvider = class {
4304
4413
  flushDebounceMs;
4305
4414
  /** Set to true during shutdown to prevent new flushes */
4306
4415
  isShuttingDown = false;
4416
+ /** Stored config for createForAddress() cloning */
4417
+ _config;
4418
+ _statePersistenceCtor;
4307
4419
  constructor(config, statePersistence) {
4420
+ this._config = config;
4421
+ this._statePersistenceCtor = statePersistence;
4308
4422
  const gateways = config?.gateways ?? getIpfsGatewayUrls();
4309
4423
  this.debug = config?.debug ?? false;
4310
4424
  this.ipnsLifetimeMs = config?.ipnsLifetimeMs ?? 99 * 365 * 24 * 60 * 60 * 1e3;
@@ -4424,6 +4538,7 @@ var IpfsStorageProvider = class {
4424
4538
  }
4425
4539
  async shutdown() {
4426
4540
  this.isShuttingDown = true;
4541
+ logger.debug("IPFS-Storage", `shutdown: ipnsName=${this.ipnsName?.slice(0, 20)}..., pendingEmpty=${this.pendingBuffer.isEmpty}, capturedIpns=${this.pendingBuffer.capturedIpnsName?.slice(0, 20) ?? "none"}`);
4427
4542
  if (this.flushTimer) {
4428
4543
  clearTimeout(this.flushTimer);
4429
4544
  this.flushTimer = null;
@@ -4456,6 +4571,8 @@ var IpfsStorageProvider = class {
4456
4571
  return { success: false, error: "Not initialized", timestamp: Date.now() };
4457
4572
  }
4458
4573
  this.pendingBuffer.txfData = data;
4574
+ this.pendingBuffer.capturedIpnsKeyPair = this.ipnsKeyPair;
4575
+ this.pendingBuffer.capturedIpnsName = this.ipnsName;
4459
4576
  this.scheduleFlush();
4460
4577
  return { success: true, timestamp: Date.now() };
4461
4578
  }
@@ -4466,8 +4583,12 @@ var IpfsStorageProvider = class {
4466
4583
  * Perform the actual upload + IPNS publish synchronously.
4467
4584
  * Called by executeFlush() and sync() — never by public save().
4468
4585
  */
4469
- async _doSave(data) {
4470
- if (!this.ipnsKeyPair || !this.ipnsName) {
4586
+ async _doSave(data, overrideIpns) {
4587
+ const ipnsKeyPair = overrideIpns?.keyPair ?? this.ipnsKeyPair;
4588
+ const ipnsName = overrideIpns?.name ?? this.ipnsName;
4589
+ const metaAddr = data?._meta?.address;
4590
+ logger.debug("IPFS-Storage", `_doSave: ipnsName=${ipnsName?.slice(0, 20)}..., override=${!!overrideIpns}, meta.address=${metaAddr?.slice(0, 20) ?? "none"}`);
4591
+ if (!ipnsKeyPair || !ipnsName) {
4471
4592
  return { success: false, error: "Not initialized", timestamp: Date.now() };
4472
4593
  }
4473
4594
  this.emitEvent({ type: "storage:saving", timestamp: Date.now() });
@@ -4476,7 +4597,7 @@ var IpfsStorageProvider = class {
4476
4597
  const metaUpdate = {
4477
4598
  ...data._meta,
4478
4599
  version: this.dataVersion,
4479
- ipnsName: this.ipnsName,
4600
+ ipnsName,
4480
4601
  updatedAt: Date.now()
4481
4602
  };
4482
4603
  if (this.remoteCid) {
@@ -4488,13 +4609,13 @@ var IpfsStorageProvider = class {
4488
4609
  const baseSeq = this.ipnsSequenceNumber > this.lastKnownRemoteSequence ? this.ipnsSequenceNumber : this.lastKnownRemoteSequence;
4489
4610
  const newSeq = baseSeq + 1n;
4490
4611
  const marshalledRecord = await createSignedRecord(
4491
- this.ipnsKeyPair,
4612
+ ipnsKeyPair,
4492
4613
  cid,
4493
4614
  newSeq,
4494
4615
  this.ipnsLifetimeMs
4495
4616
  );
4496
4617
  const publishResult = await this.httpClient.publishIpns(
4497
- this.ipnsName,
4618
+ ipnsName,
4498
4619
  marshalledRecord
4499
4620
  );
4500
4621
  if (!publishResult.success) {
@@ -4509,14 +4630,14 @@ var IpfsStorageProvider = class {
4509
4630
  this.ipnsSequenceNumber = newSeq;
4510
4631
  this.lastCid = cid;
4511
4632
  this.remoteCid = cid;
4512
- this.cache.setIpnsRecord(this.ipnsName, {
4633
+ this.cache.setIpnsRecord(ipnsName, {
4513
4634
  cid,
4514
4635
  sequence: newSeq,
4515
4636
  gateway: "local"
4516
4637
  });
4517
4638
  this.cache.setContent(cid, updatedData);
4518
- this.cache.markIpnsFresh(this.ipnsName);
4519
- await this.statePersistence.save(this.ipnsName, {
4639
+ this.cache.markIpnsFresh(ipnsName);
4640
+ await this.statePersistence.save(ipnsName, {
4520
4641
  sequenceNumber: newSeq.toString(),
4521
4642
  lastCid: cid,
4522
4643
  version: this.dataVersion
@@ -4568,7 +4689,8 @@ var IpfsStorageProvider = class {
4568
4689
  const baseData = active.txfData ?? {
4569
4690
  _meta: { version: 0, address: this.identity?.directAddress ?? "", formatVersion: "2.0", updatedAt: 0 }
4570
4691
  };
4571
- const result = await this._doSave(baseData);
4692
+ const overrideIpns = active.capturedIpnsKeyPair && active.capturedIpnsName ? { keyPair: active.capturedIpnsKeyPair, name: active.capturedIpnsName } : void 0;
4693
+ const result = await this._doSave(baseData, overrideIpns);
4572
4694
  if (!result.success) {
4573
4695
  throw new SphereError(result.error ?? "Save failed", "STORAGE_ERROR");
4574
4696
  }
@@ -4871,6 +4993,13 @@ var IpfsStorageProvider = class {
4871
4993
  log(message) {
4872
4994
  logger.debug("IPFS-Storage", message);
4873
4995
  }
4996
+ /**
4997
+ * Create an independent instance for a different address.
4998
+ * Shares the same gateway/timeout config but has fresh IPNS state.
4999
+ */
5000
+ createForAddress() {
5001
+ return new _IpfsStorageProvider(this._config, this._statePersistenceCtor);
5002
+ }
4874
5003
  };
4875
5004
 
4876
5005
  // impl/nodejs/ipfs/nodejs-ipfs-state-persistence.ts