An apiKey with in=query (Viator's legacy ?apiKey=) reached the add flow as a method with a '(query)' label and zero placements — an empty, unfillable card that would register as a no-auth method. Query keys now become real query placements end to end (the invoke layer always knew how to render them). Cookie-located schemes stop producing methods entirely: placements carry header|query only, and a cookie scheme is usually the vendor console's own session, not a mintable credential.
Showing the changes to packages/plugins/openapi/src (3 of 3 files). Show all
Unified · Split
| @@ -71,3 +71,45 @@ describe("detectedAuthenticationTemplates", () => { · 70 unmodified lines | |||
| 71 | ).toEqual(["OAuth2 Authorization Code · oauth_app", "OAuth2 Client Credentials · oauth_app"]); | 71 | ).toEqual(["OAuth2 Authorization Code · oauth_app", "OAuth2 Client Credentials · oauth_app"]); |
| 72 | }); | 72 | }); |
| 73 | }); | 73 | }); |
| 74 | |||
| 75 | describe("query-located api keys", () => { | ||
| 76 | it("a query apiKey preset places the secret in the query string", () => { | ||
| 77 | const templates = detectedAuthenticationTemplates( | ||
| 78 | [ | ||
| 79 | { | ||
| 80 | label: "Legacy-API-key (query)", | ||
| 81 | headers: {}, | ||
| 82 | secretHeaders: [], | ||
| 83 | secretQueryParams: ["apiKey"], | ||
| 84 | }, | ||
| 85 | ], | ||
| 86 | [], | ||
| 87 | "https://example.com", | ||
| 88 | ); | ||
| 89 | expect(templates).toHaveLength(1); | ||
| 90 | const template = templates[0]; | ||
| 91 | expect(template?.kind).toBe("apikey"); | ||
| 92 | expect(template?.kind === "apikey" ? template.placements : []).toEqual([ | ||
| 93 | { carrier: "query", name: "apiKey" }, | ||
| 94 | ]); | ||
| 95 | }); | ||
| 96 | |||
| 97 | it("a strategy mixing a header and a query param asks for two inputs", () => { | ||
| 98 | const templates = detectedAuthenticationTemplates( | ||
| 99 | [ | ||
| 100 | { | ||
| 101 | label: "exp-api-key + apiKey (query)", | ||
| 102 | headers: { "exp-api-key": null }, | ||
| 103 | secretHeaders: ["exp-api-key"], | ||
| 104 | secretQueryParams: ["apiKey"], | ||
| 105 | }, | ||
| 106 | ], | ||
| 107 | [], | ||
| 108 | "https://example.com", | ||
| 109 | ); | ||
| 110 | const template = templates[0]; | ||
| 111 | const placements = template?.kind === "apikey" ? template.placements : []; | ||
| 112 | expect(placements.map((placement) => placement.carrier)).toEqual(["header", "query"]); | ||
| 113 | expect(new Set(placements.map((placement) => placement.variable)).size).toBe(2); | ||
| 114 | }); | ||
| 115 | }); | ||
| @@ -104,20 +104,33 @@ const apiKeyTemplateFromHeaderPreset = ( · 103 unmodified lines | |||
| 104 | preset: HeaderPreset, | 104 | preset: HeaderPreset, |
| 105 | slug: AuthTemplateSlug, | 105 | slug: AuthTemplateSlug, |
| 106 | ): APIKeyAuthentication => { | 106 | ): APIKeyAuthentication => { |
| 107 | const variables = variablesForHeaders(preset.secretHeaders); | 107 | const secretQueryParams = preset.secretQueryParams ?? []; |
| 108 | // One variable namespace across carriers, so a strategy sending two secrets | ||
| 109 | // (a header and a query param) asks for two distinct inputs. | ||
| 110 | const variables = variablesForHeaders([...preset.secretHeaders, ...secretQueryParams]); | ||
| 108 | return { | 111 | return { |
| 109 | slug, | 112 | slug, |
| 110 | kind: "apikey", | 113 | kind: "apikey", |
| 111 | placements: preset.secretHeaders.map((headerName) => { | 114 | placements: [ |
| 112 | const prefix = headerPrefix(preset, headerName); | 115 | ...preset.secretHeaders.map((headerName) => { |
| 113 | const variable = variables.get(headerName); | 116 | const prefix = headerPrefix(preset, headerName); |
| 114 | return { | 117 | const variable = variables.get(headerName); |
| 115 | carrier: "header" as const, | 118 | return { |
| 116 | name: headerName, | 119 | carrier: "header" as const, |
| 117 | ...(prefix ? { prefix } : {}), | 120 | name: headerName, |
| 118 | ...(variable ? { variable } : {}), | 121 | ...(prefix ? { prefix } : {}), |
| 119 | }; | 122 | ...(variable ? { variable } : {}), |
| 120 | }), | 123 | }; |
| 124 | }), | ||
| 125 | ...secretQueryParams.map((paramName) => { | ||
| 126 | const variable = variables.get(paramName); | ||
| 127 | return { | ||
| 128 | carrier: "query" as const, | ||
| 129 | name: paramName, | ||
| 130 | ...(variable ? { variable } : {}), | ||
| 131 | }; | ||
| 132 | }), | ||
| 133 | ], | ||
| 121 | }; | 134 | }; |
| 122 | }; | 135 | }; |
| 123 | 136 | ||
| @@ -122,6 +122,9 @@ export const HeaderPreset = Schema.Struct({ · 121 unmodified lines | |||
| 122 | headers: Schema.Record(Schema.String, Schema.NullOr(Schema.String)), | 122 | headers: Schema.Record(Schema.String, Schema.NullOr(Schema.String)), |
| 123 | /** Which headers should be stored as secrets */ | 123 | /** Which headers should be stored as secrets */ |
| 124 | secretHeaders: Schema.Array(Schema.String), | 124 | secretHeaders: Schema.Array(Schema.String), |
| 125 | /** Query parameters the strategy sends the secret in (apiKey in=query, | ||
| 126 | * e.g. Viator's legacy `?apiKey=`). Absent on older stored previews. */ | ||
| 127 | secretQueryParams: Schema.optional(Schema.Array(Schema.String)), | ||
| 125 | }); | 128 | }); |
| 126 | export type HeaderPreset = typeof HeaderPreset.Type; | 129 | export type HeaderPreset = typeof HeaderPreset.Type; |
| 127 | 130 | ||
| @@ -347,6 +350,7 @@ const buildHeaderPresets = ( · 219 unmodified lines | |||
| 347 | 350 | ||
| 348 | const headers: Record<string, string | null> = {}; | 351 | const headers: Record<string, string | null> = {}; |
| 349 | const secretHeaders: string[] = []; | 352 | const secretHeaders: string[] = []; |
| 353 | const secretQueryParams: string[] = []; | ||
| 350 | const labelParts: string[] = []; | 354 | const labelParts: string[] = []; |
| 351 | 355 | ||
| 352 | for (const scheme of resolved) { | 356 | for (const scheme of resolved) { |
| @@ -363,8 +367,16 @@ const buildHeaderPresets = ( · 10 unmodified lines | |||
| 363 | headers[headerName] = null; | 367 | headers[headerName] = null; |
| 364 | secretHeaders.push(headerName); | 368 | secretHeaders.push(headerName); |
| 365 | labelParts.push(scheme.name); | 369 | labelParts.push(scheme.name); |
| 370 | } else if (scheme.type === "apiKey" && Option.getOrElse(scheme.in, () => "") === "query") { | ||
| 371 | secretQueryParams.push(Option.getOrElse(scheme.headerName, () => scheme.name)); | ||
| 372 | labelParts.push(`${scheme.name} (query)`); | ||
| 366 | } else if (scheme.type === "apiKey") { | 373 | } else if (scheme.type === "apiKey") { |
| 367 | labelParts.push(`${scheme.name} (${Option.getOrElse(scheme.in, () => "unknown")})`); | 374 | // Cookie (and unknown) locations are not renderable as a stored |
| 375 | // method — auth placements carry header|query — and a cookie scheme | ||
| 376 | // is usually the vendor console's own session, not a mintable | ||
| 377 | // credential. Contributing a label here used to produce a method | ||
| 378 | // with zero placements: an empty, unfillable card in the add flow. | ||
| 379 | continue; | ||
| 368 | } else if (scheme.type === "oauth2" || scheme.type === "openIdConnect") { | 380 | } else if (scheme.type === "oauth2" || scheme.type === "openIdConnect") { |
| 369 | return []; | 381 | return []; |
| 370 | } else { | 382 | } else { |
| @@ -372,21 +384,16 @@ const buildHeaderPresets = ( · 1 unmodified line | |||
| 372 | } | 384 | } |
| 373 | } | 385 | } |
| 374 | 386 | ||
| 375 | if (Object.keys(headers).length === 0 && resolved.length > 0) { | 387 | // A strategy in which nothing is renderable (cookie-only, or an exotic |
| 376 | return [ | 388 | // scheme type) yields no preset rather than an empty one. |
| 377 | HeaderPreset.make({ | 389 | if (secretHeaders.length === 0 && secretQueryParams.length === 0) return []; |
| 378 | label: labelParts.join(" + "), | ||
| 379 | headers: {}, | ||
| 380 | secretHeaders: [], | ||
| 381 | }), | ||
| 382 | ]; | ||
| 383 | } | ||
| 384 | 390 | ||
| 385 | return [ | 391 | return [ |
| 386 | HeaderPreset.make({ | 392 | HeaderPreset.make({ |
| 387 | label: labelParts.join(" + "), | 393 | label: labelParts.join(" + "), |
| 388 | headers, | 394 | headers, |
| 389 | secretHeaders, | 395 | secretHeaders, |
| 396 | ...(secretQueryParams.length > 0 ? { secretQueryParams } : {}), | ||
| 390 | }), | 397 | }), |
| 391 | ]; | 398 | ]; |
| 392 | }); | 399 | }); |