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
+16 -8
View File
@@ -29,7 +29,10 @@ import { createBrowserMutationIntentFactory } from "../adapters/platform/browser
import { createTelemetryAdapter } from "../adapters/telemetry/best-effort-telemetry.ts";
import type { AuthSessionPort } from "../application/ports/auth-session-port.ts";
import type { ReleaseInfo } from "../application/ports/release-info-port.ts";
import { createRestProviderProfile } from "../contracts/rest-profiles.ts";
import {
createRestProviderProfile,
INSTALLED_REST_AUTH_PROFILES,
} from "../contracts/rest-profiles.ts";
import type { ClockPort } from "../application/ports/clock-port.ts";
import type { MutationIntent } from "../contracts/mutation-intent.ts";
import { createInstalledFeatureInputs } from "../features/installed-feature-adapters.ts";
@@ -375,7 +378,10 @@ export async function createRuntimeAdapters(
baseUrl: config.API_BASE_URL,
maxRetryAttempts: config.MAX_RETRY_ATTEMPTS,
fetcher: context.fetcher,
async attachCredentials(operation) {
// §7.7. The installed registry owns Fetch credentials and the exact
// credential-header sets; this collaborator only supplies proof headers.
authProfiles: INSTALLED_REST_AUTH_PROFILES,
async attachCredentials(operation, authContext) {
if (serverStateScope.getPhase() !== "READY") {
return Object.freeze({ kind: "SCOPE_FENCED" as const });
}
@@ -387,18 +393,20 @@ export async function createRuntimeAdapters(
return Object.freeze({ kind: "UNAUTHENTICATED" as const });
}
try {
const patch = await authSession.credentialPatch({
origin: new URL(config.API_BASE_URL).origin,
method: operation.method,
operationId: operation.operationId,
});
const patch = await authSession.credentialPatch(
{
origin: new URL(config.API_BASE_URL).origin,
method: operation.method,
operationId: operation.operationId,
},
authContext,
);
if (serverStateScope.getPhase() !== "READY") {
return Object.freeze({ kind: "SCOPE_FENCED" as const });
}
return Object.freeze({
kind: "READY" as const,
headers: patch.headers,
credentials: "omit" as const,
});
} catch {
return Object.freeze({ kind: "UNAVAILABLE" as const });