@unicitylabs/sphere-sdk 0.14.1 → 0.14.2

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.
Files changed (39) hide show
  1. package/dist/connect/index.cjs +1 -1
  2. package/dist/connect/index.cjs.map +1 -1
  3. package/dist/connect/index.js +1 -1
  4. package/dist/connect/index.js.map +1 -1
  5. package/dist/core/index.cjs +69 -27
  6. package/dist/core/index.cjs.map +1 -1
  7. package/dist/core/index.d.cts +21 -1
  8. package/dist/core/index.d.ts +21 -1
  9. package/dist/core/index.js +69 -27
  10. package/dist/core/index.js.map +1 -1
  11. package/dist/impl/browser/connect/index.cjs +1 -1
  12. package/dist/impl/browser/connect/index.cjs.map +1 -1
  13. package/dist/impl/browser/connect/index.js +1 -1
  14. package/dist/impl/browser/connect/index.js.map +1 -1
  15. package/dist/impl/browser/index.cjs +33 -0
  16. package/dist/impl/browser/index.cjs.map +1 -1
  17. package/dist/impl/browser/index.js +33 -0
  18. package/dist/impl/browser/index.js.map +1 -1
  19. package/dist/impl/nodejs/index.cjs +33 -0
  20. package/dist/impl/nodejs/index.cjs.map +1 -1
  21. package/dist/impl/nodejs/index.d.cts +41 -1
  22. package/dist/impl/nodejs/index.d.ts +41 -1
  23. package/dist/impl/nodejs/index.js +33 -0
  24. package/dist/impl/nodejs/index.js.map +1 -1
  25. package/dist/impl/shared/wallet-api/index.d.cts +21 -1
  26. package/dist/impl/shared/wallet-api/index.d.ts +21 -1
  27. package/dist/index.cjs +69 -27
  28. package/dist/index.cjs.map +1 -1
  29. package/dist/index.d.cts +21 -1
  30. package/dist/index.d.ts +21 -1
  31. package/dist/index.js +69 -27
  32. package/dist/index.js.map +1 -1
  33. package/dist/modules/payments-v2/index.cjs +36 -26
  34. package/dist/modules/payments-v2/index.cjs.map +1 -1
  35. package/dist/modules/payments-v2/index.d.cts +16 -6
  36. package/dist/modules/payments-v2/index.d.ts +16 -6
  37. package/dist/modules/payments-v2/index.js +35 -26
  38. package/dist/modules/payments-v2/index.js.map +1 -1
  39. package/package.json +1 -1
@@ -1,3 +1,5 @@
1
+ type ConnectionStatus$1 = 'connected' | 'degraded' | 'offline';
2
+
1
3
  /**
2
4
  * SDK2 Core Types
3
5
  * Platform-independent type definitions
@@ -308,6 +310,19 @@ interface PeerInfo {
308
310
  chainPubkey: string;
309
311
  /** L3 DIRECT address (DIRECT://...) */
310
312
  directAddress: string;
313
+ /**
314
+ * Network the peer's identity binding DECLARES ('testnet2', …) — absent when
315
+ * the binding carries none, which is every binding published so far. Parsed
316
+ * only, never invented: money refuses a PROVEN foreign network (§5.6).
317
+ *
318
+ * Absent is also the STRUCTURAL answer on the `@nametag` and `DIRECT://`
319
+ * routes of the Nostr transport, whichever binding is out there: those two
320
+ * resolve through nostr-js-sdk's `queryBindingBy*`, whose parser drops every
321
+ * content field outside its whitelist and never hands back the signed event
322
+ * (#734 tracks the upstream fix). Only routes that parse the raw event —
323
+ * `resolveTransportPubkeyInfo`, `discoverAddresses` — can populate it today.
324
+ */
325
+ network?: string;
311
326
  /** Event timestamp */
312
327
  timestamp: number;
313
328
  }
@@ -459,12 +474,17 @@ interface FacadeSession {
459
474
  * stream nudge resumes. The facade registers handleEpochChange here.
460
475
  */
461
476
  subscribeEpochChange(handler: (epoch: string) => Promise<void>): () => void;
477
+ /**
478
+ * The session's CURRENT status — REQUIRED, and the ONE source `connectionStatus()`
479
+ * and `connection:status` both come from ('offline' before first contact).
480
+ */
481
+ status(): ConnectionStatus$1;
462
482
  /**
463
483
  * Optional connection-status feed (same wiring pattern as the streams; the
464
484
  * emission point is the session's existing `connection:status` transition).
465
485
  * The facade's heartbeat resets its backoff on a 'connected' recovery.
466
486
  */
467
- subscribeStatus?(handler: (status: 'connected' | 'degraded' | 'offline') => void): () => void;
487
+ subscribeStatus?(handler: (status: ConnectionStatus$1) => void): () => void;
468
488
  }
