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

Viewing files at commit 922a6b480fb8Commit detailsView latest

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