@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
@@ -165,6 +165,7 @@ interface PendingTransfer {
165
165
  deliveryPending: boolean;
166
166
  createdAt: number;
167
167
  }
168
+ type ConnectionStatus$1 = 'connected' | 'degraded' | 'offline';
168
169
  interface PaymentsV2 {
169
170
  assets(coinId?: string): Promise<Asset[]>;
170
171
  tokens(filter?: {
@@ -181,6 +182,7 @@ interface PaymentsV2 {
181
182
  }>;
182
183
  pendingTransfers(): Promise<PendingTransfer[]>;
183
184
  resumeNow(): Promise<void>;
185
+ connectionStatus(): ConnectionStatus$1;
184
186
  readonly requests: PaymentsRequestsApi;
185
187
  }
186
188
 
@@ -1138,6 +1140,19 @@ interface PeerInfo {
1138
1140
  chainPubkey: string;
1139
1141
  /** L3 DIRECT address (DIRECT://...) */
1140
1142
  directAddress: string;
1143
+ /**
1144
+ * Network the peer's identity binding DECLARES ('testnet2', …) — absent when
1145
+ * the binding carries none, which is every binding published so far. Parsed
1146
+ * only, never invented: money refuses a PROVEN foreign network (§5.6).
1147
+ *
1148
+ * Absent is also the STRUCTURAL answer on the `@nametag` and `DIRECT://`
1149
+ * routes of the Nostr transport, whichever binding is out there: those two
1150
+ * resolve through nostr-js-sdk's `queryBindingBy*`, whose parser drops every
1151
+ * content field outside its whitelist and never hands back the signed event
1152
+ * (#734 tracks the upstream fix). Only routes that parse the raw event —
1153
+ * `resolveTransportPubkeyInfo`, `discoverAddresses` — can populate it today.
1154
+ */
1155
+ network?: string;
1141
1156
  /** Event timestamp */
1142
1157
  timestamp: number;
1143
1158
  }
@@ -2640,12 +2655,17 @@ interface FacadeSession {
2640
2655
  * stream nudge resumes. The facade registers handleEpochChange here.
2641
2656
  */
2642
2657
  subscribeEpochChange(handler: (epoch: string) => Promise<void>): () => void;
2658
+ /**
2659
+ * The session's CURRENT status — REQUIRED, and the ONE source `connectionStatus()`
2660
+ * and `connection:status` both come from ('offline' before first contact).
2661
+ */
2662
+ status(): ConnectionStatus$1;
2643
2663
  /**
2644
2664
  * Optional connection-status feed (same wiring pattern as the streams; the
2645
2665
  * emission point is the session's existing `connection:status` transition).
2646
2666
  * The facade's heartbeat resets its backoff on a 'connected' recovery.
2647
2667
  */
2648
- subscribeStatus?(handler: (status: 'connected' | 'degraded' | 'offline') => void): () => void;
2668
+ subscribeStatus?(handler: (status: ConnectionStatus$1) => void): () => void;
2649
2669
  }
2650
2670
  interface IntentWireLike {
2651
2671
  transferId: string;
@@ -165,6 +165,7 @@ interface PendingTransfer {
165
165
  deliveryPending: boolean;
166
166
  createdAt: number;
167
167
  }
168
+ type ConnectionStatus$1 = 'connected' | 'degraded' | 'offline';
168
169
  interface PaymentsV2 {
169
170
  assets(coinId?: string): Promise<Asset[]>;
170
171
  tokens(filter?: {
@@ -181,6 +182,7 @@ interface PaymentsV2 {
181
182
  }>;
182
183
  pendingTransfers(): Promise<PendingTransfer[]>;
183
184
  resumeNow(): Promise<void>;
185
+ connectionStatus(): ConnectionStatus$1;
184
186
  readonly requests: PaymentsRequestsApi;
185
187
  }
186
188
 
@@ -1138,6 +1140,19 @@ interface PeerInfo {
1138
1140
  chainPubkey: string;
1139
1141
  /** L3 DIRECT address (DIRECT://...) */
1140
1142
  directAddress: string;
1143
+ /**
1144
+ * Network the peer's identity binding DECLARES ('testnet2', …) — absent when
1145
+ * the binding carries none, which is every binding published so far. Parsed
1146
+ * only, never invented: money refuses a PROVEN foreign network (§5.6).
1147
+ *
1148
+ * Absent is also the STRUCTURAL answer on the `@nametag` and `DIRECT://`
1149
+ * routes of the Nostr transport, whichever binding is out there: those two
1150
+ * resolve through nostr-js-sdk's `queryBindingBy*`, whose parser drops every
1151
+ * content field outside its whitelist and never hands back the signed event
1152
+ * (#734 tracks the upstream fix). Only routes that parse the raw event —
1153
+ * `resolveTransportPubkeyInfo`, `discoverAddresses` — can populate it today.
1154
+ */
1155
+ network?: string;
1141
1156
  /** Event timestamp */
1142
1157
  timestamp: number;
1143
1158
  }
@@ -2640,12 +2655,17 @@ interface FacadeSession {
2640
2655
  * stream nudge resumes. The facade registers handleEpochChange here.
2641
2656
  */
2642
2657
  subscribeEpochChange(handler: (epoch: string) => Promise<void>): () => void;
2658
+ /**
2659
+ * The session's CURRENT status — REQUIRED, and the ONE source `connectionStatus()`
2660
+ * and `connection:status` both come from ('offline' before first contact).
2661
+ */
2662
+ status(): ConnectionStatus$1;
2643
2663
  /**
2644
2664
  * Optional connection-status feed (same wiring pattern as the streams; the
2645
2665
  * emission point is the session's existing `connection:status` transition).
2646
2666
  * The facade's heartbeat resets its backoff on a 'connected' recovery.
2647
2667
  */
2648
- subscribeStatus?(handler: (status: 'connected' | 'degraded' | 'offline') => void): () => void;
2668
+ subscribeStatus?(handler: (status: ConnectionStatus$1) => void): () => void;
2649
2669
  }
2650
2670
  interface IntentWireLike {
2651
2671
  transferId: string;
@@ -832,6 +832,15 @@ var TIMEOUTS = {
832
832
  var COMPOSING_INDICATOR_KIND = 25050;
833
833
  var TIMESTAMP_RANDOMIZATION = 2 * 24 * 60 * 60;
834
834
  var EVENT_KINDS = NOSTR_EVENT_KINDS;
835
+ function declaredNetwork(event) {
836
+ let value;
837
+ try {
838
+ value = JSON.parse(event.content)?.network;
839
+ } catch {
840
+ return void 0;
841
+ }
842
+ return typeof value === "string" && value !== "" ? value : void 0;
843
+ }
835
844
  var NostrTransportProvider = class _NostrTransportProvider {
836
845
  id = "nostr";
837
846
  name = "Nostr Transport";
@@ -1328,6 +1337,26 @@ var NostrTransportProvider = class _NostrTransportProvider {
1328
1337
  }
1329
1338
  /**
1330
1339
  * Convert a BindingInfo (from nostr-js-sdk) to PeerInfo (sphere-sdk type).
1340
+ *
1341
+ * `network` IS DELIBERATELY ABSENT HERE — a live limitation, not an oversight
1342
+ * (#734). `queryBindingByNametag`/`queryBindingByAddress` are the whole public
1343
+ * resolution API for `@nametag` and `DIRECT://`, and both hardcode
1344
+ * `parseBindingInfo` inside the SDK's PRIVATE `queryWithFirstSeenWins`: the
1345
+ * caller never sees the signed event that resolution selected, and the parse
1346
+ * has already dropped every content field outside its whitelist. Re-querying
1347
+ * raw would mean re-deriving UNIP-01 ownership ourselves (marker preference,
1348
+ * cross-author first-seen-wins, ambiguity→null, multi-relay settle — all
1349
+ * private), and a second resolver that can disagree with the SDK's is worse
1350
+ * than a missing field on a MONEY guard. Nothing can declare a network today
1351
+ * regardless: the publish builders whitelist the same fields.
1352
+ *
1353
+ * CONSEQUENCE, stated plainly: for `@nametag` and `DIRECT://` recipients the
1354
+ * §5.6 cross-network guard can only SIGNAL (`recipient:network-unverified`),
1355
+ * never refuse. Upstream #734 must add `network` to the binding builders AND
1356
+ * to `BindingInfo`; the tripwire in
1357
+ * tests/unit/payments-v2/recipient-network.transport.test.ts fails the day it
1358
+ * lands, so this wiring cannot be forgotten. Do not "fix" this by inventing a
1359
+ * value — a stamped network is what made the guard unfalsifiable (#733).
1331
1360
  */
1332
1361
  async bindingInfoToPeerInfo(binding, nametag) {
1333
1362
  return {
@@ -1354,11 +1383,13 @@ var NostrTransportProvider = class _NostrTransportProvider {
1354
1383
  const bindingEvent = events[0];
1355
1384
  try {
1356
1385
  const content = JSON.parse(bindingEvent.content);
1386
+ const network = declaredNetwork(bindingEvent);
1357
1387
  return {
1358
1388
  nametag: content.nametag || void 0,
1359
1389
  transportPubkey: bindingEvent.pubkey,
1360
1390
  chainPubkey: content.public_key || "",
1361
1391
  directAddress: content.direct_address || "",
1392
+ ...network !== void 0 ? { network } : {},
1362
1393
  timestamp: bindingEvent.created_at * 1e3
1363
1394
  };
1364
1395
  } catch {
@@ -1394,11 +1425,13 @@ var NostrTransportProvider = class _NostrTransportProvider {
1394
1425
  for (const [pubkey, event] of byAuthor) {
1395
1426
  try {
1396
1427
  const content = JSON.parse(event.content);
1428
+ const network = declaredNetwork(event);
1397
1429
  results.push({
1398
1430
  nametag: content.nametag || void 0,
1399
1431
  transportPubkey: pubkey,
1400
1432
  chainPubkey: content.public_key || "",
1401
1433
  directAddress: content.direct_address || "",
1434
+ ...network !== void 0 ? { network } : {},
1402
1435
  timestamp: event.created_at * 1e3
1403
1436
  });
1404
1437
  } catch {
@@ -11995,6 +12028,31 @@ function distinctOpCount(entries) {
11995
12028
  return new Set(entries.map((entry) => entry.opIndex)).size;
11996
12029
  }
11997
12030
 
12031
+ // modules/payments-v2/recipient.ts
12032
+ var ATTENTION_RECIPIENT_NETWORK_UNVERIFIED = "recipient:network-unverified";
12033
+ async function requireSameNetworkRecipient(deps, identifier) {
12034
+ const recipient = await deps.resolveRecipient(identifier);
12035
+ if (recipient === null || recipient.chainPubkey === "") {
12036
+ throw new SphereError(`Recipient ${identifier} has no published chain pubkey`, "INVALID_RECIPIENT");
12037
+ }
12038
+ if (recipient.network === null) {
12039
+ deps.emit("transfer:attention", {
12040
+ transferId: "",
12041
+ // the gate precedes plan allocation — no transferId exists yet
12042
+ code: ATTENTION_RECIPIENT_NETWORK_UNVERIFIED,
12043
+ detail: `could not verify the network of recipient ${identifier} \u2014 assuming this session's "${deps.network}"`
12044
+ });
12045
+ return recipient;
12046
+ }
12047
+ if (recipient.network !== deps.network) {
12048
+ throw new SphereError(
12049
+ `Recipient ${identifier} is on network "${recipient.network}" but this session is on "${deps.network}" \u2014 a cross-network deposit is unrecoverable`,
12050
+ "INVALID_RECIPIENT"
12051
+ );
12052
+ }
12053
+ return recipient;
12054
+ }
12055
+
11998
12056
  // modules/payments-v2/restore.ts
11999
12057
  var ATTENTION_RESEED_REJECTED = "intent:reseed-rejected";
12000
12058
  var ALL_STREAMS = ["inventory", "mailbox", "payment_requests", "history"];
@@ -13587,6 +13645,10 @@ var PaymentsFacade = class {
13587
13645
  history(page) {
13588
13646
  return this.historyStore.page(page ?? {});
13589
13647
  }
13648
+ /** §4: read through to the session — the same value `connection:status` carries (sphere#473). No session ⇒ 'offline'. */
13649
+ connectionStatus() {
13650
+ return this.started ? this.deps.session.status() : "offline";
13651
+ }
13590
13652
  /** §4 pending-transfers UI surface — derived on read, never cached (convergence.ts). */
13591
13653
  pendingTransfers() {
13592
13654
  return derivePendingTransfers(this.machineStores, this.machineDeps.decryptPayload);
@@ -13661,7 +13723,7 @@ var PaymentsFacade = class {
13661
13723
  this.deps.emit("transfer:updated", failed);
13662
13724
  }
13663
13725
  async sendWithPolicy(request, run) {
13664
- const recipient = await this.requireSameNetworkRecipient(request.recipient);
13726
+ const recipient = await requireSameNetworkRecipient(this.deps, request.recipient);
13665
13727
  for (let attempt = 0; ; attempt++) {
13666
13728
  let ctx;
13667
13729
  try {
@@ -13684,20 +13746,6 @@ var PaymentsFacade = class {
13684
13746
  if (surfaced !== null) return surfaced;
13685
13747
  }
13686
13748
  }
13687
- /** §5.6 cross-network deposit trap: refused BEFORE any reserve/certification. */
13688
- async requireSameNetworkRecipient(identifier) {
13689
- const recipient = await this.deps.resolveRecipient(identifier);
13690
- if (recipient === null || recipient.chainPubkey === "") {
13691
- throw new SphereError(`Recipient ${identifier} has no published chain pubkey`, "INVALID_RECIPIENT");
13692
- }
13693
- if (recipient.network !== this.deps.network) {
13694
- throw new SphereError(
13695
- `Recipient ${identifier} is on network "${recipient.network}" but this session is on "${this.deps.network}" \u2014 a cross-network deposit is unrecoverable`,
13696
- "INVALID_RECIPIENT"
13697
- );
13698
- }
13699
- return recipient;
13700
- }
13701
13749
  async runAttempt(ctx) {
13702
13750
  this.activeMoneyOps.add(ctx.transferId);
13703
13751
  try {
@@ -14074,19 +14122,12 @@ var PaymentsFacade = class {
14074
14122
  }
14075
14123
  toUiToken(tokenId, coinId, amount) {
14076
14124
  const now = this.nowMs();
14077
- return toToken(
14078
- tokenId,
14079
- { assets: [], createdAt: now, updatedAt: now },
14080
- { coinId, amount: amount.toString() },
14081
- this.deps.registry,
14082
- { transferring: true, suspectedSpent: false }
14083
- );
14125
+ const snapshot = { assets: [], createdAt: now, updatedAt: now };
14126
+ const flags = { transferring: true, suspectedSpent: false };
14127
+ return toToken(tokenId, snapshot, { coinId, amount: amount.toString() }, this.deps.registry, flags);
14084
14128
  }
14085
14129
  track(op) {
14086
- const settled = op.then(
14087
- () => void 0,
14088
- () => void 0
14089
- );
14130
+ const settled = op.then(() => void 0, () => void 0);
14090
14131
  this.pendingOps.add(settled);
14091
14132
  void settled.finally(() => this.pendingOps.delete(settled));
14092
14133
  return op;
@@ -15187,7 +15228,8 @@ async function resolveRecipientInfo(identifier, network, resolvePeer) {
15187
15228
  if (!peer?.chainPubkey) return null;
15188
15229
  return {
15189
15230
  chainPubkey: peer.chainPubkey,
15190
- network,
15231
+ // Only what the binding declares (blank declares nothing) — never a local stamp.
15232
+ network: peer.network || null,
15191
15233
  ...peer.nametag !== void 0 ? { nametag: peer.nametag } : {}
15192
15234
  };
15193
15235
  }