469
489
  interface IntentWireLike {
470
490
  transferId: string;
@@ -1,3 +1,5 @@
1
+ type ConnectionStatus$1 = 'connected' | 'degraded' | 'offline';
2
+
1
3
  /**
2
4
  * SDK2 Core Types
3
5
  * Platform-independent type definitions
@@ -308,6 +310,19 @@ interface PeerInfo {
308
310
  chainPubkey: string;
309
311
  /** L3 DIRECT address (DIRECT://...) */
310
312
  directAddress: string;
313
+ /**
314
+ * Network the peer's identity binding DECLARES ('testnet2', …) — absent when
315
+ * the binding carries none, which is every binding published so far. Parsed
316
+ * only, never invented: money refuses a PROVEN foreign network (§5.6).
317
+ *
318
+ * Absent is also the STRUCTURAL answer on the `@nametag` and `DIRECT://`
319
+ * routes of the Nostr transport, whichever binding is out there: those two
320
+ * resolve through nostr-js-sdk's `queryBindingBy*`, whose parser drops every
321
+ * content field outside its whitelist and never hands back the signed event
322
+ * (#734 tracks the upstream fix). Only routes that parse the raw event —
323
+ * `resolveTransportPubkeyInfo`, `discoverAddresses` — can populate it today.
324
+ */
325
+ network?: string;
311
326
  /** Event timestamp */
312
327
  timestamp: number;
313
328
  }
@@ -459,12 +474,17 @@ interface FacadeSession {
459
474
  * stream nudge resumes. The facade registers handleEpochChange here.
460
475
  */
461
476
  subscribeEpochChange(handler: (epoch: string) => Promise<void>): () => void;
477
+ /**
478
+ * The session's CURRENT status — REQUIRED, and the ONE source `connectionStatus()`
479
+ * and `connection:status` both come from ('offline' before first contact).
480
+ */
481
+ status(): ConnectionStatus$1;
462
482
  /**
463
483
  * Optional connection-status feed (same wiring pattern as the streams; the
464
484
  * emission point is the session's existing `connection:status` transition).
465
485
  * The facade's heartbeat resets its backoff on a 'connected' recovery.
466
486
  */
467
- subscribeStatus?(handler: (status: 'connected' | 'degraded' | 'offline') => void): () => void;
487
+ subscribeStatus?(handler: (status: ConnectionStatus$1) => void): () => void;
468
488
  }
