a2acalling 0.6.64 → 0.6.66
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/.a2a-manifest.json +2 -2
- package/CONVENTIONS.md +3 -0
- package/bin/cli.js +77 -9
- package/package.json +1 -1
- package/src/dashboard/public/app.js +220 -96
- package/src/dashboard/public/index.html +102 -71
- package/src/dashboard/public/style.css +33 -0
- package/src/lib/client.js +29 -4
- package/src/lib/config.js +22 -3
- package/src/lib/conversation-driver.js +7 -1
- package/src/lib/crypto.js +113 -0
- package/src/lib/tokens.js +4 -1
- package/src/routes/a2a.js +78 -4
- package/src/routes/dashboard.js +1 -1
- package/src/server.js +3 -0
package/src/routes/a2a.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
const { TokenStore } = require('../lib/tokens');
|
|
12
12
|
const crypto = require('crypto');
|
|
13
13
|
const { createLogger, createTraceId } = require('../lib/logger');
|
|
14
|
+
const { verifySignature, isTimestampValid, fingerprint } = require('../lib/crypto');
|
|
14
15
|
|
|
15
16
|
// Lazy-load conversation store (optional dependency)
|
|
16
17
|
let ConversationStore = null;
|
|
@@ -187,19 +188,75 @@ function createRoutes(options = {}) {
|
|
|
187
188
|
} catch (_) {}
|
|
188
189
|
}
|
|
189
190
|
|
|
191
|
+
// A2A-52: shared signature verification helper for /invoke and /end
|
|
192
|
+
function verifySigHeaders(req, validation, endpoint, reqLogger, withTracePayload) {
|
|
193
|
+
const sigHeader = req.headers['x-a2a-signature'];
|
|
194
|
+
const pubKeyHeader = req.headers['x-a2a-public-key'];
|
|
195
|
+
const tsHeader = req.headers['x-a2a-timestamp'];
|
|
196
|
+
const result = { identityVerified: false, publicKeyFingerprint: null, error: null };
|
|
197
|
+
|
|
198
|
+
if (!sigHeader || !pubKeyHeader || !tsHeader) return result;
|
|
199
|
+
|
|
200
|
+
if (!isTimestampValid(tsHeader)) {
|
|
201
|
+
result.error = { status: 403, body: { success: false, error: 'timestamp_expired', message: 'Request timestamp outside allowed window' } };
|
|
202
|
+
reqLogger.warn('Signature timestamp outside window', { tokenId: validation.id, error_code: 'SIGNATURE_TIMESTAMP_EXPIRED', status_code: 403 });
|
|
203
|
+
return result;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
try {
|
|
207
|
+
crypto.createPublicKey({ key: Buffer.from(pubKeyHeader, 'base64'), format: 'der', type: 'spki' });
|
|
208
|
+
} catch (_) {
|
|
209
|
+
result.error = { status: 400, body: { success: false, error: 'malformed_public_key', message: 'X-A2A-Public-Key is not a valid Ed25519 public key' } };
|
|
210
|
+
reqLogger.warn('Malformed public key', { tokenId: validation.id, error_code: 'MALFORMED_PUBLIC_KEY', status_code: 400 });
|
|
211
|
+
return result;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const existingContact = tokenStore.getContact(validation.id) ||
|
|
215
|
+
(tokenStore.listContacts().find(c => c.linked_token_id === validation.id));
|
|
216
|
+
if (existingContact && existingContact.public_key && existingContact.public_key !== pubKeyHeader) {
|
|
217
|
+
result.error = { status: 403, body: { success: false, error: 'public_key_mismatch', message: 'Public key does not match previously pinned key' } };
|
|
218
|
+
reqLogger.warn('Public key mismatch (TOFU violation)', { tokenId: validation.id, error_code: 'PUBLIC_KEY_MISMATCH', status_code: 403 });
|
|
219
|
+
return result;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const rawBody = JSON.stringify(req.body);
|
|
223
|
+
try {
|
|
224
|
+
const valid = verifySignature({ signature: sigHeader, publicKey: pubKeyHeader, timestamp: tsHeader, method: 'POST', endpoint, body: rawBody });
|
|
225
|
+
if (valid) {
|
|
226
|
+
result.identityVerified = true;
|
|
227
|
+
result.publicKeyFingerprint = fingerprint(pubKeyHeader);
|
|
228
|
+
if (existingContact && !existingContact.public_key) {
|
|
229
|
+
tokenStore.updateContact(existingContact.name || existingContact.id, { public_key: pubKeyHeader });
|
|
230
|
+
}
|
|
231
|
+
} else {
|
|
232
|
+
result.error = { status: 403, body: { success: false, error: 'invalid_signature', message: 'Ed25519 signature verification failed' } };
|
|
233
|
+
reqLogger.warn('Signature verification failed', { tokenId: validation.id, error_code: 'SIGNATURE_INVALID', status_code: 403 });
|
|
234
|
+
}
|
|
235
|
+
} catch (sigErr) {
|
|
236
|
+
result.error = { status: 403, body: { success: false, error: 'invalid_signature', message: 'Signature verification failed' } };
|
|
237
|
+
reqLogger.warn('Signature verification error', { tokenId: validation.id, error_code: 'SIGNATURE_VERIFY_ERROR', status_code: 403, error: sigErr });
|
|
238
|
+
}
|
|
239
|
+
return result;
|
|
240
|
+
}
|
|
241
|
+
|
|
190
242
|
/**
|
|
191
243
|
* GET /status
|
|
192
244
|
* Check if A2A is enabled
|
|
193
245
|
*/
|
|
194
246
|
router.get('/status', (req, res) => {
|
|
195
247
|
const activeCalls = monitor ? monitor.getActiveCount() : 0;
|
|
196
|
-
|
|
248
|
+
const response = {
|
|
197
249
|
a2a: true,
|
|
198
250
|
version: require('../../package.json').version,
|
|
199
251
|
capabilities: ['invoke', 'multi-turn'],
|
|
200
252
|
rate_limits: limits,
|
|
201
253
|
active_calls: activeCalls
|
|
202
|
-
}
|
|
254
|
+
};
|
|
255
|
+
// A2A-52: include agent public key so contacts can fetch it
|
|
256
|
+
if (options.publicKey) {
|
|
257
|
+
response.public_key = options.publicKey;
|
|
258
|
+
}
|
|
259
|
+
res.json(response);
|
|
203
260
|
});
|
|
204
261
|
|
|
205
262
|
/**
|
|
@@ -291,6 +348,14 @@ function createRoutes(options = {}) {
|
|
|
291
348
|
}));
|
|
292
349
|
}
|
|
293
350
|
|
|
351
|
+
// A2A-52: Ed25519 signature verification (after token auth, before message handling)
|
|
352
|
+
const sigCheck = verifySigHeaders(req, validation, '/api/a2a/invoke', reqLogger, withTracePayload);
|
|
353
|
+
if (sigCheck.error) {
|
|
354
|
+
return res.status(sigCheck.error.status).json(withTracePayload(sigCheck.error.body));
|
|
355
|
+
}
|
|
356
|
+
const identityVerified = sigCheck.identityVerified;
|
|
357
|
+
const publicKeyFingerprint = sigCheck.publicKeyFingerprint;
|
|
358
|
+
|
|
294
359
|
// Extract and validate request
|
|
295
360
|
const { message, conversation_id, caller, context, timeout_seconds = 60 } = req.body;
|
|
296
361
|
|
|
@@ -353,7 +418,10 @@ function createRoutes(options = {}) {
|
|
|
353
418
|
caller: sanitizedCaller,
|
|
354
419
|
conversation_id: conversation_id || `conv_${Date.now()}_${crypto.randomBytes(6).toString('hex')}`,
|
|
355
420
|
trace_id: traceId,
|
|
356
|
-
request_id: requestId
|
|
421
|
+
request_id: requestId,
|
|
422
|
+
// A2A-52: cryptographic identity verification status
|
|
423
|
+
identity_verified: identityVerified,
|
|
424
|
+
public_key_fingerprint: publicKeyFingerprint
|
|
357
425
|
};
|
|
358
426
|
|
|
359
427
|
// Ensure inbound caller exists as a contact (best-effort).
|
|
@@ -568,10 +636,16 @@ function createRoutes(options = {}) {
|
|
|
568
636
|
return res.status(401).json(withTracePayload({
|
|
569
637
|
success: false,
|
|
570
638
|
error: 'unauthorized',
|
|
571
|
-
message: 'Invalid or expired token'
|
|
639
|
+
message: 'Invalid or expired token'
|
|
572
640
|
}));
|
|
573
641
|
}
|
|
574
642
|
|
|
643
|
+
// A2A-52: Ed25519 signature verification for /end (same as /invoke)
|
|
644
|
+
const endSigCheck = verifySigHeaders(req, validation, '/api/a2a/end', reqLogger, withTracePayload);
|
|
645
|
+
if (endSigCheck.error) {
|
|
646
|
+
return res.status(endSigCheck.error.status).json(withTracePayload(endSigCheck.error.body));
|
|
647
|
+
}
|
|
648
|
+
|
|
575
649
|
const { conversation_id } = req.body;
|
|
576
650
|
if (!conversation_id) {
|
|
577
651
|
reqLogger.warn('End request missing conversation_id', {
|
package/src/routes/dashboard.js
CHANGED
|
@@ -1331,7 +1331,7 @@ function createDashboardApiRouter(options = {}) {
|
|
|
1331
1331
|
success: true,
|
|
1332
1332
|
onboarding_complete: context.config.isOnboarded(),
|
|
1333
1333
|
defaults: cfg.defaults || {},
|
|
1334
|
-
agent: cfg.agent || {},
|
|
1334
|
+
agent: (() => { const { private_key, ...pub } = cfg.agent || {}; return pub; })(),
|
|
1335
1335
|
tiers,
|
|
1336
1336
|
manifest: {
|
|
1337
1337
|
never_disclose: manifest.never_disclose || [],
|
package/src/server.js
CHANGED
|
@@ -901,9 +901,12 @@ app.use('/dashboard', createDashboardUiRouter({
|
|
|
901
901
|
}));
|
|
902
902
|
app.use('/callbook', createCallbookRouter());
|
|
903
903
|
|
|
904
|
+
// A2A-52: pass agent's public key so /status can advertise it
|
|
905
|
+
const _a2aKeypair = config.getKeypair();
|
|
904
906
|
app.use('/api/a2a', createRoutes({
|
|
905
907
|
tokenStore,
|
|
906
908
|
eventStore,
|
|
909
|
+
publicKey: _a2aKeypair ? _a2aKeypair.publicKey : null,
|
|
907
910
|
logger: logger.child({ component: 'a2a.routes' }),
|
|
908
911
|
onCallMonitor: (monitor) => {
|
|
909
912
|
activeCallMonitor = monitor;
|