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

Viewing files at commit af23d8c9b675Commit detailsView latest

api/index.ts 92 lines · 4.3 KB · typescript
1
import type { IncomingMessage, ServerResponse } from 'node:http';
2
import { randomBytes } from 'node:crypto';
3
import { buildApi } from '../apps/api/src/app.js';
4
import { TrustEngine } from '../packages/core/src/index.js';
5
import { HostedRepository } from '../packages/persistence/src/hosted.js';
6
import { dashboardTokenFromCookie, loadAuth0Profile, verifyAuth0Token } from './auth0.js';
7
import { ScopeError, assertScopes, requiredAgentApiScopes } from './access-control.js';
8
import { assertProductionConfiguration } from './production-config.js';
9
10
assertProductionConfiguration('api');
11
12
const databaseUrl = process.env.DATABASE_URL;
13
const repository = databaseUrl ? new HostedRepository(databaseUrl) : undefined;
14
const internalAuthSecret = randomBytes(32).toString('base64url');
15
const app = buildApi(new TrustEngine(), undefined, repository, { internalAuthSecret });
16
const ready = app.ready();
17
18
const unsafeMethods = new Set(['POST', 'PUT', 'PATCH', 'DELETE']);
19
20
function reject(response: ServerResponse, statusCode: number, error: string) {
21
  response.statusCode = statusCode;
22
  response.setHeader('content-type', 'application/json');
23
  response.setHeader('cache-control', 'no-store');
24
  response.end(JSON.stringify({ error }));
25
}
26
27
export default async function handler(request: IncomingMessage, response: ServerResponse) {
28
  delete request.headers['x-openclasp-operator'];
29
  delete request.headers['x-openclasp-bound-agent'];
30
  delete request.headers['x-openclasp-credential-type'];
31
  delete request.headers['x-openclasp-internal-auth'];
32
  const incoming = new URL(request.url ?? '/', 'https://openclasp.local');
33
  const target = incoming.searchParams.get('path');
34
  if (target) request.url = target;
35
  if ((request.url ?? '').startsWith('/v0.1/')) {
36
    const authorization = request.headers.authorization;
37
    const bearerToken = authorization?.startsWith('Bearer ') ? authorization.slice(7) : undefined;
38
    const dashboardToken = bearerToken
39
      ? undefined
40
      : dashboardTokenFromCookie(request.headers.cookie);
41
    const token = bearerToken ?? dashboardToken;
42
    if (!token) {
43
      reject(response, 401, 'authentication_required');
44
      return;
45
    }
46
    try {
47
      if (dashboardToken && unsafeMethods.has(request.method ?? 'GET')) {
48
        const origin = request.headers.origin;
49
        const forwardedProtocol = request.headers['x-forwarded-proto'];
50
        const forwardedHost = request.headers['x-forwarded-host'];
51
        const protocol = typeof forwardedProtocol === 'string' ? forwardedProtocol : 'https';
52
        const host = typeof forwardedHost === 'string' ? forwardedHost : request.headers.host;
53
        const expectedOrigin = `${protocol}://${host}`;
54
        if (origin !== expectedOrigin) {
55
          reject(response, 403, 'invalid_request_origin');
56
          return;
57
        }
58
      }
59
      if (token.startsWith('oc_at_')) {
60
        if (!repository) throw new Error('Agent access tokens are not configured');
61
        const requiredScopes = requiredAgentApiScopes(request.method ?? 'GET', request.url ?? '/');
62
        if (!requiredScopes) {
63
          reject(response, 403, 'agent_route_forbidden');
64
          return;
65
        }
66
        const authentication = await repository.verifyAgentAccessToken(token);
67
        assertScopes(authentication.scopes, requiredScopes);
68
        request.headers['x-openclasp-operator'] = authentication.operatorId;
69
        request.headers['x-openclasp-bound-agent'] = authentication.agentId;
70
        request.headers['x-openclasp-credential-type'] = 'agent_access_token';
71
      } else {
72
        const authentication = await verifyAuth0Token(token, { dashboard: true });
73
        request.headers['x-openclasp-operator'] = authentication.payload.sub!;
74
        if ((request.url ?? '').startsWith('/v0.1/account')) {
75
          const user = await loadAuth0Profile(token);
76
          request.headers['x-openclasp-email'] = encodeURIComponent(user.email ?? '');
77
          request.headers['x-openclasp-name'] = encodeURIComponent(user.name ?? '');
78
        }
79
      }
80
      request.headers['x-openclasp-internal-auth'] = internalAuthSecret;
81
    } catch (error) {
82
      reject(
83
        response,
84
        error instanceof ScopeError ? 403 : 401,
85
        error instanceof ScopeError ? 'insufficient_scope' : 'invalid_session',
86
      );
87
      return;
88
    }
89
  }
90
  await ready;
91
  app.server.emit('request', request, response);
92
}