fix(oauth): add public RFC 7591 DCR endpoint at POST /oauth/register

Claude.ai's start-auth flow POSTs to the registration_endpoint advertised
in the discovery document, but no public handler existed at /oauth/register
(only /api/oauth/register with browser cookie auth). This caused a
start_error redirect immediately on every connect attempt.

- Add POST /oauth/register to oauthPublicRouter following RFC 7591
- Make oauth_clients.user_id nullable via a raw (no-transaction) migration
  so anonymous DCR clients can be created without a user context
- Update migration runner to support { raw: () => void } migrations for
  DDL that requires PRAGMA foreign_keys = OFF outside a transaction
- Update createOAuthClient to accept userId: number | null with a global
  cap (500) for anonymous DCR clients in place of the per-user limit
This commit is contained in:
jubnl
2026-04-10 05:42:00 +02:00
parent 9b1baaf7b8
commit cb3aeda8e0
3 changed files with 101 additions and 5 deletions
+9 -3
View File
@@ -123,7 +123,7 @@ export function isValidRedirectUri(uri: string): boolean {
}
export function createOAuthClient(
userId: number,
userId: number | null,
name: string,
redirectUris: string[],
allowedScopes: string[],
@@ -151,8 +151,14 @@ export function createOAuthClient(
const { valid, invalid } = validateScopes(allowedScopes);
if (!valid) return { error: `Invalid scopes: ${invalid.join(', ')}`, status: 400 };
const count = (db.prepare('SELECT COUNT(*) as count FROM oauth_clients WHERE user_id = ?').get(userId) as { count: number }).count;
if (count >= 10) return { error: 'Maximum of 10 OAuth clients per user', status: 400 };
if (userId !== null) {
const count = (db.prepare('SELECT COUNT(*) as count FROM oauth_clients WHERE user_id = ?').get(userId) as { count: number }).count;
if (count >= 10) return { error: 'Maximum of 10 OAuth clients per user', status: 400 };
} else {
// Anonymous DCR clients: enforce a global cap to prevent unbounded registration abuse
const count = (db.prepare('SELECT COUNT(*) as count FROM oauth_clients WHERE user_id IS NULL').get() as { count: number }).count;
if (count >= 500) return { error: 'server_error', status: 503 };
}
const isPublic = options?.isPublic ?? false;
const createdVia = options?.createdVia ?? 'settings_ui';