diff --git a/src/app-profiles.ts b/src/app-profiles.ts index 6f6092f..42b37cc 100644 --- a/src/app-profiles.ts +++ b/src/app-profiles.ts @@ -265,11 +265,13 @@ export function isAllowedReturnOrigin(profile: AppProfile, returnTo: string): bo /** * A connection target names something: at least one character that is not - * whitespace or an invisible format character (U+200B, U+FEFF and the like). - * The one definition the start gate and the callback mint both use. + * whitespace, a control, a default-ignorable code point (format characters, + * variation selectors, the Hangul and Khmer fillers, the combining grapheme + * joiner) or the Braille blank U+2800. The one definition the start gate and + * the callback mint both use. */ export function isUsableConnectionTarget(targetId: string): boolean { - return /[^\s\p{Cf}]/u.test(targetId); + return /[^\s\p{Z}\p{Cc}\p{Cf}\p{Default_Ignorable_Code_Point}\u2800]/u.test(targetId); } /** diff --git a/src/app.ts b/src/app.ts index 5b0fb9b..e2ba003 100644 --- a/src/app.ts +++ b/src/app.ts @@ -702,23 +702,6 @@ export async function startOAuthFlow( }; } - if (input.connection !== undefined && !isUsableConnectionTarget(input.connection.targetId)) { - return { - statusCode: 400, - body: { error: "invalid_connection_target", detail: "connection.targetId must name a target." }, - }; - } - - if (callbackMint(profile, provider, input.mode, input.connection) === "refused") { - return { - statusCode: 400, - body: { - error: "creator_connection_required", - detail: `${provider} on ${profile.slug} is a managed creator connection: connect needs connection.kind creator and a targetId.`, - }, - }; - } - const descriptor = providerCatalog[provider]; // A provider that never mints an account attaches an identity only through // a bound link: the user's own access token, since the app's word does not @@ -818,6 +801,24 @@ export async function startOAuthFlow( return { statusCode: 401, body: { error: "app_auth_required" } }; } + // After the app-secret gate, so a caller without the secret learns nothing about what a target must look like. + if (input.connection !== undefined && !isUsableConnectionTarget(input.connection.targetId)) { + return { + statusCode: 400, + body: { error: "invalid_connection_target", detail: "connection.targetId must name a target." }, + }; + } + + if (callbackMint(profile, provider, input.mode, input.connection) === "refused") { + return { + statusCode: 400, + body: { + error: "creator_connection_required", + detail: `${provider} on ${profile.slug} is a managed creator connection: connect needs connection.kind creator and a targetId.`, + }, + }; + } + const providerConfig = config.providers[provider]; if (!providerConfig.clientId) { return {