Merge remote-tracking branch 'origin/dev' into cl/romantic-mendel-5a2c25

This commit is contained in:
Bilal Godil 2026-05-26 13:40:34 -07:00
commit d59cc378e8
39 changed files with 126 additions and 48 deletions

View File

@ -1,6 +1,6 @@
{
"name": "@stackframe/backend",
"version": "2.8.105",
"version": "2.8.106",
"repository": "https://github.com/hexclave/hexclave",
"private": true,
"type": "module",

View File

@ -13,7 +13,7 @@ import { InvalidClientError, InvalidScopeError, Request as OAuthRequest, Respons
import { KnownError, KnownErrors } from "@stackframe/stack-shared";
import { yupMixed, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields";
import { HexclaveAssertionError, StatusError, captureError } from "@stackframe/stack-shared/dist/utils/errors";
import { deindent, extractScopes } from "@stackframe/stack-shared/dist/utils/strings";
import { deindent, extractScopes, mergeScopeStrings } from "@stackframe/stack-shared/dist/utils/strings";
import { cookies } from "next/headers";
import { redirect } from "next/navigation";
import { oauthResponseToSmartResponse } from "../../oauth-helpers";
@ -224,12 +224,13 @@ const handler = createSmartRouteHandler({
});
const storeTokens = async (oauthAccountId: string) => {
const tokenScopes = extractScopes(mergeScopeStrings(providerObj.scope, providerScope ?? ""));
if (tokenSet.refreshToken) {
await prisma.oAuthToken.create({
data: {
tenancyId: outerInfo.tenancyId,
refreshToken: tokenSet.refreshToken,
scopes: extractScopes(providerObj.scope + " " + providerScope),
scopes: tokenScopes,
oauthAccountId,
}
});
@ -239,7 +240,7 @@ const handler = createSmartRouteHandler({
data: {
tenancyId: outerInfo.tenancyId,
accessToken: tokenSet.accessToken,
scopes: extractScopes(providerObj.scope + " " + providerScope),
scopes: tokenScopes,
expiresAt: tokenSet.accessTokenExpiredAt,
oauthAccountId,
}

View File

@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { getOAuthAccessTokenRefreshError, getOAuthAccessTokenRefreshErrorDisposition, isRetryableOAuthUserInfoError } from "./base";
import { getOAuthAccessTokenRefreshError, getOAuthAccessTokenRefreshErrorDisposition, isRetryableOAuthUserInfoError, resolveOAuthAccessTokenExpiredAt } from "./base";
describe("isRetryableOAuthUserInfoError", () => {
it("returns true for openid-client timeout errors", () => {
@ -101,3 +101,32 @@ describe("getOAuthAccessTokenRefreshError", () => {
});
});
});
describe("resolveOAuthAccessTokenExpiredAt", () => {
it("uses finite provider expires_in values", () => {
expect(resolveOAuthAccessTokenExpiredAt({
expiresInSeconds: 120,
expiresAtSeconds: undefined,
defaultExpiresInMillis: null,
nowMillis: 1000,
})?.toISOString()).toBe("1970-01-01T00:02:01.000Z");
});
it("ignores non-finite provider expires_at values and uses explicit null defaults", () => {
expect(resolveOAuthAccessTokenExpiredAt({
expiresInSeconds: undefined,
expiresAtSeconds: Number.NaN,
defaultExpiresInMillis: null,
nowMillis: 1000,
})).toBeNull();
});
it("ignores non-finite provider expiry values and falls back to one hour", () => {
expect(resolveOAuthAccessTokenExpiredAt({
expiresInSeconds: Number.NaN,
expiresAtSeconds: Number.NaN,
defaultExpiresInMillis: undefined,
nowMillis: 1000,
})?.toISOString()).toBe("1970-01-01T01:00:01.000Z");
});
});

View File

@ -219,6 +219,48 @@ export function getOAuthAccessTokenRefreshError(error: unknown, options: {
type DefaultAccessTokenExpiresInMillis = number | null | ((tokenSet: OIDCTokenSet) => number | null | undefined);
function getFiniteNumber(value: unknown): number | undefined {
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
}
function dateFromMillis(millis: number, context: string): Date {
const date = new Date(millis);
if (!Number.isFinite(date.getTime())) {
throw new HexclaveAssertionError(`Invalid OAuth access token expiry computed from ${context}`, { millis });
}
return date;
}
export function resolveOAuthAccessTokenExpiredAt(options: {
expiresInSeconds: unknown,
expiresAtSeconds: unknown,
defaultExpiresInMillis: number | null | undefined,
nowMillis: number,
}): Date | null {
const expiresInSeconds = getFiniteNumber(options.expiresInSeconds);
if (expiresInSeconds !== undefined) {
return dateFromMillis(options.nowMillis + expiresInSeconds * 1000, "expires_in");
}
const expiresAtSeconds = getFiniteNumber(options.expiresAtSeconds);
if (expiresAtSeconds !== undefined) {
return dateFromMillis(expiresAtSeconds * 1000, "expires_at");
}
if (options.defaultExpiresInMillis === null) {
return null;
}
if (options.defaultExpiresInMillis !== undefined) {
if (!Number.isFinite(options.defaultExpiresInMillis)) {
throw new HexclaveAssertionError("Invalid default OAuth access token expiry", { defaultExpiresInMillis: options.defaultExpiresInMillis });
}
return dateFromMillis(options.nowMillis + options.defaultExpiresInMillis, "provider default");
}
return dateFromMillis(options.nowMillis + 3600 * 1000, "generic fallback");
}
function processTokenSet(providerName: string, tokenSet: OIDCTokenSet, defaultAccessTokenExpiresInMillis?: DefaultAccessTokenExpiresInMillis): TokenSet {
if (!tokenSet.access_token) {
throw new HexclaveAssertionError(`No access token received from ${providerName}.`, { tokenSet, providerName });
@ -230,7 +272,14 @@ function processTokenSet(providerName: string, tokenSet: OIDCTokenSet, defaultAc
// one-hour fallback and capture telemetry.
const defaultExpiresInMillis = typeof defaultAccessTokenExpiresInMillis === "function" ? defaultAccessTokenExpiresInMillis(tokenSet) : defaultAccessTokenExpiresInMillis;
if (tokenSet.expires_in == null && tokenSet.expires_at == null && defaultExpiresInMillis === undefined) {
const hasInvalidProviderExpiry =
(tokenSet.expires_in != null && getFiniteNumber(tokenSet.expires_in) === undefined)
|| (tokenSet.expires_at != null && getFiniteNumber(tokenSet.expires_at) === undefined);
if (hasInvalidProviderExpiry) {
captureError("processTokenSet", new HexclaveAssertionError(`Invalid expires_in or expires_at received from OAuth provider ${providerName}. Falling back to provider/default expiry handling`, { tokenSetKeys: Object.keys(tokenSet) }));
}
if (getFiniteNumber(tokenSet.expires_in) === undefined && getFiniteNumber(tokenSet.expires_at) === undefined && defaultExpiresInMillis === undefined) {
captureError("processTokenSet", new HexclaveAssertionError(`No expires_in or expires_at received from OAuth provider ${providerName}. Falling back to 1h`, { tokenSetKeys: Object.keys(tokenSet) }));
}
@ -238,13 +287,12 @@ function processTokenSet(providerName: string, tokenSet: OIDCTokenSet, defaultAc
idToken: tokenSet.id_token,
accessToken: tokenSet.access_token,
refreshToken: tokenSet.refresh_token,
accessTokenExpiredAt: tokenSet.expires_in != null ?
new Date(Date.now() + tokenSet.expires_in * 1000) :
tokenSet.expires_at != null ? new Date(tokenSet.expires_at * 1000) :
defaultExpiresInMillis === null ? null :
defaultExpiresInMillis !== undefined ?
new Date(Date.now() + defaultExpiresInMillis) :
new Date(Date.now() + 3600 * 1000),
accessTokenExpiredAt: resolveOAuthAccessTokenExpiredAt({
expiresInSeconds: tokenSet.expires_in,
expiresAtSeconds: tokenSet.expires_at,
defaultExpiresInMillis,
nowMillis: Date.now(),
}),
};
}

View File

@ -1,6 +1,6 @@
{
"name": "@stackframe/dashboard",
"version": "2.8.105",
"version": "2.8.106",
"repository": "https://github.com/hexclave/hexclave",
"private": true,
"scripts": {

View File

@ -1,6 +1,6 @@
{
"name": "@stackframe/dev-launchpad",
"version": "2.8.105",
"version": "2.8.106",
"repository": "https://github.com/hexclave/hexclave",
"private": true,
"scripts": {

View File

@ -1,6 +1,6 @@
{
"name": "@stackframe/e2e-tests",
"version": "2.8.105",
"version": "2.8.106",
"repository": "https://github.com/hexclave/hexclave",
"private": true,
"type": "module",

View File

@ -1,7 +1,7 @@
{
"name": "@stackframe/hosted-components",
"private": true,
"version": "2.8.105",
"version": "2.8.106",
"type": "module",
"scripts": {
"dev": "vite dev --port ${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}09",

View File

@ -1,7 +1,7 @@
{
"name": "@stackframe/internal-tool",
"private": true,
"version": "2.8.105",
"version": "2.8.106",
"type": "module",
"scripts": {
"dev": "node scripts/pre-dev.mjs && next dev --turbopack --port ${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}41",

View File

@ -1,6 +1,6 @@
{
"name": "@stackframe/mcp",
"version": "2.8.105",
"version": "2.8.106",
"repository": "https://github.com/hexclave/hexclave",
"private": true,
"type": "module",

View File

@ -1,6 +1,6 @@
{
"name": "@stackframe/mock-oauth-server",
"version": "2.8.105",
"version": "2.8.106",
"repository": "https://github.com/hexclave/hexclave",
"private": true,
"main": "index.js",

View File

@ -1,6 +1,6 @@
{
"name": "@stackframe/skills",
"version": "2.8.105",
"version": "2.8.106",
"repository": "https://github.com/hexclave/hexclave",
"private": true,
"type": "module",

View File

@ -1,6 +1,6 @@
{
"name": "@stackframe/docs-mintlify",
"version": "2.8.105",
"version": "2.8.106",
"private": true,
"scripts": {
"dev": "mint dev --port ${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}04 --no-open",

View File

@ -1,6 +1,6 @@
{
"name": "@stackframe/stack-docs",
"version": "2.8.105",
"version": "2.8.106",
"repository": "https://github.com/hexclave/stack-auth",
"description": "",
"main": "index.js",

View File

@ -1,6 +1,6 @@
{
"name": "@stackframe/example-cjs-test",
"version": "2.8.105",
"version": "2.8.106",
"repository": "https://github.com/hexclave/hexclave",
"private": true,
"scripts": {

View File

@ -1,6 +1,6 @@
{
"name": "@stackframe/convex-example",
"version": "2.8.105",
"version": "2.8.106",
"repository": "https://github.com/hexclave/hexclave",
"private": true,
"scripts": {

View File

@ -1,6 +1,6 @@
{
"name": "@stackframe/example-demo-app",
"version": "2.8.105",
"version": "2.8.106",
"repository": "https://github.com/hexclave/hexclave",
"description": "",
"private": true,

View File

@ -1,6 +1,6 @@
{
"name": "@stackframe/docs-examples",
"version": "2.8.105",
"version": "2.8.106",
"repository": "https://github.com/hexclave/hexclave",
"description": "",
"private": true,

View File

@ -1,6 +1,6 @@
{
"name": "@stackframe/e-commerce-demo",
"version": "2.8.105",
"version": "2.8.106",
"repository": "https://github.com/hexclave/hexclave",
"private": true,
"scripts": {

View File

@ -1,6 +1,6 @@
{
"name": "@stackframe/js-example",
"version": "2.8.105",
"version": "2.8.106",
"repository": "https://github.com/hexclave/hexclave",
"private": true,
"description": "",

View File

@ -1,7 +1,7 @@
{
"name": "@stackframe/lovable-react-18-example",
"private": true,
"version": "2.8.105",
"version": "2.8.106",
"repository": "https://github.com/hexclave/hexclave",
"type": "module",
"scripts": {

View File

@ -1,6 +1,6 @@
{
"name": "@stackframe/example-middleware-demo",
"version": "2.8.105",
"version": "2.8.106",
"repository": "https://github.com/hexclave/hexclave",
"private": true,
"scripts": {

View File

@ -1,7 +1,7 @@
{
"name": "react-example",
"private": true,
"version": "2.8.105",
"version": "2.8.106",
"repository": "https://github.com/hexclave/hexclave",
"type": "module",
"scripts": {

View File

@ -1,6 +1,6 @@
{
"name": "@stackframe/example-supabase",
"version": "2.8.105",
"version": "2.8.106",
"repository": "https://github.com/hexclave/hexclave",
"private": true,
"scripts": {

View File

@ -1,6 +1,6 @@
{
"name": "@stackframe/example-tanstack-start-demo",
"version": "2.8.105",
"version": "2.8.106",
"repository": "https://github.com/hexclave/hexclave",
"description": "TanStack Start demo app for Hexclave",
"private": true,

View File

@ -1,6 +1,6 @@
{
"name": "@stackframe/dashboard-ui-components",
"version": "2.8.105",
"version": "2.8.106",
"repository": "https://github.com/hexclave/hexclave",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",

View File

@ -1,6 +1,6 @@
{
"name": "@stackframe/init-stack",
"version": "2.8.105",
"version": "2.8.106",
"repository": "https://github.com/hexclave/hexclave",
"description": "The setup wizard for Hexclave. https://hexclave.com",
"main": "dist/index.mjs",

View File

@ -1,7 +1,7 @@
{
"//": "THIS FILE IS AUTO-GENERATED FROM TEMPLATE. DO NOT EDIT IT DIRECTLY, INSTEAD EDIT THE CORRESPONDING FILE IN packages/template (FOR package.json FILES, PLEASE EDIT package-template.json)",
"name": "@stackframe/js",
"version": "2.8.105",
"version": "2.8.106",
"repository": "https://github.com/hexclave/hexclave",
"sideEffects": false,
"main": "./dist/index.js",

View File

@ -1,7 +1,7 @@
{
"//": "THIS FILE IS AUTO-GENERATED FROM TEMPLATE. DO NOT EDIT IT DIRECTLY, INSTEAD EDIT THE CORRESPONDING FILE IN packages/template (FOR package.json FILES, PLEASE EDIT package-template.json)",
"name": "@stackframe/react",
"version": "2.8.105",
"version": "2.8.106",
"repository": "https://github.com/hexclave/hexclave",
"sideEffects": false,
"main": "./dist/index.js",

View File

@ -1,6 +1,6 @@
{
"name": "@stackframe/stack-cli",
"version": "2.8.105",
"version": "2.8.106",
"repository": "https://github.com/hexclave/hexclave",
"description": "The CLI for Hexclave. https://hexclave.com",
"main": "dist/index.js",

View File

@ -1,6 +1,6 @@
{
"name": "@stackframe/stack-sc",
"version": "2.8.105",
"version": "2.8.106",
"repository": "https://github.com/hexclave/hexclave",
"exports": {
"./force-react-server": {

View File

@ -1,6 +1,6 @@
{
"name": "@stackframe/stack-shared",
"version": "2.8.105",
"version": "2.8.106",
"repository": "https://github.com/hexclave/hexclave",
"scripts": {
"build": "rimraf dist && tsdown",

View File

@ -1,6 +1,6 @@
{
"name": "@stackframe/stack-ui",
"version": "2.8.105",
"version": "2.8.106",
"repository": "https://github.com/hexclave/hexclave",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",

View File

@ -1,7 +1,7 @@
{
"//": "THIS FILE IS AUTO-GENERATED FROM TEMPLATE. DO NOT EDIT IT DIRECTLY, INSTEAD EDIT THE CORRESPONDING FILE IN packages/template (FOR package.json FILES, PLEASE EDIT package-template.json)",
"name": "@stackframe/stack",
"version": "2.8.105",
"version": "2.8.106",
"repository": "https://github.com/hexclave/hexclave",
"sideEffects": false,
"main": "./dist/index.js",

View File

@ -1,7 +1,7 @@
{
"//": "THIS FILE IS AUTO-GENERATED FROM TEMPLATE. DO NOT EDIT IT DIRECTLY, INSTEAD EDIT THE CORRESPONDING FILE IN packages/template (FOR package.json FILES, PLEASE EDIT package-template.json)",
"name": "@stackframe/tanstack-start",
"version": "2.8.105",
"version": "2.8.106",
"repository": "https://github.com/hexclave/hexclave",
"sideEffects": false,
"main": "./dist/index.js",

View File

@ -13,7 +13,7 @@
"//": "NEXT_LINE_PLATFORM template",
"private": true,
"version": "2.8.105",
"version": "2.8.106",
"repository": "https://github.com/hexclave/hexclave",
"sideEffects": false,
"main": "./dist/index.js",

View File

@ -2,7 +2,7 @@
"//": "THIS FILE IS AUTO-GENERATED FROM TEMPLATE. DO NOT EDIT IT DIRECTLY, INSTEAD EDIT THE CORRESPONDING FILE IN packages/template (FOR package.json FILES, PLEASE EDIT package-template.json)",
"name": "@stackframe/template",
"private": true,
"version": "2.8.105",
"version": "2.8.106",
"repository": "https://github.com/hexclave/hexclave",
"sideEffects": false,
"main": "./dist/index.js",

View File

@ -1,6 +1,6 @@
{
"name": "@stackframe/swift-sdk",
"version": "2.8.105",
"version": "2.8.106",
"private": true,
"description": "Hexclave Swift SDK",
"scripts": {

View File

@ -1,6 +1,6 @@
{
"name": "@stackframe/sdk-spec",
"version": "2.8.105",
"version": "2.8.106",
"private": true,
"description": "Hexclave SDK specification files",
"scripts": {}