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

Viewing files at commit af23d8c9b675Commit detailsView latest

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