@vallum/registry 0.0.0-prerelease
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 +36 -0
- package/dist/a2aCard.d.ts +149 -0
- package/dist/a2aCard.js +387 -0
- package/dist/a2aDiscoveryBundle.d.ts +53 -0
- package/dist/a2aDiscoveryBundle.js +326 -0
- package/dist/a2aJwks.d.ts +37 -0
- package/dist/a2aJwks.js +102 -0
- package/dist/a2aWellKnown.d.ts +28 -0
- package/dist/a2aWellKnown.js +58 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +9 -0
- package/dist/iotaIdentityAdapter.d.ts +92 -0
- package/dist/iotaIdentityAdapter.js +329 -0
- package/dist/iotaNamesAdapter.d.ts +49 -0
- package/dist/iotaNamesAdapter.js +188 -0
- package/dist/profileSchema.d.ts +76 -0
- package/dist/profileSchema.js +187 -0
- package/dist/resolveAgent.d.ts +26 -0
- package/dist/resolveAgent.js +63 -0
- package/package.json +32 -0
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import { isIP } from "node:net";
|
|
2
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { A2A_AGENT_CARD_WELL_KNOWN_PATH, } from "./a2aCard.js";
|
|
5
|
+
import { A2A_JWKS_WELL_KNOWN_PATH, } from "./a2aJwks.js";
|
|
6
|
+
export function createA2APublicDiscoveryBundle(options) {
|
|
7
|
+
const publicBaseUrl = publicHttpsUrl(options.publicBaseUrl, "A2A public base URL");
|
|
8
|
+
const publicJwksUrl = publicHttpsUrl(options.publicJwksUrl, "A2A public JWKS URL");
|
|
9
|
+
const agentCard = sanitizeAgentCard(options.agentCard, publicBaseUrl, publicJwksUrl);
|
|
10
|
+
const jwks = sanitizeJwks(options.jwks);
|
|
11
|
+
const signingKeyIds = signatureKeyIds(agentCard.signatures);
|
|
12
|
+
const jwksKeyIds = new Set(jwks.body.keys.map((key) => key.kid).filter((kid) => typeof kid === "string"));
|
|
13
|
+
for (const keyId of signingKeyIds) {
|
|
14
|
+
if (!jwksKeyIds.has(keyId)) {
|
|
15
|
+
throw new Error("A2A public discovery bundle signing key is missing from JWKS.");
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
const cacheControl = options.cacheControl ?? "no-store";
|
|
19
|
+
return {
|
|
20
|
+
publicBaseUrl,
|
|
21
|
+
publicJwksUrl,
|
|
22
|
+
files: [
|
|
23
|
+
{
|
|
24
|
+
path: A2A_AGENT_CARD_WELL_KNOWN_PATH,
|
|
25
|
+
headers: {
|
|
26
|
+
"content-type": "application/a2a+json; charset=utf-8",
|
|
27
|
+
"cache-control": cacheControl,
|
|
28
|
+
},
|
|
29
|
+
json: `${JSON.stringify(agentCard, null, 2)}\n`,
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
path: A2A_JWKS_WELL_KNOWN_PATH,
|
|
33
|
+
headers: {
|
|
34
|
+
"content-type": "application/jwk-set+json; charset=utf-8",
|
|
35
|
+
"cache-control": cacheControl,
|
|
36
|
+
},
|
|
37
|
+
json: jwks.json.endsWith("\n") ? jwks.json : `${jwks.json}\n`,
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export async function writeA2APublicDiscoveryBundle(options) {
|
|
43
|
+
const outDir = options.outDir.trim();
|
|
44
|
+
if (outDir === "")
|
|
45
|
+
throw new Error("A2A public discovery bundle output directory is required.");
|
|
46
|
+
const expectedPaths = new Set([A2A_AGENT_CARD_WELL_KNOWN_PATH, A2A_JWKS_WELL_KNOWN_PATH]);
|
|
47
|
+
const seenPaths = new Set();
|
|
48
|
+
const writtenFiles = [];
|
|
49
|
+
for (const file of options.bundle.files) {
|
|
50
|
+
if (!expectedPaths.has(file.path) || seenPaths.has(file.path)) {
|
|
51
|
+
throw new Error("A2A public discovery bundle contains an unexpected static file path.");
|
|
52
|
+
}
|
|
53
|
+
seenPaths.add(file.path);
|
|
54
|
+
const absolutePath = join(outDir, file.path.slice(1));
|
|
55
|
+
await mkdir(dirname(absolutePath), { recursive: true });
|
|
56
|
+
await writeFile(absolutePath, file.json, { encoding: "utf8", mode: 0o644 });
|
|
57
|
+
writtenFiles.push({
|
|
58
|
+
path: absolutePath,
|
|
59
|
+
sourcePath: file.path,
|
|
60
|
+
headers: { ...file.headers },
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
if (seenPaths.size !== expectedPaths.size) {
|
|
64
|
+
throw new Error("A2A public discovery bundle is missing required static files.");
|
|
65
|
+
}
|
|
66
|
+
const manifestPath = join(outDir, options.manifestFileName ?? "a2a-discovery-bundle-manifest.json");
|
|
67
|
+
await writeFile(manifestPath, `${JSON.stringify({
|
|
68
|
+
schemaVersion: 1,
|
|
69
|
+
kind: "vallum.a2a-static-discovery-bundle",
|
|
70
|
+
publicBaseUrl: options.bundle.publicBaseUrl,
|
|
71
|
+
publicJwksUrl: options.bundle.publicJwksUrl,
|
|
72
|
+
files: writtenFiles.map((file) => ({
|
|
73
|
+
path: file.sourcePath,
|
|
74
|
+
headers: file.headers,
|
|
75
|
+
})),
|
|
76
|
+
}, null, 2)}\n`, { encoding: "utf8", mode: 0o644 });
|
|
77
|
+
return {
|
|
78
|
+
outDir,
|
|
79
|
+
publicBaseUrl: options.bundle.publicBaseUrl,
|
|
80
|
+
publicJwksUrl: options.bundle.publicJwksUrl,
|
|
81
|
+
files: writtenFiles,
|
|
82
|
+
manifestPath,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
export async function validateA2APublicDiscoveryBundleArtifacts(options) {
|
|
86
|
+
const outDir = options.outDir.trim();
|
|
87
|
+
if (outDir === "")
|
|
88
|
+
throw new Error("A2A public discovery bundle output directory is required.");
|
|
89
|
+
const manifestPath = join(outDir, options.manifestFileName ?? "a2a-discovery-bundle-manifest.json");
|
|
90
|
+
const manifest = parseManifest(await readFile(manifestPath, "utf8"));
|
|
91
|
+
if (manifest.schemaVersion !== 1 || manifest.kind !== "vallum.a2a-static-discovery-bundle") {
|
|
92
|
+
throw new Error("A2A public discovery bundle manifest is invalid.");
|
|
93
|
+
}
|
|
94
|
+
if (typeof manifest.publicBaseUrl !== "string" || typeof manifest.publicJwksUrl !== "string") {
|
|
95
|
+
throw new Error("A2A public discovery bundle manifest is missing public URLs.");
|
|
96
|
+
}
|
|
97
|
+
if (options.expectedPublicBaseUrl && publicHttpsUrl(options.expectedPublicBaseUrl, "A2A expected public base URL") !== manifest.publicBaseUrl) {
|
|
98
|
+
throw new Error("A2A public discovery bundle public base URL does not match the expected value.");
|
|
99
|
+
}
|
|
100
|
+
if (options.expectedPublicJwksUrl && publicHttpsUrl(options.expectedPublicJwksUrl, "A2A expected public JWKS URL") !== manifest.publicJwksUrl) {
|
|
101
|
+
throw new Error("A2A public discovery bundle public JWKS URL does not match the expected value.");
|
|
102
|
+
}
|
|
103
|
+
const manifestFiles = parseManifestFiles(manifest.files);
|
|
104
|
+
const agentCardJson = await readFile(join(outDir, A2A_AGENT_CARD_WELL_KNOWN_PATH.slice(1)), "utf8");
|
|
105
|
+
const jwksJson = await readFile(join(outDir, A2A_JWKS_WELL_KNOWN_PATH.slice(1)), "utf8");
|
|
106
|
+
const agentCard = JSON.parse(agentCardJson);
|
|
107
|
+
const jwksBody = JSON.parse(jwksJson);
|
|
108
|
+
const jwksHeaders = manifestFiles.find((file) => file.sourcePath === A2A_JWKS_WELL_KNOWN_PATH)?.headers ?? {};
|
|
109
|
+
const bundle = createA2APublicDiscoveryBundle({
|
|
110
|
+
agentCard,
|
|
111
|
+
jwks: {
|
|
112
|
+
path: A2A_JWKS_WELL_KNOWN_PATH,
|
|
113
|
+
status: 200,
|
|
114
|
+
headers: jwksHeaders,
|
|
115
|
+
body: jwksBody,
|
|
116
|
+
json: jwksJson,
|
|
117
|
+
},
|
|
118
|
+
publicBaseUrl: manifest.publicBaseUrl,
|
|
119
|
+
publicJwksUrl: manifest.publicJwksUrl,
|
|
120
|
+
cacheControl: sharedCacheControl(manifestFiles),
|
|
121
|
+
});
|
|
122
|
+
for (const file of bundle.files) {
|
|
123
|
+
const manifestFile = manifestFiles.find((candidate) => candidate.sourcePath === file.path);
|
|
124
|
+
if (!manifestFile) {
|
|
125
|
+
throw new Error("A2A public discovery bundle manifest is missing required static file metadata.");
|
|
126
|
+
}
|
|
127
|
+
assertContentType(file.path, manifestFile.headers);
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
outDir,
|
|
131
|
+
publicBaseUrl: bundle.publicBaseUrl,
|
|
132
|
+
publicJwksUrl: bundle.publicJwksUrl,
|
|
133
|
+
files: manifestFiles.map((file) => ({
|
|
134
|
+
path: join(outDir, file.sourcePath.slice(1)),
|
|
135
|
+
sourcePath: file.sourcePath,
|
|
136
|
+
headers: file.headers,
|
|
137
|
+
})),
|
|
138
|
+
manifestPath,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
function sanitizeAgentCard(card, publicBaseUrl, publicJwksUrl) {
|
|
142
|
+
if (containsSecretLikeField(card)) {
|
|
143
|
+
throw new Error("A2A public discovery bundle must not contain private Agent Card fields.");
|
|
144
|
+
}
|
|
145
|
+
if (!card.signatures || card.signatures.length === 0) {
|
|
146
|
+
throw new Error("A2A public discovery bundle requires a signed Agent Card.");
|
|
147
|
+
}
|
|
148
|
+
if (!card.supportedInterfaces.some((entry) => entry.url === publicBaseUrl && entry.protocolBinding === "HTTP+JSON")) {
|
|
149
|
+
throw new Error("A2A public discovery bundle Agent Card does not match the public base URL.");
|
|
150
|
+
}
|
|
151
|
+
for (const signature of card.signatures) {
|
|
152
|
+
const protectedHeader = protectedHeaderJson(signature);
|
|
153
|
+
if (protectedHeader.jku !== publicJwksUrl) {
|
|
154
|
+
throw new Error("A2A public discovery bundle Agent Card signature JWKS URL does not match.");
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return JSON.parse(JSON.stringify(card));
|
|
158
|
+
}
|
|
159
|
+
function parseManifest(json) {
|
|
160
|
+
const parsed = JSON.parse(json);
|
|
161
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
162
|
+
throw new Error("A2A public discovery bundle manifest is invalid.");
|
|
163
|
+
}
|
|
164
|
+
if (containsSecretLikeField(parsed)) {
|
|
165
|
+
throw new Error("A2A public discovery bundle manifest must not contain private fields.");
|
|
166
|
+
}
|
|
167
|
+
return parsed;
|
|
168
|
+
}
|
|
169
|
+
function parseManifestFiles(value) {
|
|
170
|
+
if (!Array.isArray(value)) {
|
|
171
|
+
throw new Error("A2A public discovery bundle manifest files are invalid.");
|
|
172
|
+
}
|
|
173
|
+
const expectedPaths = new Set([A2A_AGENT_CARD_WELL_KNOWN_PATH, A2A_JWKS_WELL_KNOWN_PATH]);
|
|
174
|
+
const seenPaths = new Set();
|
|
175
|
+
const files = [];
|
|
176
|
+
for (const entry of value) {
|
|
177
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
178
|
+
throw new Error("A2A public discovery bundle manifest file entry is invalid.");
|
|
179
|
+
}
|
|
180
|
+
const path = entry.path;
|
|
181
|
+
const headers = entry.headers;
|
|
182
|
+
if (typeof path !== "string" || !expectedPaths.has(path) || seenPaths.has(path)) {
|
|
183
|
+
throw new Error("A2A public discovery bundle manifest contains an unexpected static file path.");
|
|
184
|
+
}
|
|
185
|
+
if (!headers || typeof headers !== "object" || Array.isArray(headers) || containsSecretLikeField(headers)) {
|
|
186
|
+
throw new Error("A2A public discovery bundle manifest headers are invalid.");
|
|
187
|
+
}
|
|
188
|
+
if (!Object.entries(headers).every(([key, nested]) => typeof key === "string" && typeof nested === "string")) {
|
|
189
|
+
throw new Error("A2A public discovery bundle manifest headers are invalid.");
|
|
190
|
+
}
|
|
191
|
+
seenPaths.add(path);
|
|
192
|
+
files.push({
|
|
193
|
+
path,
|
|
194
|
+
sourcePath: path,
|
|
195
|
+
headers: { ...headers },
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
if (seenPaths.size !== expectedPaths.size) {
|
|
199
|
+
throw new Error("A2A public discovery bundle manifest is missing required static file metadata.");
|
|
200
|
+
}
|
|
201
|
+
return files;
|
|
202
|
+
}
|
|
203
|
+
function assertContentType(path, headers) {
|
|
204
|
+
const contentType = headers["content-type"];
|
|
205
|
+
if (path === A2A_AGENT_CARD_WELL_KNOWN_PATH && typeof contentType === "string" && contentType.includes("application/a2a+json"))
|
|
206
|
+
return;
|
|
207
|
+
if (path === A2A_JWKS_WELL_KNOWN_PATH && typeof contentType === "string" && contentType.includes("application/jwk-set+json"))
|
|
208
|
+
return;
|
|
209
|
+
throw new Error("A2A public discovery bundle manifest has invalid content-type metadata.");
|
|
210
|
+
}
|
|
211
|
+
function sharedCacheControl(files) {
|
|
212
|
+
const values = files.map((file) => file.headers["cache-control"]).filter((value) => typeof value === "string" && value.trim() !== "");
|
|
213
|
+
if (values.length === 0)
|
|
214
|
+
return undefined;
|
|
215
|
+
const [first] = values;
|
|
216
|
+
return values.every((value) => value === first) ? first : undefined;
|
|
217
|
+
}
|
|
218
|
+
function sanitizeJwks(jwks) {
|
|
219
|
+
if (jwks.path !== A2A_JWKS_WELL_KNOWN_PATH) {
|
|
220
|
+
throw new Error("A2A public discovery bundle JWKS path must be canonical.");
|
|
221
|
+
}
|
|
222
|
+
if (jwks.status !== 200 || !Array.isArray(jwks.body.keys) || jwks.body.keys.length === 0) {
|
|
223
|
+
throw new Error("A2A public discovery bundle JWKS is invalid.");
|
|
224
|
+
}
|
|
225
|
+
if (containsSecretLikeField(jwks.body)) {
|
|
226
|
+
throw new Error("A2A public discovery bundle JWKS must not contain private key material.");
|
|
227
|
+
}
|
|
228
|
+
return jwks;
|
|
229
|
+
}
|
|
230
|
+
function signatureKeyIds(signatures) {
|
|
231
|
+
if (!signatures || signatures.length === 0) {
|
|
232
|
+
throw new Error("A2A public discovery bundle requires a signed Agent Card.");
|
|
233
|
+
}
|
|
234
|
+
return signatures.map((signature) => {
|
|
235
|
+
const kid = protectedHeaderJson(signature).kid;
|
|
236
|
+
if (typeof kid !== "string" || kid.trim() === "") {
|
|
237
|
+
throw new Error("A2A public discovery bundle signature key id is invalid.");
|
|
238
|
+
}
|
|
239
|
+
return kid;
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
function protectedHeaderJson(signature) {
|
|
243
|
+
try {
|
|
244
|
+
const parsed = JSON.parse(Buffer.from(signature.protected, "base64url").toString("utf8"));
|
|
245
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed))
|
|
246
|
+
return parsed;
|
|
247
|
+
}
|
|
248
|
+
catch {
|
|
249
|
+
// handled below
|
|
250
|
+
}
|
|
251
|
+
throw new Error("A2A public discovery bundle signature is malformed.");
|
|
252
|
+
}
|
|
253
|
+
function publicHttpsUrl(value, label) {
|
|
254
|
+
try {
|
|
255
|
+
const url = new URL(value);
|
|
256
|
+
if (url.protocol === "https:"
|
|
257
|
+
&& url.hostname
|
|
258
|
+
&& !url.username
|
|
259
|
+
&& !url.password
|
|
260
|
+
&& !url.search
|
|
261
|
+
&& !url.hash
|
|
262
|
+
&& !isUnsafePublicHost(url.hostname))
|
|
263
|
+
return url.toString();
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
// handled below
|
|
267
|
+
}
|
|
268
|
+
throw new Error(`${label} must be public HTTPS without credentials, query strings, or fragments.`);
|
|
269
|
+
}
|
|
270
|
+
function containsSecretLikeField(value) {
|
|
271
|
+
const stack = [value];
|
|
272
|
+
while (stack.length > 0) {
|
|
273
|
+
const current = stack.pop();
|
|
274
|
+
if (!current || typeof current !== "object")
|
|
275
|
+
continue;
|
|
276
|
+
for (const [key, nested] of Object.entries(current)) {
|
|
277
|
+
const normalized = key.toLowerCase();
|
|
278
|
+
if (normalized.includes("private")
|
|
279
|
+
|| normalized.includes("secret")
|
|
280
|
+
|| normalized.includes("token")
|
|
281
|
+
|| normalized.includes("credential")
|
|
282
|
+
|| normalized.includes("signer")
|
|
283
|
+
|| normalized.includes("mnemonic")
|
|
284
|
+
|| normalized.includes("seed")
|
|
285
|
+
|| normalized === "d"
|
|
286
|
+
|| normalized === "p"
|
|
287
|
+
|| normalized === "q"
|
|
288
|
+
|| normalized === "dp"
|
|
289
|
+
|| normalized === "dq"
|
|
290
|
+
|| normalized === "qi"
|
|
291
|
+
|| normalized === "oth") {
|
|
292
|
+
return true;
|
|
293
|
+
}
|
|
294
|
+
if (nested && typeof nested === "object")
|
|
295
|
+
stack.push(nested);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
function isUnsafePublicHost(hostname) {
|
|
301
|
+
const normalized = hostname.trim().toLowerCase();
|
|
302
|
+
if (normalized === "localhost" || normalized.endsWith(".localhost") || normalized.endsWith(".local"))
|
|
303
|
+
return true;
|
|
304
|
+
const ipVersion = isIP(normalized);
|
|
305
|
+
if (ipVersion === 4) {
|
|
306
|
+
const [first = 0, second = 0] = normalized.split(".").map((octet) => Number.parseInt(octet, 10));
|
|
307
|
+
return first === 0
|
|
308
|
+
|| first === 10
|
|
309
|
+
|| first === 127
|
|
310
|
+
|| (first === 100 && second >= 64 && second <= 127)
|
|
311
|
+
|| (first === 169 && second === 254)
|
|
312
|
+
|| (first === 172 && second >= 16 && second <= 31)
|
|
313
|
+
|| (first === 192 && second === 168)
|
|
314
|
+
|| (first === 198 && (second === 18 || second === 19))
|
|
315
|
+
|| first >= 224;
|
|
316
|
+
}
|
|
317
|
+
if (ipVersion === 6) {
|
|
318
|
+
return normalized === "::"
|
|
319
|
+
|| normalized === "::1"
|
|
320
|
+
|| normalized.startsWith("fc")
|
|
321
|
+
|| normalized.startsWith("fd")
|
|
322
|
+
|| normalized.startsWith("fe80:");
|
|
323
|
+
}
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
326
|
+
//# sourceMappingURL=a2aDiscoveryBundle.js.map
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { JsonWebKey, KeyObject } from "node:crypto";
|
|
2
|
+
export declare const A2A_JWKS_WELL_KNOWN_PATH: "/.well-known/jwks.json";
|
|
3
|
+
export declare const A2A_JWKS_MEDIA_TYPE: "application/jwk-set+json";
|
|
4
|
+
export interface A2APublicJwksKeyInput {
|
|
5
|
+
readonly keyId: string;
|
|
6
|
+
readonly publicKey?: KeyObject;
|
|
7
|
+
readonly jwk?: JsonWebKey;
|
|
8
|
+
}
|
|
9
|
+
export interface A2APublicJwksOptions {
|
|
10
|
+
readonly keys: readonly A2APublicJwksKeyInput[];
|
|
11
|
+
readonly cacheControl?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface A2APublicJwksResponse {
|
|
14
|
+
readonly path: typeof A2A_JWKS_WELL_KNOWN_PATH;
|
|
15
|
+
readonly status: 200;
|
|
16
|
+
readonly headers: Record<string, string>;
|
|
17
|
+
readonly body: {
|
|
18
|
+
readonly keys: readonly JsonWebKey[];
|
|
19
|
+
};
|
|
20
|
+
readonly json: string;
|
|
21
|
+
}
|
|
22
|
+
export type A2APublicJwksErrorCode = "A2A_JWKS_NOT_FOUND" | "A2A_JWKS_METHOD_NOT_ALLOWED" | "A2A_JWKS_UNAVAILABLE";
|
|
23
|
+
export interface A2APublicJwksErrorResponse {
|
|
24
|
+
readonly path: string;
|
|
25
|
+
readonly status: 404 | 405;
|
|
26
|
+
readonly headers: Record<string, string>;
|
|
27
|
+
readonly body?: undefined;
|
|
28
|
+
readonly json: string;
|
|
29
|
+
}
|
|
30
|
+
export type A2APublicJwksHandlerResponse = A2APublicJwksResponse | A2APublicJwksErrorResponse;
|
|
31
|
+
export interface A2APublicJwksRequest {
|
|
32
|
+
readonly method?: string;
|
|
33
|
+
readonly path?: string;
|
|
34
|
+
}
|
|
35
|
+
export declare function createA2APublicJwksResponse(options: A2APublicJwksOptions): A2APublicJwksResponse;
|
|
36
|
+
export declare function handleA2APublicJwksRequest(request: A2APublicJwksRequest, options: A2APublicJwksOptions): A2APublicJwksHandlerResponse;
|
|
37
|
+
//# sourceMappingURL=a2aJwks.d.ts.map
|
package/dist/a2aJwks.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
export const A2A_JWKS_WELL_KNOWN_PATH = "/.well-known/jwks.json";
|
|
2
|
+
export const A2A_JWKS_MEDIA_TYPE = "application/jwk-set+json";
|
|
3
|
+
const PRIVATE_JWK_FIELDS = new Set(["d", "p", "q", "dp", "dq", "qi", "oth"]);
|
|
4
|
+
export function createA2APublicJwksResponse(options) {
|
|
5
|
+
if (options.keys.length === 0) {
|
|
6
|
+
throw new Error("A2A JWKS requires at least one public key.");
|
|
7
|
+
}
|
|
8
|
+
const keys = options.keys.map(sanitizePublicJwk);
|
|
9
|
+
const body = { keys };
|
|
10
|
+
return {
|
|
11
|
+
path: A2A_JWKS_WELL_KNOWN_PATH,
|
|
12
|
+
status: 200,
|
|
13
|
+
headers: successHeaders(options.cacheControl),
|
|
14
|
+
body,
|
|
15
|
+
json: `${JSON.stringify(body, null, 2)}\n`,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export function handleA2APublicJwksRequest(request, options) {
|
|
19
|
+
const path = normalizePath(request.path ?? A2A_JWKS_WELL_KNOWN_PATH);
|
|
20
|
+
if (path !== A2A_JWKS_WELL_KNOWN_PATH) {
|
|
21
|
+
return errorResponse(path, 404, "A2A_JWKS_NOT_FOUND", "A2A JWKS is not served at this path.");
|
|
22
|
+
}
|
|
23
|
+
const method = (request.method ?? "GET").trim().toUpperCase();
|
|
24
|
+
if (method !== "GET") {
|
|
25
|
+
return errorResponse(path, 405, "A2A_JWKS_METHOD_NOT_ALLOWED", "A2A JWKS only supports GET.", { allow: "GET" });
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
return createA2APublicJwksResponse(options);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return errorResponse(path, 404, "A2A_JWKS_UNAVAILABLE", "A2A JWKS is unavailable.");
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function sanitizePublicJwk(input) {
|
|
35
|
+
const keyId = input.keyId.trim();
|
|
36
|
+
if (keyId === "")
|
|
37
|
+
throw new Error("A2A JWKS key id is required.");
|
|
38
|
+
if (input.publicKey && input.jwk) {
|
|
39
|
+
throw new Error("A2A JWKS key input must provide either publicKey or jwk.");
|
|
40
|
+
}
|
|
41
|
+
if (!input.publicKey && !input.jwk) {
|
|
42
|
+
throw new Error("A2A JWKS key input requires public key material.");
|
|
43
|
+
}
|
|
44
|
+
if (input.publicKey && input.publicKey.type !== "public") {
|
|
45
|
+
throw new Error("A2A JWKS requires public key material.");
|
|
46
|
+
}
|
|
47
|
+
const jwk = input.publicKey
|
|
48
|
+
? input.publicKey.export({ format: "jwk" })
|
|
49
|
+
: { ...input.jwk };
|
|
50
|
+
assertPublicJwk(jwk);
|
|
51
|
+
return {
|
|
52
|
+
...jwk,
|
|
53
|
+
kid: keyId,
|
|
54
|
+
use: "sig",
|
|
55
|
+
alg: "EdDSA",
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
function assertPublicJwk(jwk) {
|
|
59
|
+
if (typeof jwk.kty !== "string" || jwk.kty.trim() === "") {
|
|
60
|
+
throw new Error("A2A JWKS key is missing public key metadata.");
|
|
61
|
+
}
|
|
62
|
+
for (const field of Object.keys(jwk)) {
|
|
63
|
+
if (field in jwk) {
|
|
64
|
+
const normalized = field.toLowerCase();
|
|
65
|
+
if (PRIVATE_JWK_FIELDS.has(field)
|
|
66
|
+
|| normalized.includes("private")
|
|
67
|
+
|| normalized.includes("secret")
|
|
68
|
+
|| normalized.includes("token")
|
|
69
|
+
|| normalized.includes("mnemonic")
|
|
70
|
+
|| normalized.includes("seed")) {
|
|
71
|
+
throw new Error("A2A JWKS must not contain private key material.");
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function successHeaders(cacheControl = "no-store") {
|
|
77
|
+
return {
|
|
78
|
+
"content-type": `${A2A_JWKS_MEDIA_TYPE}; charset=utf-8`,
|
|
79
|
+
"cache-control": cacheControl,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function errorResponse(path, status, code, message, headers = {}) {
|
|
83
|
+
return {
|
|
84
|
+
path,
|
|
85
|
+
status,
|
|
86
|
+
headers: {
|
|
87
|
+
"content-type": "application/json; charset=utf-8",
|
|
88
|
+
"cache-control": "no-store",
|
|
89
|
+
...headers,
|
|
90
|
+
},
|
|
91
|
+
json: `${JSON.stringify({ error: { code, message } })}\n`,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function normalizePath(path) {
|
|
95
|
+
try {
|
|
96
|
+
return new URL(path, "https://agent.local").pathname;
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return path;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=a2aJwks.js.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { A2A_AGENT_CARD_WELL_KNOWN_PATH, type A2AAgentCard, type A2AAgentCardErrorCode, type CreateA2AAgentCardOptions } from "./a2aCard.js";
|
|
2
|
+
export declare const A2A_AGENT_CARD_MEDIA_TYPE: "application/a2a+json";
|
|
3
|
+
export type A2AWellKnownErrorCode = A2AAgentCardErrorCode | "A2A_WELL_KNOWN_NOT_FOUND" | "A2A_WELL_KNOWN_METHOD_NOT_ALLOWED" | "A2A_AGENT_CARD_UNAVAILABLE";
|
|
4
|
+
export interface A2AAgentCardWellKnownResponse {
|
|
5
|
+
readonly path: typeof A2A_AGENT_CARD_WELL_KNOWN_PATH;
|
|
6
|
+
readonly status: 200;
|
|
7
|
+
readonly headers: Record<string, string>;
|
|
8
|
+
readonly body: A2AAgentCard;
|
|
9
|
+
readonly json: string;
|
|
10
|
+
}
|
|
11
|
+
export interface A2AWellKnownErrorResponse {
|
|
12
|
+
readonly path: string;
|
|
13
|
+
readonly status: 404 | 405 | 410;
|
|
14
|
+
readonly headers: Record<string, string>;
|
|
15
|
+
readonly body?: undefined;
|
|
16
|
+
readonly json: string;
|
|
17
|
+
}
|
|
18
|
+
export type A2AWellKnownResponse = A2AAgentCardWellKnownResponse | A2AWellKnownErrorResponse;
|
|
19
|
+
export interface A2AWellKnownRequest {
|
|
20
|
+
readonly method?: string;
|
|
21
|
+
readonly path?: string;
|
|
22
|
+
}
|
|
23
|
+
export interface A2AAgentCardWellKnownOptions extends CreateA2AAgentCardOptions {
|
|
24
|
+
readonly cacheControl?: string;
|
|
25
|
+
}
|
|
26
|
+
export declare function createA2AAgentCardWellKnownResponse(profile: unknown, options?: A2AAgentCardWellKnownOptions): A2AAgentCardWellKnownResponse;
|
|
27
|
+
export declare function handleA2AAgentCardWellKnownRequest(request: A2AWellKnownRequest, profile: unknown, options?: A2AAgentCardWellKnownOptions): A2AWellKnownResponse;
|
|
28
|
+
//# sourceMappingURL=a2aWellKnown.d.ts.map
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { A2A_AGENT_CARD_WELL_KNOWN_PATH, A2AAgentCardError, createA2AAgentCardFromProfile, } from "./a2aCard.js";
|
|
2
|
+
export const A2A_AGENT_CARD_MEDIA_TYPE = "application/a2a+json";
|
|
3
|
+
export function createA2AAgentCardWellKnownResponse(profile, options = {}) {
|
|
4
|
+
const body = createA2AAgentCardFromProfile(profile, options);
|
|
5
|
+
return {
|
|
6
|
+
path: A2A_AGENT_CARD_WELL_KNOWN_PATH,
|
|
7
|
+
status: 200,
|
|
8
|
+
headers: successHeaders(options.cacheControl),
|
|
9
|
+
body,
|
|
10
|
+
json: `${JSON.stringify(body, null, 2)}\n`,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export function handleA2AAgentCardWellKnownRequest(request, profile, options = {}) {
|
|
14
|
+
const path = normalizePath(request.path ?? A2A_AGENT_CARD_WELL_KNOWN_PATH);
|
|
15
|
+
if (path !== A2A_AGENT_CARD_WELL_KNOWN_PATH) {
|
|
16
|
+
return errorResponse(path, 404, "A2A_WELL_KNOWN_NOT_FOUND", "A2A Agent Card is not served at this path.");
|
|
17
|
+
}
|
|
18
|
+
const method = (request.method ?? "GET").trim().toUpperCase();
|
|
19
|
+
if (method !== "GET") {
|
|
20
|
+
return errorResponse(path, 405, "A2A_WELL_KNOWN_METHOD_NOT_ALLOWED", "A2A Agent Card discovery only supports GET.", { allow: "GET" });
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
return createA2AAgentCardWellKnownResponse(profile, options);
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
if (error instanceof A2AAgentCardError) {
|
|
27
|
+
return errorResponse(path, error.code === "PROFILE_REVOKED" || error.code === "PROFILE_EXPIRED" ? 410 : 404, error.code, "A2A Agent Card is unavailable.");
|
|
28
|
+
}
|
|
29
|
+
return errorResponse(path, 404, "A2A_AGENT_CARD_UNAVAILABLE", "A2A Agent Card is unavailable.");
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function successHeaders(cacheControl = "no-store") {
|
|
33
|
+
return {
|
|
34
|
+
"content-type": `${A2A_AGENT_CARD_MEDIA_TYPE}; charset=utf-8`,
|
|
35
|
+
"cache-control": cacheControl,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function errorResponse(path, status, code, message, headers = {}) {
|
|
39
|
+
return {
|
|
40
|
+
path,
|
|
41
|
+
status,
|
|
42
|
+
headers: {
|
|
43
|
+
"content-type": "application/json; charset=utf-8",
|
|
44
|
+
"cache-control": "no-store",
|
|
45
|
+
...headers,
|
|
46
|
+
},
|
|
47
|
+
json: `${JSON.stringify({ error: { code, message } })}\n`,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function normalizePath(path) {
|
|
51
|
+
try {
|
|
52
|
+
return new URL(path, "https://agent.local").pathname;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return path;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=a2aWellKnown.js.map
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export * from "./profileSchema.js";
|
|
2
|
+
export * from "./resolveAgent.js";
|
|
3
|
+
export * from "./iotaIdentityAdapter.js";
|
|
4
|
+
export * from "./iotaNamesAdapter.js";
|
|
5
|
+
export * from "./a2aCard.js";
|
|
6
|
+
export * from "./a2aWellKnown.js";
|
|
7
|
+
export * from "./a2aJwks.js";
|
|
8
|
+
export * from "./a2aDiscoveryBundle.js";
|
|
9
|
+
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export * from "./profileSchema.js";
|
|
2
|
+
export * from "./resolveAgent.js";
|
|
3
|
+
export * from "./iotaIdentityAdapter.js";
|
|
4
|
+
export * from "./iotaNamesAdapter.js";
|
|
5
|
+
export * from "./a2aCard.js";
|
|
6
|
+
export * from "./a2aWellKnown.js";
|
|
7
|
+
export * from "./a2aJwks.js";
|
|
8
|
+
export * from "./a2aDiscoveryBundle.js";
|
|
9
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { AgentProfile } from "./profileSchema.js";
|
|
2
|
+
import type { AgentResolver } from "./resolveAgent.js";
|
|
3
|
+
export type IotaIdentityVerificationErrorCode = "PROFILE_UNVERIFIABLE" | "PROFILE_REVOKED" | "PROFILE_EXPIRED";
|
|
4
|
+
export interface IotaIdentityVerificationError {
|
|
5
|
+
readonly code: IotaIdentityVerificationErrorCode;
|
|
6
|
+
readonly message: string;
|
|
7
|
+
}
|
|
8
|
+
export type IotaIdentityVerificationResult = {
|
|
9
|
+
readonly ok: true;
|
|
10
|
+
readonly agentDidDocument: unknown;
|
|
11
|
+
readonly ownerDidDocument: unknown;
|
|
12
|
+
readonly credentialRefsChecked: readonly string[];
|
|
13
|
+
} | {
|
|
14
|
+
readonly ok: false;
|
|
15
|
+
readonly error: IotaIdentityVerificationError;
|
|
16
|
+
};
|
|
17
|
+
export type IotaIdentityVerificationCacheEntry = Extract<IotaIdentityVerificationResult, {
|
|
18
|
+
ok: true;
|
|
19
|
+
}> & {
|
|
20
|
+
readonly cachedAt: string;
|
|
21
|
+
readonly expiresAt: string;
|
|
22
|
+
};
|
|
23
|
+
export interface IotaIdentityVerificationCache {
|
|
24
|
+
readonly get: (key: string) => IotaIdentityVerificationCacheEntry | undefined;
|
|
25
|
+
readonly set: (key: string, entry: IotaIdentityVerificationCacheEntry) => void;
|
|
26
|
+
readonly delete?: (key: string) => void;
|
|
27
|
+
}
|
|
28
|
+
export declare function createInMemoryIotaIdentityVerificationCache(): IotaIdentityVerificationCache;
|
|
29
|
+
export interface IotaIdentityDidResolver {
|
|
30
|
+
/**
|
|
31
|
+
* Matches the current IOTA Identity read surfaces:
|
|
32
|
+
* IdentityClientReadOnly/IdentityClient expose resolveDid, while Resolver exposes
|
|
33
|
+
* resolve. Keep both dependency-injected so tests do not require live IOTA.
|
|
34
|
+
*/
|
|
35
|
+
readonly resolveDid?: (did: string) => Promise<unknown>;
|
|
36
|
+
readonly resolve?: (did: string) => Promise<unknown>;
|
|
37
|
+
}
|
|
38
|
+
export interface IotaIdentityCredentialValidationContext {
|
|
39
|
+
readonly profile: AgentProfile;
|
|
40
|
+
readonly agentDidDocument: unknown;
|
|
41
|
+
readonly ownerDidDocument: unknown;
|
|
42
|
+
}
|
|
43
|
+
export type IotaIdentityCredentialValidationResult = {
|
|
44
|
+
readonly ok: true;
|
|
45
|
+
readonly evidence?: IotaIdentityCredentialEvidence;
|
|
46
|
+
} | {
|
|
47
|
+
readonly ok: false;
|
|
48
|
+
readonly code: "CREDENTIAL_REVOKED" | "CREDENTIAL_EXPIRED" | "CREDENTIAL_UNVERIFIABLE";
|
|
49
|
+
readonly message: string;
|
|
50
|
+
};
|
|
51
|
+
export interface IotaIdentityCredentialEvidence {
|
|
52
|
+
readonly issuerDid: string;
|
|
53
|
+
readonly verificationMethod: string;
|
|
54
|
+
readonly credentialTypes?: readonly string[];
|
|
55
|
+
readonly credentialStatus?: IotaIdentityCredentialStatusEvidence;
|
|
56
|
+
readonly issuedAt?: string;
|
|
57
|
+
readonly expiresAt?: string;
|
|
58
|
+
}
|
|
59
|
+
export interface IotaIdentityCredentialStatusEvidence {
|
|
60
|
+
readonly id?: string;
|
|
61
|
+
readonly type: string;
|
|
62
|
+
readonly revoked?: boolean;
|
|
63
|
+
}
|
|
64
|
+
export interface IotaIdentityVcTrustPolicy {
|
|
65
|
+
readonly trustedIssuerDids: readonly string[];
|
|
66
|
+
readonly allowedVerificationMethods: readonly string[];
|
|
67
|
+
readonly requiredCredentialTypes?: readonly string[];
|
|
68
|
+
readonly acceptedCredentialStatusTypes?: readonly string[];
|
|
69
|
+
readonly requireCredentialStatus?: boolean;
|
|
70
|
+
readonly maxCredentialAgeMs?: number;
|
|
71
|
+
}
|
|
72
|
+
export interface IotaIdentityCredentialValidator {
|
|
73
|
+
/**
|
|
74
|
+
* Adapter boundary for the current IOTA Identity JWT credential validator.
|
|
75
|
+
* Concrete live implementations should resolve the issuer DID Document and use
|
|
76
|
+
* the official validator; local tests only need this deterministic interface.
|
|
77
|
+
*/
|
|
78
|
+
readonly validateCredentialRef: (credentialRef: string, context: IotaIdentityCredentialValidationContext) => Promise<IotaIdentityCredentialValidationResult>;
|
|
79
|
+
}
|
|
80
|
+
export interface IotaIdentityVerificationOptions {
|
|
81
|
+
readonly didResolver: IotaIdentityDidResolver;
|
|
82
|
+
readonly credentialValidator?: IotaIdentityCredentialValidator;
|
|
83
|
+
readonly trustPolicy?: IotaIdentityVcTrustPolicy;
|
|
84
|
+
readonly verificationCache?: IotaIdentityVerificationCache;
|
|
85
|
+
readonly cacheTtlMs?: number;
|
|
86
|
+
readonly forceRefresh?: boolean;
|
|
87
|
+
readonly now?: () => Date;
|
|
88
|
+
}
|
|
89
|
+
export declare function createIotaIdentityVerifiedResolver(baseResolver: AgentResolver, options: IotaIdentityVerificationOptions): AgentResolver;
|
|
90
|
+
export declare function verifyAgentProfileIdentity(profile: AgentProfile, options: IotaIdentityVerificationOptions): Promise<IotaIdentityVerificationResult>;
|
|
91
|
+
export declare function evaluateIotaIdentityCredentialTrustPolicy(evidence: IotaIdentityCredentialEvidence | undefined, policy: IotaIdentityVcTrustPolicy | undefined, now?: Date): IotaIdentityCredentialValidationResult;
|
|
92
|
+
//# sourceMappingURL=iotaIdentityAdapter.d.ts.map
|