fix: enforce installed HTTP auth profiles

Install the REST auth profile registry once at composition and make it the
single transport authority for V3. Contract composition now rejects an
unregistered authProfileId, so the executor never resolves a profile at
runtime.

The credential collaborator contributes proof headers only: Fetch credentials
come from the resolved profile, transport-owned and forbidden headers are
rejected, headers outside the profile's allowed set are rejected, and a missing
required header fails closed as AUTH_INTEGRATION_FAILURE with zero fetch calls.
The final invariant re-proves credentials mode and the exact header sets.

Demo mode satisfies the strict bearer profile with a fixed non-secret marker
instead of weakening REFERENCE_EXTERNAL_BEARER. Credential owners now receive
the operation lifetime through AuthOperationContext.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
DongHyeonka
2026-08-13 22:56:44 +09:00
co-authored by Claude Opus 5
parent 67cc5b6d2c
commit 4e87bacdf3
21 changed files with 5045 additions and 41 deletions
+2 -2
View File
@@ -61,7 +61,7 @@ const readPolicy = Object.freeze({
responseByteLimit: 32_768,
totalDeadlineMs: 10_000,
retryBudget: 2 as const,
authProfileId: "TEST_AUTH",
authProfileId: "ANONYMOUS",
diagnosticsOperation: "test.read",
});
@@ -192,7 +192,7 @@ export const TEST_CREATE_HTTP_CONTRACT: InstalledHttpContract<
responseByteLimit: 32_768,
totalDeadlineMs: 10_000,
retryBudget: 0 as const,
authProfileId: "TEST_AUTH",
authProfileId: "ANONYMOUS",
diagnosticsOperation: "test.create",
}),
});
@@ -0,0 +1,289 @@
import { describe, expect, it, vi } from "vitest";
import { createContractHttpExecutor } from "../../src/adapters/http/http-execution-v3.ts";
import {
composeContractContributions,
ContractContributionError,
} from "../../src/contracts/external-contract-runtime.ts";
import {
installRestAuthProfileRegistry,
REST_AUTH_PROFILES,
} from "../../src/contracts/rest-profiles.ts";
import {
TEST_CONTRACT_CONTRIBUTION,
TEST_LIST_HTTP_CONTRACT,
} from "../helpers/external-contract-fixture.ts";
const ROUTE_ID = "TEST_ROUTE";
/**
* The shipped fixture policy declares `TEST_AUTH`; the installed registry owns
* the concrete transport rules, so this suite installs an equivalent bearer
* profile under that identity.
*/
const TEST_PROFILES = installRestAuthProfileRegistry({
TEST_AUTH: {
authProfileId: "TEST_AUTH",
transport: "BEARER_HEADER",
credentials: "omit",
allowedCredentialHeaders: ["authorization"],
requiredCredentialHeaders: ["authorization"],
},
TEST_ANONYMOUS: {
authProfileId: "TEST_ANONYMOUS",
transport: "ANONYMOUS",
credentials: "omit",
allowedCredentialHeaders: [],
requiredCredentialHeaders: [],
},
});
function scopeSnapshot(isCurrent: () => boolean = () => true) {
return Object.freeze({
generation: 1,
fingerprint: "scope-1",
identities: Object.freeze({}) as never,
signal: new AbortController().signal,
isCurrent,
});
}
function operationWithProfile(authProfileId: string, deadlineMs?: number) {
return {
...TEST_LIST_HTTP_CONTRACT,
frontend: {
...TEST_LIST_HTTP_CONTRACT.frontend,
authProfileId,
...(deadlineMs === undefined ? {} : { totalDeadlineMs: deadlineMs }),
},
};
}
const bearerOperation = () => operationWithProfile("TEST_AUTH");
const anonymousOperation = () => operationWithProfile("TEST_ANONYMOUS");
describe("V3 installed auth profile authority", () => {
it("rejects an unknown auth profile during composition", () => {
const contribution = {
...TEST_CONTRACT_CONTRIBUTION,
http: [
{
...TEST_LIST_HTTP_CONTRACT,
frontend: {
...TEST_LIST_HTTP_CONTRACT.frontend,
authProfileId: "NO_SUCH_PROFILE",
},
},
] as unknown as readonly never[],
};
let thrown: unknown;
try {
composeContractContributions([contribution] as never);
} catch (error) {
thrown = error;
}
expect(thrown).toBeInstanceOf(ContractContributionError);
expect((thrown as ContractContributionError).reason).toContain(
"unknown authProfileId NO_SUCH_PROFILE",
);
expect(() =>
installRestAuthProfileRegistry({
BROKEN: {
authProfileId: "BROKEN",
transport: "BEARER_HEADER",
credentials: "include",
allowedCredentialHeaders: [],
requiredCredentialHeaders: ["authorization"],
},
}),
).toThrow(TypeError);
});
it("rejects credential attempts to replace Accept Content-Type or credentials", async () => {
for (const hostileHeaders of [
{ accept: "text/plain" },
{ "content-type": "text/plain" },
{ cookie: "session=1" },
]) {
const fetcher = vi.fn();
const executor = createContractHttpExecutor({
baseUrl: "https://api.example/",
maxRetryAttempts: 0,
authProfiles: TEST_PROFILES,
attachCredentials: () =>
({
kind: "READY" as const,
headers: {
authorization: "Bearer token",
...hostileHeaders,
},
}) as never,
fetcher: fetcher as unknown as typeof fetch,
});
const outcome = await executor.execute(
bearerOperation(),
{ limit: 5 },
{ routeId: ROUTE_ID, scope: scopeSnapshot() },
);
expect(outcome).toMatchObject({
kind: "AUTH_INTEGRATION_FAILURE",
effect: "NOT_APPLICABLE",
});
expect(fetcher).not.toHaveBeenCalled();
}
});
it("requires authorization for a bearer profile before fetch", async () => {
const fetcher = vi.fn();
const executor = createContractHttpExecutor({
baseUrl: "https://api.example/",
maxRetryAttempts: 0,
authProfiles: TEST_PROFILES,
attachCredentials: () => ({
kind: "READY" as const,
headers: {},
}),
fetcher: fetcher as unknown as typeof fetch,
});
const outcome = await executor.execute(
bearerOperation(),
{ limit: 5 },
{ routeId: ROUTE_ID, scope: scopeSnapshot() },
);
expect(outcome).toMatchObject({
kind: "AUTH_INTEGRATION_FAILURE",
reason: "MISSING_REQUIRED_CREDENTIAL_HEADER",
});
expect(fetcher).not.toHaveBeenCalled();
});
it("forbids credential headers for an anonymous profile", async () => {
const withCredential = vi.fn();
const rejected = createContractHttpExecutor({
baseUrl: "https://api.example/",
maxRetryAttempts: 0,
authProfiles: TEST_PROFILES,
attachCredentials: () => ({
kind: "READY" as const,
headers: { authorization: "Bearer token" },
}),
fetcher: withCredential as unknown as typeof fetch,
});
const rejectedOutcome = await rejected.execute(
anonymousOperation(),
{ limit: 5 },
{ routeId: ROUTE_ID, scope: scopeSnapshot() },
);
expect(rejectedOutcome).toMatchObject({
kind: "AUTH_INTEGRATION_FAILURE",
reason: "CREDENTIAL_HEADER_NOT_ALLOWED",
});
expect(withCredential).not.toHaveBeenCalled();
const observed: Array<Readonly<Record<string, string>>> = [];
const accepted = createContractHttpExecutor({
baseUrl: "https://api.example/",
maxRetryAttempts: 0,
authProfiles: TEST_PROFILES,
attachCredentials: () => ({ kind: "READY" as const, headers: {} }),
fetcher: (async (_input: RequestInfo | URL, init?: RequestInit) => {
const headers = new Headers(init?.headers);
observed.push(
Object.freeze(Object.fromEntries(headers.entries())),
);
expect(init?.credentials).toBe("omit");
return Response.json([]);
}) as unknown as typeof fetch,
});
const acceptedOutcome = await accepted.execute(
anonymousOperation(),
{ limit: 5 },
{ routeId: ROUTE_ID, scope: scopeSnapshot() },
);
expect(acceptedOutcome.kind).toBe("SUCCESS");
expect(observed).toHaveLength(1);
expect(observed[0]?.authorization).toBeUndefined();
expect(observed[0]?.accept).toBe("application/json");
});
it("bounds a non-cooperative credential owner by the operation lifetime", async () => {
const fetcher = vi.fn();
let observedSignal: AbortSignal | undefined;
const executor = createContractHttpExecutor({
baseUrl: "https://api.example/",
maxRetryAttempts: 0,
authProfiles: TEST_PROFILES,
attachCredentials: (_operation, context) => {
observedSignal = context.signal;
// Non-cooperative: it never settles on its own.
return new Promise<never>(() => {});
},
fetcher: fetcher as unknown as typeof fetch,
});
const outcome = await executor.execute(
operationWithProfile("TEST_AUTH", 10),
{ limit: 5 },
{ routeId: ROUTE_ID, scope: scopeSnapshot() },
);
expect(outcome).toMatchObject({
kind: "TRANSPORT_FAILURE",
failure: { kind: "TIMEOUT" },
});
expect(fetcher).not.toHaveBeenCalled();
expect(observedSignal?.aborted).toBe(true);
});
it("ignores a late credential completion after caller abort or scope fence", async () => {
const fetcher = vi.fn();
let release: ((patch: unknown) => void) | undefined;
const caller = new AbortController();
const executor = createContractHttpExecutor({
baseUrl: "https://api.example/",
maxRetryAttempts: 0,
authProfiles: TEST_PROFILES,
attachCredentials: () =>
new Promise((resolve) => {
release = resolve as (patch: unknown) => void;
}) as never,
fetcher: fetcher as unknown as typeof fetch,
});
const execution = executor.execute(
bearerOperation(),
{ limit: 5 },
{
routeId: ROUTE_ID,
scope: scopeSnapshot(),
signal: caller.signal,
},
);
await Promise.resolve();
caller.abort();
const outcome = await execution;
expect(outcome.kind).toBe("CANCELLED");
release?.({
kind: "READY",
headers: { authorization: "Bearer late" },
});
await Promise.resolve();
expect(fetcher).not.toHaveBeenCalled();
});
it("keeps the shipped bearer profile strict", () => {
expect(REST_AUTH_PROFILES.REFERENCE_EXTERNAL_BEARER).toMatchObject({
transport: "BEARER_HEADER",
credentials: "omit",
requiredCredentialHeaders: ["authorization"],
});
expect(REST_AUTH_PROFILES.ANONYMOUS.requiredCredentialHeaders).toEqual([]);
});
});
@@ -231,10 +231,11 @@ async function executeScenario(
const executor = createContractHttpExecutor({
baseUrl: "https://api.test",
maxRetryAttempts: 2,
// The reference contribution declares REFERENCE_EXTERNAL_BEARER, so the
// credential owner must supply the required proof header before dispatch.
attachCredentials: () => ({
kind: "READY",
headers: {},
credentials: "omit",
headers: { authorization: "Bearer scenario-token" },
}),
fetcher,
random: () => 0,