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

Viewing files at commit 32cb43fd4001Commit detailsView latest

api/index.ts 114 lines · 5.1 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 isPublicConnectorClaimRoute(method: string, requestUrl: string) {
29
  const path = new URL(requestUrl, 'https://openclasp.local').pathname;
30
  if (method === 'POST' && path === '/v0.1/provider-connections/botpress/complete') return true;
31
  if (method === 'POST' && path === '/v0.1/connector-claims') return true;
32
  if (method === 'GET' && /^\/v0\.1\/connector-claims\/[0-9a-f-]+$/.test(path)) return true;
33
  return method === 'POST' && /^\/v0\.1\/connector-claims\/[0-9a-f-]+\/poll$/.test(path);
34
}
35
36
function reject(response: ServerResponse, statusCode: number, error: string) {
37
  response.statusCode = statusCode;
38
  response.setHeader('content-type', 'application/json');
39
  response.setHeader('cache-control', 'no-store');
40
  response.end(JSON.stringify({ error }));
41
}
42
43
export default async function handler(request: IncomingMessage, response: ServerResponse) {
44
  delete request.headers['x-openclasp-operator'];
45
  delete request.headers['x-openclasp-bound-agent'];
46
  delete request.headers['x-openclasp-credential-type'];
47
  delete request.headers['x-openclasp-internal-auth'];
48
  const incoming = new URL(request.url ?? '/', 'https://openclasp.local');
49
  const target = incoming.searchParams.get('path');
50
  if (target) request.url = target;
51
  if ((request.url ?? '').startsWith('/v0.1/')) {
52
    if (isPublicConnectorClaimRoute(request.method ?? 'GET', request.url ?? '/')) {
53
      request.headers['x-openclasp-internal-auth'] = internalAuthSecret;
54
      await ready;
55
      app.server.emit('request', request, response);
56
      return;
57
    }
58
    const authorization = request.headers.authorization;
59
    const bearerToken = authorization?.startsWith('Bearer ') ? authorization.slice(7) : undefined;
60
    const dashboardToken = bearerToken
61
      ? undefined
62
      : dashboardTokenFromCookie(request.headers.cookie);
63
    const token = bearerToken ?? dashboardToken;
64
    if (!token) {
65
      reject(response, 401, 'authentication_required');
66
      return;
67
    }
68
    try {
69
      if (dashboardToken && unsafeMethods.has(request.method ?? 'GET')) {
70
        const origin = request.headers.origin;
71
        const forwardedProtocol = request.headers['x-forwarded-proto'];
72
        const forwardedHost = request.headers['x-forwarded-host'];
73
        const protocol = typeof forwardedProtocol === 'string' ? forwardedProtocol : 'https';
74
        const host = typeof forwardedHost === 'string' ? forwardedHost : request.headers.host;
75
        const expectedOrigin = `${protocol}://${host}`;
76
        if (origin !== expectedOrigin) {
77
          reject(response, 403, 'invalid_request_origin');
78
          return;
79
        }
80
      }
81
      if (token.startsWith('oc_at_')) {
82
        if (!repository) throw new Error('Agent access tokens are not configured');
83
        const requiredScopes = requiredAgentApiScopes(request.method ?? 'GET', request.url ?? '/');
84
        if (!requiredScopes) {
85
          reject(response, 403, 'agent_route_forbidden');
86
          return;
87
        }
88
        const authentication = await repository.verifyAgentAccessToken(token);
89
        assertScopes(authentication.scopes, requiredScopes);
90
        request.headers['x-openclasp-operator'] = authentication.operatorId;
91
        request.headers['x-openclasp-bound-agent'] = authentication.agentId;
92
        request.headers['x-openclasp-credential-type'] = 'agent_access_token';
93
      } else {
94
        const authentication = await verifyAuth0Token(token, { dashboard: true });
95
        request.headers['x-openclasp-operator'] = authentication.payload.sub!;
96
        if ((request.url ?? '').startsWith('/v0.1/account')) {
97
          const user = await loadAuth0Profile(token);
98
          request.headers['x-openclasp-email'] = encodeURIComponent(user.email ?? '');
99
          request.headers['x-openclasp-name'] = encodeURIComponent(user.name ?? '');
100
        }
101
      }
102
      request.headers['x-openclasp-internal-auth'] = internalAuthSecret;
103
    } catch (error) {
104
      reject(
105
        response,
106
        error instanceof ScopeError ? 403 : 401,
107
        error instanceof ScopeError ? 'insufficient_scope' : 'invalid_session',
108
      );
109
      return;
110
    }
111
  }
112
  await ready;
113
  app.server.emit('request', request, response);
114
}