Skip to content

Commit 0e7e932

Browse files
Merge pull request #1179 from objectstack-ai/copilot/fix-issue-with-pull-request-1178
fix(hono-adapter): always expose `set-auth-token` via CORS
2 parents 4737f9c + 8f8fa93 commit 0e7e932

3 files changed

Lines changed: 78 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88
## [Unreleased]
99

1010
### Fixed
11+
- **Cross-origin auth tokens stripped in `@objectstack/hono` adapter (follow-up to PR #1178)**`createHonoApp()` was not exposing `set-auth-token` via `Access-Control-Expose-Headers`, diverging from `plugin-hono-server`'s CORS wiring. On Vercel deployments (where all traffic flows through `createHonoApp()`), the browser stripped the header from every response, preventing the better-auth `bearer()` plugin from delivering rotated session tokens to cross-origin clients. Cross-origin sessions silently broke even after the wildcard fixes in #1177/#1178. The adapter now always includes `set-auth-token` in `exposeHeaders`, merged with any user-supplied values, mirroring the invariant established in commit `151dd19c`. (`packages/adapters/hono/src/index.ts`)
1112
- **CORS wildcard patterns in `@objectstack/hono` adapter (follow-up to PR #1177)**`createHonoApp()` was the third CORS code path that still treated wildcard origins (e.g. `https://*.objectui.org`) as literal strings when passing them to Hono's `cors()` middleware. Because `apps/server` routes all non-OPTIONS requests through this adapter on Vercel, the browser would see a successful preflight (handled by the Vercel short-circuit) followed by a POST/GET response with no `Access-Control-Allow-Origin` header, blocking every real request. The adapter now imports `hasWildcardPattern` / `createOriginMatcher` from `@objectstack/plugin-hono-server` and uses the same matcher-function branch as `plugin-hono-server`, so all three Hono-based CORS paths share a single source of truth. (`packages/adapters/hono/src/index.ts`)
1213
- **CORS wildcard patterns on Vercel deployments**`CORS_ORIGIN` values containing wildcard patterns (e.g. `https://*.objectui.org,https://*.objectstack.ai,http://localhost:*`) no longer cause browser CORS errors when `apps/server` is deployed to Vercel. The Vercel entrypoint's OPTIONS preflight short-circuit previously matched origins with a literal `Array.includes()`, treating `*` as a plain character and rejecting legitimate subdomains. It now shares the same pattern-matching logic as the Hono plugin's `cors()` middleware via new exports `createOriginMatcher` / `hasWildcardPattern` / `matchOriginPattern` / `normalizeOriginPatterns` from `@objectstack/plugin-hono-server`. (`apps/server/server/index.ts`, `packages/plugins/plugin-hono-server/src/pattern-matcher.ts`)
1314

packages/adapters/hono/src/hono.test.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1025,4 +1025,65 @@ describe('createHonoApp', () => {
10251025
expect(res.headers.get('access-control-allow-origin')).toBe('https://app.objectui.org');
10261026
});
10271027
});
1028+
1029+
describe('CORS expose-headers defaults', () => {
1030+
// `set-auth-token` must always be exposed so the better-auth bearer()
1031+
// plugin can deliver rotated session tokens to cross-origin clients.
1032+
// This mirrors plugin-hono-server's CORS wiring — all three Hono-based
1033+
// CORS sites must stay in lockstep on this default.
1034+
const ORIG_CORS_ORIGIN = process.env.CORS_ORIGIN;
1035+
1036+
beforeEach(() => {
1037+
delete process.env.CORS_ORIGIN;
1038+
});
1039+
1040+
afterAll(() => {
1041+
if (ORIG_CORS_ORIGIN === undefined) delete process.env.CORS_ORIGIN;
1042+
else process.env.CORS_ORIGIN = ORIG_CORS_ORIGIN;
1043+
});
1044+
1045+
it('always exposes set-auth-token by default', async () => {
1046+
const app = createHonoApp({ kernel: mockKernel, prefix: '/api/v1' });
1047+
1048+
const res = await app.request('/api/v1/meta', {
1049+
method: 'GET',
1050+
headers: { Origin: 'https://app.example.com' },
1051+
});
1052+
const exposed = res.headers.get('access-control-expose-headers') || '';
1053+
expect(exposed.toLowerCase()).toContain('set-auth-token');
1054+
});
1055+
1056+
it('merges user-supplied exposeHeaders with set-auth-token (does not replace)', async () => {
1057+
const app = createHonoApp({
1058+
kernel: mockKernel,
1059+
prefix: '/api/v1',
1060+
cors: { exposeHeaders: ['X-Custom-Header'] },
1061+
});
1062+
1063+
const res = await app.request('/api/v1/meta', {
1064+
method: 'GET',
1065+
headers: { Origin: 'https://app.example.com' },
1066+
});
1067+
const exposed = (res.headers.get('access-control-expose-headers') || '').toLowerCase();
1068+
expect(exposed).toContain('set-auth-token');
1069+
expect(exposed).toContain('x-custom-header');
1070+
});
1071+
1072+
it('does not duplicate set-auth-token when user also supplies it', async () => {
1073+
const app = createHonoApp({
1074+
kernel: mockKernel,
1075+
prefix: '/api/v1',
1076+
cors: { exposeHeaders: ['set-auth-token', 'X-Other'] },
1077+
});
1078+
1079+
const res = await app.request('/api/v1/meta', {
1080+
method: 'GET',
1081+
headers: { Origin: 'https://app.example.com' },
1082+
});
1083+
const exposed = (res.headers.get('access-control-expose-headers') || '').toLowerCase();
1084+
const occurrences = exposed.split(',').map(s => s.trim()).filter(s => s === 'set-auth-token');
1085+
expect(occurrences.length).toBe(1);
1086+
expect(exposed).toContain('x-other');
1087+
});
1088+
});
10281089
});

packages/adapters/hono/src/index.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,11 +118,26 @@ export function createHonoApp(options: ObjectStackHonoOptions): Hono {
118118
origin = configuredOrigin;
119119
}
120120

121+
// Always include `set-auth-token` in exposed headers so that the
122+
// better-auth `bearer()` plugin (registered by plugin-auth) can
123+
// deliver rotated session tokens to cross-origin clients. Without
124+
// this, browsers strip the header from every response, the client
125+
// never sees the new token, and cross-origin sessions silently
126+
// break even when preflight and the actual request both succeed.
127+
//
128+
// This mirrors `plugin-hono-server`'s CORS wiring — all three
129+
// Hono-based CORS sites must stay in lockstep on this default.
130+
const defaultExposeHeaders = ['set-auth-token'];
131+
const exposeHeaders = Array.from(new Set([
132+
...defaultExposeHeaders,
133+
...(corsOpts.exposeHeaders ?? []),
134+
]));
135+
121136
app.use('*', cors({
122137
origin: origin as any,
123138
allowMethods: corsOpts.methods || ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'],
124139
allowHeaders: corsOpts.allowHeaders || ['Content-Type', 'Authorization', 'X-Requested-With'],
125-
exposeHeaders: corsOpts.exposeHeaders || [],
140+
exposeHeaders,
126141
credentials,
127142
maxAge,
128143
}));

0 commit comments

Comments
 (0)