From 09ea233592eabf89bc94e12f22b2ca92e863c32f Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Mon, 19 Oct 2026 14:34:43 +0200 Subject: [PATCH] Token answers: an expires_in beyond a year is unusable, so the default lifetime applies A value like 3e11 overflowed the Date the expiry is stored as and surfaced as an untyped error. usableExpiresIn now caps a usable value at 365 days; the refresh path is pinned on memory and live Postgres. Co-Authored-By: Claude Opus 5.5 --- src/oauth.ts | 13 +++++++++++-- tests/managed-credential.test.ts | 21 +++++++++++++++++++-- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/src/oauth.ts b/src/oauth.ts index 2ad2c76..e5411ff 100644 --- a/src/oauth.ts +++ b/src/oauth.ts @@ -228,10 +228,19 @@ export async function requestProviderTokens( return tokenSet; } -/** A positive finite number, or a string that is a positive integer. */ +/** The longest access token lifetime taken at its word: a year. Anything above is not a lifetime. */ +const MAX_USABLE_EXPIRES_IN_SECONDS = 365 * 24 * 60 * 60; + +/** + * A positive finite number of seconds, or a string that is a positive integer, + * no longer than a year. A larger value would overflow the Date the expiry is + * stored as, so it is unusable and the descriptor's default lifetime applies. + */ function usableExpiresIn(value: unknown): number | undefined { const seconds = typeof value === "string" && /^\s*\d+\s*$/.test(value) ? Number(value) : value; - return typeof seconds === "number" && Number.isFinite(seconds) && seconds > 0 ? seconds : undefined; + return typeof seconds === "number" && Number.isFinite(seconds) && seconds > 0 && seconds <= MAX_USABLE_EXPIRES_IN_SECONDS + ? seconds + : undefined; } function sharedRuntimeExchange(provider: Provider): OAuthProviderRuntime["exchangeAuthorizationCode"] { diff --git a/tests/managed-credential.test.ts b/tests/managed-credential.test.ts index 4cd7396..8a9b8de 100644 --- a/tests/managed-credential.test.ts +++ b/tests/managed-credential.test.ts @@ -65,10 +65,23 @@ type Mode = | "no_expires_in" | "unusable_expires_in" | "string_expires_in" + | "huge_expires_in" + | "huge_string_expires_in" | "hang"; function expiresInFor(mode: Mode): unknown { - return mode === "unusable_expires_in" ? "soon" : mode === "string_expires_in" ? "7200" : 3600; + switch (mode) { + case "unusable_expires_in": + return "soon"; + case "string_expires_in": + return "7200"; + case "huge_expires_in": + return 3e11; + case "huge_string_expires_in": + return "300000000000"; + default: + return 3600; + } } /** @@ -122,7 +135,9 @@ function installTokenEndpoint(provider: Provider, firstRefreshToken: string) { case "normal": case "no_expires_in": case "unusable_expires_in": - case "string_expires_in": { + case "string_expires_in": + case "huge_expires_in": + case "huge_string_expires_in": { if (state.installationRevoked || presented !== state.live) { state.installationRevoked = true; return new Response(JSON.stringify({ error: "invalid_grant", error_description: "reused CANARY" }), { status: 400 }); @@ -487,6 +502,8 @@ for (const { kind, live } of rigKinds) describe.skipIf(live && !adminUrl)(`manag ["no expires_in", "no_expires_in", 3600], ["an unusable expires_in", "unusable_expires_in", 3600], ["a numeric-string expires_in", "string_expires_in", 7200], + ["a numeric expires_in beyond a year", "huge_expires_in", 3600], + ["a numeric-string expires_in beyond a year", "huge_string_expires_in", 3600], ] as const)( "refresh-once-per-token: an answer with %s stores now plus %s s, not the old due expiry, and resolve refreshes again only once it passes", timeout,