@terreno/admin-spa 58.0.0-beta.2 → 58.0.0-beta.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -20,6 +20,121 @@ Upgrade notes for consumer action live in [`mcp-server/src/docs/upgrades/`](mcp-
20
20
 
21
21
  Unreleased changes live in [`changelog/unreleased/`](changelog/unreleased/). Add one Markdown file per feature (see that directory's README) instead of editing this section.
22
22
 
23
+ ## [58.0.0-beta.4] - 2026-11-04
24
+
25
+ Upgrade note: [`mcp-server/src/docs/upgrades/58.0.0-beta.4.md`](mcp-server/src/docs/upgrades/58.0.0-beta.4.md).
26
+
27
+ ### Breaking
28
+
29
+ - `@terreno/admin-spa` now server-renders every admin page in the backend's Express process, behind an access gate that runs before anything renders. The first HTML holds the login form, the shell and the screen's data. Upgrade steps: [Upgrade to the server-rendered admin SPA](docs/how-to/upgrade-admin-spa-ssr.md).
30
+ - `AdminSpaServeApp`'s `distDir` (and example-backend's `ADMIN_SPA_DIST_DIR`) now names the export root, which holds `client/` and `server/`. `build:web` no longer writes `dist/index.html`. Rebuild, and copy the whole `dist` into images. Without `dist/server` the plugin answers 503 on `/console/*`.
31
+ - `basePath` must equal the `experiments.baseUrl` baked into the bundle (`/console` by default), or `register()` throws. Any other mount needs `experiments.baseUrl` set in `admin-spa/app.json` and a rebuild from source; the published package ships only the `/console` build. Mounting at the site root (`basePath: "/"`) is no longer supported.
32
+ - Terreno 58 targets Expo SDK 58 (`~58.0.14`), React Native `0.88.0`, and React `19.3.0`. `@terreno/ui` peers `react-native` `0.88.0`. Native catalog moves with the SDK: `react-native-gesture-handler` `~3.3.0`, `react-native-reanimated` `4.7.1` + `react-native-worklets` `0.13.0`, `react-native-screens` `~4.30.0`, `react-native-webview` `14.0.1`, `@sentry/react-native` `~8.28.0`, `@shopify/flash-list` `2.3.2`, `@shopify/react-native-skia` `2.14.0`. Rebuild dev clients and store binaries.
33
+ - Expo Router 58: set `export const unstable_settings = {initialRouteName}` on the layout instead of passing `initialRouteName` to `<Slot>` / navigators. `SidebarNavigation` ignores the prop.
34
+ - Expo Router 58 static rendering: a custom `app/+html.tsx` must render `headNodes` and `bodyNodes` from `useServerDocumentContext()` (`expo-router/html`), or exported pages ship without their JavaScript. `example-frontend` is updated.
35
+ - React Native 0.88 generated types: type refs as `ViewInstance`, `TextInstance`, `ScrollViewInstance`, and `TextInputInstance` (`Box.scrollRef`, `TextField.inputRef`, `Text` `ref`).
36
+ - `create-terreno-app` scaffolds Expo `~58.0.14`, React Native `0.88.0`, and React `19.3.0`.
37
+ - The Terreno 58 native module baseline lands in `example-frontend` and `demo`: `@stripe/stripe-react-native` 0.81.0, `react-native-purchases` and `react-native-purchases-ui` 10.12.2, `expo-device`, `expo-crypto`, `expo-local-authentication`, `expo-system-ui`, and `expo-build-properties` (Android Kotlin 2.2.21 and `compileSdkVersion` 37 pins). Rebuild every dev client and store binary. `react-native-otp-verify` is left out because it does not support the New Architecture. The demo has a hidden **Native Modules** page that shows each module's version and whether the running binary links it. See the 58.0.0 upgrade note.
38
+ - `TerrenoApp` turns HTTP rate limiting on by default. Omitting `rateLimit` now mounts the limiter with the memory store (auth 20 / 15 min, api 600 / 15 min). Pass `rateLimit: false` to turn it off. On Cloud Run pass `rateLimit: {trustProxy: 1}` so unauthenticated traffic keys by client IP. See the 58.0.0 upgrade note.
39
+ - `@terreno/admin-frontend` no longer takes an RTK `api`. The `api` prop is removed from every admin component (`AdminProvider`, `AdminShellLayout`, `AdminHome`, `AdminModelList`, `AdminModelTable`, `AdminModelForm`, `AdminScreenRouter`, `ConfigurationScreen`, `DocumentStorageBrowser`, `ConsentHistory`, comms, jobs, org, consent, and announcement screens, and widget props), and admin hooks drop their `api` argument (`useAdminConfig(apiBase)`, `useAdminScripts(apiBase)`, `useAdminRoles(apiBase)`, `useConfigurationApi({basePath})`, `useDocumentStorageApi(basePath)`, `useConsentHistory(baseUrl)`, `useOrganizationsApi(basePath)`, `useCommsDashboardApi()`, `useJobsDashboardApi()`, and the `useAiObservability*Api()` hooks).
40
+ - `useAdminApi(api, routePath, modelName)` is removed. Use `useAdminModelApi(routePath, modelName)`, which keeps the same hook names and calls the same `/admin/{routePath}` URLs through the admin fetch client. `AdminApi`, `EndpointBuilder`, and admin `injectEndpoints` are gone; `@reduxjs/toolkit` and `react-redux` are no longer peers of admin-frontend.
41
+ - `AdminProvider` is now the required auth binding for every admin request, including ObjectId and API-only model CRUD. Cookie SPAs pass `credentials="same-origin"`; embedded apps pass `apiOrigin` and `getAuthHeaders`. Product screens that reuse admin components wrap them in `AdminProvider`. See the 58.0.0 upgrade guide.
42
+ - Removed the legacy RTK realtime path. Use `sync` on `modelRouter` with `@terreno/syncdb` for live collection updates. `RealtimeApp`, `SyncApp`, socket auth, `sync:delta`, and admin `admin:model.changed` events are unchanged. See the 58.0.0 upgrade note.
43
+ - `@terreno/api`: `modelRouter` no longer accepts `realtime` (now a TypeScript error), and the change stream no longer emits the `sync` websocket event. Removed `RealtimeConfig`, `RealtimeEvent`, `DocumentSubscription`, `QuerySubscription`, `registerRealtime`, `getRealtimeRegistry`, `findRegistryEntryByCollection`, `findRegistryEntryByRoutePath`, `updateRealtimeRegistryOptions`, `clearRealtimeRegistry`, `RealtimeRegistryEntry`, the query-subscription store (`addQuerySubscription`, `removeQuerySubscription`, `removeAllSocketQueries`, `getQuerySubscriptionsForCollection`, `computeQueryId`, `clearQueryStore`), and `MAX_MODEL_SUBSCRIPTIONS` / `MAX_DOCUMENT_SUBSCRIPTIONS` / `MAX_QUERY_SUBSCRIPTIONS`. Sockets no longer handle `subscribe:model`, `subscribe:document`, or `subscribe:query`.
44
+ - `@terreno/rtk`: removed `realtimeList`, `realtimeDocument`, `setRealtimeSocket`, `getRealtimeSocket`, `useSyncConnection`, and the `RealtimeEvent` type. The rest of `@terreno/rtk` (including `useSocketConnection` and `useRealtimeDebug`) is removed in the same release; see `remove-rtk.md`.
45
+ - Removed `@terreno/rtk` and every Redux dependency (`@reduxjs/toolkit`, `react-redux`, `redux-persist`, `@rtk-query/codegen-openapi`). `@terreno/rtk` is not published for 58. `@terreno/syncdb` is the only frontend data, request, and session layer. Follow `docs/how-to/migrate-rtk-to-syncdb.md`, which maps every removed export to its replacement, and the 58.0.0 upgrade note.
46
+ - `@terreno/syncdb` adds `@terreno/syncdb/api` (`createApiClient`, `ApiError`), request hooks in `@terreno/syncdb/react` (`ApiClientProvider`, `useApiQuery`, `useLazyApiQuery`, `useApiMutation`, `useSocketEvent`), `@terreno/syncdb/config` (`baseUrl`, `baseWebsocketsUrl`, `appHeaders`), `@terreno/syncdb/auth` (`createBetterAuthClient`, `SessionProvider`, `useSession`, `jwtAuthAdapter`, `useJwtAuth`, token-expiry helpers), `@terreno/syncdb/flags` (`useFeatureFlags`, `useTerrenoFeatureFlags` on the sync socket), `@terreno/syncdb/app` (`useUpgradeCheck`, `hasPermission`, `canOpenAdminPage`, `installDevConsoleLogger`), `@terreno/syncdb/mcp` (`useMCPTools`), and `@terreno/syncdb/buildNumber`.
47
+ - `terreno-syncdb-codegen --api-out` generates typed hooks for every non-synced OpenAPI operation (same names as the RTK codegen), replacing `@rtk-query/codegen-openapi`. `bun run sdk` writes `store/apiSdk.ts`.
48
+ - `@terreno/ui` `ConsentNavigator`, `AnnouncementNavigator`, `useConsentForms`, `useSubmitConsent`, `useAnnouncements`, and `useAcknowledgeAnnouncement` take a request `client` instead of an RTK `api`.
49
+ - `GptHistory` (`gpt/histories`) is a synced collection and keeps its ObjectId `_id` (no migration). `McpServiceToken` (`mcp/service-tokens`, read-only) is a synced collection with String ids; run `mcpServiceTokenStringIdsMigration` (`@terreno/api`) before deploying 58 frontends.
50
+ - `create-terreno-app`, the `terreno_bootstrap` prompt, and the `terreno_generate_screen` tool scaffold apps without Redux or `@terreno/rtk`. The terreno-mcp-local `get_rtk_state` tool, the MCP resource `terreno://docs/rtk`, and `terreno state --slice rtk` are removed; use `get_syncdb_state` and `terreno state`.
51
+ - `@terreno/syncdb`: `useMCPTools` (and `MCPToolInfo`, `UseMCPToolsOptions`, `UseMCPToolsResult`) moved from `@terreno/syncdb/app` to the new `@terreno/syncdb/mcp` entry point. Change `import {useMCPTools} from "@terreno/syncdb/app"` to `import {useMCPTools} from "@terreno/syncdb/mcp"`. The hook imports the optional `@modelcontextprotocol/client` peer at the top level, so exporting it from `/app` broke every app that imported `/app` without that package installed, including apps scaffolded by `create-terreno-app`, which import `installDevConsoleLogger` from `/app`.
52
+
53
+ ### Added
54
+
55
+ - `AdminSpaServeApp` options `internalOrigin` (where server renders fetch API data, default `http://127.0.0.1:${PORT ?? 4000}`) and `ssrApiPrefixes` (API paths a render may fetch). The plugin also serves Expo's root `/_expo/loaders/*`, with `Cache-Control: no-store, private` on pages and loader data.
56
+ - `@terreno/syncdb`: `SessionProvider` / session store `initialSession`, and the request client's `primeQuery(args, data)`, to start from data a server render already read.
57
+ - `@terreno/admin-frontend`: `AdminRpcSeed` and `adminRpcSeedKey` seed `useAdminRpcQuery` with server-fetched bodies; `buildAdminReadUrl`, `buildAdminListQueryParams`, `buildAdminApiListQueryRequest` and `homeWidgetUrls` build the exact URLs the screens request.
58
+ - `DataTable` columns size themselves when `width` is omitted. Each auto column starts at the wider of its header and its content (sampled from at most 50 rows, measured with a canvas on web and estimated on native), clamped to 80–360px, and auto columns grow to fill the table's container. Columns that set `width` keep it. `DataTableColumn.width` is now optional.
59
+ - `@terreno/syncdb` `createSyncDb({idFormats})` mints 24-hex ObjectIds for a collection set to `"objectId"`, so a model that keeps Mongoose's default ObjectId `_id` can sync without migrating its ids. `mutate()` throws on an explicit non-hex id for such a collection. `generateObjectId` and the `SyncIdFormat` type are exported.
60
+ - Sync `validation` nacks carry an optional `reason`: `invalid_id` when an ObjectId-keyed collection gets a non-hex id (rejected before the mutation is claimed), and `id_collision` when a create names an `_id` another document holds. A collision never touches the existing document and its message never names the holder. The ledger records the reason (`SyncMutation.nackReason`) so a redelivery replays it.
61
+ - `GET /sync/entities` leaves out non-hex ids for an ObjectId-keyed model instead of answering 400, so a rejected create queued for repair cannot fail the rest of the repair batch.
62
+
63
+ ### Changed
64
+
65
+ - The admin comms dashboard's default 7-day range now ends at the next whole minute, so a server render and the hydrating browser request the same URL.
66
+
67
+ `useAdminRpcQuery` treats the request's `X-Organization-Id` as part of its key. When the selected organization changes on the same URL, it drops the previous organization's data and loads again, even with `keepPreviousData`, so one organization's rows never show under another.
68
+
69
+ `useAdminModelApi`'s `useReadQuery` builds its URL with `buildAdminReadUrl`, which encodes the id.
70
+ - The repository's agent rules now generate with rulesync 17 (up from 9). The root rule `.rulesync/rules/00-root.md` now generates `AGENTS.md`, which Devin and other AGENTS.md readers load, in place of `.devin/rules/00-root.md`. Never edit `AGENTS.md` by hand: edit the root rule and run `bun run rules`. The examples that existed only in the old hand-edited `AGENTS.md` (syncdb and RTK SDK usage) moved into the root rule, so `CLAUDE.md`, the Cursor root rule, and Copilot instructions get them too. Links in the root rule now resolve from the repository root. Generated Copilot hooks use the portable `command` field. Generated Copilot subagents no longer list `agent/runSubagent` as their only tool, so they get every default tool. Generated skills carry their `license`, `disable-model-invocation`, and Devin `triggers` frontmatter.
71
+ - `@terreno/syncdb/auth` `SessionProvider`: `logout()` no longer clears the session before the server answers. The session stays in the state until `authClient.signOut()` finishes (`signOutTimeoutMs`, default 10 seconds), and then it is cleared. Meanwhile the new `isSigningOut` state (`useSession().isSigningOut`) is true; `isLoading` is left alone, so a navigator gated on it keeps its screen. Before, a login screen that opened at once could find the still-live session and sign the user straight back in. A sign-out that fails or times out checks the session first. A session that is still live, or a cookie or native session the server cannot be asked about, stays signed in, and `error` is set to `SIGN_OUT_FAILED_MESSAGE` ("Couldn't sign out. Check your connection and try again."). A web bearer client, which drops its token before the request leaves, ends signed out. The global `auth/logout` action (a refused session) now ends signed out even when the sign-out request fails. With `apisToReset` (57.17.0), `logout()` purges the cache when the session clears, not when it is dispatched: a purge at dispatch refetched every mounted screen mid-sign-out, and a refetch that could not reach the server answered 401 and signed the user out even though the sign-out failed. A failed sign-out keeps both the session and the cache. The global `auth/logout` action still purges at once (only the first of several in flight) and again when the session clears. A sign-in for another user that lands while a sign-out is in flight is kept; a sign-out that started signed out asks `getSession` first, so a restore of the session it revoked is cleared. Show `selectBetterAuthIsSigningOut` on the sign-out control and `selectBetterAuthError` after a failed sign-out.
72
+
73
+ ### Deprecated
74
+
75
+ - `@terreno/admin-spa` `rewriteIndexHtml` and `injectBaseGlobal` are deprecated: the server-rendered build has no `index.html`, and `app/+html.tsx` writes `window.__ADMIN_SPA_BASE__` itself. They will be removed in the next major release.
76
+
77
+ ### Removed
78
+
79
+ - `@terreno/ui` no longer exports the `Filter` and `FilterProps` aliases deprecated in 57.7.0. Import `DropdownPanel` and `DropdownPanelProps` instead. `FilterSelectMenu`, `FilterBoolean`, `FilterAccordion`, and `FilterChangesBadge` keep their names.
80
+
81
+ ### Fixed
82
+
83
+ - The admin configuration screen shows stored values again: `useConfigurationApi`'s values query unwraps the API's `{data}` envelope.
84
+ - syncdb window hydration no longer lets a REST page overwrite a newer synced entity. REST rows only fill ids the store lacks, and a stored entity at least as new as the fetched one is kept, so a live delta is never skipped.
85
+ - `@terreno/api`: `modelRouter("/path", Model, options)` no longer builds a router at registration. `TerrenoApp` mounts the router from `_buildWithContext`, which has OpenAPI wired in, so the eager one was built for nothing and logged "No options.openApi provided" for every model at import. `registration.router` now builds on first read, for code that mounts a registration by hand.
86
+ - `@terreno/api`: admin responses keep ObjectId, Date and other non-plain values intact. `scrubAdminFields` used to copy the own keys of every object it reached, so an ObjectId in an array field (or a bare ObjectId ref) came back as a plain `{i0, i1, i2, i3}` object and a Date became `{}`. It now scrubs only plain objects and Mongoose documents, and never treats a bare ObjectId as a populated document.
87
+ - `@terreno/syncdb`: logging out while `syncDb.start()` is still running no longer loses local writes or logs errors. `start()` now resolves the web encryption key from `GET /sync/key` before it loads the store, so the flush in `stop()` never needs the network after the session has ended. A `stop()` or `signOut()` call now cancels a `start()` that is running or queued ahead of it, so the transport never connects without a session. A failed key fetch is now retried on the next save instead of failing every save for the rest of the session.
88
+ - `@terreno/syncdb`: a record you create now gets the server's copy, including `_id` and other server-added fields. Before, the mutation ACK stamped the row's seq before the server's delta for that seq arrived, so the delta was dropped as a duplicate and the row kept only its optimistic data. Filters on real ids and anything else that reads server fields failed for that row. The ACK sets a marker on that row; the equal-seq delta applies only while the marker is set, and a later local edit clears it. Deltas older than the row's seq, and repeats of a delta the row already took, are still skipped.
89
+ - `@terreno/ui`: the `DateTimeActionSheet` time wheels show their numbers on physical iPhones. iOS now uses UIPickerView's natural 216pt height. The forced 104pt height looked fine in the simulator but showed empty rows on devices. Wheel items also take the theme's text color on iOS, not just Android, so they're no longer white on white on a dark-mode phone.
90
+
91
+ ## [58.0.0-beta.3] - 2026-11-03
92
+
93
+ Upgrade note: [`mcp-server/src/docs/upgrades/58.0.0-beta.3.md`](mcp-server/src/docs/upgrades/58.0.0-beta.3.md).
94
+
95
+ ### Breaking
96
+
97
+ - Terreno 58 targets Expo SDK 58 (`~58.0.14`), React Native `0.88.0`, and React `19.3.0`. `@terreno/ui` peers `react-native` `0.88.0`. Native catalog moves with the SDK: `react-native-gesture-handler` `~3.3.0`, `react-native-reanimated` `4.7.1` + `react-native-worklets` `0.13.0`, `react-native-screens` `~4.30.0`, `react-native-webview` `14.0.1`, `@sentry/react-native` `~8.28.0`, `@shopify/flash-list` `2.3.2`, `@shopify/react-native-skia` `2.14.0`. Rebuild dev clients and store binaries.
98
+ - Expo Router 58: set `export const unstable_settings = {initialRouteName}` on the layout instead of passing `initialRouteName` to `<Slot>` / navigators. `SidebarNavigation` ignores the prop.
99
+ - Expo Router 58 static rendering: a custom `app/+html.tsx` must render `headNodes` and `bodyNodes` from `useServerDocumentContext()` (`expo-router/html`), or exported pages ship without their JavaScript. `example-frontend` is updated.
100
+ - React Native 0.88 generated types: type refs as `ViewInstance`, `TextInstance`, `ScrollViewInstance`, and `TextInputInstance` (`Box.scrollRef`, `TextField.inputRef`, `Text` `ref`).
101
+ - `create-terreno-app` scaffolds Expo `~58.0.14`, React Native `0.88.0`, and React `19.3.0`.
102
+ - The Terreno 58 native module baseline lands in `example-frontend` and `demo`: `@stripe/stripe-react-native` 0.81.0, `react-native-purchases` and `react-native-purchases-ui` 10.12.2, `expo-device`, `expo-crypto`, `expo-local-authentication`, `expo-system-ui`, and `expo-build-properties` (Android Kotlin 2.2.21 and `compileSdkVersion` 37 pins). Rebuild every dev client and store binary. `react-native-otp-verify` is left out because it does not support the New Architecture. The demo has a hidden **Native Modules** page that shows each module's version and whether the running binary links it. See the 58.0.0 upgrade note.
103
+ - `TerrenoApp` turns HTTP rate limiting on by default. Omitting `rateLimit` now mounts the limiter with the memory store (auth 20 / 15 min, api 600 / 15 min). Pass `rateLimit: false` to turn it off. On Cloud Run pass `rateLimit: {trustProxy: 1}` so unauthenticated traffic keys by client IP. See the 58.0.0 upgrade note.
104
+ - `@terreno/admin-frontend` no longer takes an RTK `api`. The `api` prop is removed from every admin component (`AdminProvider`, `AdminShellLayout`, `AdminHome`, `AdminModelList`, `AdminModelTable`, `AdminModelForm`, `AdminScreenRouter`, `ConfigurationScreen`, `DocumentStorageBrowser`, `ConsentHistory`, comms, jobs, org, consent, and announcement screens, and widget props), and admin hooks drop their `api` argument (`useAdminConfig(apiBase)`, `useAdminScripts(apiBase)`, `useAdminRoles(apiBase)`, `useConfigurationApi({basePath})`, `useDocumentStorageApi(basePath)`, `useConsentHistory(baseUrl)`, `useOrganizationsApi(basePath)`, `useCommsDashboardApi()`, `useJobsDashboardApi()`, and the `useAiObservability*Api()` hooks).
105
+ - `useAdminApi(api, routePath, modelName)` is removed. Use `useAdminModelApi(routePath, modelName)`, which keeps the same hook names and calls the same `/admin/{routePath}` URLs through the admin fetch client. `AdminApi`, `EndpointBuilder`, and admin `injectEndpoints` are gone; `@reduxjs/toolkit` and `react-redux` are no longer peers of admin-frontend.
106
+ - `AdminProvider` is now the required auth binding for every admin request, including ObjectId and API-only model CRUD. Cookie SPAs pass `credentials="same-origin"`; embedded apps pass `apiOrigin` and `getAuthHeaders`. Product screens that reuse admin components wrap them in `AdminProvider`. See the 58.0.0 upgrade guide.
107
+ - Removed the legacy RTK realtime path. Use `sync` on `modelRouter` with `@terreno/syncdb` for live collection updates. `RealtimeApp`, `SyncApp`, socket auth, `sync:delta`, and admin `admin:model.changed` events are unchanged. See the 58.0.0 upgrade note.
108
+ - `@terreno/api`: `modelRouter` no longer accepts `realtime` (now a TypeScript error), and the change stream no longer emits the `sync` websocket event. Removed `RealtimeConfig`, `RealtimeEvent`, `DocumentSubscription`, `QuerySubscription`, `registerRealtime`, `getRealtimeRegistry`, `findRegistryEntryByCollection`, `findRegistryEntryByRoutePath`, `updateRealtimeRegistryOptions`, `clearRealtimeRegistry`, `RealtimeRegistryEntry`, the query-subscription store (`addQuerySubscription`, `removeQuerySubscription`, `removeAllSocketQueries`, `getQuerySubscriptionsForCollection`, `computeQueryId`, `clearQueryStore`), and `MAX_MODEL_SUBSCRIPTIONS` / `MAX_DOCUMENT_SUBSCRIPTIONS` / `MAX_QUERY_SUBSCRIPTIONS`. Sockets no longer handle `subscribe:model`, `subscribe:document`, or `subscribe:query`.
109
+ - `@terreno/rtk`: removed `realtimeList`, `realtimeDocument`, `setRealtimeSocket`, `getRealtimeSocket`, `useSyncConnection`, and the `RealtimeEvent` type. The rest of `@terreno/rtk` (including `useSocketConnection` and `useRealtimeDebug`) is removed in the same release; see `remove-rtk.md`.
110
+ - Removed `@terreno/rtk` and every Redux dependency (`@reduxjs/toolkit`, `react-redux`, `redux-persist`, `@rtk-query/codegen-openapi`). `@terreno/rtk` is not published for 58. `@terreno/syncdb` is the only frontend data, request, and session layer. Follow `docs/how-to/migrate-rtk-to-syncdb.md`, which maps every removed export to its replacement, and the 58.0.0 upgrade note.
111
+ - Apps upgrading from `@terreno/rtk` must install `expo-constants` and `expo-network` themselves (rtk depended on them; `@terreno/syncdb` lists them as optional peers), plus the optional peers for the features they use: `better-auth`, `@better-auth/expo`, `@better-auth/passkey`, `@openfeature/web-sdk`, `@openfeature/react-sdk`, `@modelcontextprotocol/client`. See the upgrade note.
112
+ - `@terreno/syncdb` adds `@terreno/syncdb/api` (`createApiClient`, `ApiError`), request hooks in `@terreno/syncdb/react` (`ApiClientProvider`, `useApiQuery`, `useLazyApiQuery`, `useApiMutation`, `useSocketEvent`), `@terreno/syncdb/config` (`baseUrl`, `baseWebsocketsUrl`, `appHeaders`), `@terreno/syncdb/auth` (`createBetterAuthClient`, `SessionProvider`, `useSession`, `jwtAuthAdapter`, `useJwtAuth`, token-expiry helpers), `@terreno/syncdb/flags` (`useFeatureFlags`, `useTerrenoFeatureFlags` on the sync socket), `@terreno/syncdb/app` (`useUpgradeCheck`, `hasPermission`, `canOpenAdminPage`, `installDevConsoleLogger`, `useMCPTools`), and `@terreno/syncdb/buildNumber`.
113
+ - `terreno-syncdb-codegen --api-out` generates typed hooks for every non-synced OpenAPI operation (same names as the RTK codegen), replacing `@rtk-query/codegen-openapi`. `bun run sdk` writes `store/apiSdk.ts`.
114
+ - `@terreno/ui` `ConsentNavigator`, `AnnouncementNavigator`, `useConsentForms`, `useSubmitConsent`, `useAnnouncements`, and `useAcknowledgeAnnouncement` take a request `client` instead of an RTK `api`.
115
+ - `GptHistory` (`gpt/histories`) is a synced collection and keeps its ObjectId `_id` (no migration). `McpServiceToken` (`mcp/service-tokens`, read-only) is a synced collection with String ids; run `mcpServiceTokenStringIdsMigration` (`@terreno/api`) before deploying 58 frontends.
116
+ - `create-terreno-app`, the `terreno_bootstrap` prompt, and the `terreno_generate_screen` tool scaffold apps without Redux or `@terreno/rtk`. The terreno-mcp-local `get_rtk_state` tool, the MCP resource `terreno://docs/rtk`, and `terreno state --slice rtk` are removed; use `get_syncdb_state` and `terreno state`.
117
+
118
+ ### Added
119
+
120
+ - `DataTable` columns size themselves when `width` is omitted. Each auto column starts at the wider of its header and its content (sampled from at most 50 rows, measured with a canvas on web and estimated on native), clamped to 80–360px, and auto columns grow to fill the table's container. Columns that set `width` keep it. `DataTableColumn.width` is now optional.
121
+ - `@terreno/syncdb` `createSyncDb({idFormats})` mints 24-hex ObjectIds for a collection set to `"objectId"`, so a model that keeps Mongoose's default ObjectId `_id` can sync without migrating its ids. `mutate()` throws on an explicit non-hex id for such a collection. `generateObjectId` and the `SyncIdFormat` type are exported.
122
+ - Sync `validation` nacks carry an optional `reason`: `invalid_id` when an ObjectId-keyed collection gets a non-hex id (rejected before the mutation is claimed), and `id_collision` when a create names an `_id` another document holds. A collision never touches the existing document and its message never names the holder. The ledger records the reason (`SyncMutation.nackReason`) so a redelivery replays it.
123
+ - `GET /sync/entities` leaves out non-hex ids for an ObjectId-keyed model instead of answering 400, so a rejected create queued for repair cannot fail the rest of the repair batch.
124
+
125
+ ### Changed
126
+
127
+ - The repository's agent rules now generate with rulesync 17 (up from 9). The root rule `.rulesync/rules/00-root.md` now generates `AGENTS.md`, which Devin and other AGENTS.md readers load, in place of `.devin/rules/00-root.md`. Never edit `AGENTS.md` by hand: edit the root rule and run `bun run rules`. The examples that existed only in the old hand-edited `AGENTS.md` (syncdb and RTK SDK usage) moved into the root rule, so `CLAUDE.md`, the Cursor root rule, and Copilot instructions get them too. Links in the root rule now resolve from the repository root. Generated Copilot hooks use the portable `command` field. Generated Copilot subagents no longer list `agent/runSubagent` as their only tool, so they get every default tool. Generated skills carry their `license`, `disable-model-invocation`, and Devin `triggers` frontmatter.
128
+ - `@terreno/syncdb/auth` `SessionProvider`: `logout()` no longer clears the session before the server answers. The session stays in the state until `authClient.signOut()` finishes (`signOutTimeoutMs`, default 10 seconds), and then it is cleared. Meanwhile the new `isSigningOut` state (`useSession().isSigningOut`) is true; `isLoading` is left alone, so a navigator gated on it keeps its screen. Before, a login screen that opened at once could find the still-live session and sign the user straight back in. A sign-out that fails or times out checks the session first. A session that is still live, or a cookie or native session the server cannot be asked about, stays signed in, and `error` is set to `SIGN_OUT_FAILED_MESSAGE` ("Couldn't sign out. Check your connection and try again."). A web bearer client, which drops its token before the request leaves, ends signed out. The global `auth/logout` action (a refused session) now ends signed out even when the sign-out request fails. With `apisToReset` (57.17.0), `logout()` purges the cache when the session clears, not when it is dispatched: a purge at dispatch refetched every mounted screen mid-sign-out, and a refetch that could not reach the server answered 401 and signed the user out even though the sign-out failed. A failed sign-out keeps both the session and the cache. The global `auth/logout` action still purges at once (only the first of several in flight) and again when the session clears. A sign-in for another user that lands while a sign-out is in flight is kept; a sign-out that started signed out asks `getSession` first, so a restore of the session it revoked is cleared. Show `selectBetterAuthIsSigningOut` on the sign-out control and `selectBetterAuthError` after a failed sign-out.
129
+
130
+ ### Removed
131
+
132
+ - `@terreno/ui` no longer exports the `Filter` and `FilterProps` aliases deprecated in 57.7.0. Import `DropdownPanel` and `DropdownPanelProps` instead. `FilterSelectMenu`, `FilterBoolean`, `FilterAccordion`, and `FilterChangesBadge` keep their names.
133
+
134
+ ### Fixed
135
+
136
+ - `@terreno/syncdb`: logging out while `syncDb.start()` is still running no longer loses local writes or logs errors. `start()` now resolves the web encryption key from `GET /sync/key` before it loads the store, so the flush in `stop()` never needs the network after the session has ended. A `stop()` or `signOut()` call now cancels a `start()` that is running or queued ahead of it, so the transport never connects without a session. A failed key fetch is now retried on the next save instead of failing every save for the rest of the session.
137
+
23
138
  ## [58.0.0-beta.2] - 2026-11-03
24
139
 
25
140
  Upgrade note: [`mcp-server/src/docs/upgrades/58.0.0-beta.2.md`](mcp-server/src/docs/upgrades/58.0.0-beta.2.md).
@@ -109,6 +224,37 @@ Upgrade note: [`mcp-server/src/docs/upgrades/58.0.0-beta.1.md`](mcp-server/src/d
109
224
 
110
225
  - `@terreno/syncdb`: logging out while `syncDb.start()` is still running no longer loses local writes or logs errors. `start()` now resolves the web encryption key from `GET /sync/key` before it loads the store, so the flush in `stop()` never needs the network after the session has ended. A `stop()` or `signOut()` call now cancels a `start()` that is running or queued ahead of it, so the transport never connects without a session. A failed key fetch is now retried on the next save instead of failing every save for the rest of the session.
111
226
 
227
+ ## [57.22.0] - 2026-11-03
228
+
229
+ Upgrade note: [`mcp-server/src/docs/upgrades/57.22.0.md`](mcp-server/src/docs/upgrades/57.22.0.md).
230
+
231
+ ### Breaking
232
+
233
+ - `@terreno/ai` no longer integrates with Langfuse. The built-in observability prompt registry, traces, and review queue (`ObservabilityApp` with `createLocalObservabilityPlugin()`) replace it. Removed exports: `LangfuseApp`, `LangfuseCache`, `getCached`, `setCached`, `invalidateCache`, `initLangfuseClient`, `getLangfuseClient`, `isLangfuseInitialized`, `shutdownLangfuseClient`, `compilePrompt`, `createPrompt`, `getPrompt`, `invalidatePromptCache`, `initTracing`, `shutdownTracing`, `createTelemetryConfig`, `preparePromptForAI`, and the types `ChatMessage`, `GetPromptOptions`, `LangfuseAppOptions`, `LangfuseCachedPrompt`, `PaginatedResult`, `PreparePromptResult`, `PromptListItem`, `ScoreSubmission`, `ScoringFunction`, `TelemetrySettings`, `TraceListItem`. The `@terreno/ai/langfuseApp` and `@terreno/ai/langfuseClient` subpath exports are gone, along with the `@langfuse/client`, `@langfuse/otel`, and `@opentelemetry/*` dependencies of `@terreno/ai`.
234
+ - `addGptRoutes` drops `langfuseSystemPromptName`; use `systemPromptName` (a registry prompt) instead. Chat turns no longer set `experimental_telemetry`.
235
+ - `ControlPrimary` is now only `"local"`.
236
+ - The example backend no longer reads `LANGFUSE_SECRET_KEY`, `LANGFUSE_PUBLIC_KEY`, `LANGFUSE_BASE_URL`, `LANGFUSE_ORGANIZATION`, `LANGFUSE_PROJECT`, or `LANGFUSE_PROJECT_ID`. Deploys (`.github/workflows/cd.yml`, `scripts/ci/gcp-deploy.sh`) no longer mount the Langfuse secrets, and `infra/flourish/main.tf` drops the `backend_secret_langfuse_secret_key` and `backend_secret_langfuse_public_key` modules, so the next `terraform apply` deletes those two Secret Manager secrets. Deploy a revision without the mounts before applying.
237
+
238
+ ### Added
239
+
240
+ - `addGptRoutes` accepts `systemPromptName` and `systemPromptLabel`. A chat turn prepends that observability registry prompt to the system prompt and records its version on the trace.
241
+ - The example chat reads `chat-assistant` and the `generate_joke` tool reads `joke-generator` from the observability prompt registry.
242
+ - `bun run backend:seed` fills every AI Observability screen: labelled prompt versions for `example-summarize`, `joke-generator`, and `chat-assistant`, golden datasets, finished experiments, traces with scores, and review items in every status. Run it on an existing database with the `seedAiObservabilityDemo` admin script.
243
+ - Review batches: pick a human question set and press **Grab 10 recent traces** to give reviewers a fixed set of inputs and outputs (`POST /ai/observability/review/batches`).
244
+ - The review item screen shows one input, one output, and every question as Yes / No rows. On web, `Y` / `N` answer the highlighted question, `Enter` submits and moves to the next batch item, and `S` skips. Evaluator dimensions accept a reviewer `question`.
245
+ - Run any LLM judge over a batch and see overall and per-question agreement with the human answers, plus a list of disagreements (`POST /ai/observability/review/batches/:id/judgeRuns`).
246
+ - The AI observability seed adds the `summary-review-questions` question set, two judges for it, and a 10-item batch with 8 answers where one judge agrees 97% and the other 78%.
247
+ - Observability registry prompts can reference other prompts with `{{> base-rules}}`, `{{> base-rules label=staging}}`, or `{{> base-rules version=3}}` (default label `production`). Shared instructions live in one prompt; `PromptRegistry.get`, the playground, experiments, and judges use the resolved text.
248
+ - Every reference must resolve when it is saved. Creating a prompt, saving a version, and moving a label return 400 `Invalid prompt reference` for a malformed tag, an unknown prompt or version, or a label the referenced prompt does not have yet, 400 `Prompt reference cycle` when the change would make prompts include each other, and 400 `Prompt references nested too deeply` when it would nest the prompt, or any prompt that includes it, more than 5 levels.
249
+ - Traces stay named after the called prompt. `ObsTrace.prompts` lists it first, then each shared prompt version it pulled in, marked with `includedBy`. `PromptVersionRef.includes` and harness prompt refs carry the same list.
250
+
251
+ ### Fixed
252
+
253
+ - Review queue: `Y` and `N` answer the highlighted question when a Yes/No segment is focused. **What the AI was given** and **What the AI wrote** are headings above the field cards.
254
+ - Review batches: **Save batch** sits on the New batch card. Search and **More filters** share a baseline.
255
+ - Prompt hub: description, folder, and tags sit on the Versions screen with the editor. System and template fields explain what to write.
256
+ - Evaluator detail loads the judge prompt `outputSchema` (so helpfulness no longer reports a missing dimension) and experiment rows open that experiment.
257
+
112
258
  ## [57.21.0] - 2026-11-01
113
259
 
114
260
  Upgrade note: [`mcp-server/src/docs/upgrades/57.21.0.md`](mcp-server/src/docs/upgrades/57.21.0.md).
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Opt-in admin SPA (Expo Router web app) plus an Express plugin that serves it from a Terreno backend.
4
4
 
5
- Opt-in package that lets any Terreno backend serve a pre-built admin SPA from the
5
+ Opt-in package that lets any Terreno backend server-render a pre-built admin SPA from the
6
6
  same Node process — no separate static-site deploy to GCS/Netlify/Cloudflare Pages.
7
7
 
8
8
  The default backend image stays small: React/Expo assets only ship in the npm
@@ -12,14 +12,18 @@ does) remains supported and unchanged.
12
12
 
13
13
  ## What's here
14
14
 
15
- - **Backend serving plugin** (`src/`, `AdminSpaServeApp`): serves the pre-built Expo
16
- Router static export (`dist/`) and a runtime `app-config.json` from a Terreno backend.
15
+ - **Backend serving plugin** (`src/`, `AdminSpaServeApp`): server-renders the pre-built
16
+ Expo Router export (`dist/client` + `dist/server`) behind an access gate, and serves a
17
+ runtime `app-config.json`, from a Terreno backend.
18
+ - **Route loaders** (`server/`): fetch each screen's data with the visitor's cookie, so the
19
+ first HTML holds the page and its data.
17
20
  - **The SPA frontend** (`app/`, `store/`, `components/`): an Expo Router web app that
18
21
  wires `@terreno/admin-frontend`'s screens with a Better-Auth session gate. It uses the
19
22
  `@terreno/admin-frontend` `apiBase`/`routeBase` prop split so API calls go to `/admin`
20
23
  while in-app navigation stays inside the SPA.
21
24
 
22
- Boot flow: `AppConfigGate` fetches `app-config.json` → `StoreProvider` builds the
25
+ Boot flow: the Express gate decides access and the server renders the page →
26
+ `AppConfigGate` hydrates from the config written into the page → `StoreProvider` builds the
23
27
  Better-Auth client and a same-origin request client and mounts `SessionProvider` from
24
28
  `@terreno/syncdb/auth` → `AdminGate` reads `useSession()` and redirects
25
29
  anonymous users to `/login` and non-admins (admin API returns 403) to `/forbidden`,
@@ -64,6 +68,9 @@ bun add @terreno/admin-spa
64
68
 
65
69
  ## Register with a backend
66
70
 
71
+ Register `BetterAuthApp` before `AdminSpaServeApp`: the gate reads `req.user`, which the
72
+ Better Auth session middleware sets.
73
+
67
74
  ```typescript
68
75
  import {AdminSpaServeApp} from "@terreno/admin-spa";
69
76
  import {AdminApp} from "@terreno/admin-backend";
@@ -95,8 +102,10 @@ cookies attach automatically, no CORS.
95
102
  |---|---|---|
96
103
  | `basePath` | `/console` | Path the SPA mounts at. The `/admin` API is untouched. |
97
104
  | `appConfig` | see below | Runtime config served at `${basePath}/app-config.json`. Merged over defaults. |
98
- | `distDir` | `<pkg>/dist` | Override the pre-built bundle directory (tests / custom builds). |
99
- | `devProxyTarget` | — | In dev, proxy all SPA paths to a running `expo start --web`, e.g. `http://localhost:8083`. |
105
+ | `distDir` | `<pkg>/dist` | The export root holding `client/` and `server/` (compiled binaries, custom builds, tests). |
106
+ | `devProxyTarget` | — | In dev, proxy all SPA paths to a running `expo start --web`, e.g. `http://localhost:8083`. No gate or server render is mounted. |
107
+ | `internalOrigin` | `http://127.0.0.1:${PORT ?? 4000}` | Where server renders fetch API data, with the visitor's cookie. Never taken from `Host` or `X-Forwarded-*`. |
108
+ | `ssrApiPrefixes` | admin API base, `/auth/me`, `/orgs`, `/jobs`, `/comms`, `/configuration`, `/rbac` | API prefixes a render may fetch. Replaces the defaults; keep the admin API base. |
100
109
 
101
110
  ### App config
102
111
 
@@ -115,34 +124,42 @@ auth/admin API paths per consumer without rebuilding. Defaults:
115
124
 
116
125
  ## How serving works
117
126
 
118
- - `${basePath}/_expo` and `${basePath}/assets` are served with
119
- `Cache-Control: public, max-age=31536000, immutable`.
120
- - `${basePath}/app-config.json` and `index.html` are served with
121
- `Cache-Control: no-store`.
122
- - `index.html`'s absolute `/_expo/` and `/assets/` references are rewritten once at
123
- boot to `${basePath}/...` (a no-op when the bundle was already built for that base).
124
- - The serve plugin injects `window.__ADMIN_SPA_BASE__ = "${basePath}"` into `index.html`
125
- so the SPA can resolve `app-config.json` on deep refreshes.
126
- - SPA fallback: both the bare `${basePath}` and `${basePath}/*splat` return
127
- `index.html` (Express 5 named-splat convention).
127
+ | Path | `Cache-Control` |
128
+ |---|---|
129
+ | `${basePath}/_expo/*`, `${basePath}/assets/*` (from `dist/client`) | `public, max-age=31536000, immutable` |
130
+ | `${basePath}/app-config.json` | `no-store` |
131
+ | Pages under `${basePath}` and loader data at `${basePath}/_expo/loaders/*` and root `/_expo/loaders/*` | `no-store, private` |
132
+
133
+ - Pages and loader data can hold PHI. Never cache them in a proxy or CDN.
134
+ - Expo SDK 58's client fetches loader data from the site root, `/_expo/loaders/<route>`,
135
+ ignoring the baked base URL. The plugin claims that root path too; route it to the backend.
136
+ - The gate runs before any render. Anonymous visitors get `/login` (other pages redirect to
137
+ it; loaders answer 401). Signed-in non-admins get `/forbidden` (loaders answer 403). If a
138
+ loopback read fails or takes over 5 s, the gate answers 503 and renders nothing.
139
+ - Each signed-in page makes 3 gate calls (`get-session`, `/auth/me`, `/admin/config`) plus its
140
+ loader's calls to `internalOrigin` (1 to 3 on most screens; on the home page one per model plus
141
+ up to 3), counted in the visitor's own rate-limit bucket.
142
+ - Without `dist/server`, the plugin logs a warning and answers 503 on `${basePath}/*`.
143
+ - `rewriteIndexHtml` and `injectBaseGlobal` are deprecated and will be removed in the next
144
+ major release.
128
145
 
