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>
290 lines
8.8 KiB
TypeScript
290 lines
8.8 KiB
TypeScript
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([]);
|
|
});
|
|
});
|