diff --git a/.changeset/cors-allow-if-match.md b/.changeset/cors-allow-if-match.md new file mode 100644 index 0000000000..ad84a7e414 --- /dev/null +++ b/.changeset/cors-allow-if-match.md @@ -0,0 +1,13 @@ +--- +'@objectstack/plugin-hono-server': patch +'@objectstack/hono': patch +--- + +CORS default `allowHeaders` now includes `If-Match`. The REST record update +accepts the OCC token as an `If-Match` header (objectui's record-level inline +edit sends it on every save), but the preflight allow-list omitted it — so on +any split-origin deployment (console dev server against a backend on another +origin) the browser failed the preflight and every inline-edit save died with +"Failed to fetch". Found live while dogfooding objectui#2572; same +split-origin failure class as the #2548 Bearer fixes. Explicit user-supplied +`allowHeaders` still win unchanged. diff --git a/packages/adapters/hono/src/index.ts b/packages/adapters/hono/src/index.ts index 9dd4469076..920109559f 100644 --- a/packages/adapters/hono/src/index.ts +++ b/packages/adapters/hono/src/index.ts @@ -186,7 +186,9 @@ export function createHonoApp(options: ObjectStackHonoOptions): Hono { app.use('*', cors({ origin: origin as any, allowMethods: corsOpts.methods || ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'], - allowHeaders: corsOpts.allowHeaders || ['Content-Type', 'Authorization', 'X-Requested-With', 'X-Tenant-ID', 'X-Environment-Id'], + // Keep in sync with plugin-hono-server's defaultAllowHeaders — `If-Match` + // carries the OCC token on cross-origin record PATCHes (objectui#2572). + allowHeaders: corsOpts.allowHeaders || ['Content-Type', 'Authorization', 'X-Requested-With', 'X-Tenant-ID', 'X-Environment-Id', 'If-Match'], exposeHeaders, credentials, maxAge, diff --git a/packages/plugins/plugin-hono-server/src/hono-plugin.test.ts b/packages/plugins/plugin-hono-server/src/hono-plugin.test.ts index 43f8f9b7a7..f9f52efbc8 100644 --- a/packages/plugins/plugin-hono-server/src/hono-plugin.test.ts +++ b/packages/plugins/plugin-hono-server/src/hono-plugin.test.ts @@ -255,6 +255,18 @@ describe('HonoServerPlugin', () => { expect(corsConfigCapture.last.allowHeaders).toContain('Authorization'); }); + it('should allow If-Match by default (OCC token on cross-origin record PATCHes)', async () => { + corsConfigCapture.last = undefined; + + const plugin = new HonoServerPlugin(); + await plugin.init(context as PluginContext); + + // objectui#2572 dogfood find: the record-level inline edit sends the + // OCC token as an `If-Match` header; a preflight that doesn't allow + // it makes every split-origin save fail with "Failed to fetch". + expect(corsConfigCapture.last.allowHeaders).toContain('If-Match'); + }); + it('should merge user-supplied exposeHeaders with set-auth-token default', async () => { corsConfigCapture.last = undefined; diff --git a/packages/plugins/plugin-hono-server/src/hono-plugin.ts b/packages/plugins/plugin-hono-server/src/hono-plugin.ts index 7db54bd902..b6ea84ca8e 100644 --- a/packages/plugins/plugin-hono-server/src/hono-plugin.ts +++ b/packages/plugins/plugin-hono-server/src/hono-plugin.ts @@ -279,7 +279,12 @@ export class HonoServerPlugin implements Plugin { // the better-auth `bearer()` plugin can deliver rotated // session tokens to cross-origin clients (see plugin-auth). // User-supplied exposeHeaders are merged with this default. - const defaultAllowHeaders = ['Content-Type', 'Authorization', 'X-Requested-With', 'X-Tenant-ID', 'X-Environment-Id']; + // `If-Match` carries the OCC token on record PATCHes (objectui's + // record-level inline edit, REST `update` with `ifMatch`) — without + // it in the preflight allow-list, every cross-origin save fails in + // the browser with "Failed to fetch" (objectui#2572 dogfood find; + // same split-origin class as the #2548 Bearer fixes). + const defaultAllowHeaders = ['Content-Type', 'Authorization', 'X-Requested-With', 'X-Tenant-ID', 'X-Environment-Id', 'If-Match']; const defaultExposeHeaders = ['set-auth-token']; const allowHeaders = corsOpts.allowHeaders ?? defaultAllowHeaders; const exposeHeaders = Array.from(new Set([