@twin3-ai/agent-id 0.3.48 → 0.3.49

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/browser-sdk.js CHANGED
@@ -16,7 +16,7 @@
16
16
  // browser script and shipped in the package byte-for-byte, so it can
17
17
  // neither read the package at runtime nor go through a build step:
18
18
  // the version is written once here and CI refuses a mismatch.
19
- var SDK_VERSION = "0.3.48";
19
+ var SDK_VERSION = "0.3.49";
20
20
  var w = window;
21
21
  var d = document;
22
22
  var s = d.currentScript || {};
@@ -0,0 +1,233 @@
1
+ "use strict";
2
+
3
+ // Counterparty-side SDK for the xAgent Inbox: what an *external* agent needs
4
+ // to complete a KYA handshake with an owner and send it signed envelopes.
5
+ //
6
+ // This side never holds the owner's credentials. It produces three things -
7
+ // a public key, a signed handshake answer, and signed envelopes - and hands
8
+ // them to the owner over whatever channel the two agents already share
9
+ // (A2A, a relay, a test harness). The owner's own agent then calls its
10
+ // /api/site_agents/v1/inbox/* routes. `createRelayTransport` covers the case
11
+ // where one party operates both ends, e.g. integration tests.
12
+ //
13
+ // Wire compatibility with agent_id/inbox/kya.py is guaranteed two ways:
14
+ // 1. The handshake answer signs the exact bytes the server publishes as
15
+ // `sign_payload`, so no re-serialization happens on this side.
16
+ // 2. Envelopes use integer timestamps and canonical JSON (sorted keys, no
17
+ // spaces, non-ASCII unescaped), which is byte-identical to Python's
18
+ // json.dumps(sort_keys=True, separators=(",",":"), ensure_ascii=False)
19
+ // for the value types an envelope may carry.
20
+
21
+ const crypto = require("node:crypto");
22
+ const fs = require("node:fs");
23
+
24
+ const ENVELOPE_SCHEMA = "agentx-inbox-envelope-v1";
25
+ const CHALLENGE_SCHEMA = "agentx-inbox-kya-challenge-v1";
26
+ const SIGNATURE_SCHEMA = "agentx-inbox-envelope-signature-v1";
27
+ // The owner is migrating the published schema prefix from agentx-* to twin3-*.
28
+ // Accept both while that is in flight; the owner signature covers the schema
29
+ // field itself, so accepting a second name does not widen what is verified.
30
+ const VALID_CHALLENGE_SCHEMAS = new Set([CHALLENGE_SCHEMA, "twin3-trust-kya-challenge-v1"]);
31
+ const ENVELOPE_KINDS = new Set(["quote_request", "task_offer", "introduction", "message"]);
32
+ const CHALLENGE_SIGNED_FIELDS = ["schema", "handshake_id", "owner", "identity", "counterparty_key_id", "owner_key_id", "nonce", "issued_at", "expires_at"];
33
+
34
+ function sdkError(code) {
35
+ const error = new Error(code);
36
+ error.code = code;
37
+ return error;
38
+ }
39
+
40
+ function stableValue(value) {
41
+ if (Array.isArray(value)) return value.map(stableValue);
42
+ if (!value || typeof value !== "object") return value;
43
+ return Object.fromEntries(Object.keys(value).sort().map((key) => [key, stableValue(value[key])]));
44
+ }
45
+
46
+ function canonicalJson(value) {
47
+ return Buffer.from(JSON.stringify(stableValue(value)), "utf8");
48
+ }
49
+
50
+ function b64url(buffer) {
51
+ return Buffer.from(buffer).toString("base64url");
52
+ }
53
+
54
+ function b64urlDecode(text) {
55
+ return Buffer.from(String(text || ""), "base64url");
56
+ }
57
+
58
+ // --- keys --------------------------------------------------------------------
59
+
60
+ function loadPrivateKey({ keyPath, privateKeyPem } = {}) {
61
+ const pem = privateKeyPem || (keyPath ? fs.readFileSync(keyPath, "utf8") : "");
62
+ if (!pem) throw sdkError("private_key_required");
63
+ const key = crypto.createPrivateKey(pem);
64
+ if (key.asymmetricKeyType !== "ed25519") throw sdkError("private_key_not_ed25519");
65
+ return key;
66
+ }
67
+
68
+ function rawPublicBytes(publicKey) {
69
+ // SPKI DER for Ed25519 is a fixed 12-byte prefix followed by the 32 raw bytes.
70
+ const der = publicKey.export({ type: "spki", format: "der" });
71
+ return der.subarray(der.length - 32);
72
+ }
73
+
74
+ function keyId(publicKey) {
75
+ return "ed25519:" + crypto.createHash("sha256").update(rawPublicBytes(publicKey)).digest("hex").slice(0, 24);
76
+ }
77
+
78
+ function exportPublicKey(publicKey, format = "compact") {
79
+ if (format === "pem") return publicKey.export({ type: "spki", format: "pem" });
80
+ if (format === "compact") return "ed25519:" + b64url(rawPublicBytes(publicKey));
81
+ throw sdkError("public_key_format_unknown");
82
+ }
83
+
84
+ function loadPublicKey(value) {
85
+ const text = String(value || "").trim();
86
+ if (!text) throw sdkError("public_key_required");
87
+ if (text.startsWith("-----BEGIN")) {
88
+ const key = crypto.createPublicKey(text);
89
+ if (key.asymmetricKeyType !== "ed25519") throw sdkError("public_key_not_ed25519");
90
+ return key;
91
+ }
92
+ if (text.startsWith("ed25519:")) {
93
+ const raw = b64urlDecode(text.slice("ed25519:".length));
94
+ if (raw.length !== 32) throw sdkError("invalid_ed25519_public_key_length");
95
+ const prefix = Buffer.from("302a300506032b6570032100", "hex");
96
+ return crypto.createPublicKey({ key: Buffer.concat([prefix, raw]), type: "spki", format: "der" });
97
+ }
98
+ throw sdkError("public_key_format_unknown");
99
+ }
100
+
101
+ // --- handshake ---------------------------------------------------------------------
102
+
103
+ function challengeSignedBody(challenge) {
104
+ return Object.fromEntries(CHALLENGE_SIGNED_FIELDS.map((field) => [field, challenge[field]]));
105
+ }
106
+
107
+ function verifyOwnerSignature(challenge, ownerPublicKey) {
108
+ if (!challenge || !VALID_CHALLENGE_SCHEMAS.has(challenge.schema)) throw sdkError("challenge_schema_mismatch");
109
+ const key = loadPublicKey(ownerPublicKey);
110
+ const ok = crypto.verify(null, canonicalJson(challengeSignedBody(challenge)), key, b64urlDecode(challenge.owner_signature));
111
+ if (!ok) throw sdkError("owner_signature_invalid");
112
+ return true;
113
+ }
114
+
115
+ // --- envelope ------------------------------------------------------------------------
116
+
117
+ function envelopeDigest(envelope) {
118
+ const body = Object.fromEntries(Object.entries(envelope).filter(([k]) => k !== "digest" && k !== "sender_signature"));
119
+ return crypto.createHash("sha256").update(canonicalJson(body)).digest("hex");
120
+ }
121
+
122
+ function buildEnvelope(identity, kind, payload, { ttlSeconds = 900, now = Math.floor(Date.now() / 1000) } = {}) {
123
+ if (!identity || !identity.kind || !identity.value) throw sdkError("identity_required");
124
+ if (!ENVELOPE_KINDS.has(kind)) throw sdkError("envelope_kind_unknown");
125
+ if (!payload || typeof payload !== "object" || Array.isArray(payload) || Object.keys(payload).length === 0) throw sdkError("envelope_payload_empty");
126
+ const at = Math.floor(now);
127
+ const envelope = {
128
+ schema: ENVELOPE_SCHEMA,
129
+ envelope_id: crypto.randomUUID(),
130
+ sender: { kind: String(identity.kind), value: String(identity.value) },
131
+ kind,
132
+ authority: "none",
133
+ payload: JSON.parse(JSON.stringify(payload)),
134
+ provenance: Object.fromEntries(Object.keys(payload).map((field) => [field, { origin: "counterparty", treat_as: "data", at }])),
135
+ nonce: crypto.randomBytes(16).toString("hex"),
136
+ issued_at: at,
137
+ deadline: at + Math.floor(ttlSeconds),
138
+ };
139
+ envelope.digest = envelopeDigest(envelope);
140
+ return envelope;
141
+ }
142
+
143
+ function signEnvelope(envelope, privateKey) {
144
+ const body = Object.fromEntries(Object.entries(envelope).filter(([k]) => k !== "sender_signature"));
145
+ const signed = JSON.parse(JSON.stringify(body));
146
+ signed.sender_signature = {
147
+ schema: SIGNATURE_SCHEMA,
148
+ key_id: keyId(crypto.createPublicKey(privateKey)),
149
+ value: b64url(crypto.sign(null, canonicalJson(body), privateKey)),
150
+ };
151
+ return signed;
152
+ }
153
+
154
+ // --- the agent -------------------------------------------------------------------------
155
+
156
+ function createCounterpartyAgent({ identity, keyPath, privateKeyPem, now = () => Math.floor(Date.now() / 1000) } = {}) {
157
+ if (!identity || !identity.kind || !identity.value) throw sdkError("identity_required");
158
+ const privateKey = loadPrivateKey({ keyPath, privateKeyPem });
159
+ const publicKey = crypto.createPublicKey(privateKey);
160
+ const self = {
161
+ identity: { kind: String(identity.kind), value: String(identity.value) },
162
+ keyId: keyId(publicKey),
163
+ publicKey: (format = "compact") => exportPublicKey(publicKey, format),
164
+
165
+ // Step 1 of the handshake is the owner's; the counterparty's part is to
166
+ // check who is asking, then sign exactly what the owner published.
167
+ answerChallenge(challenge, { ownerPublicKey, signPayload } = {}) {
168
+ if (!challenge || !VALID_CHALLENGE_SCHEMAS.has(challenge.schema)) throw sdkError("challenge_schema_mismatch");
169
+ if (challenge.counterparty_key_id !== self.keyId) throw sdkError("challenge_key_mismatch");
170
+ const id = challenge.identity || {};
171
+ if (String(id.kind) !== self.identity.kind.toLowerCase() || String(id.value) !== self.identity.value) throw sdkError("challenge_identity_mismatch");
172
+ const at = now();
173
+ if (!(at <= Number(challenge.expires_at))) throw sdkError("challenge_expired");
174
+ if (ownerPublicKey) verifyOwnerSignature(challenge, ownerPublicKey);
175
+ if (!signPayload) throw sdkError("sign_payload_required");
176
+ return { handshake_id: challenge.handshake_id, signature: b64url(crypto.sign(null, b64urlDecode(signPayload), privateKey)) };
177
+ },
178
+
179
+ buildEnvelope: (kind, payload, options = {}) => buildEnvelope(self.identity, kind, payload, { ...options, now: options.now ?? now() }),
180
+ signEnvelope: (envelope) => signEnvelope(envelope, privateKey),
181
+ envelope: (kind, payload, options = {}) => signEnvelope(buildEnvelope(self.identity, kind, payload, { ...options, now: options.now ?? now() }), privateKey),
182
+
183
+ // Full flow when a transport to the owner's routes is available.
184
+ async handshake(transport) {
185
+ const opened = await transport.post("/api/site_agents/v1/inbox/kya/challenge", { identity: self.identity, public_key: self.publicKey("pem") });
186
+ const answer = self.answerChallenge(opened.challenge, { ownerPublicKey: opened.owner_public_key, signPayload: opened.sign_payload });
187
+ return transport.post("/api/site_agents/v1/inbox/kya/complete", answer);
188
+ },
189
+ async submit(transport, kind, payload, { idempotencyKey, minimumTier = "G1", ttlSeconds } = {}) {
190
+ if (!idempotencyKey) throw sdkError("idempotency_key_required");
191
+ const envelope = self.envelope(kind, payload, { ttlSeconds });
192
+ return transport.post("/api/site_agents/v1/inbox/submit", { envelope, idempotency_key: idempotencyKey, minimum_tier: minimumTier });
193
+ },
194
+ };
195
+ return self;
196
+ }
197
+
198
+ // A transport that speaks to the owner's routes with the owner's access
199
+ // token. Only meaningful when the same operator runs both ends.
200
+ function createRelayTransport({ serviceOrigin, accessToken, fetch: fetcher = globalThis.fetch } = {}) {
201
+ const origin = String(serviceOrigin || "").replace(/\/+$/, "");
202
+ if (!origin) throw sdkError("service_origin_required");
203
+ return {
204
+ async post(pathname, body) {
205
+ const token = typeof accessToken === "function" ? await accessToken() : accessToken;
206
+ const response = await fetcher(origin + pathname, {
207
+ method: "POST",
208
+ headers: { "content-type": "application/json", authorization: "Bearer " + token },
209
+ body: JSON.stringify(body),
210
+ });
211
+ const payload = await response.json();
212
+ if (!response.ok || payload.ok === false) {
213
+ const error = sdkError(payload && payload.error ? payload.error : `inbox_http_${response.status}`);
214
+ error.verdict = payload && payload.verdict;
215
+ throw error;
216
+ }
217
+ return payload;
218
+ },
219
+ };
220
+ }
221
+
222
+ module.exports = {
223
+ createCounterpartyAgent,
224
+ createRelayTransport,
225
+ buildEnvelope,
226
+ signEnvelope,
227
+ envelopeDigest,
228
+ verifyOwnerSignature,
229
+ loadPublicKey,
230
+ exportPublicKey,
231
+ keyId,
232
+ canonicalJson,
233
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@twin3-ai/agent-id",
3
- "version": "0.3.48",
3
+ "version": "0.3.49",
4
4
  "description": "Domain-bound AgentX portable alias, AEO evidence, and website-Agent integration SDK.",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
@@ -20,7 +20,7 @@
20
20
  "npm": ">=10"
21
21
  },
