diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3459cf14fb..762ecd829f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -27,6 +27,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Updated `content/docs/guides/packages.mdx` and `content/docs/concepts/packages.mdx` to reflect the actual **42 package** inventory and to include `service-package` and `service-tenant`
### Fixed
+- **Studio left metadata list not refreshing on package switch** — In `apps/studio/src/routes/$package.tsx`, the `AppSidebar` package-switcher's `onSelectPackage` handler only updated local `selectedPackage` state. A URL→state `useEffect` in the same layout then immediately reverted that state back to match the unchanged `$package` route param, so `AppSidebar.loadMetadata` (keyed on `selectedPackage`) never re-ran and the left metadata tree stayed stuck on the previous package. The dropdown now navigates to `/$newPackage`, making the URL the single source of truth; the URL→state effect then updates `selectedPackage` normally and the metadata list refreshes for the new package. (`apps/studio/src/routes/$package.tsx`)
- **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`)
- **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`)
- **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`)
diff --git a/apps/studio/src/routes/$package.tsx b/apps/studio/src/routes/$package.tsx
index d0695cc0ae..4e0142d579 100644
--- a/apps/studio/src/routes/$package.tsx
+++ b/apps/studio/src/routes/$package.tsx
@@ -1,9 +1,10 @@
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
-import { createFileRoute, Outlet } from '@tanstack/react-router';
+import { createFileRoute, Outlet, useNavigate } from '@tanstack/react-router';
import { AppSidebar } from '../components/app-sidebar';
import { usePackages } from '../hooks/usePackages';
-import { useEffect } from 'react';
+import { useCallback, useEffect } from 'react';
+import type { InstalledPackage } from '@objectstack/spec/kernel';
/**
* Layout for every `/$package/*` route.
@@ -14,12 +15,19 @@ import { useEffect } from 'react';
* object view, metadata view) provide accurate breadcrumbs without prop-
* drilling. It also prevents the duplicated-shell bug that occurred when
* both this layout and its children each rendered their own `AppSidebar`.
+ *
+ * The URL `$package` param is the single source of truth for the current
+ * package. Selecting a new package in the sidebar dropdown navigates to
+ * `/$newPackage`; the URL→state effect below then updates `selectedPackage`,
+ * which in turn invalidates `AppSidebar.loadMetadata` (its dependency) and
+ * causes the left metadata list to refresh for the newly selected package.
*/
function PackageLayoutComponent() {
const { package: packageId } = Route.useParams();
const { packages, selectedPackage, setSelectedPackage } = usePackages();
+ const navigate = useNavigate();
- // Update selected package when route param changes
+ // Sync selection from the URL param (single source of truth).
useEffect(() => {
const pkg = packages.find(p => p.manifest?.id === packageId);
if (pkg && pkg !== selectedPackage) {
@@ -27,12 +35,22 @@ function PackageLayoutComponent() {
}
}, [packageId, packages, selectedPackage, setSelectedPackage]);
+ // Selecting a package in the sidebar dropdown must drive the URL;
+ // otherwise the URL→state effect above would immediately revert the
+ // local state back to the old package and the metadata list would
+ // never refresh.
+ const handleSelectPackage = useCallback((pkg: InstalledPackage) => {
+ const nextId = pkg.manifest?.id;
+ if (!nextId || nextId === packageId) return;
+ navigate({ to: '/$package', params: { package: nextId } });
+ }, [navigate, packageId]);
+
return (
<>