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

Download ZIP
api/oauth-register.ts 113 lines · 3.6 KB · typescript
1
import { oauthStore, type OAuthStore } from '../apps/api/src/oauth-store.js';
2
import { guardRequest } from '../apps/api/src/request-security.js';
3
import { assertProductionConfiguration } from '../apps/api/src/production-config.js';
4
5
assertProductionConfiguration('auth');
6
7
const maximumRegistrationBytes = 32_768;
8
9
function validRedirectUri(value: unknown): value is string {
10
  if (typeof value !== 'string' || value.length > 2048) return false;
11
  try {
12
    const url = new URL(value);
13
    if (url.hash || url.username || url.password) return false;
14
    if (url.protocol === 'https:') return true;
15
    if (url.protocol === 'http:') return ['127.0.0.1', '[::1]', 'localhost'].includes(url.hostname);
16
    return !['javascript:', 'data:', 'file:', 'vbscript:'].includes(url.protocol);
17
  } catch {
18
    return false;
19
  }
20
}
21
22
export async function register(request: Request, store: OAuthStore): Promise<Response> {
23
  const body = await request.text();
24
  if (new TextEncoder().encode(body).byteLength > maximumRegistrationBytes)
25
    return Response.json({ error: 'invalid_client_metadata' }, { status: 413 });
26
27
  let metadata: Record<string, unknown>;
28
  try {
29
    const parsed = JSON.parse(body) as unknown;
30
    if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) throw new Error();
31
    metadata = parsed as Record<string, unknown>;
32
  } catch {
33
    return Response.json({ error: 'invalid_client_metadata' }, { status: 400 });
34
  }
35
36
  const redirectUris = metadata.redirect_uris;
37
  if (
38
    !Array.isArray(redirectUris) ||
39
    redirectUris.length === 0 ||
40
    redirectUris.length > 10 ||
41
    !redirectUris.every(validRedirectUri)
42
  )
43
    return Response.json(
44
      { error: 'invalid_redirect_uri', error_description: 'Valid redirect_uris are required' },
45
      { status: 400 },
46
    );
47
  if (
48
    metadata.token_endpoint_auth_method !== undefined &&
49
    metadata.token_endpoint_auth_method !== 'none'
50
  )
51
    return Response.json(
52
      {
53
        error: 'invalid_client_metadata',
54
        error_description: 'Only public PKCE clients are supported',
55
      },
56
      { status: 400 },
57
    );
58
59
  const client = await store.registerClient({
60
    ...metadata,
61
    redirect_uris: redirectUris,
62
    token_endpoint_auth_method: 'none',
63
    grant_types: ['authorization_code', 'refresh_token'],
64
    response_types: ['code'],
65
  });
66
  console.info('[oauth.dcr] OpenClasp client registered', {
67
    clientId: client.clientId,
68
    redirectUriCount: client.redirectUris.length,
69
  });
70
  return Response.json(
71
    {
72
      ...metadata,
73
      client_id: client.clientId,
74
      client_id_issued_at: Math.floor(Date.now() / 1000),
75
      redirect_uris: client.redirectUris,
76
      token_endpoint_auth_method: 'none',
77
      grant_types: ['authorization_code', 'refresh_token'],
78
      response_types: ['code'],
79
    },
80
    {
81
      status: 201,
82
      headers: {
83
        'access-control-allow-origin': '*',
84
        'cache-control': 'no-store',
85
      },
86
    },
87
  );
88
}
89
90
export async function POST(request: Request): Promise<Response> {
91
  const rejected = await guardRequest(request, 'oauth-register', {
92
    limit: 20,
93
    maximumBytes: maximumRegistrationBytes,
94
  });
95
  if (rejected) return rejected;
96
  try {
97
    return await register(request, oauthStore());
98
  } catch {
99
    return Response.json({ error: 'temporarily_unavailable' }, { status: 503 });
100
  }
101
}
102
103
export function OPTIONS(): Response {
104
  return new Response(null, {
105
    status: 204,
106
    headers: {
107
      'access-control-allow-headers': 'content-type',
108
      'access-control-allow-methods': 'POST, OPTIONS',
109
      'access-control-allow-origin': '*',
110
      'access-control-max-age': '86400',
111
    },
112
  });
113
}