@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
package/dist/index.d.cts CHANGED
@@ -164,6 +164,7 @@ interface PendingTransfer {
164
164
  deliveryPending: boolean;
165
165
  createdAt: number;
166
166
  }
167
+ type ConnectionStatus$1 = 'connected' | 'degraded' | 'offline';
167
168
  interface PaymentsV2 {
168
169
  assets(coinId?: string): Promise<Asset[]>;
169
170
  tokens(filter?: {
@@ -180,6 +181,7 @@ interface PaymentsV2 {
180
181
  }>;
181
182
  pendingTransfers(): Promise<PendingTransfer[]>;
182
183
  resumeNow(): Promise<void>;
184
+ connectionStatus(): ConnectionStatus$1;
183
185
  readonly requests: PaymentsRequestsApi;
184
186
  }
185
187
 
@@ -1148,6 +1150,19 @@ interface PeerInfo {
1148
1150
  chainPubkey: string;
1149
1151
  /** L3 DIRECT address (DIRECT://...) */
1150
1152
  directAddress: string;
1153
+ /**
1154
+ * Network the peer's identity binding DECLARES ('testnet2', …) — absent when
1155
+ * the binding carries none, which is every binding published so far. Parsed
1156
+ * only, never invented: money refuses a PROVEN foreign network (§5.6).
1157
+ *
1158
+ * Absent is also the STRUCTURAL answer on the `@nametag` and `DIRECT://`
1159
+ * routes of the Nostr transport, whichever binding is out there: those two
1160
+ * resolve through nostr-js-sdk's `queryBindingBy*`, whose parser drops every
1161
+ * content field outside its whitelist and never hands back the signed event
1162
+ * (#734 tracks the upstream fix). Only routes that parse the raw event —
1163
+ * `resolveTransportPubkeyInfo`, `discoverAddresses` — can populate it today.
1164
+ */
1165
+ network?: string;
1151
1166
  /** Event timestamp */
1152
1167
  timestamp: number;
1153
1168
  }
@@ -2298,12 +2313,17 @@ interface FacadeSession {
2298
2313
  * stream nudge resumes. The facade registers handleEpochChange here.
2299
2314
  */
2300
2315
  subscribeEpochChange(handler: (epoch: string) => Promise<void>): () => void;
2316
+ /**
2317
+ * The session's CURRENT status — REQUIRED, and the ONE source `connectionStatus()`
2318
+ * and `connection:status` both come from ('offline' before first contact).
2319
+ */
2320
+ status(): ConnectionStatus$1;
2301
2321
  /**
2302
2322
  * Optional connection-status feed (same wiring pattern as the streams; the
2303
2323
  * emission point is the session's existing `connection:status` transition).
2304
2324
  * The facade's heartbeat resets its backoff on a 'connected' recovery.
2305
2325
  */
2306
- subscribeStatus?(handler: (status: 'connected' | 'degraded' | 'offline') => void): () => void;
2326
+ subscribeStatus?(handler: (status: ConnectionStatus$1) => void): () => void;
2307
2327
  }
2308
2328
  interface IntentWireLike {
2309
2329
  transferId: string;
package/dist/index.d.ts CHANGED
@@ -164,6 +164,7 @@ interface PendingTransfer {
164
164
  deliveryPending: boolean;
165
165
  createdAt: number;
166
166
  }
167
+ type ConnectionStatus$1 = 'connected' | 'degraded' | 'offline';
167
168
  interface PaymentsV2 {
168
169
  assets(coinId?: string): Promise<Asset[]>;
169
170
  tokens(filter?: {
@@ -180,6 +181,7 @@ interface PaymentsV2 {
180
181
  }>;
181
182
  pendingTransfers(): Promise<PendingTransfer[]>;
182
183
  resumeNow(): Promise<void>;
184
+ connectionStatus(): ConnectionStatus$1;
183
185
  readonly requests: PaymentsRequestsApi;
184
186
  }
185
187
 
@@ -1148,6 +1150,19 @@ interface PeerInfo {
1148
1150
  chainPubkey: string;
1149
1151
  /** L3 DIRECT address (DIRECT://...) */
1150
1152
  directAddress: string;
1153
+ /**
1154
+ * Network the peer's identity binding DECLARES ('testnet2', …) — absent when
1155
+ * the binding carries none, which is every binding published so far. Parsed
1156
+ * only, never invented: money refuses a PROVEN foreign network (§5.6).
1157
+ *
1158
+ * Absent is also the STRUCTURAL answer on the `@nametag` and `DIRECT://`
1159
+ * routes of the Nostr transport, whichever binding is out there: those two
1160
+ * resolve through nostr-js-sdk's `queryBindingBy*`, whose parser drops every
1161
+ * content field outside its whitelist and never hands back the signed event
1162
+ * (#734 tracks the upstream fix). Only routes that parse the raw event —
1163
+ * `resolveTransportPubkeyInfo`, `discoverAddresses` — can populate it today.
1164
+ */
1165
+ network?: string;
1151
1166
  /** Event timestamp */
1152
1167
  timestamp: number;
1153
1168
  }
@@ -2298,12 +2313,17 @@ interface FacadeSession {
2298
2313
  * stream nudge resumes. The facade registers handleEpochChange here.
2299
2314
  */
2300
2315
  subscribeEpochChange(handler: (epoch: string) => Promise<void>): () => void;
2316
+ /**
2317
+ * The session's CURRENT status — REQUIRED, and the ONE source `connectionStatus()`
2318
+ * and `connection:status` both come from ('offline' before first contact).
2319
+ */
2320
+ status(): ConnectionStatus$1;
2301
2321
  /**
2302
2322
  * Optional connection-status feed (same wiring pattern as the streams; the
2303
2323
  * emission point is the session's existing `connection:status` transition).
2304
2324
  * The facade's heartbeat resets its backoff on a 'connected' recovery.
2305
2325
  */
2306
- subscribeStatus?(handler: (status: 'connected' | 'degraded' | 'offline') => void): () => void;
2326
+ subscribeStatus?(handler: (status: ConnectionStatus$1) => void): () => void;
2307
2327
  }
2308
2328
  interface IntentWireLike {
2309
2329
  transferId: string;
package/dist/index.js CHANGED
@@ -851,6 +851,15 @@ var LIMITS = {
851
851
  var COMPOSING_INDICATOR_KIND = 25050;
852
852
  var TIMESTAMP_RANDOMIZATION = 2 * 24 * 60 * 60;
853
853
  var EVENT_KINDS = NOSTR_EVENT_KINDS;
854
+ function declaredNetwork(event) {
855
+ let value;
856
+ try {
857
+ value = JSON.parse(event.content)?.network;
858
+ } catch {
859
+ return void 0;
860
+ }
861
+ return typeof value === "string" && value !== "" ? value : void 0;
862
+ }
854
863
  var NostrTransportProvider = class _NostrTransportProvider {
855
864
  id = "nostr";
856
865
  name = "Nostr Transport";
@@ -1347,6 +1356,26 @@ var NostrTransportProvider = class _NostrTransportProvider {
1347
1356
  }
1348
1357
  /**
1349
1358
  * Convert a BindingInfo (from nostr-js-sdk) to PeerInfo (sphere-sdk type).
1359
+ *
1360
+ * `network` IS DELIBERATELY ABSENT HERE — a live limitation, not an oversight
1361
+ * (#734). `queryBindingByNametag`/`queryBindingByAddress` are the whole public
1362
+ * resolution API for `@nametag` and `DIRECT://`, and both hardcode
1363
+ * `parseBindingInfo` inside the SDK's PRIVATE `queryWithFirstSeenWins`: the
1364
+ * caller never sees the signed event that resolution selected, and the parse
1365
+ * has already dropped every content field outside its whitelist. Re-querying
1366
+ * raw would mean re-deriving UNIP-01 ownership ourselves (marker preference,
1367
+ * cross-author first-seen-wins, ambiguity→null, multi-relay settle — all
1368
+ * private), and a second resolver that can disagree with the SDK's is worse
1369
+ * than a missing field on a MONEY guard. Nothing can declare a network today
1370
+ * regardless: the publish builders whitelist the same fields.
1371
+ *
1372
+ * CONSEQUENCE, stated plainly: for `@nametag` and `DIRECT://` recipients the
1373
+ * §5.6 cross-network guard can only SIGNAL (`recipient:network-unverified`),
1374
+ * never refuse. Upstream #734 must add `network` to the binding builders AND
1375
+ * to `BindingInfo`; the tripwire in
1376
+ * tests/unit/payments-v2/recipient-network.transport.test.ts fails the day it
1377
+ * lands, so this wiring cannot be forgotten. Do not "fix" this by inventing a
1378
+ * value — a stamped network is what made the guard unfalsifiable (#733).
1350
1379
  */
1351
1380
  async bindingInfoToPeerInfo(binding, nametag) {
1352
1381
  return {
@@ -1373,11 +1402,13 @@ var NostrTransportProvider = class _NostrTransportProvider {
1373
1402
  const bindingEvent = events[0];
1374
1403
  try {
1375
1404
  const content = JSON.parse(bindingEvent.content);
1405
+ const network = declaredNetwork(bindingEvent);
1376
1406
  return {
1377
1407
  nametag: content.nametag || void 0,
1378
1408
  transportPubkey: bindingEvent.pubkey,
1379
1409
  chainPubkey: content.public_key || "",
1380
1410
  directAddress: content.direct_address || "",
1411
+ ...network !== void 0 ? { network } : {},
1381
1412
  timestamp: bindingEvent.created_at * 1e3
1382
1413
  };
1383
1414
  } catch {
@@ -1413,11 +1444,13 @@ var NostrTransportProvider = class _NostrTransportProvider {
1413
1444
  for (const [pubkey, event] of byAuthor) {
1414
1445
  try {
1415
1446
  const content = JSON.parse(event.content);
1447
+ const network = declaredNetwork(event);
1416
1448
  results.push({
1417
1449
  nametag: content.nametag || void 0,
1418
1450
  transportPubkey: pubkey,
1419
1451
  chainPubkey: content.public_key || "",
1420
1452
  directAddress: content.direct_address || "",
1453
+ ...network !== void 0 ? { network } : {},
1421
1454
  timestamp: event.created_at * 1e3
1422
1455
  });
1423
1456
  } catch {
@@ -12011,6 +12044,31 @@ function distinctOpCount(entries) {
12011
12044
  return new Set(entries.map((entry) => entry.opIndex)).size;
12012
12045
  }
12013
12046
 
12047
+ // modules/payments-v2/recipient.ts
12048
+ var ATTENTION_RECIPIENT_NETWORK_UNVERIFIED = "recipient:network-unverified";
12049
+ async function requireSameNetworkRecipient(deps, identifier) {
12050
+ const recipient = await deps.resolveRecipient(identifier);
12051
+ if (recipient === null || recipient.chainPubkey === "") {
12052
+ throw new SphereError(`Recipient ${identifier} has no published chain pubkey`, "INVALID_RECIPIENT");
12053
+ }
12054
+ if (recipient.network === null) {
12055
+ deps.emit("transfer:attention", {
12056
+ transferId: "",
12057
+ // the gate precedes plan allocation — no transferId exists yet
12058
+ code: ATTENTION_RECIPIENT_NETWORK_UNVERIFIED,
12059
+ detail: `could not verify the network of recipient ${identifier} \u2014 assuming this session's "${deps.network}"`
12060
+ });
12061
+ return recipient;
12062
+ }
12063
+ if (recipient.network !== deps.network) {
12064
+ throw new SphereError(
12065
+ `Recipient ${identifier} is on network "${recipient.network}" but this session is on "${deps.network}" \u2014 a cross-network deposit is unrecoverable`,
12066
+ "INVALID_RECIPIENT"
12067
+ );
12068
+ }
12069
+ return recipient;
12070
+ }
12071
+
12014
12072
  // modules/payments-v2/restore.ts
12015
12073
  var ATTENTION_RESEED_REJECTED = "intent:reseed-rejected";
12016
12074
  var ALL_STREAMS = ["inventory", "mailbox", "payment_requests", "history"];
@@ -13603,6 +13661,10 @@ var PaymentsFacade = class {
13603
13661
  history(page) {
13604
13662
  return this.historyStore.page(page ?? {});
13605
13663
  }
13664
+ /** §4: read through to the session — the same value `connection:status` carries (sphere#473). No session ⇒ 'offline'. */
13665
+ connectionStatus() {
13666
+ return this.started ? this.deps.session.status() : "offline";
13667
+ }
13606
13668
  /** §4 pending-transfers UI surface — derived on read, never cached (convergence.ts). */
13607
13669
  pendingTransfers() {
13608
13670
  return derivePendingTransfers(this.machineStores, this.machineDeps.decryptPayload);
@@ -13677,7 +13739,7 @@ var PaymentsFacade = class {
13677
13739
  this.deps.emit("transfer:updated", failed);
13678
13740
  }
13679
13741
  async sendWithPolicy(request, run) {
13680
- const recipient = await this.requireSameNetworkRecipient(request.recipient);
13742
+ const recipient = await requireSameNetworkRecipient(this.deps, request.recipient);
13681
13743
  for (let attempt = 0; ; attempt++) {
13682
13744
  let ctx;
13683
13745
  try {
@@ -13700,20 +13762,6 @@ var PaymentsFacade = class {
13700
13762
  if (surfaced !== null) return surfaced;
13701
13763
  }
13702
13764
  }
13703
- /** §5.6 cross-network deposit trap: refused BEFORE any reserve/certification. */
13704
- async requireSameNetworkRecipient(identifier) {
13705
- const recipient = await this.deps.resolveRecipient(identifier);
13706
- if (recipient === null || recipient.chainPubkey === "") {
13707
- throw new SphereError(`Recipient ${identifier} has no published chain pubkey`, "INVALID_RECIPIENT");
13708
- }
13709
- if (recipient.network !== this.deps.network) {
13710
- throw new SphereError(
13711
- `Recipient ${identifier} is on network "${recipient.network}" but this session is on "${this.deps.network}" \u2014 a cross-network deposit is unrecoverable`,
13712
- "INVALID_RECIPIENT"
13713
- );
13714
- }
13715
- return recipient;
13716
- }
13717
13765
  async runAttempt(ctx) {
13718
13766
  this.activeMoneyOps.add(ctx.transferId);
13719
13767
  try {
@@ -14090,19 +14138,12 @@ var PaymentsFacade = class {
14090
14138
  }
14091
14139
  toUiToken(tokenId, coinId, amount) {
14092
14140
  const now = this.nowMs();
14093
- return toToken(
14094
- tokenId,
14095
- { assets: [], createdAt: now, updatedAt: now },
14096
- { coinId, amount: amount.toString() },
14097
- this.deps.registry,
14098
- { transferring: true, suspectedSpent: false }
14099
- );
14141
+ const snapshot = { assets: [], createdAt: now, updatedAt: now };
14142
+ const flags = { transferring: true, suspectedSpent: false };
14143
+ return toToken(tokenId, snapshot, { coinId, amount: amount.toString() }, this.deps.registry, flags);
14100
14144
  }
14101
14145
  track(op) {
14102
- const settled = op.then(
14103
- () => void 0,
14104
- () => void 0
14105
- );
14146
+ const settled = op.then(() => void 0, () => void 0);
14106
14147
  this.pendingOps.add(settled);
14107
14148
  void settled.finally(() => this.pendingOps.delete(settled));
14108
14149
  return op;
@@ -15203,7 +15244,8 @@ async function resolveRecipientInfo(identifier, network, resolvePeer) {
15203
15244
  if (!peer?.chainPubkey) return null;
15204
15245
  return {
15205
15246
  chainPubkey: peer.chainPubkey,
15206
- network,
15247
+ // Only what the binding declares (blank declares nothing) — never a local stamp.
15248
+ network: peer.network || null,
15207
15249
  ...peer.nametag !== void 0 ? { nametag: peer.nametag } : {}
15208
15250
  };
15209
15251
  }