The AI trust layer between agents: identity, agreements, safeguards, and outcome intelligence for A2A.

Viewing files at commit 989559e001cdCommit detailsView latest

packages/persistence/src/runtime.ts 133 lines · 4.4 KB · typescript
1
import { createHmac, timingSafeEqual } from 'node:crypto';
2
import { lookup } from 'node:dns/promises';
3
import { request as httpsRequest } from 'node:https';
4
import { isIP } from 'node:net';
5
6
const RESPONSE_LIMIT = 64 * 1024;
7
8
function isPrivateIpv4(address: string) {
9
  const values = address.split('.').map(Number);
10
  const [a, b] = values;
11
  if (values.length !== 4 || values.some((value) => !Number.isInteger(value))) return true;
12
  return (
13
    a === 0 ||
14
    a === 10 ||
15
    a === 127 ||
16
    (a === 100 && b! >= 64 && b! <= 127) ||
17
    (a === 169 && b === 254) ||
18
    (a === 172 && b! >= 16 && b! <= 31) ||
19
    (a === 192 && b === 168) ||
20
    (a === 192 && b === 0) ||
21
    (a === 192 && b === 2) ||
22
    (a === 198 && (b === 18 || b === 19 || b === 51)) ||
23
    (a === 203 && b === 0) ||
24
    a! >= 224
25
  );
26
}
27
28
function isPrivateIp(address: string) {
29
  if (isIP(address) === 4) return isPrivateIpv4(address);
30
  const normalized = address.toLowerCase();
31
  if (normalized.startsWith('::ffff:')) return isPrivateIpv4(normalized.slice(7));
32
  return (
33
    normalized === '::' ||
34
    normalized === '::1' ||
35
    normalized.startsWith('fc') ||
36
    normalized.startsWith('fd') ||
37
    /^fe[89ab]/.test(normalized) ||
38
    normalized.startsWith('2001:db8:')
39
  );
40
}
41
42
export async function resolvePublicRuntimeEndpoint(value: string) {
43
  const url = new URL(value);
44
  if (url.protocol !== 'https:') throw new Error('Runtime endpoint must use HTTPS');
45
  if (url.username || url.password) throw new Error('Runtime endpoint cannot contain credentials');
46
  if (url.port && url.port !== '443') throw new Error('Runtime endpoint must use port 443');
47
  if (isIP(url.hostname) || url.hostname === 'localhost' || url.hostname.endsWith('.local'))
48
    throw new Error('Runtime endpoint must use a public DNS hostname');
49
  const addresses = await lookup(url.hostname, { all: true, verbatim: true });
50
  if (!addresses.length || addresses.some(({ address }) => isPrivateIp(address)))
51
    throw new Error('Runtime endpoint resolves to a private or reserved network');
52
  return { url, address: addresses[0]!.address };
53
}
54
55
export async function postRuntimeJson(
56
  endpoint: string,
57
  body: unknown,
58
  headers: Record<string, string> = {},
59
) {
60
  const target = await resolvePublicRuntimeEndpoint(endpoint);
61
  const encoded = JSON.stringify(body);
62
  return new Promise<{ status: number; body: unknown }>((resolve, reject) => {
63
    const request = httpsRequest(
64
      {
65
        protocol: 'https:',
66
        hostname: target.address,
67
        port: 443,
68
        path: `${target.url.pathname}${target.url.search}`,
69
        method: 'POST',
70
        servername: target.url.hostname,
71
        headers: {
72
          host: target.url.host,
73
          'content-type': 'application/json',
74
          'content-length': Buffer.byteLength(encoded),
75
          ...headers,
76
        },
77
        timeout: 10_000,
78
      },
79
      (response) => {
80
        const chunks: Buffer[] = [];
81
        let size = 0;
82
        response.on('data', (chunk: Buffer) => {
83
          size += chunk.length;
84
          if (size > RESPONSE_LIMIT) request.destroy(new Error('Runtime response is too large'));
85
          else chunks.push(chunk);
86
        });
87
        response.on('end', () => {
88
          const text = Buffer.concat(chunks).toString('utf8');
89
          let parsed: unknown = undefined;
90
          if (text) {
91
            try {
92
              parsed = JSON.parse(text);
93
            } catch {
94
              parsed = text;
95
            }
96
          }
97
          resolve({ status: response.statusCode ?? 500, body: parsed });
98
        });
99
      },
100
    );
101
    request.on('timeout', () => request.destroy(new Error('Runtime endpoint timed out')));
102
    request.on('error', reject);
103
    request.end(encoded);
104
  });
105
}
106
107
export function runtimeDeliverySignature(
108
  secret: string,
109
  deliveryId: string,
110
  timestamp: string,
111
  body: string,
112
) {
113
  return createHmac('sha256', secret)
114
    .update(`${timestamp}.${deliveryId}.${body}`)
115
    .digest('base64url');
116
}
117
118
export function verifyRuntimeDeliverySignature(
119
  secret: string,
120
  deliveryId: string,
121
  timestamp: string,
122
  body: string,
123
  signature: string,
124
  now = Date.now(),
125
) {
126
  if (Math.abs(now - Date.parse(timestamp)) > 5 * 60_000) return false;
127
  const expected = Buffer.from(
128
    runtimeDeliverySignature(secret, deliveryId, timestamp, body),
129
    'base64url',
130
  );
131
  const actual = Buffer.from(signature.replace(/^v1=/, ''), 'base64url');
132
  return actual.length === expected.length && timingSafeEqual(actual, expected);
133
}