22
22
  "scripts": {
23
- "prepublishOnly": "node --check bin/agent-id.js && node --check bin/agent-id-cloudflare-a1.js && node --check bin/agent-id-b1-sync.js && node --check installer.js && node --check enterprise-identity.js && node --check domain-proof.js && node --check release-verifier.js && node --check production-preflight.js && node --check static-edge-adapter.js && node --check edge-html-injection.js && node --check optimization-loop.js && node --check artifact-bundle.js && node --check cloudflare-a1-sync.js && node --check b1-sync.js"
23
+ "prepublishOnly": "node --check bin/agent-id.js && node --check bin/agent-id-cloudflare-a1.js && node --check bin/agent-id-b1-sync.js && node --check installer.js && node --check enterprise-identity.js && node --check inbox-counterparty.js && node --check domain-proof.js && node --check release-verifier.js && node --check production-preflight.js && node --check static-edge-adapter.js && node --check edge-html-injection.js && node --check optimization-loop.js && node --check artifact-bundle.js && node --check cloudflare-a1-sync.js && node --check b1-sync.js"
24
24
  },
25
25
  "main": "site-agent.js",
26
26
  "exports": {
@@ -31,6 +31,7 @@
31
31
  "./sync-service": "./sync-service.js",
32
32
  "./installer": "./installer.js",
33
33
  "./enterprise-identity": "./enterprise-identity.js",
34
+ "./inbox-counterparty": "./inbox-counterparty.js",
34
35
  "./domain-proof": "./domain-proof.js",
35
36
  "./local-policy": "./local-policy.js",
36
37
  "./task-executor": "./task-executor.js",