@@ -36,9 +36,37 @@ import { MetadataPlugin } from '@objectstack/metadata';
3636import { AIServicePlugin } from '@objectstack/service-ai' ;
3737import { handle } from '@hono/node-server/vercel' ;
3838import { Hono } from 'hono' ;
39+ import { cors } from 'hono/cors' ;
3940import { createBrokerShim } from '../src/lib/create-broker-shim.js' ;
4041import studioConfig from '../objectstack.config.js' ;
4142
43+ // ---------------------------------------------------------------------------
44+ // Vercel origin helpers
45+ // ---------------------------------------------------------------------------
46+
47+ /**
48+ * Collect all Vercel deployment origins from environment variables.
49+ *
50+ * Reused for both:
51+ * - better-auth `trustedOrigins` (CSRF)
52+ * - Hono CORS middleware `origin` allowlist
53+ *
54+ * Centralised to avoid drift between the two allowlists.
55+ */
56+ function getVercelOrigins ( ) : string [ ] {
57+ const origins : string [ ] = [ ] ;
58+ if ( process . env . VERCEL_URL ) {
59+ origins . push ( `https://${ process . env . VERCEL_URL } ` ) ;
60+ }
61+ if ( process . env . VERCEL_BRANCH_URL ) {
62+ origins . push ( `https://${ process . env . VERCEL_BRANCH_URL } ` ) ;
63+ }
64+ if ( process . env . VERCEL_PROJECT_PRODUCTION_URL ) {
65+ origins . push ( `https://${ process . env . VERCEL_PROJECT_PRODUCTION_URL } ` ) ;
66+ }
67+ return origins ;
68+ }
69+
4270// ---------------------------------------------------------------------------
4371// Singleton state — persists across warm Vercel invocations
4472// ---------------------------------------------------------------------------
@@ -85,17 +113,8 @@ async function ensureKernel(): Promise<ObjectKernel> {
85113 ? `https://${ process . env . VERCEL_URL } `
86114 : 'http://localhost:3000' ;
87115
88- // Collect all Vercel URL variants so better-auth trusts each one
89- const trustedOrigins : string [ ] = [ ] ;
90- if ( process . env . VERCEL_URL ) {
91- trustedOrigins . push ( `https://${ process . env . VERCEL_URL } ` ) ;
92- }
93- if ( process . env . VERCEL_BRANCH_URL ) {
94- trustedOrigins . push ( `https://${ process . env . VERCEL_BRANCH_URL } ` ) ;
95- }
96- if ( process . env . VERCEL_PROJECT_PRODUCTION_URL ) {
97- trustedOrigins . push ( `https://${ process . env . VERCEL_PROJECT_PRODUCTION_URL } ` ) ;
98- }
116+ // Reuse the shared helper so CORS and CSRF allowlists stay in sync
117+ const trustedOrigins = getVercelOrigins ( ) ;
99118
100119 await kernel . use ( new AuthPlugin ( {
101120 secret : process . env . AUTH_SECRET || 'dev-secret-please-change-in-production-min-32-chars' ,
@@ -218,6 +237,41 @@ async function ensureApp(): Promise<Hono> {
218237 */
219238const app = new Hono ( ) ;
220239
240+ // ---------------------------------------------------------------------------
241+ // CORS middleware
242+ // ---------------------------------------------------------------------------
243+ // Placed on the outer app so preflight (OPTIONS) requests are answered
244+ // immediately, without waiting for the kernel cold-start. This is essential
245+ // when the SPA is loaded from a Vercel temporary/preview domain but the
246+ // API base URL points to a different deployment (cross-origin).
247+ //
248+ // Allowed origins:
249+ // 1. All Vercel deployment URLs exposed via env vars (current deployment)
250+ // 2. Any *.vercel.app subdomain (covers all preview/branch deployments)
251+ // 3. localhost (local development)
252+ // ---------------------------------------------------------------------------
253+
254+ const vercelOrigins = getVercelOrigins ( ) ;
255+
256+ app . use ( '*' , cors ( {
257+ origin : ( origin ) => {
258+ // Same-origin or non-browser requests (no Origin header)
259+ if ( ! origin ) return origin ;
260+ // Explicitly listed Vercel deployment origins
261+ if ( vercelOrigins . includes ( origin ) ) return origin ;
262+ // Any *.vercel.app subdomain (preview / temp deployments)
263+ if ( origin . endsWith ( '.vercel.app' ) && origin . startsWith ( 'https://' ) ) return origin ;
264+ // Localhost for development
265+ if ( / ^ h t t p s ? : \/ \/ l o c a l h o s t ( : \d + ) ? $ / . test ( origin ) ) return origin ;
266+ // Deny — return empty string so no Access-Control-Allow-Origin is set
267+ return '' ;
268+ } ,
269+ credentials : true ,
270+ allowMethods : [ 'GET' , 'POST' , 'PUT' , 'PATCH' , 'DELETE' , 'OPTIONS' ] ,
271+ allowHeaders : [ 'Content-Type' , 'Authorization' , 'X-Requested-With' ] ,
272+ maxAge : 86400 ,
273+ } ) ) ;
274+
221275app . all ( '*' , async ( c ) => {
222276 console . log ( `[Vercel] ${ c . req . method } ${ c . req . url } ` ) ;
223277
@@ -241,7 +295,7 @@ export default handle(app);
241295/**
242296 * Vercel per-function configuration.
243297 *
244- * Picked up by the @vercel/node runtime from the deployed api/index .js bundle.
298+ * Picked up by the @vercel/node runtime from the deployed api/[[...route]] .js bundle.
245299 * Replaces the top-level "functions" key in vercel.json so there is no
246300 * pre-build file-pattern validation against a not-yet-bundled artifact.
247301 */
0 commit comments