History

Detect query-located API keys; drop unrenderable auth strategies

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.

Rhys Sullivan committed 2 months ago

Commit bcaf9e11 parent 12ae0913 files · +83 −21Browse files

Showing the changes to packages/plugins/openapi/src (3 of 3 files). Show all

Unified · Split

3 files
  1. packages/plugins/openapi/src/sdk/derive-auth.test.ts +42 −0
  2. packages/plugins/openapi/src/sdk/derive-auth.ts +24 −11
  3. packages/plugins/openapi/src/sdk/preview.ts +17 −10
packages/plugins/openapi/src/sdk/derive-auth.test.ts+42−0
@@ -71,3 +71,45 @@ describe("detectedAuthenticationTemplates", () => { · 70 unmodified lines
7171 ).toEqual(["OAuth2 Authorization Code · oauth_app", "OAuth2 Client Credentials · oauth_app"]);
7272 });
7373 });
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+});
packages/plugins/openapi/src/sdk/derive-auth.ts+24−11
@@ -104,20 +104,33 @@ const apiKeyTemplateFromHeaderPreset = ( · 103 unmodified lines
104104 preset: HeaderPreset,
105105 slug: AuthTemplateSlug,
106106 ): 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]);
108111 return {
109112 slug,
110113 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+ ],
121134 };
122135 };
123136
packages/plugins/openapi/src/sdk/preview.ts+17−10
@@ -122,6 +122,9 @@ export const HeaderPreset = Schema.Struct({ · 121 unmodified lines
122122 headers: Schema.Record(Schema.String, Schema.NullOr(Schema.String)),
123123 /** Which headers should be stored as secrets */
124124 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)),
125128 });
126129 export type HeaderPreset = typeof HeaderPreset.Type;
127130
@@ -347,6 +350,7 @@ const buildHeaderPresets = ( · 219 unmodified lines
347350
348351 const headers: Record<string, string | null> = {};
349352 const secretHeaders: string[] = [];
353+ const secretQueryParams: string[] = [];
350354 const labelParts: string[] = [];
351355
352356 for (const scheme of resolved) {
@@ -363,8 +367,16 @@ const buildHeaderPresets = ( · 10 unmodified lines
363367 headers[headerName] = null;
364368 secretHeaders.push(headerName);
365369 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)`);
366373 } 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;
368380 } else if (scheme.type === "oauth2" || scheme.type === "openIdConnect") {
369381 return [];
370382 } else {
@@ -372,21 +384,16 @@ const buildHeaderPresets = ( · 1 unmodified line
372384 }
373385 }
374386
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 [];
384390
385391 return [
386392 HeaderPreset.make({
387393 label: labelParts.join(" + "),
388394 headers,
389395 secretHeaders,
396+ ...(secretQueryParams.length > 0 ? { secretQueryParams } : {}),
390397 }),
391398 ];
392399 });

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t