@terreno/mcp 57.16.1 → 57.17.0
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/dist/docs/templates/bootstrap/frontend/app/(tabs)/profile.template.tsx +26 -5
- package/dist/docs/templates/bootstrap/frontend/store/sdk.template.md +2 -0
- package/dist/docs/upgrades/57.17.0.md +126 -0
- package/dist/docs/versioned/next/approval-rubric.md +171 -0
- package/dist/docs/versioned/next/explanation/admin-interface.md +5 -1
- package/dist/docs/versioned/next/explanation/agent-ui-asks.md +1 -0
- package/dist/docs/versioned/next/explanation/authentication.md +60 -10
- package/dist/docs/versioned/next/explanation/example-coverage.md +2 -1
- package/dist/docs/versioned/next/explanation/model-router-actions.md +32 -1
- package/dist/docs/versioned/next/how-to/README.md +1 -0
- package/dist/docs/versioned/next/how-to/add-github-oauth.md +49 -16
- package/dist/docs/versioned/next/how-to/admin-add-model.md +4 -1
- package/dist/docs/versioned/next/how-to/ai-feature-development.md +5 -0
- package/dist/docs/versioned/next/how-to/background-jobs.md +18 -5
- package/dist/docs/versioned/next/how-to/circleci.md +39 -3
- package/dist/docs/versioned/next/how-to/configure-better-auth.md +283 -5
- package/dist/docs/versioned/next/how-to/connect-mcp-service-token.md +3 -1
- package/dist/docs/versioned/next/how-to/deploy-backend-to-cloud-run.md +1 -1
- package/dist/docs/versioned/next/how-to/enable-mcp-oauth.md +134 -0
- package/dist/docs/versioned/next/how-to/expose-mcp-tools.md +43 -4
- package/dist/docs/versioned/next/how-to/install-agent-skills.md +8 -1
- package/dist/docs/versioned/next/how-to/migrate-rtk-to-syncdb.md +7 -4
- package/dist/docs/versioned/next/how-to/password-reset.md +8 -2
- package/dist/docs/versioned/next/how-to/product-announcements.md +5 -3
- package/dist/docs/versioned/next/how-to/rate-limiting.md +2 -2
- package/dist/docs/versioned/next/how-to/run-tests-locally.md +50 -0
- package/dist/docs/versioned/next/how-to/use-syncdb-correctly.md +1 -1
- package/dist/docs/versioned/next/how-to/websocket-integration.md +42 -9
- package/dist/docs/versioned/next/how-to/whats-new-banner.md +55 -0
- package/dist/docs/versioned/next/implementationPlans/agent-ui-asks.md +2 -0
- package/dist/docs/versioned/next/implementationPlans/app-mcp-server.md +7 -4
- package/dist/docs/versioned/next/implementationPlans/fix-1594-cta-realtime-better-auth.md +41 -0
- package/dist/docs/versioned/next/implementationPlans/fix-better-auth-sessions-and-config.md +80 -0
- package/dist/docs/versioned/next/implementationPlans/rbac-permissions.md +5 -2
- package/dist/docs/versioned/next/implementationPlans/rtk-token-source-web-bearer.md +246 -0
- package/dist/docs/versioned/next/reference/admin-backend.md +46 -0
- package/dist/docs/versioned/next/reference/admin-config.md +2 -2
- package/dist/docs/versioned/next/reference/ai.md +48 -4
- package/dist/docs/versioned/next/reference/api.md +410 -12
- package/dist/docs/versioned/next/reference/blocks.md +1 -1
- package/dist/docs/versioned/next/reference/comms.md +19 -1
- package/dist/docs/versioned/next/reference/environment-variables.md +6 -1
- package/dist/docs/versioned/next/reference/feature-flags.md +1 -1
- package/dist/docs/versioned/next/reference/jobs.md +8 -6
- package/dist/docs/versioned/next/reference/legacy/rtk.md +145 -18
- package/dist/docs/versioned/next/reference/lifecycle-plugin.md +5 -0
- package/dist/docs/versioned/next/reference/syncdb.md +64 -3
- package/dist/docs/versioned/next/reference/ui.md +23 -1
- package/package.json +3 -3
- package/src/docs/templates/bootstrap/frontend/app/(tabs)/profile.template.tsx +26 -5
- package/src/docs/templates/bootstrap/frontend/store/sdk.template.md +2 -0
- package/src/docs/upgrades/57.17.0.md +126 -0
- package/src/docs/versioned/next/approval-rubric.md +171 -0
- package/src/docs/versioned/next/explanation/admin-interface.md +5 -1
- package/src/docs/versioned/next/explanation/agent-ui-asks.md +1 -0
- package/src/docs/versioned/next/explanation/authentication.md +60 -10
- package/src/docs/versioned/next/explanation/example-coverage.md +2 -1
- package/src/docs/versioned/next/explanation/model-router-actions.md +32 -1
- package/src/docs/versioned/next/how-to/README.md +1 -0
- package/src/docs/versioned/next/how-to/add-github-oauth.md +49 -16
- package/src/docs/versioned/next/how-to/admin-add-model.md +4 -1
- package/src/docs/versioned/next/how-to/ai-feature-development.md +5 -0
- package/src/docs/versioned/next/how-to/background-jobs.md +18 -5
- package/src/docs/versioned/next/how-to/circleci.md +39 -3
- package/src/docs/versioned/next/how-to/configure-better-auth.md +283 -5
- package/src/docs/versioned/next/how-to/connect-mcp-service-token.md +3 -1
- package/src/docs/versioned/next/how-to/deploy-backend-to-cloud-run.md +1 -1
- package/src/docs/versioned/next/how-to/enable-mcp-oauth.md +134 -0
- package/src/docs/versioned/next/how-to/expose-mcp-tools.md +43 -4
- package/src/docs/versioned/next/how-to/install-agent-skills.md +8 -1
- package/src/docs/versioned/next/how-to/migrate-rtk-to-syncdb.md +7 -4
- package/src/docs/versioned/next/how-to/password-reset.md +8 -2
- package/src/docs/versioned/next/how-to/product-announcements.md +5 -3
- package/src/docs/versioned/next/how-to/rate-limiting.md +2 -2
- package/src/docs/versioned/next/how-to/run-tests-locally.md +50 -0
- package/src/docs/versioned/next/how-to/use-syncdb-correctly.md +1 -1
- package/src/docs/versioned/next/how-to/websocket-integration.md +42 -9
- package/src/docs/versioned/next/how-to/whats-new-banner.md +55 -0
- package/src/docs/versioned/next/implementationPlans/agent-ui-asks.md +2 -0
- package/src/docs/versioned/next/implementationPlans/app-mcp-server.md +7 -4
- package/src/docs/versioned/next/implementationPlans/fix-1594-cta-realtime-better-auth.md +41 -0
- package/src/docs/versioned/next/implementationPlans/fix-better-auth-sessions-and-config.md +80 -0
- package/src/docs/versioned/next/implementationPlans/rbac-permissions.md +5 -2
- package/src/docs/versioned/next/implementationPlans/rtk-token-source-web-bearer.md +246 -0
- package/src/docs/versioned/next/reference/admin-backend.md +46 -0
- package/src/docs/versioned/next/reference/admin-config.md +2 -2
- package/src/docs/versioned/next/reference/ai.md +48 -4
- package/src/docs/versioned/next/reference/api.md +410 -12
- package/src/docs/versioned/next/reference/blocks.md +1 -1
- package/src/docs/versioned/next/reference/comms.md +19 -1
- package/src/docs/versioned/next/reference/environment-variables.md +6 -1
- package/src/docs/versioned/next/reference/feature-flags.md +1 -1
- package/src/docs/versioned/next/reference/jobs.md +8 -6
- package/src/docs/versioned/next/reference/legacy/rtk.md +145 -18
- package/src/docs/versioned/next/reference/lifecycle-plugin.md +5 -0
- package/src/docs/versioned/next/reference/syncdb.md +64 -3
- package/src/docs/versioned/next/reference/ui.md +23 -1
|
@@ -47,7 +47,7 @@ Comprehensive guide to environment variables used across Terreno packages and ex
|
|
|
47
47
|
|
|
48
48
|
| Variable | Read by | Required | Default | Secret | Scope |
|
|
49
49
|
|----------|---------|----------|---------|--------|-------|
|
|
50
|
-
| `BETTER_AUTH_SECRET` | `@terreno/api
|
|
50
|
+
| `BETTER_AUTH_SECRET` | `@terreno/api`, example-backend | ✅ if Better Auth | example-backend outside production: a published dev-only value. With `NODE_ENV=production`, example-backend refuses to start when it is unset or equals that value | Yes | server |
|
|
51
51
|
| `BETTER_AUTH_URL` | `@terreno/api` | ✅ if Better Auth | — | No | server |
|
|
52
52
|
| `GOOGLE_CLIENT_ID` | `@terreno/api` | ❌ | — | No | server |
|
|
53
53
|
| `GOOGLE_CLIENT_SECRET` | `@terreno/api` | ❌ | — | Yes | server |
|
|
@@ -282,6 +282,7 @@ example-backend **test** setup only. They are not read by `@terreno/jobs`. Use
|
|
|
282
282
|
| Variable | Read by | Required | Default | Secret | Scope |
|
|
283
283
|
|----------|---------|----------|---------|--------|-------|
|
|
284
284
|
| `OTEL_SERVICE_NAME` | OpenTelemetry (example-backend) | ❌ | `example-backend` | No | server |
|
|
285
|
+
| `SEED_DEFAULTS` | example-backend boot | ❌ | — | No | server |
|
|
285
286
|
| `PR_NUMBER` | PR preview deploy | ❌ | — | No | server |
|
|
286
287
|
| `PR_SERVICE_URL` | PR preview deploy | ❌ | — | No | server |
|
|
287
288
|
| `JOBS_RUNNER` | example-backend | ❌ | `mongo` | No | server |
|
|
@@ -297,6 +298,10 @@ example-backend **test** setup only. They are not read by `@terreno/jobs`. Use
|
|
|
297
298
|
| `WIDGET_CLIENT_SECRET` | widgets | ❌ | — | Yes | server |
|
|
298
299
|
| `API_KEY` | integrations | ❌ | — | Yes | server |
|
|
299
300
|
|
|
301
|
+
`SEED_DEFAULTS=true` runs the example seed (default users, all with the published password
|
|
302
|
+
`testpassword123`) when the server starts. A process with `NODE_ENV=production` logs a warning and
|
|
303
|
+
skips it, so no deployed revision, PR previews included, holds those accounts.
|
|
304
|
+
|
|
300
305
|
## Testing & CI (tooling only)
|
|
301
306
|
|
|
302
307
|
| Variable | Read by | Scope |
|
|
@@ -301,4 +301,4 @@ Segment names can be referenced in flag rules via the admin UI.
|
|
|
301
301
|
|
|
302
302
|
| Variable | Description | Default |
|
|
303
303
|
|----------|-------------|---------|
|
|
304
|
-
| `FEATURE_FLAGS_DEBUG` | Set to `"
|
|
304
|
+
| `FEATURE_FLAGS_DEBUG` | Set to `"true"` to write evaluation diagnostics at `debug` level: flag key, rule field, operator, and result. User attribute values are never logged | Disabled |
|
|
@@ -33,7 +33,7 @@ Peers: `mongoose ^8.0.0 || ^9.0.0`; optional `@google-cloud/tasks`, `@vercel/que
|
|
|
33
33
|
| `basePath` | `"/jobs"` | Admin routes and execute route prefix |
|
|
34
34
|
| `runner` | `MongoJobRunner` | Dispatch + optional poll loop |
|
|
35
35
|
| `timezone` | `"UTC"` | Default IANA zone for schedules without `schedule.timezone` |
|
|
36
|
-
| `lockTtlMs` | `900_000` (15 min) |
|
|
36
|
+
| `lockTtlMs` | `900_000` (15 min) | Claim lease length; renewed every `lockTtlMs / 3` while the handler runs, reclaimable once stale |
|
|
37
37
|
| `pollIntervalMs` | `1_000` | Mongo runner idle sleep between polls |
|
|
38
38
|
| `accessControl` | — | Terreno RBAC; jobs admin needs `admin:access` **and** `admin:jobs` |
|
|
39
39
|
| `redactPayload` | — | Admin payload projection hook (see below) |
|
|
@@ -90,8 +90,10 @@ singleton (tests).
|
|
|
90
90
|
|
|
91
91
|
`@terreno/admin-backend` `defineAdminScriptJob` registers handler name `admin/script` with
|
|
92
92
|
`retry.maxAttempts: 1`. Payload: `{scriptName, taskId, wetRun, args, createdByName?}`.
|
|
93
|
-
The Scripts UI still polls `BackgroundTask` by `taskId`.
|
|
94
|
-
|
|
93
|
+
The Scripts UI still polls `BackgroundTask` by `taskId`. The handler runs the script only
|
|
94
|
+
when it wins the task's `pending` -> `running` transition: a task that already started
|
|
95
|
+
throws a 409 `APIError` (the job goes `dead`), and a task cancelled before it started is
|
|
96
|
+
skipped. Without `JobsApp`, script HTTP runs stay in-process.
|
|
95
97
|
|
|
96
98
|
## JobRunner
|
|
97
99
|
|
|
@@ -116,13 +118,13 @@ Built-in ids: `mongo`, `gcp-cloud-tasks`, `vercel-queues`.
|
|
|
116
118
|
| `payload` | — | Handler input |
|
|
117
119
|
| `payloadRedacted` | `false` | Admin omission flag |
|
|
118
120
|
| `runAt` | — | Earliest claim (indexed) |
|
|
119
|
-
| `attemptCount` | `0` |
|
|
121
|
+
| `attemptCount` | `0` | Failed or lost (lease-expired) attempts so far |
|
|
120
122
|
| `attempts` | `[]` | `{at, error?, errorClass?}` |
|
|
121
123
|
| `maxAttempts` | `5` | From definition at enqueue |
|
|
122
124
|
| `backoffMs` | `1_000` | Retry base |
|
|
123
125
|
| `maxBackoffMs` | `900_000` | Retry cap |
|
|
124
126
|
| `lastError` | — | Latest failure message; `$unset` when the job completes |
|
|
125
|
-
| `lockedAt` / `lockedBy` | — | Claim lease |
|
|
127
|
+
| `lockedAt` / `lockedBy` | — | Claim lease; `lockedAt` is renewed while the handler runs |
|
|
126
128
|
| `idempotencyKey` | — | Partial unique with `name` |
|
|
127
129
|
| `scheduleId` | — | Parent schedule ref |
|
|
128
130
|
| `retriedFromId` / `retriedById` | — | Admin retry linkage |
|
|
@@ -138,7 +140,7 @@ Indexes: `(name, idempotencyKey)` partial unique; `(status, runAt)`; `(scheduleI
|
|
|
138
140
|
| `running` | Claimed; handler executing |
|
|
139
141
|
| `scheduled` | Reserved in schema / in-flight filter |
|
|
140
142
|
| `completed` | Success |
|
|
141
|
-
| `dead` | Exhausted `maxAttempts` |
|
|
143
|
+
| `dead` | Exhausted `maxAttempts` (handler failures and lease-expired reclaims both count) |
|
|
142
144
|
| `failed` | No handler registered at run time |
|
|
143
145
|
| `cancelled` | Admin cancel |
|
|
144
146
|
|
|
@@ -46,7 +46,7 @@ export const store = configureStore({
|
|
|
46
46
|
**Returns:**
|
|
47
47
|
- `authReducer` — Redux reducer for auth state
|
|
48
48
|
- `authSlice` — Full Redux slice
|
|
49
|
-
- `logout` — Action to clear tokens and reset state
|
|
49
|
+
- `logout` — Action to clear tokens and reset state. A logout that ends a signed-in session also purges the API's RTK Query cache (`resetApiState`), so the next user never gets the previous user's cached responses
|
|
50
50
|
- `setUserId` — Action to set current user ID
|
|
51
51
|
- `tokenRefreshedSuccess` — Action signaling token refresh
|
|
52
52
|
- `middleware` — Login/logout listener middleware array
|
|
@@ -88,46 +88,158 @@ const userId = selectCurrentUserId(state);
|
|
|
88
88
|
For apps using Better Auth instead of JWT:
|
|
89
89
|
|
|
90
90
|
``````typescript
|
|
91
|
-
import {createBetterAuthClient, generateBetterAuthSlice} from "@terreno/rtk";
|
|
92
91
|
import {configureStore} from "@reduxjs/toolkit";
|
|
92
|
+
import {createBetterAuthClient, generateBetterAuthSlice} from "@terreno/rtk";
|
|
93
|
+
import {api} from "./sdk"; // the app's generated RTK Query API
|
|
93
94
|
|
|
94
|
-
// Create Better Auth client
|
|
95
|
+
// Create Better Auth client. `scheme` is the app's deep-link scheme (required).
|
|
95
96
|
export const authClient = createBetterAuthClient({
|
|
96
97
|
baseURL: process.env.EXPO_PUBLIC_API_URL || "http://localhost:4000",
|
|
98
|
+
scheme: "myapp",
|
|
97
99
|
});
|
|
98
100
|
|
|
99
|
-
// Generate session Redux slice
|
|
100
|
-
const
|
|
101
|
+
// Generate the session Redux slice; list the APIs whose cache belongs to the signed-in user
|
|
102
|
+
const betterAuth = generateBetterAuthSlice({apisToReset: [api], authClient});
|
|
101
103
|
|
|
102
104
|
export const store = configureStore({
|
|
103
105
|
reducer: {
|
|
104
|
-
|
|
106
|
+
betterAuth: betterAuth.reducer, // selectors read `state.betterAuth`
|
|
105
107
|
// ... other reducers
|
|
106
108
|
},
|
|
107
|
-
middleware: (getDefault) => getDefault().concat(
|
|
109
|
+
middleware: (getDefault) => getDefault().concat(...betterAuth.middleware),
|
|
108
110
|
});
|
|
111
|
+
|
|
112
|
+
// Call after sign-in, sign-out, and at app startup to copy the session into Redux
|
|
113
|
+
void betterAuth.syncSession(store.dispatch);
|
|
109
114
|
``````
|
|
110
115
|
|
|
111
116
|
**Key exports:**
|
|
112
|
-
- `createBetterAuthClient` — Factory for Better Auth client with Expo support
|
|
113
|
-
- `
|
|
114
|
-
- `
|
|
115
|
-
|
|
116
|
-
|
|
117
|
+
- `createBetterAuthClient({baseURL, scheme, storagePrefix?, webAuth?, bearerStorage?})` — Factory for Better Auth client with Expo support. `webAuth: "bearer"` authenticates the web client with a stored bearer token and no cookies, for a frontend and API on different sites ([how-to](../../how-to/configure-better-auth.md#frontend-and-api-on-different-sites))
|
|
118
|
+
- `getBetterAuthBearerToken({storagePrefix?, bearerStorage?})` — Reads the token bearer mode stored, or `null`
|
|
119
|
+
- `signInSocialWithHandoff({client, provider, callbackURL?, errorCallbackURL?})` — Starts a social or generic OAuth sign-in for a web bearer client: stores a random nonce in `sessionStorage` and navigates the page to the server's `{basePath}/oauth-handoff/start` with `handoff=<nonce>` on the callback URLs. Throws on native and in cookie mode ([how-to](../../how-to/configure-better-auth.md#social-sign-in-in-bearer-mode))
|
|
120
|
+
- `completeBetterAuthOAuthHandoff({client}): Promise<boolean>` — On the return page, removes `#ott` and `handoff` from the URL first, then exchanges the token at `{basePath}/one-time-token/verify` only when `handoff` matches the stored nonce. Resolves `true` only when a bearer token was stored; never rejects. Await it at startup before session reads and Sentry
|
|
121
|
+
- `generateBetterAuthSlice({authClient, apisToReset?, syncInterval?})` — Returns `{reducer, middleware, actions, slice, syncSession, authClient}`. Pass the app's RTK Query APIs (for example `apisToReset: [terrenoApi]`) so their caches are purged (`util.resetApiState()`) when a signed-in session logs out (`betterAuth/logout` or `auth/logout`), when `syncSession` finds the session gone, and when the session changes to another user. Without it, the next account on the same client can be served the previous account's cached responses
|
|
122
|
+
- `selectBetterAuthIsAuthenticated`, `selectBetterAuthUserId`, `selectBetterAuthUser` — Selectors over `state.betterAuth`
|
|
123
|
+
- `BetterAuthUser`, `BetterAuthState` — TypeScript types for session/user data
|
|
124
|
+
|
|
125
|
+
**Session State (`state.betterAuth`):**
|
|
117
126
|
``````typescript
|
|
118
127
|
{
|
|
119
|
-
|
|
128
|
+
isAuthenticated: boolean;
|
|
129
|
+
userId: string | null;
|
|
120
130
|
user: BetterAuthUser | null;
|
|
121
131
|
isLoading: boolean;
|
|
132
|
+
error: string | null;
|
|
133
|
+
lastSyncTimestamp: number | null;
|
|
122
134
|
}
|
|
123
135
|
``````
|
|
124
136
|
|
|
125
137
|
**Learn more:** [Configure Better Auth](../../how-to/configure-better-auth.md)
|
|
126
138
|
|
|
139
|
+
### Token source
|
|
140
|
+
|
|
141
|
+
rtk reads the request token from one registered **token source**, typed as syncdb's
|
|
142
|
+
`AuthProvider` (`getToken`, `getUserId`, `onAuthChange`, optional `refresh`). A Better Auth
|
|
143
|
+
app registers the same adapter it builds for syncdb, once at startup:
|
|
144
|
+
|
|
145
|
+
``````typescript
|
|
146
|
+
import {setTokenSource} from "@terreno/rtk";
|
|
147
|
+
import {betterAuthAdapter, bridgeBetterAuthReactClient} from "@terreno/syncdb";
|
|
148
|
+
import {authClient} from "./betterAuth";
|
|
149
|
+
|
|
150
|
+
export const authProvider = betterAuthAdapter(bridgeBetterAuthReactClient(authClient));
|
|
151
|
+
setTokenSource(authProvider); // before the first RTK Query request
|
|
152
|
+
``````
|
|
153
|
+
|
|
154
|
+
With `webAuth: "bearer"`, also pass the adapter `getBearerToken` so it reads the stored token
|
|
155
|
+
without a `get-session` call ([syncdb adapter](../syncdb.md#createsyncdb-configuration)).
|
|
156
|
+
|
|
157
|
+
**Exports:**
|
|
158
|
+
- `setTokenSource(source | undefined)` — Registers the source. `undefined` restores the default.
|
|
159
|
+
- `getTokenSource()` — The registered source, or `undefined` when none is registered.
|
|
160
|
+
- `jwtTokenSource` — The legacy JWT source: `getToken` is `getAuthToken`, `refresh` runs
|
|
161
|
+
`refreshAuthToken` and resolves `true` on success, `getUserId` reads the stored JWT's `id`
|
|
162
|
+
(or `sub`) claim, and `onAuthChange` fires on JWT login, logout, and token refresh.
|
|
163
|
+
- `isJwtTokenSource(source)` — `true` for `undefined` or `jwtTokenSource`.
|
|
164
|
+
|
|
165
|
+
**Base query behavior (`emptySplitApi` / `staggeredBaseQuery`):**
|
|
166
|
+
- **No source registered, or `jwtTokenSource`:** the JWT path below, unchanged. JWT apps need
|
|
167
|
+
no code change.
|
|
168
|
+
- **Any other source:** sends `authorization: Bearer <token>` from `getToken()` on every
|
|
169
|
+
request, or no `authorization` header when the token is null. It never logs out because a
|
|
170
|
+
token is missing. On a 401 it calls `refresh()` once (shared by concurrent requests), re-reads
|
|
171
|
+
the token, and retries once; only a second 401 dispatches `auth/logout`. The public endpoints
|
|
172
|
+
`emailLogin`, `emailSignUp`, `getVersionCheck`, `getZoomSignature`, and `googleLogin` skip
|
|
173
|
+
the refresh and retry.
|
|
174
|
+
|
|
175
|
+
The registry is module-global, like `baseUrl`: one app, one source. Apps that forked the base
|
|
176
|
+
query to read the Better Auth session can point their SDK back at `emptySplitApi` once they
|
|
177
|
+
register the source.
|
|
178
|
+
|
|
179
|
+
**Other consumers.** The socket, chat, MCP, and offline APIs read the same source. Each takes a
|
|
180
|
+
`tokenSource` option that overrides it for that one consumer. Without the option they use the
|
|
181
|
+
registered source, else `jwtTokenSource`. Pass `useSocketConnection` a stable object, not a new
|
|
182
|
+
one per render, or it re-subscribes to `onAuthChange`.
|
|
183
|
+
|
|
184
|
+
| API | Option | How it uses the source |
|
|
185
|
+
| --- | --- | --- |
|
|
186
|
+
| `useSocketConnection` | `tokenSource`, used when `getAuthToken` is omitted | Reads `getToken()` on every connect and reconnect, refreshes once per auth failure, and reconnects when `onAuthChange` brings a new token. See [useSocketConnection](#usesocketconnection). |
|
|
187
|
+
| `useTerrenoChat` | `tokenSource` | Reads `getToken()` per chat request. Sends no `Authorization` header when it returns null. |
|
|
188
|
+
| `useMCPTools` | `tokenSource` | Reads `getToken()` before every MCP request, so a refreshed token needs no refetch. A fetch that starts with no token connects without auth. |
|
|
189
|
+
| `createOfflineMiddleware` | `tokenSource` | With a non-JWT source, tags queued mutations with `getUserId()` and replays them with `getToken()`. See [Offline queue](#offline-queue). |
|
|
190
|
+
|
|
191
|
+
#### Offline queue
|
|
192
|
+
|
|
193
|
+
`createOfflineMiddleware({endpoints, api, tokenSource?})` queues the listed mutations while
|
|
194
|
+
offline and replays them on reconnect.
|
|
195
|
+
|
|
196
|
+
- **JWT source:** the queue reads `state.auth.userId` and the stored JWT, as before.
|
|
197
|
+
- **Any other source** (for example syncdb's `betterAuthAdapter`): each queued mutation carries
|
|
198
|
+
`userId` from `getUserId()`. Replay runs only while that same user is signed in. It sends
|
|
199
|
+
`authorization: Bearer <token>` from `getToken()`, or no header when the token is null.
|
|
200
|
+
- **Last user id:** `state.offline.lastUserId` holds the last id the source confirmed. It
|
|
201
|
+
refreshes on the first action, on rehydrate, on `onAuthChange`, and on reconnect, so a failed
|
|
202
|
+
session read while offline does not untag new mutations.
|
|
203
|
+
- **Persist the `offline` slice** with redux-persist (do not blacklist it). Otherwise mutations
|
|
204
|
+
made after an app launches offline carry no user and are dropped on reconnect.
|
|
205
|
+
- **Unconfirmed ids:** a rehydrated id is unconfirmed until a read in this process returns a
|
|
206
|
+
user. Mutations tagged with it carry `userIdConfirmed: false`. A later read keeps them only
|
|
207
|
+
when it names the same user and no `onAuthChange` or source swap happened since. Any auth
|
|
208
|
+
change drops them, even for the same user.
|
|
209
|
+
- **Cost:** syncdb's `betterAuthAdapter` fires `onAuthChange` on the session's first successful
|
|
210
|
+
load. After an offline launch that happens when the network returns, so it drops
|
|
211
|
+
offline-launch writes the reconnect read has not confirmed yet.
|
|
212
|
+
- **Residual risk:** the signed-in user changes while no running instance of the middleware sees
|
|
213
|
+
it (for example in another browser tab that shares the persisted slice), between an
|
|
214
|
+
unconfirmed write and the next launch's first read. Then that write can replay as a user who
|
|
215
|
+
did not make it.
|
|
216
|
+
- **Sign-out:** `auth/logout`, `betterAuth/logout` (on a non-JWT source), or the source
|
|
217
|
+
reporting no user after `onAuthChange` clears the queue, conflicts, and `lastUserId`, and
|
|
218
|
+
discards in-flight enqueues and replays.
|
|
219
|
+
- **No user on reconnect:** when the source has no user, or the user changes during a replay,
|
|
220
|
+
the rest of the queue stays for a later reconnect.
|
|
221
|
+
- **Different user on reconnect:** a replay drops each mutation queued by a user other than the
|
|
222
|
+
one signed in.
|
|
223
|
+
|
|
224
|
+
Offline-slice exports the middleware uses for the user tag (exported from the package root):
|
|
225
|
+
|
|
226
|
+
- `setLastUserId(userId | undefined)` — Action that sets `state.offline.lastUserId`.
|
|
227
|
+
- `resolveUnconfirmedMutations(userId)` — Action that keeps unconfirmed mutations tagged with
|
|
228
|
+
`userId`, marks them confirmed, and drops the other unconfirmed ones.
|
|
229
|
+
- `dropUnconfirmedMutations()` — Action that drops every mutation whose user id is unconfirmed.
|
|
230
|
+
- `selectLastUserId(state)` — The last user id the source confirmed, or `undefined` when unknown
|
|
231
|
+
or signed out.
|
|
232
|
+
|
|
127
233
|
## WebSocket Integration
|
|
128
234
|
|
|
129
235
|
> **Deprecated (removed in Terreno 58):** `realtimeList`, `realtimeDocument`, `setRealtimeSocket`, and `getRealtimeSocket` patch RTK Query caches from `sync` events. Use [`@terreno/syncdb`](../syncdb.md). `useSocketConnection` remains for feature flags and other sockets.
|
|
130
236
|
|
|
237
|
+
A `realtimeList` query subscription (`subscribe:query`) may filter only on fields in the router's
|
|
238
|
+
`queryFields`, the same as the REST list; the server ignores a subscription that names any other
|
|
239
|
+
field. On RBAC routers with field views, each subscriber's create, update, or delete event is
|
|
240
|
+
decided against the document as their field view masks it, so a hidden field never changes which
|
|
241
|
+
event they receive.
|
|
242
|
+
|
|
131
243
|
### useSocketConnection
|
|
132
244
|
|
|
133
245
|
React hook for managing Socket.io connections with automatic reconnection, token refresh, and user feedback.
|
|
@@ -135,10 +247,10 @@ React hook for managing Socket.io connections with automatic reconnection, token
|
|
|
135
247
|
``````typescript
|
|
136
248
|
import {useSocketConnection} from "@terreno/rtk";
|
|
137
249
|
|
|
250
|
+
// Reads the token source registered with setTokenSource (the JWT in storage by default).
|
|
138
251
|
const {socket, isSocketConnected} = useSocketConnection({
|
|
139
252
|
baseUrl: "wss://api.example.com",
|
|
140
253
|
shouldConnect: !!userId,
|
|
141
|
-
getAuthToken: () => getAuthToken(),
|
|
142
254
|
onConnect: () => console.info("WebSocket connected"),
|
|
143
255
|
onDisconnect: () => console.warn("WebSocket disconnected"),
|
|
144
256
|
onConnectError: (error) => console.error("Connection error:", error),
|
|
@@ -162,7 +274,11 @@ useEffect(() => {
|
|
|
162
274
|
**Options:**
|
|
163
275
|
- `baseUrl` (string, required) — WebSocket server URL
|
|
164
276
|
- `shouldConnect` (boolean, required) — Whether to connect (typically `!!userId`)
|
|
165
|
-
- `
|
|
277
|
+
- `tokenSource` (`AuthProvider`) — Source for the handshake token. Defaults to the
|
|
278
|
+
[registered source](#token-source), else `jwtTokenSource`. Used only when `getAuthToken` is
|
|
279
|
+
omitted.
|
|
280
|
+
- `getAuthToken` (function) — Legacy getter. When passed, the hook reads the token once per
|
|
281
|
+
connect, sends it as a static `auth` payload, and ignores `tokenSource`.
|
|
166
282
|
- `onConnect` (function) — Callback on successful connection
|
|
167
283
|
- `onDisconnect` (function) — Callback on disconnection
|
|
168
284
|
- `onConnectError` (function) — Callback on connection error
|
|
@@ -175,7 +291,7 @@ useEffect(() => {
|
|
|
175
291
|
|
|
176
292
|
**Features:**
|
|
177
293
|
- **Automatic reconnection:** 5 attempts with exponential backoff (1-5 seconds)
|
|
178
|
-
- **Bearer token authentication:**
|
|
294
|
+
- **Bearer token authentication:** Sends `Bearer <token>` in `socket.auth`
|
|
179
295
|
- **Token refresh integration:** Reconnects automatically when tokens are refreshed
|
|
180
296
|
- **User feedback:** Toast notifications for disconnections (after 9+ seconds) and token errors
|
|
181
297
|
- **Connection monitoring:** Periodic checks with automatic reconnection attempts
|
|
@@ -186,7 +302,18 @@ useEffect(() => {
|
|
|
186
302
|
- **Reconnection:** Shows "You have been reconnected" (suppressed if reconnect within 10 seconds)
|
|
187
303
|
- **Token error:** Shows "Error refreshing token. Please log out and log back in..." with persistent error state
|
|
188
304
|
|
|
189
|
-
**Token
|
|
305
|
+
**Token source behavior (no `getAuthToken`):**
|
|
306
|
+
- The handshake reads `getToken()` on every connect and reconnect, including socket.io's own
|
|
307
|
+
reconnects.
|
|
308
|
+
- On an auth `connect_error` (the server's `UnauthorizedError`), the hook calls `refresh()`
|
|
309
|
+
once and reconnects. A second auth failure before a successful connect stops reconnecting and
|
|
310
|
+
calls `onConnectError`. If the source has no `refresh`, or it fails, the hook stops at once.
|
|
311
|
+
A successful connect resets this.
|
|
312
|
+
- While `shouldConnect` is true, an `onAuthChange` with a different token drops a live
|
|
313
|
+
connection and reconnects with the new token. With no token it disconnects. The same token
|
|
314
|
+
keeps the connection up.
|
|
315
|
+
|
|
316
|
+
**JWT token management** (`getAuthToken` passed, or the JWT source):
|
|
190
317
|
- Checks token expiration on disconnect and connection errors
|
|
191
318
|
- Automatically refreshes tokens if expiring within 60 seconds
|
|
192
319
|
- Attempts reconnection after successful token refresh
|
|
@@ -335,7 +462,7 @@ Base RTK Query API with authentication, retry logic, and automatic token refresh
|
|
|
335
462
|
- **Query serialization:** Uses `qs.stringify()` for complex queries (`$in`, `$lt`, `$gte`)
|
|
336
463
|
|
|
337
464
|
**Automatic Headers:**
|
|
338
|
-
- `authorization: Bearer <token>`
|
|
465
|
+
- `authorization: Bearer <token>` (from the registered [token source](#token-source); the JWT in storage by default)
|
|
339
466
|
- `App-Version` (from Expo config)
|
|
340
467
|
- `App-Platform` ("web" or "mobile")
|
|
341
468
|
|
|
@@ -68,6 +68,11 @@ fresh subagent with no parent conversation. If the repository root defines a `pr
|
|
|
68
68
|
package script, Taste runs it as the authoritative local gate; otherwise it falls back
|
|
69
69
|
to lint, typecheck, and locally affected tests in each affected package. Taste then
|
|
70
70
|
pushes and watches product CI.
|
|
71
|
+
Every stage that pushes follows `references/push-ownership.md`: it owns the PR until
|
|
72
|
+
CI is green on every host and every review-bot comment is fixed, declined with a
|
|
73
|
+
reply, or raised as a decision. After three fix rounds that leave the PR red, Taste
|
|
74
|
+
stops pushing and escalates as `BLOCKED`; the final message starts with
|
|
75
|
+
`BLOCKED — PR #<n> still red after <k> fix rounds`. No stage ends quietly on a red PR.
|
|
71
76
|
Taste observes product CI on every discovered host (GitHub Actions, CircleCI,
|
|
72
77
|
Buildkite, and similar), not only GitHub checks. A documented not-applicable host
|
|
73
78
|
counts as skipped; an unexplained untriggered host prevents Brew `PASS`. Brew still
|
|
@@ -91,6 +91,10 @@ todoSchema.plugin(syncPlugin); // stamps per-stream _syncSeq on every write
|
|
|
91
91
|
|
|
92
92
|
Apply plugins **before** the model compiles. Registration validates their presence.
|
|
93
93
|
|
|
94
|
+
`_syncSeq` and `_syncPrevStream` are server-owned. modelRouter create and update (REST, MCP
|
|
95
|
+
tools, and sync mutations) drop them from the client body, and a save that changes only these
|
|
96
|
+
fields gets a freshly claimed seq instead of keeping the value it was given.
|
|
97
|
+
|
|
94
98
|
### modelRouter sync config
|
|
95
99
|
|
|
96
100
|
Use the three-argument `modelRouter` form:
|
|
@@ -132,8 +136,16 @@ new TerrenoApp({userModel: User})
|
|
|
132
136
|
- **`ensureSyncIndexes`** — `TerrenoApp.start()` binds the HTTP port first (or attaches to `httpServer` if the process already listened), then awaits index builds for snapshot queries and sync bookkeeping (`SyncMutation.mutationId` unique, `SyncCounter.stream` unique, etc.). Registration queues this work without contacting MongoDB, so models can load before the database connects. Hosts that build Express without `TerrenoApp.start()` should await `ensureSyncIndexes()` after connecting.
|
|
133
137
|
|
|
134
138
|
Socket auth requires at least one configured authentication method. It enables legacy JWT
|
|
135
|
-
validation when `tokenSecret` or `TOKEN_SECRET` is available.
|
|
136
|
-
|
|
139
|
+
validation when `tokenSecret` or `TOKEN_SECRET` is available. When a `BetterAuthApp` is
|
|
140
|
+
registered on the same `TerrenoApp`, `RealtimeApp` validates Better Auth sessions with its
|
|
141
|
+
instance automatically, with or without a JWT secret. The handshake creates the app `User` on
|
|
142
|
+
first contact and refuses disabled users. A `RealtimeApp` without its own `userModel` uses the
|
|
143
|
+
`TerrenoApp`'s, for JWT and Better Auth sockets alike: socket authorization reads the user from
|
|
144
|
+
the database instead of the token's claims, and the revalidation sweep disconnects users who
|
|
145
|
+
were disabled or deleted. With a user model, the JWT handshake also refuses a token whose user
|
|
146
|
+
is disabled or no longer exists, so a disconnected client cannot reconnect with the same token.
|
|
147
|
+
Pass `betterAuth` only for a `RealtimeApp` mounted without
|
|
148
|
+
`TerrenoApp`:
|
|
137
149
|
|
|
138
150
|
```typescript
|
|
139
151
|
new RealtimeApp({betterAuth: {auth, userModel: User}})
|
|
@@ -162,6 +174,49 @@ bridge exposes the client's `$store.atoms.session` atom and degrades to the poll
|
|
|
162
174
|
that atom is absent. Clients that already expose `getSession` plus a subscribable `useSession` can
|
|
163
175
|
be passed to `betterAuthAdapter` directly.
|
|
164
176
|
|
|
177
|
+
`betterAuthAdapter(client, options)` options:
|
|
178
|
+
|
|
179
|
+
| Option | Type | Default | Description |
|
|
180
|
+
|--------|------|---------|-------------|
|
|
181
|
+
| `getBearerToken` | `() => string \| null` | — | Reads a locally stored session token. When it returns a token, `getToken` uses it and skips `getSession()`. A null or empty result, or a throw, falls back to `getSession()`. `getUserId`, `onAuthChange` and `refresh` always read the session. |
|
|
182
|
+
| `pollIntervalMs` | `number` | `5000` | Polling cadence when the client has no subscribable session |
|
|
183
|
+
| `setIntervalFn` / `clearIntervalFn` | test hooks | global timers | Inject timers for deterministic tests |
|
|
184
|
+
|
|
185
|
+
For a web app in Better Auth bearer mode (`createBetterAuthClient({webAuth: "bearer"})`), pass
|
|
186
|
+
rtk's `getBetterAuthBearerToken` so every sync request reads the stored token without a
|
|
187
|
+
`get-session` round trip. syncdb does not import rtk; the app wires the two together:
|
|
188
|
+
|
|
189
|
+
```typescript
|
|
190
|
+
import {getBetterAuthBearerToken} from "@terreno/rtk";
|
|
191
|
+
import {betterAuthAdapter, bridgeBetterAuthReactClient} from "@terreno/syncdb";
|
|
192
|
+
|
|
193
|
+
const authProvider = betterAuthAdapter(bridgeBetterAuthReactClient(authClient), {
|
|
194
|
+
getBearerToken: () => getBetterAuthBearerToken({storagePrefix: "myapp"}),
|
|
195
|
+
});
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Pass the same `storagePrefix` and `bearerStorage` you gave `createBetterAuthClient`. The option
|
|
199
|
+
is explicit: the adapter never detects bearer mode from the client.
|
|
200
|
+
|
|
201
|
+
The adapter never throws on a failed session read (a rejected `getSession()`, or a `{error}`
|
|
202
|
+
envelope other than a 401). `getToken` and `refresh` answer null and false. `getUserId` answers
|
|
203
|
+
with the user id the server last confirmed, through a successful read or a session-atom
|
|
204
|
+
emission, and null before the first confirmation. A read that fails while the device is offline
|
|
205
|
+
is therefore never reported as a sign-out, which would clear the client's current user and make
|
|
206
|
+
`mutate()` throw. A successful read with no session, or a 401 (from `getSession()` or the session
|
|
207
|
+
atom), is a sign-out; a 401 emission also fires `onAuthChange`, deduped like any other
|
|
208
|
+
signed-out emission.
|
|
209
|
+
|
|
210
|
+
The last confirmed user id follows the newest answer, not the last one to arrive. A read counts
|
|
211
|
+
from when it started; an emission that changes or clears the identity counts from when it
|
|
212
|
+
arrived. A read that started before a sign-out or user switch and resolves after it still
|
|
213
|
+
answers its own caller, but does not update the confirmed user id. Emissions that repeat the
|
|
214
|
+
atom's cached session are not new answers: a refetch-start emission (`isRefetching: true`) is
|
|
215
|
+
ignored, and the value a new subscriber receives on subscribing only fills the confirmed user id
|
|
216
|
+
before anything else has confirmed one.
|
|
217
|
+
|
|
218
|
+
`createSyncDb(options)` options:
|
|
219
|
+
|
|
165
220
|
| Option | Type | Default | Description |
|
|
166
221
|
|--------|------|---------|-------------|
|
|
167
222
|
| `name` | `string` | — (required) | Persisted database name |
|
|
@@ -616,7 +671,7 @@ is 2 requests (`GET /sync/streams` plus one batch) instead of 101.
|
|
|
616
671
|
|
|
617
672
|
| Endpoint | Purpose |
|
|
618
673
|
|----------|---------|
|
|
619
|
-
| `GET /sync/snapshot?collection=&stream=&cursor=&limit=` | Bootstrap + catch-up for one stream (`limit` max 100) |
|
|
674
|
+
| `GET /sync/snapshot?collection=&stream=&cursor=&limit=` | Bootstrap + catch-up for one stream (`limit` max 100). Soft-deleted documents, including pre-sync rows paged by `legacyCursor` (seq 0), arrive as tombstones with `data: null` |
|
|
620
675
|
| `POST /sync/snapshot/batch` | Bootstrap + catch-up for many streams in one round trip. Body `{streams: [{stream, cursor?, legacyCursor?}], limit?}`, at most 500 streams, no repeats. `limit` (default 5,000, max 10,000) is the entity budget for the whole response, split evenly across the streams. Returns `{pages}`, one `GET /sync/snapshot`-shaped page per stream in request order. Any stream the caller may not read fails the whole request |
|
|
621
676
|
| `GET /sync/streams` | Current stream membership for the user |
|
|
622
677
|
| `GET /sync/entities` | Point lookup for entity repair and admin window hydrate. Admin-window callers (`admin:access` with RBAC, else `user.admin`) on `adminBroadcast` collections receive requested ids across product streams. When AdminApp registered a scope, list/read/`queryFilter` still apply; unknown or out-of-scope ids are omitted |
|
|
@@ -642,6 +697,12 @@ Conflict responses on mutate: **409** with `{nack}` body (`code: "conflict"`).
|
|
|
642
697
|
|
|
643
698
|
Limits: 50 collection subscriptions per socket; 100 mutations/second per socket (each batch mutation counts); batches capped at 100.
|
|
644
699
|
|
|
700
|
+
A redelivered `mutationId` returns the outcome recorded for it only to the same user and only
|
|
701
|
+
for the collection it was first sent to; any other replay gets an `unauthorized` nack. A
|
|
702
|
+
replayed `conflict` nack includes `serverDoc` only when the caller can still read the document
|
|
703
|
+
now (read permission and stream membership, or the admin broadcast scope for an admin-window
|
|
704
|
+
mutation); otherwise it carries `serverSeq` alone.
|
|
705
|
+
|
|
645
706
|
## Encryption at rest
|
|
646
707
|
|
|
647
708
|
**Web:** AES-256-GCM encrypted IndexedDB by default via pluggable `KeyProvider`:
|
|
@@ -97,7 +97,7 @@ const AppearanceSetting = () => {
|
|
|
97
97
|
};
|
|
98
98
|
``````
|
|
99
99
|
|
|
100
|
-
Dark `surface.warningLight` uses `warning200`. The lighter warning orange, `warning100`, is 2.52:1 against warning text and misses WCAG AA. Highlighted `DataTable` cells switch to `text.inverted` when `text.primary` is below 4.5:1 on that fill. Inverted `MarkdownView` code uses `surface.neutralDark` when `surface.neutralLight` is below 4.5:1 against that text, and blockquotes do the same with `surface.baseAlternate`. Primary `Button` labels use `text.secondaryDark` when that pair meets 4.5:1 on `surface.primary`, and `text.inverted` otherwise, so `setTheme(darkThemeConfig)` stays readable without changing `colorScheme`. The demo palette generator emits status `050` steps so custom palettes update those dark error, success, and warning surfaces.
|
|
100
|
+
Dark `surface.warningLight` uses `warning200`. The lighter warning orange, `warning100`, is 2.52:1 against warning text and misses WCAG AA. Striped `DataTable` rows use `neutral900` in dark mode instead of `surface.neutralLight`, which is too light for ghost and link text. Highlighted `DataTable` cells switch to `text.inverted` when `text.primary` is below 4.5:1 on that fill. Inverted `MarkdownView` code uses `surface.neutralDark` when `surface.neutralLight` is below 4.5:1 against that text, and blockquotes do the same with `surface.baseAlternate`. Primary `Button` labels use `text.secondaryDark` when that pair meets 4.5:1 on `surface.primary`, and `text.inverted` otherwise, so `setTheme(darkThemeConfig)` stays readable without changing `colorScheme`. The demo palette generator emits status `050` steps so custom palettes update those dark error, success, and warning surfaces.
|
|
101
101
|
|
|
102
102
|
`themeColorSchemeOptions` labels those values Light, Dark, and Follow system.
|
|
103
103
|
`useTheme()` also returns the resolved `colorScheme` (`"light"` or `"dark"`)
|
|
@@ -280,6 +280,26 @@ and do not expose dismiss or read-state actions.
|
|
|
280
280
|
| `preferences` | `{inapp, mail, push, sms}` | Current toggles |
|
|
281
281
|
| `onChange` | `(channel, value) => void` | Per-channel updates |
|
|
282
282
|
|
|
283
|
+
## WhatsNewBanner
|
|
284
|
+
|
|
285
|
+
Non-blocking release notes banner. It shows `What's new in <version>` (plus a count of earlier
|
|
286
|
+
unseen releases) until the viewer opens or dismisses it, and stays hidden until a newer release
|
|
287
|
+
is listed. **See what's new** opens a `Modal` with each unseen release's Markdown notes.
|
|
288
|
+
Steps: [Show release notes with WhatsNewBanner](../how-to/whats-new-banner.md).
|
|
289
|
+
|
|
290
|
+
| Prop | Type | Description |
|
|
291
|
+
|---|---|---|
|
|
292
|
+
| `releases` | `WhatsNewRelease[]` | `{version, body, date?}` entries, newest first. `body` is Markdown |
|
|
293
|
+
| `storageKey` | `string` | Device storage key for the last seen version. Default `@TerrenoUI:whatsNew:lastSeenVersion` |
|
|
294
|
+
| `title` | `string` | Modal title. Default `What's new` |
|
|
295
|
+
| `onOpen` | `() => void` | Called when the viewer opens the notes |
|
|
296
|
+
| `testID` | `string` | Default `whats-new-banner`. Children: `-text`, `-open-button`, `-dismiss`, `-modal`, `-release-<version>` |
|
|
297
|
+
|
|
298
|
+
`getUnseenWhatsNewReleases({releases, lastSeenVersion})` returns the releases newer than
|
|
299
|
+
`lastSeenVersion`; an unknown or missing version counts as nothing seen.
|
|
300
|
+
|
|
301
|
+
`BannerButton` also accepts an optional `testID`.
|
|
302
|
+
|
|
283
303
|
## Component Behaviors
|
|
284
304
|
|
|
285
305
|
### Button Layout Behavior
|
|
@@ -428,6 +448,8 @@ Pass `mascot` when the app owns a character. Terreno ships none. The node render
|
|
|
428
448
|
|
|
429
449
|
The composer row (attachment picker, tools, input, Send) is vertically centered, so controls stay aligned with the input as it grows. The composer input grows with its text up to 200px, then scrolls. The attachment cell is omitted when `onAttachFiles` is not provided.
|
|
430
450
|
|
|
451
|
+
The history sidebar (model selector, toolbar, New chat, and the history list) is a 250px-minimum column. Pass `showHistory={false}` to leave it out so the chat panel takes the full width, as on a phone. The host then owns history navigation and the sidebar's controls. `showHistory` defaults to `true`.
|
|
452
|
+
|
|
431
453
|
Each history row keeps rename and delete in a three-dot overflow menu (`gpt-history-menu-{id}`), anchored with `DropdownPanel`. Rename uses an outlined pencil in the dark secondary text color. Titles truncate before the menu trigger, so the trigger stays aligned at any sidebar width.
|
|
432
454
|
|
|
433
455
|
"Scroll to bottom" appears only when content sits more than 100px below the viewport. It never shows in an empty chat.
|
package/package.json
CHANGED
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
"@modelcontextprotocol/node": "^2.0.0",
|
|
12
12
|
"@modelcontextprotocol/server": "^2.0.0",
|
|
13
13
|
"@sentry/bun": "^10.39.0",
|
|
14
|
-
"@terreno/api": "57.
|
|
15
|
-
"@terreno/blocks": "57.
|
|
14
|
+
"@terreno/api": "57.17.0",
|
|
15
|
+
"@terreno/blocks": "57.17.0",
|
|
16
16
|
"express": "^5.2.1",
|
|
17
17
|
"luxon": "^3.7.2",
|
|
18
18
|
"minisearch": "^7.2.0",
|
|
@@ -105,5 +105,5 @@
|
|
|
105
105
|
"test:coverage": "bun run ../scripts/check-coverage.ts"
|
|
106
106
|
},
|
|
107
107
|
"type": "module",
|
|
108
|
-
"version": "57.
|
|
108
|
+
"version": "57.17.0"
|
|
109
109
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {Box, Button, Card, Heading, Page, Spinner, TapToEdit, Text} from "@terreno/ui";
|
|
1
|
+
import {Box, Button, Card, Heading, Page, Spinner, TapToEdit, Text, TextField} from "@terreno/ui";
|
|
2
2
|
import type React from "react";
|
|
3
|
-
import {useCallback, useEffect, useState} from "react";
|
|
3
|
+
import {useCallback, useEffect, useMemo, useState} from "react";
|
|
4
4
|
import {signOut} from "@/lib/betterAuth";
|
|
5
5
|
import {logout, syncBetterAuthSession, useAppDispatch} from "@/store/index";
|
|
6
6
|
import {useGetMeQuery, usePatchMeMutation} from "@/store/sdk";
|
|
@@ -12,6 +12,7 @@ const ProfileScreen: React.FC = () => {
|
|
|
12
12
|
const [name, setName] = useState<string>("");
|
|
13
13
|
const [email, setEmail] = useState<string>("");
|
|
14
14
|
const [password, setPassword] = useState<string>("");
|
|
15
|
+
const [currentPassword, setCurrentPassword] = useState<string>("");
|
|
15
16
|
const [saveError, setSaveError] = useState<string | null>(null);
|
|
16
17
|
|
|
17
18
|
const user = profile?.data;
|
|
@@ -51,17 +52,27 @@ const ProfileScreen: React.FC = () => {
|
|
|
51
52
|
[updateProfile]
|
|
52
53
|
);
|
|
53
54
|
|
|
55
|
+
// Changing the email needs the current password; an unchanged or re-cased email does not.
|
|
56
|
+
const isEmailChanged = useMemo(
|
|
57
|
+
(): boolean => email.trim().toLowerCase() !== (user?.email ?? "").trim().toLowerCase(),
|
|
58
|
+
[email, user?.email]
|
|
59
|
+
);
|
|
60
|
+
|
|
54
61
|
const handleSaveEmail = useCallback(
|
|
55
62
|
async (value: string): Promise<void> => {
|
|
56
63
|
setSaveError(null);
|
|
57
64
|
try {
|
|
58
|
-
|
|
65
|
+
const body = isEmailChanged ? {currentPassword, email: value} : {email: value};
|
|
66
|
+
await updateProfile(body).unwrap();
|
|
67
|
+
setCurrentPassword("");
|
|
59
68
|
} catch (err) {
|
|
60
69
|
console.error("Error updating email:", err);
|
|
61
|
-
setSaveError(
|
|
70
|
+
setSaveError(
|
|
71
|
+
(err as {data?: {message?: string}})?.data?.message || "Failed to update email"
|
|
72
|
+
);
|
|
62
73
|
}
|
|
63
74
|
},
|
|
64
|
-
[updateProfile]
|
|
75
|
+
[currentPassword, isEmailChanged, updateProfile]
|
|
65
76
|
);
|
|
66
77
|
|
|
67
78
|
const handleSavePassword = useCallback(
|
|
@@ -111,6 +122,16 @@ const ProfileScreen: React.FC = () => {
|
|
|
111
122
|
type="email"
|
|
112
123
|
value={email}
|
|
113
124
|
/>
|
|
125
|
+
{isEmailChanged && (
|
|
126
|
+
<TextField
|
|
127
|
+
helperText="Enter your current password to change your email"
|
|
128
|
+
onChange={setCurrentPassword}
|
|
129
|
+
testID="profile-current-password-input"
|
|
130
|
+
title="Current password"
|
|
131
|
+
type="password"
|
|
132
|
+
value={currentPassword}
|
|
133
|
+
/>
|
|
134
|
+
)}
|
|
114
135
|
<TapToEdit
|
|
115
136
|
helperText="Leave blank to keep your current password"
|
|
116
137
|
onSave={handleSavePassword}
|