129
146
  ### Mount path / `baseUrl`
130
147
 
131
- Client-side routing under a sub-path requires the bundle to be built with a matching
132
- router base. `app.json` sets `experiments.baseUrl: "/console"`, so the default build is
133
- served at `/console` and `basePath` must match. To mount elsewhere, rebuild with the
134
- matching base (e.g. `EXPO_BASE_URL=/admin-ui bun run build:web`) and set `basePath`
135
- accordingly. Mounting at the origin root (`basePath: "/"`) needs no base.
148
+ `app.json` sets `experiments.baseUrl: "/console"`, and that value is baked into the build.
149
+ `basePath` must equal it, or `register()` throws. To mount elsewhere, change
150
+ `experiments.baseUrl` in `app.json`, run `bun run build:web`, and set `basePath` to match.
151
+ `EXPO_BASE_URL` in the shell has no effect: Metro reads the app config. The published
152
+ package ships the `/console` build only. Mounting at the origin root is not supported.
136
153
 
137
154
  ## Develop, build, and test
138
155
 
139
156
  ```bash
140
157
  bun run compile # compile the server plugin (src/ -> src/dist, CommonJS)
141
- bun run build:web # produce the static export in dist/
158
+ bun run build:web # produce the server export in dist/ (client/ + server/)
142
159
  bun run dev # expo start --web for local SPA development
143
160
  bun run test:ci # serve-plugin unit tests (supertest)
144
- bun run smoke # backend-free smoke: compiled serve plugin + built dist/
145
- bun run test:e2e # Playwright e2e (anonymous -> login) over the built dist/
161
+ bun run smoke # backend-free smoke: compiled serve plugin over the built dist/ (SSR login, root loader, redirect)
162
+ bun run test:e2e # Playwright e2e (SSR, login, shell hydration) over the built dist/
146
163
  ```
