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 | 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 | 104 | preset: HeaderPreset, |
| 105 | 105 | slug: AuthTemplateSlug, |
| 106 | 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 | 111 | return { |
| 109 | 112 | slug, |
| 110 | 113 | kind: "apikey", |
| 111 | − placements: preset.secretHeaders.map((headerName) => { | |
| 112 | − const prefix = headerPrefix(preset, headerName); | |
| 113 | − const variable = variables.get(headerName); | |
| 114 | − return { | |
| 115 | − carrier: "header" as const, | |
| 116 | − name: headerName, | |
| 117 | − ...(prefix ? { prefix } : {}), | |
| 118 | − ...(variable ? { variable } : {}), | |
| 119 | − }; | |
| 120 | − }), | |
| 114 | + placements: [ | |
| 115 | + ...preset.secretHeaders.map((headerName) => { | |
| 116 | + const prefix = headerPrefix(preset, headerName); | |
| 117 | + const variable = variables.get(headerName); | |
| 118 | + return { | |
| 119 | + carrier: "header" as const, | |
| 120 | + name: headerName, | |
| 121 | + ...(prefix ? { prefix } : {}), | |
| 122 | + ...(variable ? { variable } : {}), | |
| 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 | 122 | headers: Schema.Record(Schema.String, Schema.NullOr(Schema.String)), |
| 123 | 123 | /** Which headers should be stored as secrets */ |
| 124 | 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 | 129 | export type HeaderPreset = typeof HeaderPreset.Type; |
| 127 | 130 | |
| @@ -347,6 +350,7 @@ const buildHeaderPresets = ( · 219 unmodified lines | ||
| 347 | 350 | |
| 348 | 351 | const headers: Record<string, string | null> = {}; |
| 349 | 352 | const secretHeaders: string[] = []; |
| 353 | + const secretQueryParams: string[] = []; | |
| 350 | 354 | const labelParts: string[] = []; |
| 351 | 355 | |
| 352 | 356 | for (const scheme of resolved) { |
| @@ -363,8 +367,16 @@ const buildHeaderPresets = ( · 10 unmodified lines | ||
| 363 | 367 | headers[headerName] = null; |
| 364 | 368 | secretHeaders.push(headerName); |
| 365 | 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 | 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 | 380 | } else if (scheme.type === "oauth2" || scheme.type === "openIdConnect") { |
| 369 | 381 | return []; |
| 370 | 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) { | |
| 376 | − return [ | |
| 377 | − HeaderPreset.make({ | |
| 378 | − label: labelParts.join(" + "), | |
| 379 | − headers: {}, | |
| 380 | − secretHeaders: [], | |
| 381 | − }), | |
| 382 | − ]; | |
| 383 | − } | |
| 387 | + // A strategy in which nothing is renderable (cookie-only, or an exotic | |
| 388 | + // scheme type) yields no preset rather than an empty one. | |
| 389 | + if (secretHeaders.length === 0 && secretQueryParams.length === 0) return []; | |
| 384 | 390 | |
| 385 | 391 | return [ |
| 386 | 392 | HeaderPreset.make({ |
| 387 | 393 | label: labelParts.join(" + "), |
| 388 | 394 | headers, |
| 389 | 395 | secretHeaders, |
| 396 | + ...(secretQueryParams.length > 0 ? { secretQueryParams } : {}), | |
| 390 | 397 | }), |
| 391 | 398 | ]; |
| 392 | 399 | }); |