different-ai/openwork · error · EnterpriseMcpOAuthContractError
MCP_OAUTH_PERSISTENCE_INVALID
MCP_OAUTH_PERSISTENCE_INVALID
Error message
The OAuth persistence adapter returned an invalid ${field}. What it means
assertFiniteEpoch() rejected a numeric epoch value produced by the OAuth persistence layer because it was not a finite, non-negative number. The error (EnterpriseMcpOAuthContractError, code MCP_OAUTH_PERSISTENCE_INVALID) names the offending field via the message (e.g. "client expiration" or "token expiration"). The library treats NaN/Infinity/negative expiration epochs as a contract violation by the persistence adapter, since such values cannot be compared against clock.now().
Source
Thrown at packages/enterprise-mcp-client/src/oauth-provider.ts:42
import { isAuthorizationServerDiscoveryBound } from "./oauth-discovery-binding.js"
type OAuthFlowContext =
| { kind: "connect"; authorizationId?: string }
| { kind: "callback"; authorizationId: string }
| { kind: "runtime" }
type VerifiedOAuthDiscoveryState = OAuthDiscoveryState & {
openworkMetadataVerification?: {
version: 1
issuer: string
}
}
const oauthClientInformationMixedSchema = OAuthClientInformationFullSchema.or(OAuthClientInformationSchema)
function assertFiniteEpoch(value: number, field: string): number {
if (!Number.isFinite(value) || value < 0) {
throw new EnterpriseMcpOAuthContractError(
"MCP_OAUTH_PERSISTENCE_INVALID",
`The OAuth persistence adapter returned an invalid ${field}.`,
)
}
return value
}
function clientExpiration(clientInformation: StoredOAuthClientInformation): number | undefined {
const parsed = OAuthClientInformationFullSchema.safeParse(clientInformation)
const seconds = parsed.success ? parsed.data.client_secret_expires_at : undefined
if (seconds === undefined || seconds === 0) return undefined
return assertFiniteEpoch(seconds * 1_000, "client expiration")
}
function tokenExpiration(tokens: StoredOAuthTokens, now: number): number | undefined {
if (tokens.expires_in === undefined) return undefined
if (!Number.isFinite(tokens.expires_in) || tokens.expires_in < 0) {
throw new EnterpriseMcpOAuthContractError(View on GitHub (pinned to 2b7df46e8a)
Solutions
- Fix the persistence adapter to store expiresAt / client_secret_expires_at as finite epoch numbers (milliseconds for record.expiresAt, seconds for client_secret_expires_at).
- Use undefined (not -1/0-sentinels or NaN) when there is no expiration.
- Validate/sanitize the numeric values when writing records, not just when reading.
- Inspect the stored record for the named field to confirm it is a finite number ≥ 0.
Example fix
// before (persistence adapter) expiresAt: row.expires_at === null ? -1 : Date.parse(row.expires_at) // after expiresAt: row.expires_at === null ? undefined : Date.parse(row.expires_at)
Defensive patterns
Strategy: validation
Validate before calling
function isValidEpochMs(v: unknown): v is number {
return typeof v === "number" && Number.isFinite(v) && v >= 0;
}
// run on expiresAt / client_secret_expires_at before persisting Type guard
function isFiniteEpoch(v: unknown): v is number {
return typeof v === "number" && Number.isFinite(v) && v >= 0;
} Try / catch
try {
await client.connect(input);
} catch (e) {
if (e.code === "MCP_OAUTH_PERSISTENCE_INVALID") {
// audit the stored OAuth records for the named field and clear the corrupt entry
}
throw e;
} Prevention
- Enforce numeric finite epoch columns (NOT NULL numeric) in the persistence store.
- Use undefined — never -1 or NaN — for "no expiration".
- Unit-test the persistence adapter's save/load round-trip for expiration fields.
- Validate records at write time, not just at read time.
When it happens
Trigger: clientExpiration() computes client_secret_expires_at * 1000 into a non-finite/negative value and passes it to assertFiniteEpoch; or the credentials record's expiresAt loaded from persistence is NaN/Infinity/negative (reached via tokenExpiration / clientInformation / tokens).
Common situations: Persistence adapter stores dates as strings or null and a numeric coercion yields NaN; clock skew or corrupted records producing negative epochs; client_secret_expires_at stored in ms instead of s (huge but finite — usually fine) or as a sentinel like -1 for "never".
Related errors
- `${t("providers.no_oauth_prefix")} ${resolved}. ${t("provide
- `${t("providers.not_oauth_flow_prefix")} ${resolved}.`
- t("providers.oauth_method_required")
- OpenWork-managed OAuth requires a remote MCP URL.
- An enterprise MCP OAuth redirect URI must use HTTP or HTTPS.
AI-assisted analysis of different-ai/openwork@2b7df46e8a (2026-09-01).
Data as JSON: /api/errors/024640a03cc15046.
Report an issue: GitHub.