Skip to content

Commit 37bff36

Browse files
committed
fix(provider): do not treat a keyless meta row as an api key
1 parent 876d118 commit 37bff36

5 files changed

Lines changed: 68 additions & 10 deletions

File tree

‎src/tui/onboarding.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
telemetryFirstRunPending,
1313
} from "../telemetry/first-run.js";
1414
import { getTelemetry } from "../telemetry/singleton.js";
15+
import { catalogRowsForMetaOAuthGuard } from "./provider/oauth.js";
1516
import { runProviderSetup } from "./provider/setup.js";
1617
import { runWelcome } from "./welcome.js";
1718

@@ -46,7 +47,7 @@ export async function runOnboarding(
4647

4748
const submitted = await runProviderSetup({
4849
showTelemetryNotice,
49-
existingProviderNames: Object.keys(existing?.providers ?? {}),
50+
existingProviderNames: catalogRowsForMetaOAuthGuard(existing?.providers),
5051
onSubmit: buildProviderSubmitHandler(
5152
settingsPath,
5253
existing,

‎src/tui/provider-setup.test.ts‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import {
4343
LOGIN_TIMEOUT_MESSAGE,
4444
suggestOAuthProfileSlug,
4545
suggestMetaOAuthProfileSlug,
46+
catalogRowsForMetaOAuthGuard,
4647
metaOAuthSlugBlockedByApiKey,
4748
validateOAuthProfileSlug,
4849
} from "./provider/oauth.js";
@@ -344,6 +345,22 @@ describe("provider setup pure helpers", () => {
344345
expect(suggestMetaOAuthProfileSlug([], [])).toBe("default");
345346
expect(metaOAuthSlugBlockedByApiKey("work", ["meta/work"])).toBe(true);
346347
expect(metaOAuthSlugBlockedByApiKey("work", ["meta-api/work"])).toBe(true);
348+
expect(metaOAuthSlugBlockedByApiKey("work", [{ name: "meta/work" }])).toBe(
349+
false,
350+
);
351+
expect(
352+
catalogRowsForMetaOAuthGuard({
353+
"meta/work": {},
354+
}),
355+
).toEqual([{ name: "meta/work" }]);
356+
expect(
357+
metaOAuthSlugBlockedByApiKey(
358+
"work",
359+
catalogRowsForMetaOAuthGuard({
360+
"meta/work": { apiKey: "LLM|pasted" },
361+
}),
362+
),
363+
).toBe(true);
347364
const authProfiles: readonly string[] = [];
348365
const existingProviderNames = ["meta-api/default"];
349366
const suggested = suggestMetaOAuthProfileSlug(

‎src/tui/provider/connect.ts‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import type { Settings } from "../../config/settings.js";
88
import { getTelemetry } from "../../telemetry/singleton.js";
99
import { runProviderSetup } from "./setup.js";
1010
import type { ProviderSetupConfig } from "./types.js";
11+
import { catalogRowsForMetaOAuthGuard } from "./oauth.js";
1112
import {
1213
buildProviderSubmitHandler,
1314
type PersistProviderSettings,
@@ -59,7 +60,9 @@ export async function connectProviderInline(
5960
...(input.initialOAuthProfile !== undefined
6061
? { initialOAuthProfile: input.initialOAuthProfile }
6162
: {}),
62-
existingProviderNames: Object.keys(input.existing?.providers ?? {}),
63+
existingProviderNames: catalogRowsForMetaOAuthGuard(
64+
input.existing?.providers,
65+
),
6366
...(input.createRenderer !== undefined
6467
? { createRenderer: input.createRenderer }
6568
: {}),

‎src/tui/provider/oauth.ts‎

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
} from "../../auth/meta/verification-uri.js";
2020
import type {
2121
AccountNameFlow,
22+
ExistingProviderRef,
2223
LoginFlow,
2324
OAuthKind,
2425
OAuthLoginStart,
@@ -82,19 +83,40 @@ export function suggestOAuthProfileSlug(existing: readonly string[]): string {
8283
return `default-${String(n)}`;
8384
}
8485

86+
function providerNameList(rows: readonly ExistingProviderRef[]): string[] {
87+
return rows.map((row) => (typeof row === "string" ? row : row.name));
88+
}
89+
8590
// `meta/<slug>` and `meta-api/<slug>` are one namespace: signing in under a
8691
// slug that already names an API-key row is rejected.
8792
export function suggestMetaOAuthProfileSlug(
8893
authProfiles: readonly string[],
89-
existingProviderNames: readonly string[],
94+
existingProviderNames: readonly ExistingProviderRef[],
9095
): string {
96+
const names = providerNameList(existingProviderNames);
9197
return suggestOAuthProfileSlug([
9298
...authProfiles,
93-
...instanceSlugsForKind("meta", existingProviderNames),
94-
...instanceSlugsForKind("meta-api", existingProviderNames),
99+
...instanceSlugsForKind("meta", names),
100+
...instanceSlugsForKind("meta-api", names),
95101
]);
96102
}
97103

104+
/** Settings rows for the slug guard. A keyless `meta/<slug>` is not an API key. */
105+
export function catalogRowsForMetaOAuthGuard(
106+
providers: Readonly<Record<string, { readonly apiKey?: string }>> | undefined,
107+
): ExistingProviderRef[] {
108+
const rows: ExistingProviderRef[] = [];
109+
for (const [name, provider] of Object.entries(providers ?? {})) {
110+
const apiKey = provider.apiKey;
111+
if (typeof apiKey === "string" && apiKey.length > 0) {
112+
rows.push({ name, apiKey });
113+
} else {
114+
rows.push({ name });
115+
}
116+
}
117+
return rows;
118+
}
119+
98120
/** True when `slug` names an API-key row, not an OAuth profile. */
99121
export function metaOAuthSlugBlockedByApiKey(
100122
slug: string,
@@ -422,7 +444,10 @@ export function createAccountNameFlow(
422444
}
423445
applySuggestion(
424446
suggestOAuthProfileSlug(
425-
instanceSlugsForKind(state.choice.id, state.existingProviderNames),
447+
instanceSlugsForKind(
448+
state.choice.id,
449+
providerNameList(state.existingProviderNames),
450+
),
426451
),
427452
);
428453
};
@@ -442,7 +467,7 @@ export function createAccountNameFlow(
442467
state.values.name = resolveApiKeyInstanceName(
443468
state.choice.id,
444469
slug,
445-
state.existingProviderNames,
470+
providerNameList(state.existingProviderNames),
446471
);
447472
}
448473
state.stepIndex += 1;
@@ -502,7 +527,10 @@ export function createAccountNameFlow(
502527
return;
503528
}
504529
handleNames(
505-
instanceSlugsForKind(state.choice.id, state.existingProviderNames),
530+
instanceSlugsForKind(
531+
state.choice.id,
532+
providerNameList(state.existingProviderNames),
533+
),
506534
);
507535
};
508536

‎src/tui/provider/types.ts‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,15 @@ import type { SetupStep } from "./steps.js";
3030

3131
export type OAuthKind = FirstClassOAuthProvider;
3232

33+
/** Catalog row the Meta OAuth slug guard can tell from an API key. */
34+
export type ExistingProviderRef =
35+
| string
36+
| {
37+
readonly name: string;
38+
readonly apiKey?: string;
39+
readonly metaProfile?: string;
40+
};
41+
3342
/**
3443
* A selectable provider. Preset rows carry everything the settings write needs
3544
* except the key; the custom row carries nothing and opens the manual form.
@@ -158,7 +167,7 @@ export interface ProviderSetupConfig {
158167
/** Prefill the OAuth account-name step with the profile slug being re-keyed. */
159168
readonly initialOAuthProfile?: string;
160169
/** Existing catalog keys, for suggested slugs and collision confirms on the API-key path. */
161-
readonly existingProviderNames?: readonly string[];
170+
readonly existingProviderNames?: readonly ExistingProviderRef[];
162171
}
163172

164173
/** Mutable state shared by the setup surface and its extracted flows. */
@@ -168,7 +177,7 @@ export interface SetupState {
168177
readonly externalRenderer: boolean;
169178
readonly clipboard: ClipboardPort;
170179
readonly choices: readonly ProviderChoice[];
171-
readonly existingProviderNames: readonly string[];
180+
readonly existingProviderNames: readonly ExistingProviderRef[];
172181
readonly values: ProviderFormValues;
173182
readonly margin: number;
174183
choice: ProviderChoice | null;

0 commit comments

Comments
 (0)