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:
gsxdsm
2026-08-01 12:53:07 -07:00
parent 494362cd23
commit 5d06c92b77
3 changed files with 77 additions and 6 deletions

View 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.

View File

@@ -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");
});
});

View File

@@ -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");