Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion src/oauth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import { detectClaudeCodeToken, detectGrokCliToken, hasComparableGrokIdentity, i
import { logOAuthEvent } from "./log";
import { captureConfigGeneration, sweepExpiredOnWrite, type GenerationContext } from "../lib/state-store-sweeper";
import { retainedUtf8Bytes } from "../lib/admission";
import { randomUUID } from "node:crypto";
import { createHash, randomUUID } from "node:crypto";
export {
CODEX_HEALTH_AUTH_FAILED_NOTE,
CODEX_HEALTH_MANAGEMENT_API_UNAVAILABLE_NOTE,
Expand Down Expand Up @@ -52,6 +52,8 @@ import { codexAccountNamespaceProviderCollisionError } from "../codex/account-na
const REFRESH_SKEW_MS = 60_000;
export interface OAuthAccessSnapshot {
provider: string;
/** Stable pseudonymous subject for account-bound continuation; absent without an immutable account id. */
credentialSubjectHash?: string;
accountId: string;
generation: string;
accessToken: string;
Expand Down Expand Up @@ -299,13 +301,30 @@ export class OAuthLoginRequiredError extends Error {
}

function accessSnapshot(provider: string, accountId: string, cred: OAuthCredentials): OAuthAccessSnapshot {
// Email is not an immutable upstream subject: multiple accounts may share or later reuse it.
// Fail closed unless the provider supplied its stable account id. Preserve the exact stored
// subject because the credential store also matches it exactly; trimming only here would make
// two distinct slots share one continuation owner.
const stableCredentialSubject = cred.accountId?.trim()
? ["account-id", cred.accountId]
: undefined;
const storedKiroRouting = {
...(cred.kiro?.profileArn ? { profileArn: cred.kiro.profileArn } : {}),
...(cred.kiro?.apiRegion ? { apiRegion: cred.kiro.apiRegion } : {}),
...(cred.kiro?.ssoRegion ? { ssoRegion: cred.kiro.ssoRegion } : {}),
};
return {
provider,
...(stableCredentialSubject
? {
credentialSubjectHash: createHash("sha256")
.update("opencodex-oauth-credential-subject\0")
.update(provider)
.update("\0")
.update(JSON.stringify(stableCredentialSubject))
.digest("hex"),
}
: {}),
accountId,
generation: credentialGeneration(cred),
accessToken: cred.access,
Expand Down
22 changes: 22 additions & 0 deletions src/responses/provider-continuation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { OcxProviderContinuationOwner } from "../types";

const bounded = (value: unknown, max: number): value is string =>
typeof value === "string" && value.length > 0 && value.length <= max;

/** Validate proxy-authored continuation ownership before trusting persisted state. */
export function isValidProviderContinuationOwner(
value: unknown,
): value is OcxProviderContinuationOwner {
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
const owner = value as Record<string, unknown>;
return owner.version === 1
&& bounded(owner.providerName, 256)
&& typeof owner.providerDestinationIdentity === "string"
&& /^destination:[0-9a-f]{64}$/.test(owner.providerDestinationIdentity)
&& bounded(owner.adapterName, 128)
&& bounded(owner.modelId, 512)
&& typeof owner.credentialIdentity === "string"
&& /^(key|oauth|codex|oauth-account|forward-account):[0-9a-f]{64}$/.test(
owner.credentialIdentity,
);
}
7 changes: 7 additions & 0 deletions src/responses/reasoning-replay-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,13 @@ function credentialHeaderOverrides(headers: Record<string, string> | undefined):
));
}

/** True when provider headers change the physical credential/account boundary. */
export function reasoningReplayHasCredentialHeaderOverrides(
headers: Record<string, string> | undefined,
): boolean {
return credentialHeaderOverrides(headers).length > 0;
}

/** Produce a non-reversible process-local identity for an exact upstream destination. */
export function reasoningReplayDestinationIdentity(baseUrl: string | undefined): string | undefined {
if (!nonEmpty(baseUrl)) return undefined;
Expand Down
7 changes: 6 additions & 1 deletion src/responses/spill-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { join } from "node:path";
import { getConfigDir } from "../config";
import { forgetEphemeralSecretPath, forgetHardenedSecretPath, hardenSecretDir, hardenSecretPath } from "../lib/windows-secret-acl";
import type { OcxProviderContinuationState } from "../types";
import { isValidProviderContinuationOwner } from "./provider-continuation";

export const RESPONSE_SPILL_VERSION = 1;
export const RESPONSE_SPILL_DIR_NAME = "responses-state-spill";
Expand Down Expand Up @@ -287,7 +288,11 @@ function validPayload(value: unknown, responseId: string): value is ResponseSpil
}
if (payload.providers !== undefined) {
if (!payload.providers || typeof payload.providers !== "object" || Array.isArray(payload.providers)) return false;
for (const providerState of Object.values(payload.providers)) {
const providers = payload.providers as Record<string, unknown>;
if (providers.__ocxOwner !== undefined
&& !isValidProviderContinuationOwner(providers.__ocxOwner)) return false;
for (const [provider, providerState] of Object.entries(providers)) {
if (provider === "__ocxOwner") continue;
if (!providerState || typeof providerState !== "object" || Array.isArray(providerState)) return false;
}
}
Expand Down
11 changes: 11 additions & 0 deletions src/responses/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1059,6 +1059,17 @@ export function previousResponseReplayPrefixLength(body: unknown): number {
return replayedInputPrefixLengths.get(body) ?? 0;
}

/** Copy proxy-private replay provenance to an internal clone with the same materialized input. */
export function copyPreviousResponseReplayProvenance(source: unknown, target: unknown): void {
if (!source || typeof source !== "object" || Array.isArray(source)) return;
if (!target || typeof target !== "object" || Array.isArray(target)) return;
const prefixLength = replayedInputPrefixLengths.get(source);
if (!prefixLength) return;
const input = (target as { input?: unknown }).input;
if (!Array.isArray(input) || prefixLength > input.length) return;
replayedInputPrefixLengths.set(target, prefixLength);
}

/** True when a stale or foreign previous_response_id was removed from this exact request body. */
export function previousResponseScopeMismatch(body: unknown): boolean {
return !!body && typeof body === "object" && replayScopeMismatches.has(body as object);
Expand Down
Loading
Loading