147
164
 
148
165
  `dist/` is produced by `bun run build:web` and shipped via
@@ -162,6 +179,8 @@ populated.
162
179
 
163
180
  Full API reference: [docs/reference/admin-spa.md](https://github.com/TerrenoLabs/terreno/blob/master/docs/reference/admin-spa.md)
164
181
 
182
+ Upgrading from the static export: [docs/how-to/upgrade-admin-spa-ssr.md](https://github.com/TerrenoLabs/terreno/blob/master/docs/how-to/upgrade-admin-spa-ssr.md)
183
+
165
184
  ## License and Contributing
166
185
 
167
186
  Licensed under the [MIT License](https://github.com/TerrenoLabs/terreno/blob/master/LICENSE). See [CONTRIBUTING.md](https://github.com/TerrenoLabs/terreno/blob/master/CONTRIBUTING.md) for contribution guidelines.
package/package.json CHANGED
@@ -7,9 +7,9 @@
7
7
  "@better-auth/expo": "^1.6.25",
8
8
  "@expo/vector-icons": "^15.1.1",
9
9
  "@react-native-async-storage/async-storage": "2.2.0",
10
- "@terreno/admin-frontend": "58.0.0-beta.2",
11
- "@terreno/syncdb": "58.0.0-beta.2",
12
- "@terreno/ui": "58.0.0-beta.2",
10
+ "@terreno/admin-frontend": "58.0.0-beta.4",
11
+ "@terreno/syncdb": "58.0.0-beta.4",
12
+ "@terreno/ui": "58.0.0-beta.4",
13
13
  "better-auth": "^1.6.25",
14
14
  "expo": "~58.0.14",
15
15
  "expo-constants": "~58.0.12",
@@ -18,6 +18,7 @@
18
18
  "expo-network": "~58.0.3",
19
19
  "expo-router": "~58.0.23",
20
20
  "expo-secure-store": "~58.0.2",
21
+ "expo-server": "~58.0.5",
21
22
  "expo-splash-screen": "~58.0.10",
22
23
  "expo-status-bar": "~58.0.3",
23
24
  "express": "^5.2.1",
@@ -36,8 +37,8 @@
36
37
  "devDependencies": {
37
38
  "@biomejs/biome": "^2.3.6",
38
39
  "@playwright/test": "^1.58.2",
39
- "@terreno/api": "58.0.0-beta.2",
40
- "@terreno/test": "58.0.0-beta.2",
40
+ "@terreno/api": "58.0.0-beta.4",
41
+ "@terreno/test": "58.0.0-beta.4",
41
42
  "@types/bun": "^1.4.0",
42
43
  "@types/express": "^5.0.6",
43
44
  "@types/luxon": "^3.7.1",
@@ -78,7 +79,7 @@
78
79
  "main": "expo-router/entry",
79
80
  "name": "@terreno/admin-spa",
80
81
  "peerDependencies": {
81
- "@terreno/api": "58.0.0-beta.2"
82
+ "@terreno/api": "58.0.0-beta.4"
82
83
  },
83
84
  "publishConfig": {
84
85
  "access": "public"
@@ -92,13 +93,13 @@
92
93
  "compile": "tsc -p tsconfig.server.json",
93
94
  "compile:watch": "tsc -p tsconfig.server.json -w",
94
95
  "dev": "expo start --web --port 8083",
95
- "lint": "biome check ./src ./app ./store ./components ./e2e playwright.config.ts playwright.integration.config.ts",
96
- "lint:fix": "biome check --write ./src ./app ./store ./components ./e2e playwright.config.ts playwright.integration.config.ts",
96
+ "lint": "biome check ./src ./app ./store ./components ./server ./e2e playwright.config.ts playwright.integration.config.ts",
97
+ "lint:fix": "biome check --write ./src ./app ./store ./components ./server ./e2e playwright.config.ts playwright.integration.config.ts",
97
98
  "smoke": "bun e2e/smoke.ts",
98
99
  "start": "expo start",
99
- "test": "bun test ./src ./store ./components",
100
- "test:agent": "AGENT=1 bun test --only-failures ./src ./store ./components",
101
- "test:ci": "bun test ./src ./store ./components",
100
+ "test": "bun test ./src ./store ./components ./server",
101
+ "test:agent": "AGENT=1 bun test --only-failures ./src ./store ./components ./server",
102
+ "test:ci": "bun test ./src ./store ./components ./server",
102
103
  "test:coverage": "bun run ../scripts/check-coverage.ts",
103
104
  "test:e2e": "playwright test",
104
105
  "test:integration": "playwright test --config playwright.integration.config.ts",
@@ -106,5 +107,5 @@
106
107
  "web": "expo start --web --port 8083"
107
108
  },
108
109
  "types": "./src/dist/index.d.ts",
109
- "version": "58.0.0-beta.2"
110
+ "version": "58.0.0-beta.4"
110
111
  }
@@ -0,0 +1,31 @@
1
+ import type express from "express";
2
+ import type { AdminSpaAppConfig } from "./appConfig";
3
+ export interface AdminSpaGateOptions {
4
+ /** Base path of the admin API on this origin (`/admin`). */
5
+ adminApiBasePath: string;
6
+ appConfig: AdminSpaAppConfig;
7
+ /** Where the SPA is mounted (`/console`). */
8
+ basePath: string;
9
+ /** Renders the page or loader; called inside the request scope. */
10
+ handler: express.RequestHandler;
11
+ /** Origin of this backend for loopback reads. Never derived from the request. */
12
+ internalOrigin: string;
13
+ }
14
+ /**
15
+ * Express middleware that decides access before the admin SPA renders anything. Mounted at
16
+ * the site root, it gates `GET` page and loader requests under `basePath` and loader requests
17
+ * at the root `/_expo/loaders`, and passes every other path through.
18
+ *
19
+ * - Anonymous: pages other than `/login` redirect to `{basePath}/login?returnTo=<path>`;
20
+ * loaders answer 401 JSON (the `login` and `forbidden` loaders run).
21
+ * - Signed in but not an admin (`/admin/config` 403): signed-in-only routes (`/oauth/consent`)
22
+ * render, `/login` with a safe `returnTo` redirects there; other pages redirect to
23
+ * `{basePath}/forbidden`;
24
+ * loaders answer 403 JSON.
25
+ * - Admin: `/login` redirects to a safe `returnTo` (or `{basePath}/`); everything else renders
26
+ * inside a request scope holding the session, profile and admin config, so the page renders
27
+ * with its shell instead of a spinner.
28
+ * - An upstream answer other than 200 / 401 / 403, a network error or a 5 s timeout answers
29
+ * 503 without rendering. A 401 counts as anonymous.
30
+ */
31
+ export declare const createAdminSpaGate: (options: AdminSpaGateOptions) => express.RequestHandler;