469
489
  interface IntentWireLike {
470
490
  transferId: string;
package/dist/index.cjs CHANGED
@@ -983,6 +983,15 @@ var LIMITS = {
983
983
  var COMPOSING_INDICATOR_KIND = 25050;
984
984
  var TIMESTAMP_RANDOMIZATION = 2 * 24 * 60 * 60;
985
985
  var EVENT_KINDS = NOSTR_EVENT_KINDS;
986
+ function declaredNetwork(event) {
987
+ let value;
988
+ try {
989
+ value = JSON.parse(event.content)?.network;
990
+ } catch {
991
+ return void 0;
992
+ }
993
+ return typeof value === "string" && value !== "" ? value : void 0;
994
+ }
986
995
  var NostrTransportProvider = class _NostrTransportProvider {
987
996
  id = "nostr";
988
997
  name = "Nostr Transport";
@@ -1479,6 +1488,26 @@ var NostrTransportProvider = class _NostrTransportProvider {
1479
1488
  }
1480
1489
  /**
1481
1490
  * Convert a BindingInfo (from nostr-js-sdk) to PeerInfo (sphere-sdk type).
1491
+ *
1492
+ * `network` IS DELIBERATELY ABSENT HERE — a live limitation, not an oversight
1493
+ * (#734). `queryBindingByNametag`/`queryBindingByAddress` are the whole public
1494
+ * resolution API for `@nametag` and `DIRECT://`, and both hardcode
1495
+ * `parseBindingInfo` inside the SDK's PRIVATE `queryWithFirstSeenWins`: the
1496
+ * caller never sees the signed event that resolution selected, and the parse
1497
+ * has already dropped every content field outside its whitelist. Re-querying
1498
+ * raw would mean re-deriving UNIP-01 ownership ourselves (marker preference,
1499
+ * cross-author first-seen-wins, ambiguity→null, multi-relay settle — all
1500
+ * private), and a second resolver that can disagree with the SDK's is worse
1501
+ * than a missing field on a MONEY guard. Nothing can declare a network today
1502
+ * regardless: the publish builders whitelist the same fields.
1503
+ *
1504
+ * CONSEQUENCE, stated plainly: for `@nametag` and `DIRECT://` recipients the
1505
+ * §5.6 cross-network guard can only SIGNAL (`recipient:network-unverified`),
1506
+ * never refuse. Upstream #734 must add `network` to the binding builders AND
1507
+ * to `BindingInfo`; the tripwire in
1508
+ * tests/unit/payments-v2/recipient-network.transport.test.ts fails the day it
1509
+ * lands, so this wiring cannot be forgotten. Do not "fix" this by inventing a
1510
+ * value — a stamped network is what made the guard unfalsifiable (#733).
1482
1511
  */
1483
1512
  async bindingInfoToPeerInfo(binding, nametag) {
1484
1513
  return {
@@ -1505,11 +1534,13 @@ var NostrTransportProvider = class _NostrTransportProvider {
1505
1534
  const bindingEvent = events[0];
1506
1535
  try {
1507
1536
  const content = JSON.parse(bindingEvent.content);
1537
+ const network = declaredNetwork(bindingEvent);
1508
1538
  return {
1509
1539
  nametag: content.nametag || void 0,
1510
1540
  transportPubkey: bindingEvent.pubkey,
1511
1541
  chainPubkey: content.public_key || "",
1512
1542
  directAddress: content.direct_address || "",
1543
+ ...network !== void 0 ? { network } : {},
1513
1544
  timestamp: bindingEvent.created_at * 1e3
1514
1545
  };
1515
1546
  } catch {
@@ -1545,11 +1576,13 @@ var NostrTransportProvider = class _NostrTransportProvider {
1545
1576
  for (const [pubkey, event] of byAuthor) {
1546
1577
  try {
1547
1578
  const content = JSON.parse(event.content);
1579
+ const network = declaredNetwork(event);
1548
1580
  results.push({
1549
1581
  nametag: content.nametag || void 0,
1550
1582
  transportPubkey: pubkey,
1551
1583
  chainPubkey: content.public_key || "",
1552
1584
  directAddress: content.direct_address || "",
1585
+ ...network !== void 0 ? { network } : {},
1553
1586
  timestamp: event.created_at * 1e3
1554
1587
  });
1555
1588
  } catch {
@@ -12139,6 +12172,31 @@ function distinctOpCount(entries) {
12139
12172
  return new Set(entries.map((entry) => entry.opIndex)).size;
12140
12173
  }
12141
12174
 
12175
+ // modules/payments-v2/recipient.ts
12176
+ var ATTENTION_RECIPIENT_NETWORK_UNVERIFIED = "recipient:network-unverified";
12177
+ async function requireSameNetworkRecipient(deps, identifier) {
12178
+ const recipient = await deps.resolveRecipient(identifier);
12179
+ if (recipient === null || recipient.chainPubkey === "") {
12180
+ throw new SphereError(`Recipient ${identifier} has no published chain pubkey`, "INVALID_RECIPIENT");
12181
+ }
12182
+ if (recipient.network === null) {
12183
+ deps.emit("transfer:attention", {
12184
+ transferId: "",
12185
+ // the gate precedes plan allocation — no transferId exists yet
12186
+ code: ATTENTION_RECIPIENT_NETWORK_UNVERIFIED,
12187
+ detail: `could not verify the network of recipient ${identifier} \u2014 assuming this session's "${deps.network}"`
12188
+ });
12189
+ return recipient;
12190
+ }
12191
+ if (recipient.network !== deps.network) {
12192
+ throw new SphereError(
12193
+ `Recipient ${identifier} is on network "${recipient.network}" but this session is on "${deps.network}" \u2014 a cross-network deposit is unrecoverable`,
12194
+ "INVALID_RECIPIENT"
12195
+ );
12196
+ }
12197
+ return recipient;
12198
+ }
12199
+
12142
12200
  // modules/payments-v2/restore.ts
12143
12201
  var ATTENTION_RESEED_REJECTED = "intent:reseed-rejected";
12144
12202
  var ALL_STREAMS = ["inventory", "mailbox", "payment_requests", "history"];
@@ -13731,6 +13789,10 @@ var PaymentsFacade = class {
13731
13789
  history(page) {
13732
13790
  return this.historyStore.page(page ?? {});
13733
13791
  }
13792
+ /** §4: read through to the session — the same value `connection:status` carries (sphere#473). No session ⇒ 'offline'. */
13793
+ connectionStatus() {
13794
+ return this.started ? this.deps.session.status() : "offline";
13795
+ }
13734
13796
  /** §4 pending-transfers UI surface — derived on read, never cached (convergence.ts). */
13735
13797
  pendingTransfers() {
13736
13798
  return derivePendingTransfers(this.machineStores, this.machineDeps.decryptPayload);
@@ -13805,7 +13867,7 @@ var PaymentsFacade = class {
13805
13867
  this.deps.emit("transfer:updated", failed);
13806
13868
  }
13807
13869
  async sendWithPolicy(request, run) {
13808
- const recipient = await this.requireSameNetworkRecipient(request.recipient);
13870
+ const recipient = await requireSameNetworkRecipient(this.deps, request.recipient);
13809
13871
  for (let attempt = 0; ; attempt++) {
13810
13872
  let ctx;
13811
13873
  try {
@@ -13828,20 +13890,6 @@ var PaymentsFacade = class {
13828
13890
  if (surfaced !== null) return surfaced;
13829
13891
  }
13830
13892
  }
13831
- /** §5.6 cross-network deposit trap: refused BEFORE any reserve/certification. */
13832
- async requireSameNetworkRecipient(identifier) {
13833
- const recipient = await this.deps.resolveRecipient(identifier);
13834
- if (recipient === null || recipient.chainPubkey === "") {
13835
- throw new SphereError(`Recipient ${identifier} has no published chain pubkey`, "INVALID_RECIPIENT");
13836
- }
13837
- if (recipient.network !== this.deps.network) {
13838
- throw new SphereError(
13839
- `Recipient ${identifier} is on network "${recipient.network}" but this session is on "${this.deps.network}" \u2014 a cross-network deposit is unrecoverable`,
13840
- "INVALID_RECIPIENT"
13841
- );
13842
- }
13843
- return recipient;
13844
- }
13845
13893
  async runAttempt(ctx) {
13846
13894
  this.activeMoneyOps.add(ctx.transferId);
13847
13895
  try {
@@ -14218,19 +14266,12 @@ var PaymentsFacade = class {
14218
14266
  }
14219
14267
  toUiToken(tokenId, coinId, amount) {
14220
14268
  const now = this.nowMs();
14221
- return toToken(
14222
- tokenId,
14223
- { assets: [], createdAt: now, updatedAt: now },
14224
- { coinId, amount: amount.toString() },
14225
- this.deps.registry,
14226
- { transferring: true, suspectedSpent: false }
14227
- );
14269
+ const snapshot = { assets: [], createdAt: now, updatedAt: now };
14270
+ const flags = { transferring: true, suspectedSpent: false };
14271
+ return toToken(tokenId, snapshot, { coinId, amount: amount.toString() }, this.deps.registry, flags);
14228
14272
  }
14229
14273
  track(op) {
14230
- const settled = op.then(
14231
- () => void 0,
14232
- () => void 0
14233
- );
14274
+ const settled = op.then(() => void 0, () => void 0);
14234
14275
  this.pendingOps.add(settled);
14235
14276
  void settled.finally(() => this.pendingOps.delete(settled));
14236
14277
  return op;
@@ -15331,7 +15372,8 @@ async function resolveRecipientInfo(identifier, network, resolvePeer) {
15331
15372
  if (!peer?.chainPubkey) return null;
15332
15373
  return {
15333
15374
  chainPubkey: peer.chainPubkey,
15334
- network,
15375
+ // Only what the binding declares (blank declares nothing) — never a local stamp.
15376
+ network: peer.network || null,
15335
15377
  ...peer.nametag !== void 0 ? { nametag: peer.nametag } : {}
15336
15378
  };
15337
15379
  }