twin3-sdk 0.2.19-pre-alpha → 0.2.20-pre-alpha

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.
package/README.md CHANGED
@@ -116,7 +116,7 @@ pip install -e /path/to/twin3-sdk
116
116
  * Any protocol or algorithm improvement in `twin3-sdk` is reflected in both applications in **0 milliseconds**.
117
117
 
118
118
  #### 2. Production Distribution
119
- `v0.2.19-pre-alpha` is the current release candidate: `0.2.19-pre-alpha` for npm and `0.2.19a0` for PyPI. It is published from reviewed main through the existing Cloud Build tag trigger. Consumers must pin the exact released version and lock its registry integrity; never use a mutable range for a cryptographic protocol dependency. This release stays pre-alpha. Marketplace or sales copy may offer it for evaluation; it is not a stable release, and the npm dist-tag is `next`, not `latest`.
119
+ `v0.2.20-pre-alpha` is the current release candidate: `0.2.20-pre-alpha` for npm and `0.2.20a0` for PyPI. It is published from reviewed main through the existing Cloud Build tag trigger. Consumers must pin the exact released version and lock its registry integrity; never use a mutable range for a cryptographic protocol dependency. This release stays pre-alpha. Marketplace or sales copy may offer it for evaluation; it is not a stable release, and the npm dist-tag is `next`, not `latest`.
120
120
 
121
121
  The additive `twin3-sdk/runtime` subpath provides a single-action Agent runtime candidate. Products retain authenticated authority, context egress consent, durable storage and trusted tool brokers. Python `twin3.runtime` provides compatible contracts and digests only. OpenShell, ADK and Hermes adapters are not included. See [runtime scope and limits](docs/architecture/PERSONAL-AGENT-RUNTIME-V1.md).
122
122
 
@@ -125,15 +125,15 @@ The additive `twin3-sdk/runtime` subpath provides a single-action Agent runtime
125
125
  After publication, install the exact released version from the registry:
126
126
 
127
127
  ```bash
128
- npm install twin3-sdk@0.2.19-pre-alpha
129
- pip install twin3-sdk==0.2.19a0
128
+ npm install twin3-sdk@0.2.20-pre-alpha
129
+ pip install twin3-sdk==0.2.20a0
130
130
  ```
131
131
 
132
132
  For local evaluation, you can also use the packed npm tarball. Do not install from a git URL or a version range:
133
133
 
134
134
  ```bash
135
135
  npm pack --ignore-scripts
136
- npm install ./twin3-sdk-0.2.19-pre-alpha.tgz
136
+ npm install ./twin3-sdk-0.2.20-pre-alpha.tgz
137
137
  ```
138
138
 
139
139
  ---
@@ -146,6 +146,9 @@ Agent ID consumes Twin3 SDK to execute the **5-Stage A2A Autonomous Commercial L
146
146
  2. **Understand**: Uses `twin3.matrix` to compute tolerance matching against industrial RFQs.
147
147
  3. **Negotiate**: Uses `twin3.comm` and `twin3.shield` to exchange signed quotes without prompt-tampering risks.
148
148
  4. **Settle**: Uses the Twin3 signed-receipt adapter at an HTTP 402 gate. Acceptance additionally requires a trusted signer key, a chain-specific finality verifier, and a shared replay store.
149
+
150
+ Settlement integrations must follow the [x402 security boundaries](docs/x402-security-boundaries.md),
151
+ including case-sensitive wallet binding, canonical network names and durable payment recovery.
149
152
  5. **Dispute**: Invokes `twin3.governance` to resolve contractual non-performance.
150
153
 
151
154
  ### 👤 Human ID (Personal Sovereignty Projection)
