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 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.10-pre-alpha` for npm and `0.2.10a0` 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`.
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.10-pre-alpha
114
- pip install twin3-sdk==0.2.10a0
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.10-pre-alpha.tgz
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "twin3-sdk",
3
- "version": "0.2.10-pre-alpha",
3
+ "version": "0.2.11-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",