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

Viewing files at commit b95ad82c1c50Commit detailsView latest

api/session.ts 96 lines · 3.2 KB · typescript
1
import {
2
  auth0Config,
3
  dashboardSessionCookie,
4
  dashboardTokenFromCookie,
5
  loadAuth0Profile,
6
  verifyAuth0Token,
7
} from './auth0.js';
8
9
function cookie(value: string, maxAge: number, secure: boolean) {
10
  return [
11
    `${dashboardSessionCookie}=${encodeURIComponent(value)}`,
12
    'HttpOnly',
13
    'Path=/',
14
    'SameSite=Lax',
15
    `Max-Age=${Math.max(0, Math.floor(maxAge))}`,
16
    ...(secure ? ['Secure'] : []),
17
  ].join('; ');
18
}
19
20
export async function POST(request: Request): Promise<Response> {
21
  const input = (await request.json()) as { code?: unknown; codeVerifier?: unknown };
22
  if (typeof input.code !== 'string' || typeof input.codeVerifier !== 'string')
23
    return Response.json({ error: 'invalid_callback' }, { status: 400 });
24
  const config = auth0Config();
25
  const redirectUri = `${config.publicOrigin}/sso-callback`;
26
  const tokenResponse = await fetch(`${config.issuer}oauth/token`, {
27
    method: 'POST',
28
    headers: { 'content-type': 'application/x-www-form-urlencoded' },
29
    body: new URLSearchParams({
30
      grant_type: 'authorization_code',
31
      client_id: config.dashboardClientId,
32
      code: input.code,
33
      code_verifier: input.codeVerifier,
34
      redirect_uri: redirectUri,
35
    }),
36
  });
37
  if (!tokenResponse.ok) return Response.json({ error: 'token_exchange_failed' }, { status: 401 });
38
  const tokens = (await tokenResponse.json()) as { access_token?: unknown; expires_in?: unknown };
39
  if (typeof tokens.access_token !== 'string')
40
    return Response.json({ error: 'invalid_token_response' }, { status: 401 });
41
  const authentication = await verifyAuth0Token(tokens.access_token, { dashboard: true });
42
  const user = await loadAuth0Profile(tokens.access_token);
43
  const expiresIn =
44
    typeof tokens.expires_in === 'number'
45
      ? tokens.expires_in
46
      : typeof authentication.payload.exp === 'number'
47
        ? authentication.payload.exp - Math.floor(Date.now() / 1000)
48
        : 3600;
49
  return Response.json(
50
    { user: { ...user, sub: authentication.payload.sub } },
51
    {
52
      headers: {
53
        'cache-control': 'no-store',
54
        'set-cookie': cookie(
55
          tokens.access_token,
56
          Math.max(expiresIn - 30, 1),
57
          new URL(request.url).protocol === 'https:',
58
        ),
59
      },
60
    },
61
  );
62
}
63
64
export async function GET(request: Request): Promise<Response> {
65
  const token = dashboardTokenFromCookie(request.headers.get('cookie') ?? undefined);
66
  if (!token) return Response.json({ authenticated: false }, { status: 401 });
67
  try {
68
    const authentication = await verifyAuth0Token(token, { dashboard: true });
69
    const user = await loadAuth0Profile(token);
70
    return Response.json(
71
      { authenticated: true, user: { ...user, sub: authentication.payload.sub } },
72
      { headers: { 'cache-control': 'no-store' } },
73
    );
74
  } catch {
75
    return Response.json(
76
      { authenticated: false },
77
      {
78
        status: 401,
79
        headers: {
80
          'cache-control': 'no-store',
81
          'set-cookie': cookie('', 0, new URL(request.url).protocol === 'https:'),
82
        },
83
      },
84
    );
85
  }
86
}
87
88
export function DELETE(request: Request): Response {
89
  return new Response(null, {
90
    status: 204,
91
    headers: {
92
      'cache-control': 'no-store',
93
      'set-cookie': cookie('', 0, new URL(request.url).protocol === 'https:'),
94
    },
95
  });
96
}