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

Viewing files at commit 1f74f1f0e3abCommit detailsView latest

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