| 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 | }
|