@@ -822,7 +822,7 @@ function x402ChainAdapter(chainVerifier, demand, receipt) {
822
822
  : (chainVerifier && (chainVerifier.verifyPayment || chainVerifier.verify_payment));
823
823
  if (typeof method !== "function")
824
824
  core.fail("chain_verifier_invalid");
825
- const evidence = await method.call(chainVerifier, demand, receipt);
825
+ const evidence = await method.call(chainVerifier, structuredClone(demand), structuredClone(receipt));
826
826
  if (!evidence || typeof evidence !== "object")
827
827
  return evidence;
828
828
  return {
@@ -960,21 +960,24 @@ class TaskInbox {
960
960
  const payout = String(policy.payout_address || "").trim();
961
961
  if (!payout)
962
962
  core.fail("payment_payout_address_required");
963
- if (demand.recipient.trim().toLowerCase() !== payout.toLowerCase())
963
+ if (settlement.chainIdentifier(demand.recipient, demand.network) !== settlement.chainIdentifier(payout, demand.network))
964
964
  core.fail("payment_demand_recipient_mismatch");
965
965
  const price = core.isPlainObject(policy.price) ? policy.price : {};
966
- if (Number(demand.amount) < Number(price.amount || 0))
966
+ settlement.positiveProtocolAmount(demand.amount, "invalid_demand_amount");
967
+ if (settlement.money(demand.amount).compare(settlement.money(price.amount || "0")) < 0)
967
968
  core.fail("payment_demand_amount_insufficient");
968
969
  const policyCurrency = String(price.currency || "").trim().toUpperCase();
969
970
  if (!policyCurrency || demand.currency.toUpperCase() !== policyCurrency)
970
971
  core.fail("payment_demand_currency_mismatch");
971
972
  const partyRow = this._parties.get(identity, { now: at }) || {};
972
- const boundWallet = String(partyRow.bound_wallet || "").trim().toLowerCase();
973
- const payer = receipt.payer.trim().toLowerCase();
974
- const senderValue = String(identity.value || "").trim().toLowerCase();
975
- const senderKey = counterpartyKey(identity).toLowerCase();
973
+ const boundWallet = String(partyRow.bound_wallet || "").trim();
974
+ const payer = settlement.chainIdentifier(receipt.payer, demand.network);
975
+ const senderValue = settlement.chainIdentifier(String(identity.value || ""), demand.network);
976
+ const senderKey = settlement.chainIdentifier(counterpartyKey(identity), demand.network);
976
977
  if (boundWallet) {
977
- if (payer !== boundWallet)
978
+ if (settlement.chainIdentifier("A", demand.network) === "A" && partyRow.bound_wallet_case_preserved !== true)
979
+ core.fail("payment_wallet_rebinding_required");
980
+ if (payer !== settlement.chainIdentifier(boundWallet, demand.network))
978
981
  core.fail("payment_payer_mismatch");
979
982
  }
980
983
  else if (payer !== senderValue && payer !== senderKey) {
@@ -2578,11 +2581,14 @@ function createCounterpartyTable({ store, owner, now, ownerPublicKey, legacyTier
2578
2581
  },
2579
2582
  bindWallet(identity, walletAddress, { actor = "owner", reason = "wallet_binding", now: when } = {}) {
2580
2583
  const [row, when_] = mutate(identity, when);
2581
- const wallet = (core.pythonTruthy(walletAddress) ? core.pythonText(walletAddress, "wallet_address_required") : "")
2582
- .trim().toLowerCase();
2584
+ let wallet = (core.pythonTruthy(walletAddress) ? core.pythonText(walletAddress, "wallet_address_required") : "")
2585
+ .trim();
2586
+ if (/^0x/i.test(wallet))
2587
+ wallet = wallet.toLowerCase();
2583
2588
  if (!wallet)
2584
2589
  core.fail("wallet_address_required");
2585
2590
  row.bound_wallet = wallet;
2591
+ row.bound_wallet_case_preserved = true;
2586
2592
  audit(row, "bind_wallet", row.tier, row.tier, actor, `${reason}:${wallet}`, when_);
2587
2593
  return structuredClone(row);
2588
2594
  },
@@ -903,7 +903,7 @@ function x402Facilitator(value, host) {
903
903
  if (!endpoints || typeof endpoints !== "object" || Array.isArray(endpoints))
904
904
  core.fail("x402_facilitator_endpoints_invalid");
905
905
  for (const name of ["supported", "verify", "settle"]) {
906
- if (typeof endpoints[name] !== "string" || !endpoints[name].startsWith("/"))
906
+ if (typeof endpoints[name] !== "string" || !endpoints[name].startsWith("/") || endpoints[name].startsWith("//") || /[\\\s\x00-\x1f\x7f#]/.test(endpoints[name]))
907
907
  core.fail("x402_facilitator_endpoints_invalid");
908
908
  }
909
909
  const kinds = value.kinds;
@@ -1041,6 +1041,8 @@ function parseX402DnsTxt(records, host) {
1041
1041
  }
1042
1042
  function x402AddressIsUnsafe(address) {
1043
1043
  const text = String(address).trim();
1044
+ if (require("node:net").isIP(text) === 0 || text.includes("%"))
1045
+ core.fail("x402_fetch_address_invalid");
1044
1046
  const v4 = text.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
1045
1047
  if (v4) {
1046
1048
  const [a, b] = [Number(v4[1]), Number(v4[2])];
@@ -1052,15 +1054,11 @@ function x402AddressIsUnsafe(address) {
1052
1054
  || a >= 224;
1053
1055
  }
1054
1056
  if (text.includes(":")) {
1055
- const lower = text.toLowerCase();
1056
- if (!/^[0-9a-f:.]+$/.test(lower))
1057
- core.fail("x402_fetch_address_invalid");
1058
- if (lower === "::" || lower === "::1")
1059
- return true;
1060
- if (lower.startsWith("::ffff:"))
1061
- return x402AddressIsUnsafe(lower.slice("::ffff:".length));
1062
- return lower.startsWith("fe8") || lower.startsWith("fe9") || lower.startsWith("fea") || lower.startsWith("feb")
1063
- || lower.startsWith("fc") || lower.startsWith("fd") || lower.startsWith("ff") || lower.startsWith("2001:db8") || lower.startsWith("2001:0db8");
1057
+ const lower = new URL(`https://[${text}]/`).hostname.slice(1, -1);
1058
+ const [first, second] = lower.split(":").map((part) => parseInt(part || "0", 16));
1059
+ return first < 0x2000 || first > 0x3fff || first === 0x2002
1060
+ || (first === 0x2001 && (second < 0x200 || second === 0xdb8))
1061
+ || (first === 0x3fff && second < 0x1000);
1064
1062
  }
1065
1063
  core.fail("x402_fetch_address_invalid");
1066
1064
  }
@@ -1079,6 +1077,8 @@ function checkX402FetchTarget(url, host, { resolvedAddresses = [], hop = 0, maxR
1079
1077
  if (targetHost === "localhost" || targetHost.endsWith(".localhost"))
1080
1078
  core.fail("x402_fetch_private_address");
1081
1079
  const unbracketed = targetHost.replace(/^\[|\]$/g, "");
1080
+ if (/^(?:\d+|0x[0-9a-f]*)$/.test(unbracketed.split(".").at(-1)) && !/^(\d{1,3}\.){3}\d{1,3}$/.test(unbracketed))
1081
+ core.fail("x402_fetch_private_address");
1082
1082
  if ((/^(\d{1,3}\.){3}\d{1,3}$/.test(unbracketed) || unbracketed.includes(":")) && x402AddressIsUnsafe(unbracketed)) {
1083
1083
  core.fail("x402_fetch_private_address");
1084
1084
  }
@@ -216,6 +216,16 @@ function positiveProtocolInteger(value, code) {
216
216
  core.fail(code);
217
217
  return value;
218
218
  }
219
+ function paymentNetworkKey(value) {
220
+ value = requiredProtocolText(value, "network_required");
221
+ return value.includes(":") ? value : value.toLowerCase();
222
+ }
223
+ function chainIdentifier(value, network) {
224
+ value = requiredProtocolText(value, "chain_evidence_invalid");
225
+ network = paymentNetworkKey(network);
226
+ return network.startsWith("eip155:") || ["base", "base-sepolia", "ethereum", "ethereum-sepolia", "polygon", "arbitrum", "optimism"].includes(network)
227
+ ? value.toLowerCase() : value;
228
+ }
219
229
  function computeReceiptDigest({ demandId, txHash, payer, amount, currency, settledAt, recipient, network }) {
220
230
  demandId = requiredProtocolText(demandId, "demand_id_required");
221
231
  txHash = requiredProtocolText(txHash, "tx_hash_required");
@@ -259,7 +269,7 @@ function verifySignedPaymentReceipt({ demandAmount, receiptAmount, demandRecipie
259
269
  core.fail("payment_currency_mismatch");
260
270
  demandNetwork = requiredProtocolText(demandNetwork, "network_required");
261
271
  receiptNetwork = requiredProtocolText(receiptNetwork, "network_required");
262
- if (receiptNetwork.toLowerCase() !== demandNetwork.toLowerCase())
272
+ if (paymentNetworkKey(receiptNetwork) !== paymentNetworkKey(demandNetwork))
263
273
  core.fail("payment_network_mismatch");
264
274
  demandExpiresAt = positiveProtocolInteger(demandExpiresAt, "demand_expiry_required");
265
275
  if (typeof now !== "number" || !Number.isFinite(now))
@@ -271,7 +281,7 @@ function verifySignedPaymentReceipt({ demandAmount, receiptAmount, demandRecipie
271
281
  core.fail("settled_at_in_future");
272
282
  receiptRecipient = requiredProtocolText(receiptRecipient, "recipient_required");
273
283
  demandRecipient = requiredProtocolText(demandRecipient, "expected_recipient_required");
274
- if (receiptRecipient.toLowerCase() !== demandRecipient.toLowerCase())
284
+ if (chainIdentifier(receiptRecipient, demandNetwork) !== chainIdentifier(demandRecipient, demandNetwork))
275
285
  core.fail("payment_recipient_mismatch");
276
286
  signature = requiredProtocolText(signature, "receipt_signature_required");
277
287
  txHash = requiredProtocolText(txHash, "tx_hash_required");
@@ -314,6 +324,12 @@ function isReservationStore(store) {
314
324
  typeof store.release === "function");
315
325
  }
316
326
  async function verifyX402Payment(params) {
327
+ params = { ...params };
328
+ for (const name of ["demandRecipient", "receiptRecipient", "demandCurrency", "receiptCurrency", "demandNetwork", "receiptNetwork", "txHash", "payer", "signature", "demandId", "receiptDemandId"])
329
+ if (typeof params[name] === "string") params[name] = params[name].trim();
330
+ if (params.tokenAddress !== undefined && params.tokenAddress !== null)
331
+ params.tokenAddress = requiredProtocolText(params.tokenAddress, "token_address_invalid");
332
+ Object.freeze(params);
317
333
  verifySignedPaymentReceipt(params);
318
334
  const { chainVerifier, replayStore, minimumConfirmations = 1 } = params;
319
335
  if (!chainVerifier)
@@ -327,7 +343,7 @@ async function verifyX402Payment(params) {
327
343
  if (!reservationMode) {
328
344
  core.fail("reservation_capable_store_required");
329
345
  }
330
- if (typeof minimumConfirmations !== "number" || !Number.isInteger(minimumConfirmations) || minimumConfirmations < 1) {
346
+ if (typeof minimumConfirmations !== "number" || !Number.isSafeInteger(minimumConfirmations) || minimumConfirmations < 1) {
331
347
  core.fail("minimum_confirmations_invalid");
332
348
  }
333
349
  const requiredConfirmations = minimumConfirmations;
@@ -372,20 +388,20 @@ async function verifyX402Payment(params) {
372
388
  }
373
389
  if (!evidence || typeof evidence !== "object")
374
390
  core.fail("chain_evidence_invalid");
375
- if (typeof evidence.confirmations !== "number" || !Number.isInteger(evidence.confirmations) || evidence.confirmations < 0) {
391
+ if (typeof evidence.confirmations !== "number" || !Number.isSafeInteger(evidence.confirmations) || evidence.confirmations < 0) {
376
392
  core.fail("chain_evidence_invalid");
377
393
  }
378
394
  if (evidence.finalized !== true || evidence.confirmations < requiredConfirmations) {
379
395
  core.fail("payment_not_finalized");
380
396
  }
381
397
  const exact = [
382
- [evidence.txHash, params.txHash],
383
- [evidence.network, params.demandNetwork],
384
- [evidence.payer, params.payer],
385
- [evidence.recipient, params.demandRecipient],
386
- [String(evidence.currency || "").toUpperCase(), String(params.demandCurrency).toUpperCase()],
398
+ [chainIdentifier(evidence.txHash, params.demandNetwork), chainIdentifier(params.txHash, params.demandNetwork)],
399
+ [paymentNetworkKey(evidence.network), paymentNetworkKey(params.demandNetwork)],
400
+ [chainIdentifier(evidence.payer, params.demandNetwork), chainIdentifier(params.payer, params.demandNetwork)],
401
+ [chainIdentifier(evidence.recipient, params.demandNetwork), chainIdentifier(params.demandRecipient, params.demandNetwork)],
402
+ [requiredProtocolText(evidence.currency, "chain_evidence_invalid").toUpperCase(), params.demandCurrency.toUpperCase()],
387
403
  ];
388
- if (exact.some(([actual, expected]) => String(actual || "").toLowerCase() !== String(expected || "").toLowerCase())) {
404
+ if (exact.some(([actual, expected]) => actual !== expected)) {
389
405
  core.fail("chain_evidence_mismatch");
390
406
  }
391
407
  let evidenceAmount;
@@ -397,7 +413,7 @@ async function verifyX402Payment(params) {
397
413
  }
398
414
  if (evidenceAmount !== params.receiptAmount)
399
415
  core.fail("chain_evidence_mismatch");
400
- if (params.tokenAddress && String(evidence.tokenAddress || "").toLowerCase() !== String(params.tokenAddress).toLowerCase()) {
416
+ if (params.tokenAddress && (!evidence.tokenAddress || chainIdentifier(evidence.tokenAddress, params.demandNetwork) !== chainIdentifier(params.tokenAddress, params.demandNetwork))) {
401
417
  core.fail("chain_token_mismatch");
402
418
  }
403
419
  let committed;
@@ -461,6 +477,7 @@ module.exports = {
461
477
  requiredProtocolText,
462
478
  positiveProtocolAmount,
463
479
  positiveProtocolInteger,
480
+ chainIdentifier,
464
481
  computeReceiptDigest,
465
482
  verifySignedPaymentReceipt,
466
483
  REPLAY_RESERVATION_SECONDS,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "twin3-sdk",
3
- "version": "0.2.19-pre-alpha",
3
+ "version": "0.2.20-pre-alpha",
4
4
  "description": "Universal Capability Motherboard for Autonomous AI Agents and Self-Sovereign Humans (JS/Node runtime)",
5
5
  "type": "commonjs",
6
6
  "main": "./js/index.js",