FN-8713: handle missing auth-status query data
Keep auth-status available when direct callers omit credential-instance query data. - Default absent request query data to an empty selection. - Preserve validation and prevent dangling instances from falling back to other credentials. - Add coverage for omitted, valid, and dangling credential-instance queries. - Add a patch changeset for the auth-status fix. Files changed: .changeset/fn-8713-auth-status-query.md | 7 +++ .../register-auth-routes-hermes-additive.test.ts | 67 ++++++++++++++++++++-- .../dashboard/src/routes/register-auth-routes.ts | 9 ++- 3 files changed, 77 insertions(+), 6 deletions(-) Fusion-Task-Id: FN-8713 Fusion-Task-Lineage: 0bcb965e-0606-4c7b-aefc-e7e3461ae75d Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-8713-auth-status-query.md
Normal file
7
.changeset/fn-8713-auth-status-query.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
summary: Keep auth status available when credential-instance query data is absent.
|
||||
category: fix
|
||||
dev: Treats dangling requested instances as unauthenticated without credential fallback.
|
||||
@@ -1,4 +1,7 @@
|
||||
/*
|
||||
FNXC:ProviderAuth 2026-08-01-19:10:
|
||||
FN-8713 requires auth-status direct-handler fixtures with no `query` to behave like an empty Express query. A well-formed requested credential instance that is absent must report only that provider as unauthenticated, never borrow its default or sibling credential.
|
||||
|
||||
FNXC:ProviderAuth 2026-07-07-08:20:
|
||||
FN-7630 (GitHub #1931) item 3 coordination: the Hermes Runtime connection
|
||||
must never CAUSE the /api/auth/status provider surface to shrink. The
|
||||
@@ -15,7 +18,11 @@ import { describe, expect, it, vi } from "vitest";
|
||||
import type { Router } from "express";
|
||||
import { registerAuthRoutes } from "../routes/register-auth-routes.js";
|
||||
|
||||
function setup(oauthProviders: Array<{ id: string; name: string }>, apiKeyProviders: Array<{ id: string; name: string }>) {
|
||||
function setup(
|
||||
oauthProviders: Array<{ id: string; name: string }>,
|
||||
apiKeyProviders: Array<{ id: string; name: string }>,
|
||||
authStorageOverrides: Record<string, unknown> = {},
|
||||
) {
|
||||
const getHandlers = new Map<string, (req: unknown, res: { json: (body: unknown) => void }) => Promise<void>>();
|
||||
const postHandlers = new Map<string, unknown>();
|
||||
const router = {
|
||||
@@ -36,6 +43,7 @@ function setup(oauthProviders: Array<{ id: string; name: string }>, apiKeyProvid
|
||||
hasAuth: vi.fn(() => false),
|
||||
hasApiKey: vi.fn(() => false),
|
||||
get: vi.fn(() => undefined),
|
||||
...authStorageOverrides,
|
||||
};
|
||||
|
||||
const rethrowAsApiError = (err: unknown) => {
|
||||
@@ -58,10 +66,17 @@ function setup(oauthProviders: Array<{ id: string; name: string }>, apiKeyProvid
|
||||
return { handler: getHandlers.get("/auth/status")!, authStorage };
|
||||
}
|
||||
|
||||
async function callStatus(handler: (req: unknown, res: { json: (body: unknown) => void }) => Promise<void>) {
|
||||
async function callStatus(
|
||||
handler: (req: unknown, res: { json: (body: unknown) => void }) => Promise<void>,
|
||||
req: unknown = { headers: {} },
|
||||
) {
|
||||
const json = vi.fn();
|
||||
await handler({ headers: {} }, { json });
|
||||
return json.mock.calls[0][0] as { providers: Array<{ id: string; name: string }> };
|
||||
await handler(req, { json });
|
||||
return json.mock.calls[0][0] as {
|
||||
providers: Array<{ id: string; name: string; authenticated: boolean; instanceId?: string; instances?: unknown[] }>;
|
||||
ghCli: { available: boolean; authenticated: boolean };
|
||||
gitCli: { available: boolean };
|
||||
};
|
||||
}
|
||||
|
||||
describe("FN-7630: Hermes runtime additive — /api/auth/status", () => {
|
||||
@@ -105,4 +120,48 @@ describe("FN-7630: Hermes runtime additive — /api/auth/status", () => {
|
||||
expect(withHermes.providers.some((p) => p.id === id)).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it("treats omitted or empty query data as no selection while valid and dangling targets stay isolated", async () => {
|
||||
const { handler } = setup(
|
||||
[{ id: "github-copilot", name: "GitHub Copilot" }],
|
||||
[{ id: "openai", name: "OpenAI" }],
|
||||
{
|
||||
hasApiKey: vi.fn(() => true),
|
||||
getDefaultInstance: vi.fn((providerId: string) => providerId === "openai"
|
||||
? { providerId, instanceId: "default" }
|
||||
: undefined),
|
||||
listInstances: vi.fn((providerId: string) => providerId === "openai"
|
||||
? [{ providerId, instanceId: "default" }, { providerId, instanceId: "work" }]
|
||||
: []),
|
||||
getInstance: vi.fn((ref: { providerId: string; instanceId: string }) => ref.providerId === "openai" && ["default", "work"].includes(ref.instanceId)
|
||||
? { type: "api_key", key: ref.instanceId === "work" ? "sk-work-12345678" : "sk-default-87654321" }
|
||||
: undefined),
|
||||
},
|
||||
);
|
||||
|
||||
for (const query of [undefined, {}, { provider: undefined, instance: undefined }, { provider: "openai", instance: " " }]) {
|
||||
const response = await callStatus(handler, query === undefined ? { headers: {} } : { headers: {}, query });
|
||||
expect(response.providers.map((provider) => provider.id)).toEqual(expect.arrayContaining(["github-copilot", "openai"]));
|
||||
expect(response.ghCli).toEqual(expect.objectContaining({ available: expect.any(Boolean) }));
|
||||
expect(response.gitCli).toEqual(expect.objectContaining({ available: expect.any(Boolean) }));
|
||||
}
|
||||
|
||||
const defaultTarget = await callStatus(handler, { headers: {}, query: { provider: "openai", instance: "default" } });
|
||||
expect(defaultTarget.providers.find((provider) => provider.id === "openai")).toMatchObject({ authenticated: true, instanceId: "default" });
|
||||
|
||||
const validTarget = await callStatus(handler, { headers: {}, query: { provider: "openai", instance: "work" } });
|
||||
expect(validTarget.providers.find((provider) => provider.id === "openai")).toMatchObject({ authenticated: true, instanceId: "work" });
|
||||
expect(validTarget.providers.find((provider) => provider.id === "github-copilot")).toMatchObject({ authenticated: false });
|
||||
|
||||
const danglingTarget = await callStatus(handler, { headers: {}, query: { provider: "openai", instance: "missing" } });
|
||||
expect(danglingTarget.providers.find((provider) => provider.id === "openai")).toMatchObject({
|
||||
authenticated: false,
|
||||
instanceId: "missing",
|
||||
instances: [],
|
||||
});
|
||||
expect(danglingTarget.providers.find((provider) => provider.id === "github-copilot")).toMatchObject({ authenticated: false });
|
||||
|
||||
await expect(callStatus(handler, { headers: {}, query: { instance: "work" } })).rejects.toThrow("instance requires provider");
|
||||
await expect(callStatus(handler, { headers: {}, query: { provider: "openai", instance: "invalid instance" } })).rejects.toThrow("instance must be a valid provider instance id");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -636,8 +636,13 @@ export const registerAuthRoutes: ApiRouteRegistrar = (ctx) => {
|
||||
try {
|
||||
const origin = typeof req.headers.origin === "string" ? req.headers.origin : undefined;
|
||||
const storage = getAuthStorage();
|
||||
const requestedProvider = typeof req.query.provider === "string" ? req.query.provider : undefined;
|
||||
const rawRequestedInstance = typeof req.query.instance === "string" ? req.query.instance : undefined;
|
||||
/*
|
||||
FNXC:ProviderAuth 2026-08-01-19:10:
|
||||
FN-8713: Express normally supplies `req.query`, but direct route consumers can omit it. Treat an absent query as empty while retaining instance validation; a well-formed dangling instance stays unauthenticated below and must never fall back to the provider default.
|
||||
*/
|
||||
const query = req.query ?? {};
|
||||
const requestedProvider = typeof query.provider === "string" ? query.provider : undefined;
|
||||
const rawRequestedInstance = typeof query.instance === "string" ? query.instance : undefined;
|
||||
if (rawRequestedInstance?.trim() && !requestedProvider) throw badRequest("instance requires provider");
|
||||
if (rawRequestedInstance?.trim() && !isValidProviderInstanceId(rawRequestedInstance.trim())) throw badRequest("instance must be a valid provider instance id");
|
||||
if (rawRequestedInstance?.trim() && requestedProvider && syntheticCliProviderIds.has(requestedProvider)) throw badRequest("CLI providers do not support credential instances");
|
||||
|
||||
Reference in New Issue
Block a user