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 +7 -4
- package/js/internal/comm.js +16 -10
- package/js/internal/discovery.js +10 -10
- package/js/internal/settlement.js +28 -11
- package/package.json +1 -1
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.
|
|
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.
|
|
129
|
-
pip install twin3-sdk==0.2.
|
|
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.
|
|
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)
|
package/js/internal/comm.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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()
|
|
973
|
-
const payer = receipt.payer.
|
|
974
|
-
const senderValue = String(identity.value || "").
|
|
975
|
-
const senderKey = counterpartyKey(identity).
|
|
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 (
|
|
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
|
-
|
|
2582
|
-
.trim()
|
|
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
|
},
|
package/js/internal/discovery.js
CHANGED
|
@@ -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.
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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
|
-
[
|
|
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]) =>
|
|
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 &&
|
|
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