twin3-sdk 0.2.10-pre-alpha → 0.2.11-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 +4 -4
- package/index.d.ts +32 -0
- package/js/index.js +157 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -103,22 +103,22 @@ pip install -e /path/to/twin3-sdk
|
|
|
103
103
|
* Any protocol or algorithm improvement in `twin3-sdk` is reflected in both applications in **0 milliseconds**.
|
|
104
104
|
|
|
105
105
|
#### 2. Production Distribution
|
|
106
|
-
The next release candidate is `0.2.
|
|
106
|
+
The next release candidate is `0.2.11-pre-alpha` for npm and `0.2.11a0` for Python. It is not a production dependency until the GitHub release workflows have built, attested, rehearsed, and published the exact tagged artifacts. Consumers must then pin the exact released version and lock its registry integrity; never use a mutable range for a cryptographic protocol dependency. This candidate 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`.
|
|
107
107
|
|
|
108
108
|
`package.json` `engines` sets Node.js to `>=18` and does not set an npm engine. Any npm that can install a tarball can consume the package. Packing this repository was checked with npm 10 or newer; the stricter npm version in the release runbook applies only to Trusted Publishing.
|
|
109
109
|
|
|
110
110
|
After the release workflow has published that exact version, install the pin. These commands are not evidence that the registry already serves the package:
|
|
111
111
|
|
|
112
112
|
```bash
|
|
113
|
-
npm install twin3-sdk@0.2.
|
|
114
|
-
pip install twin3-sdk==0.2.
|
|
113
|
+
npm install twin3-sdk@0.2.11-pre-alpha
|
|
114
|
+
pip install twin3-sdk==0.2.11a0
|
|
115
115
|
```
|
|
116
116
|
|
|
117
117
|
Until publication, evaluate the packed npm tarball only. Do not install from a git URL or a version range:
|
|
118
118
|
|
|
119
119
|
```bash
|
|
120
120
|
npm pack --ignore-scripts
|
|
121
|
-
npm install ./twin3-sdk-0.2.
|
|
121
|
+
npm install ./twin3-sdk-0.2.11-pre-alpha.tgz
|
|
122
122
|
```
|
|
123
123
|
|
|
124
124
|
---
|
package/index.d.ts
CHANGED
|
@@ -375,6 +375,38 @@ export declare const A2A_PROTOCOL_VERSION: "1.0";
|
|
|
375
375
|
export declare const A2A_DOCUMENT_SUFFIXES: readonly string[];
|
|
376
376
|
export declare const A2A_DISCOVERY_DOCUMENT_PATHS: readonly string[];
|
|
377
377
|
export interface A2AInterface { url: string; protocolBinding: "HTTP+JSON" | "JSONRPC" | "GRPC"; protocolVersion: string; }
|
|
378
|
+
export interface A2AAgentCardSignature {
|
|
379
|
+
protected: string;
|
|
380
|
+
signature: string;
|
|
381
|
+
header?: Record<string, unknown>;
|
|
382
|
+
}
|
|
383
|
+
export interface A2AJwks { keys: import("node:crypto").JsonWebKey[]; }
|
|
384
|
+
export interface A2AAgentCard extends Record<string, any> {
|
|
385
|
+
name: string;
|
|
386
|
+
description: string;
|
|
387
|
+
version: string;
|
|
388
|
+
supportedInterfaces: A2AInterface[];
|
|
389
|
+
skills: Record<string, any>[];
|
|
390
|
+
url?: string;
|
|
391
|
+
protocolVersion?: string;
|
|
392
|
+
preferredTransport?: A2AInterface["protocolBinding"];
|
|
393
|
+
signatures?: A2AAgentCardSignature[];
|
|
394
|
+
signatureStatus?: string;
|
|
395
|
+
}
|
|
396
|
+
export interface A2ASignatureVerification {
|
|
397
|
+
status: "signed";
|
|
398
|
+
mode: "a2a_jcs" | "legacy_signature_status_excluded";
|
|
399
|
+
kid: string;
|
|
400
|
+
alg: "EdDSA" | "ES256";
|
|
401
|
+
jku: string | null;
|
|
402
|
+
protected: { alg: "EdDSA" | "ES256"; typ: "JOSE"; kid: string; jku?: string; [name: string]: unknown };
|
|
403
|
+
}
|
|
404
|
+
/** Signs every supplied field except signatures; preserves signatureStatus.
|
|
405
|
+
* privateKey accepts Ed25519/P-256 KeyObject or PEM, or a 32-byte Ed25519 seed. */
|
|
406
|
+
export declare function signA2AAgentCard<T extends Record<string, any>>(card: T, privateKey: string | Buffer | import("node:crypto").KeyObject, kid: string, jku?: string | null): Omit<T, "signatures"> & { signatures: A2AAgentCardSignature[] };
|
|
407
|
+
/** Uses caller-trusted public JWKS, without fetching jku. Unsupported headers,
|
|
408
|
+
* ambiguous/missing kids and invalid keys are errors even in legacy mode. */
|
|
409
|
+
export declare function verifyA2AAgentCardSignature(card: Record<string, any>, jwks: A2AJwks, options?: { allowLegacy?: boolean }): A2ASignatureVerification;
|
|
378
410
|
export interface A2AValidationReport {
|
|
379
411
|
profile: "twin3-discovery-a2a-card-v1";
|
|
380
412
|
skills: string[];
|
package/js/index.js
CHANGED
|
@@ -473,6 +473,7 @@ const A2A_KNOWN_FIELDS = [
|
|
|
473
473
|
"name", "description", "supportedInterfaces", "provider", "version", "documentationUrl", "iconUrl",
|
|
474
474
|
"capabilities", "securitySchemes", "securityRequirements", "defaultInputModes", "defaultOutputModes",
|
|
475
475
|
"skills", "signatures", "signatureStatus", "models", "authenticatedSubject",
|
|
476
|
+
"url", "protocolVersion", "preferredTransport",
|
|
476
477
|
];
|
|
477
478
|
const A2A_DEFAULT_MODES = ["application/json", "text/plain"];
|
|
478
479
|
|
|
@@ -557,8 +558,16 @@ function a2aRequirements(value, schemes, code) {
|
|
|
557
558
|
|
|
558
559
|
function validateAgentCardV1(card) {
|
|
559
560
|
if (!card || typeof card !== "object" || Array.isArray(card)) fail("a2a_card_invalid");
|
|
560
|
-
const legacy = A2A_LEGACY_FIELDS.filter((name) => name in card);
|
|
561
|
+
const legacy = A2A_LEGACY_FIELDS.filter((name) => name in card && !["url", "preferredTransport"].includes(name));
|
|
561
562
|
if (legacy.length) fail("a2a_legacy_shape:" + legacy[0]);
|
|
563
|
+
if ("url" in card) {
|
|
564
|
+
a2aUrl(card.url, "a2a_legacy_shape:url");
|
|
565
|
+
if (!Array.isArray(card.supportedInterfaces) || !card.supportedInterfaces.some((iface) => iface && iface.url === card.url)) {
|
|
566
|
+
fail("a2a_legacy_shape:url");
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
if ("preferredTransport" in card && !A2A_PROTOCOL_BINDINGS.includes(card.preferredTransport)) fail("a2a_field_invalid:preferredTransport");
|
|
570
|
+
if ("protocolVersion" in card) a2aText(card.protocolVersion, "a2a_field_invalid:protocolVersion");
|
|
562
571
|
a2aText(card.name, "a2a_field_required:name");
|
|
563
572
|
a2aText(card.description, "a2a_field_required:description");
|
|
564
573
|
a2aText(card.version, "a2a_field_required:version");
|
|
@@ -613,6 +622,7 @@ function validateAgentCardV1(card) {
|
|
|
613
622
|
ids.add(skill.id);
|
|
614
623
|
});
|
|
615
624
|
if ("signatures" in card && !Array.isArray(card.signatures)) fail("a2a_field_invalid:signatures");
|
|
625
|
+
(card.signatures || []).forEach(a2aSignatureEntry);
|
|
616
626
|
return {
|
|
617
627
|
profile: A2A_CARD_PROFILE,
|
|
618
628
|
skills: Array.from(ids).sort(),
|
|
@@ -624,6 +634,150 @@ function validateAgentCardV1(card) {
|
|
|
624
634
|
};
|
|
625
635
|
}
|
|
626
636
|
|
|
637
|
+
function a2aSignatureEntry(entry, index) {
|
|
638
|
+
const code = "a2a_signature_invalid:" + index;
|
|
639
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry) ||
|
|
640
|
+
!Object.hasOwn(entry, "protected") || !Object.hasOwn(entry, "signature") ||
|
|
641
|
+
Object.keys(entry).some((name) => !["protected", "signature", "header"].includes(name))) fail(code);
|
|
642
|
+
strictB64urlDecode(entry.protected, code);
|
|
643
|
+
strictB64urlDecode(entry.signature, code);
|
|
644
|
+
if (Object.hasOwn(entry, "header") && (!entry.header || typeof entry.header !== "object" || Array.isArray(entry.header))) fail(code);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function a2aSignatureBody(card) {
|
|
648
|
+
if (!card || typeof card !== "object" || Array.isArray(card)) fail("a2a_card_invalid");
|
|
649
|
+
const body = structuredClone(card);
|
|
650
|
+
delete body.signatures;
|
|
651
|
+
return body;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function a2aSignaturePayload(body) {
|
|
655
|
+
try {
|
|
656
|
+
return canonicalJsonBuffer(body);
|
|
657
|
+
} catch (error) {
|
|
658
|
+
fail("a2a_not_canonicalizable", error);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
function a2aSigningInput(protectedB64, payload) {
|
|
663
|
+
return Buffer.from(protectedB64 + "." + payload.toString("base64url"), "ascii");
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
function a2aPrivateKey(value) {
|
|
667
|
+
let key;
|
|
668
|
+
try {
|
|
669
|
+
if (Buffer.isBuffer(value) && value.length === 32) {
|
|
670
|
+
key = crypto.createPrivateKey({
|
|
671
|
+
key: Buffer.concat([Buffer.from("302e020100300506032b657004220420", "hex"), value]),
|
|
672
|
+
format: "der", type: "pkcs8",
|
|
673
|
+
});
|
|
674
|
+
} else {
|
|
675
|
+
key = value instanceof crypto.KeyObject ? value : crypto.createPrivateKey(value);
|
|
676
|
+
}
|
|
677
|
+
} catch (error) {
|
|
678
|
+
fail("a2a_private_key_invalid", error);
|
|
679
|
+
}
|
|
680
|
+
if (key.type !== "private") fail("a2a_private_key_invalid");
|
|
681
|
+
if (key.asymmetricKeyType === "ed25519") return { key, alg: "EdDSA" };
|
|
682
|
+
if (key.asymmetricKeyType === "ec" && key.asymmetricKeyDetails.namedCurve === "prime256v1") return { key, alg: "ES256" };
|
|
683
|
+
fail("a2a_private_key_invalid");
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/** A2A §8.4 detached JWS. Only signatures is excluded; no status is added. */
|
|
687
|
+
function signA2AAgentCard(card, privateKey, kid, jku = null) {
|
|
688
|
+
const { key, alg } = a2aPrivateKey(privateKey);
|
|
689
|
+
a2aText(kid, "a2a_kid_required");
|
|
690
|
+
const header = { alg, typ: "JOSE", kid };
|
|
691
|
+
if (jku !== null) {
|
|
692
|
+
a2aUrl(jku, "a2a_jku_invalid");
|
|
693
|
+
if (!jku.startsWith("https://")) fail("a2a_jku_invalid");
|
|
694
|
+
header.jku = jku;
|
|
695
|
+
}
|
|
696
|
+
const body = a2aSignatureBody(card);
|
|
697
|
+
validateAgentCardV1(body);
|
|
698
|
+
const protectedB64 = canonicalJsonBuffer(header).toString("base64url");
|
|
699
|
+
const input = a2aSigningInput(protectedB64, a2aSignaturePayload(body));
|
|
700
|
+
const signature = alg === "EdDSA" ? crypto.sign(null, input, key) :
|
|
701
|
+
crypto.sign("sha256", input, { key, dsaEncoding: "ieee-p1363" });
|
|
702
|
+
body.signatures = [{ protected: protectedB64, signature: signature.toString("base64url") }];
|
|
703
|
+
return body;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
function a2aProtectedHeader(entry) {
|
|
707
|
+
const code = "a2a_protected_invalid";
|
|
708
|
+
let header;
|
|
709
|
+
try {
|
|
710
|
+
header = JSON.parse(new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(strictB64urlDecode(entry.protected, code)));
|
|
711
|
+
} catch (error) {
|
|
712
|
+
fail(code, error);
|
|
713
|
+
}
|
|
714
|
+
if (!header || typeof header !== "object" || Array.isArray(header)) fail(code);
|
|
715
|
+
if (!["EdDSA", "ES256"].includes(header.alg)) fail("a2a_alg_unsupported");
|
|
716
|
+
a2aText(header.kid, code);
|
|
717
|
+
if (header.typ !== "JOSE") fail(code);
|
|
718
|
+
const unprotected = entry.header || {};
|
|
719
|
+
if (Object.keys(header).some((name) => Object.hasOwn(unprotected, name)) ||
|
|
720
|
+
[header, unprotected].some((h) => Object.hasOwn(h, "crit") || Object.hasOwn(h, "b64"))) fail(code);
|
|
721
|
+
if (Object.hasOwn(header, "jku")) {
|
|
722
|
+
a2aUrl(header.jku, code);
|
|
723
|
+
if (!header.jku.startsWith("https://")) fail(code);
|
|
724
|
+
}
|
|
725
|
+
return header;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
function a2aJwksKey(jwks, header) {
|
|
729
|
+
const matches = jwks.keys.filter((jwk) => jwk.kid === header.kid);
|
|
730
|
+
if (matches.length === 0) fail("a2a_kid_mismatch");
|
|
731
|
+
if (matches.length !== 1) fail("a2a_kid_ambiguous");
|
|
732
|
+
const jwk = matches[0];
|
|
733
|
+
const code = "a2a_jwk_invalid";
|
|
734
|
+
if (Object.hasOwn(jwk, "d") || (Object.hasOwn(jwk, "alg") && jwk.alg !== header.alg) ||
|
|
735
|
+
(Object.hasOwn(jwk, "use") && jwk.use !== "sig")) fail(code);
|
|
736
|
+
if (Object.hasOwn(jwk, "key_ops") && (!Array.isArray(jwk.key_ops) || !jwk.key_ops.includes("verify") ||
|
|
737
|
+
jwk.key_ops.some((op) => typeof op !== "string"))) fail(code);
|
|
738
|
+
const x = strictB64urlDecode(jwk.x, code);
|
|
739
|
+
if (x.length !== 32) fail(code);
|
|
740
|
+
if (header.alg === "EdDSA") {
|
|
741
|
+
if (jwk.kty !== "OKP" || jwk.crv !== "Ed25519") fail(code);
|
|
742
|
+
} else {
|
|
743
|
+
if (jwk.kty !== "EC" || jwk.crv !== "P-256" || strictB64urlDecode(jwk.y, code).length !== 32) fail(code);
|
|
744
|
+
}
|
|
745
|
+
try {
|
|
746
|
+
return crypto.createPublicKey({ key: jwk, format: "jwk" });
|
|
747
|
+
} catch (error) {
|
|
748
|
+
fail(code, error);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
/** Verify with caller-trusted JWKS; never fetch jku. All standard signatures
|
|
753
|
+
* are tried before the opt-in signatureStatus-excluded legacy payload. */
|
|
754
|
+
function verifyA2AAgentCardSignature(card, jwks, { allowLegacy = false } = {}) {
|
|
755
|
+
if (typeof allowLegacy !== "boolean") fail("a2a_options_invalid");
|
|
756
|
+
validateAgentCardV1(card);
|
|
757
|
+
if (!card.signatures || card.signatures.length === 0) fail("a2a_signature_required");
|
|
758
|
+
if (!jwks || typeof jwks !== "object" || Array.isArray(jwks) || !Array.isArray(jwks.keys) ||
|
|
759
|
+
jwks.keys.some((jwk) => !jwk || typeof jwk !== "object" || Array.isArray(jwk))) fail("a2a_jwks_invalid");
|
|
760
|
+
const entries = card.signatures.map((entry) => {
|
|
761
|
+
const header = a2aProtectedHeader(entry);
|
|
762
|
+
return { protectedB64: entry.protected, header, key: a2aJwksKey(jwks, header),
|
|
763
|
+
signature: strictB64urlDecode(entry.signature, "a2a_signature_invalid") };
|
|
764
|
+
});
|
|
765
|
+
const body = a2aSignatureBody(card);
|
|
766
|
+
const modes = ["a2a_jcs", ...(allowLegacy ? ["legacy_signature_status_excluded"] : [])];
|
|
767
|
+
for (const mode of modes) {
|
|
768
|
+
if (mode === "legacy_signature_status_excluded") delete body.signatureStatus;
|
|
769
|
+
const payload = a2aSignaturePayload(body);
|
|
770
|
+
for (const { protectedB64, header, key, signature } of entries) {
|
|
771
|
+
if (signature.length !== 64) continue;
|
|
772
|
+
const input = a2aSigningInput(protectedB64, payload);
|
|
773
|
+
const ok = header.alg === "EdDSA" ? crypto.verify(null, input, key, signature) :
|
|
774
|
+
crypto.verify("sha256", input, { key, dsaEncoding: "ieee-p1363" }, signature);
|
|
775
|
+
if (ok) return { status: "signed", mode, kid: header.kid, alg: header.alg, jku: header.jku ?? null, protected: header };
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
fail("a2a_signature_invalid");
|
|
779
|
+
}
|
|
780
|
+
|
|
627
781
|
function agentCardPaths(origin = "") {
|
|
628
782
|
if (typeof origin !== "string") fail("a2a_origin_invalid");
|
|
629
783
|
if (origin) {
|
|
@@ -6372,6 +6526,8 @@ module.exports = {
|
|
|
6372
6526
|
signDescriptor,
|
|
6373
6527
|
unsignedDescriptor,
|
|
6374
6528
|
verifyDescriptor,
|
|
6529
|
+
signA2AAgentCard,
|
|
6530
|
+
verifyA2AAgentCardSignature,
|
|
6375
6531
|
DESCRIPTOR_CANONICALIZATION,
|
|
6376
6532
|
DESCRIPTOR_SIGNATURE_FIELDS,
|
|
6377
6533
|
buildWellKnownDocument,
|
package/package.json
CHANGED