@terreno/mcp 57.21.0 → 58.0.0-beta.1
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 +43 -0
- package/README.md +57 -3
- package/dist/bootstrap.d.ts.map +1 -1
- package/dist/bootstrap.js +17 -16
- package/dist/bootstrap.js.map +1 -1
- package/dist/docs/guidelines/admin-frontend/guidelines/core.md +6 -5
- package/dist/docs/guidelines/ui/guidelines/core.md +1 -1
- package/dist/docs/prompts/debug.md +6 -4
- package/dist/docs/prompts/screen.md +11 -17
- package/dist/docs/resources/overview.md +5 -3
- package/dist/docs/resources/patterns.md +157 -238
- package/dist/docs/resources/syncdb.md +44 -16
- package/dist/docs/templates/bootstrap/CLAUDE.template.md +2 -2
- package/dist/docs/templates/bootstrap/cursorrules.template.md +7 -5
- package/dist/docs/templates/bootstrap/frontend/app/(tabs)/profile.template.tsx +8 -12
- package/dist/docs/templates/bootstrap/frontend/app/_layout.template.tsx +75 -52
- package/dist/docs/templates/bootstrap/frontend/app/login.template.tsx +46 -108
- package/dist/docs/templates/bootstrap/frontend/app/signup.template.tsx +53 -0
- package/dist/docs/templates/bootstrap/frontend/lib/betterAuth.template.md +18 -0
- package/dist/docs/templates/bootstrap/frontend/store/apiClient.template.md +33 -0
- package/dist/docs/templates/bootstrap/frontend/store/apiSdk.template.md +76 -0
- package/dist/docs/templates/bootstrap/frontend/store/syncdb.template.md +56 -0
- package/dist/docs/templates/bootstrap/frontend/utils/index.template.md +0 -4
- package/dist/docs/upgrades/58.0.0-beta.1.md +504 -0
- package/dist/docs/upgrades/58.0.0.md +495 -4
- package/dist/docs/versioned/next/README.md +2 -2
- package/dist/docs/versioned/next/approval-rubric.md +2 -2
- package/dist/docs/versioned/next/explanation/admin-interface.md +6 -5
- package/dist/docs/versioned/next/explanation/admin-plugin-frontend.md +1 -1
- package/dist/docs/versioned/next/explanation/ai-workflows.md +5 -3
- package/dist/docs/versioned/next/explanation/announcements/rtk-deprecation.md +16 -11
- package/dist/docs/versioned/next/explanation/authentication.md +62 -43
- package/dist/docs/versioned/next/explanation/dependency-management.md +1 -1
- package/dist/docs/versioned/next/explanation/deployment-baseline.md +1 -1
- package/dist/docs/versioned/next/explanation/example-coverage.md +2 -2
- package/dist/docs/versioned/next/explanation/explicit-any-policy.md +5 -6
- package/dist/docs/versioned/next/explanation/gcp-hosting-architecture.md +1 -1
- package/dist/docs/versioned/next/explanation/local-first-data.md +11 -9
- package/dist/docs/versioned/next/explanation/model-router-actions.md +1 -1
- package/dist/docs/versioned/next/explanation/modular-api-design.md +2 -2
- package/dist/docs/versioned/next/explanation/no-barrel-imports.md +5 -5
- package/dist/docs/versioned/next/explanation/roadmap-seed-issues.md +4 -3
- package/dist/docs/versioned/next/explanation/source-rules.md +2 -2
- package/dist/docs/versioned/next/explanation/typescript-configuration.md +2 -4
- package/dist/docs/versioned/next/explanation/versioning-policy.md +3 -3
- package/dist/docs/versioned/next/how-to/README.md +3 -2
- package/dist/docs/versioned/next/how-to/add-feature-flags.md +50 -25
- package/dist/docs/versioned/next/how-to/add-organizations.md +2 -1
- package/dist/docs/versioned/next/how-to/admin-custom-home.md +8 -2
- package/dist/docs/versioned/next/how-to/admin-custom-widget.md +0 -1
- package/dist/docs/versioned/next/how-to/agent-ui-asks.md +6 -6
- package/dist/docs/versioned/next/how-to/build-admin-screens.md +10 -8
- package/dist/docs/versioned/next/how-to/build-for-web.md +1 -1
- package/dist/docs/versioned/next/how-to/call-external-apis.md +2 -2
- package/dist/docs/versioned/next/how-to/circleci.md +12 -6
- package/dist/docs/versioned/next/how-to/configure-better-auth.md +114 -60
- package/dist/docs/versioned/next/how-to/connect-mcp-service-token.md +10 -4
- package/dist/docs/versioned/next/how-to/create-a-terreno-app.md +14 -2
- package/dist/docs/versioned/next/how-to/debug-with-mcp.md +8 -13
- package/dist/docs/versioned/next/how-to/expose-mcp-tools.md +15 -4
- package/dist/docs/versioned/next/how-to/generate-the-client-sdk.md +33 -0
- package/dist/docs/versioned/next/how-to/github-actions-ci.md +2 -2
- package/dist/docs/versioned/next/how-to/in-app-notifications.md +1 -1
- package/dist/docs/versioned/next/how-to/migrate-rtk-to-syncdb.md +318 -447
- package/dist/docs/versioned/next/how-to/password-reset.md +1 -1
- package/dist/docs/versioned/next/how-to/product-announcements.md +6 -3
- package/dist/docs/versioned/next/how-to/rate-limiting.md +38 -16
- package/dist/docs/versioned/next/how-to/run-mongodb-migrations.md +19 -0
- package/dist/docs/versioned/next/how-to/run-tests-locally.md +2 -3
- package/dist/docs/versioned/next/how-to/set-up-social-login.md +1 -1
- package/dist/docs/versioned/next/how-to/set-up-terreno-mcp.md +2 -2
- package/dist/docs/versioned/next/how-to/track-upstream-expo.md +1 -1
- package/dist/docs/versioned/next/how-to/upgrade-banner.md +5 -3
- package/dist/docs/versioned/next/how-to/upgrade-terreno.md +5 -5
- package/dist/docs/versioned/next/how-to/use-syncdb-correctly.md +1 -1
- package/dist/docs/versioned/next/how-to/websocket-integration.md +113 -402
- package/dist/docs/versioned/next/implementationPlans/b2b-platform-program.md +1 -1
- package/dist/docs/versioned/next/implementationPlans/migrate-admin-to-syncdb.md +5 -0
- package/dist/docs/versioned/next/implementationPlans/native-module-baseline.md +29 -5
- package/dist/docs/versioned/next/implementationPlans/remove-legacy-realtime.md +1 -1
- package/dist/docs/versioned/next/implementationPlans/remove-rtk.md +185 -0
- package/dist/docs/versioned/next/reference/README.md +1 -1
- package/dist/docs/versioned/next/reference/admin-frontend.md +85 -70
- package/dist/docs/versioned/next/reference/admin-spa.md +10 -4
- package/dist/docs/versioned/next/reference/ai.md +36 -4
- package/dist/docs/versioned/next/reference/announcements.md +3 -3
- package/dist/docs/versioned/next/reference/api.md +39 -24
- package/dist/docs/versioned/next/reference/cli.md +3 -4
- package/dist/docs/versioned/next/reference/environment-variables.md +16 -16
- package/dist/docs/versioned/next/reference/feature-flags.md +48 -7
- package/dist/docs/versioned/next/reference/jobs.md +2 -2
- package/dist/docs/versioned/next/reference/legacy/rtk.md +25 -600
- package/dist/docs/versioned/next/reference/mcp-server.md +441 -1158
- package/dist/docs/versioned/next/reference/syncdb.md +426 -16
- package/dist/docs/versioned/next/reference/test.md +28 -3
- package/dist/docs/versioned/next/reference/ui.md +132 -19
- package/dist/docs/versioned/next/tasks/native-module-baseline.md +8 -4
- package/dist/docs/versioned/next/tasks/rate-limiting.md +9 -2
- package/dist/docs/versioned/next/tasks/remove-legacy-realtime.md +5 -5
- package/dist/docs/versioned/next/tasks/remove-rtk.md +155 -0
- package/dist/local/localTools.d.ts.map +1 -1
- package/dist/local/localTools.js +3 -54
- package/dist/local/localTools.js.map +1 -1
- package/dist/local/tools/runtime.d.ts +0 -21
- package/dist/local/tools/runtime.d.ts.map +1 -1
- package/dist/local/tools/runtime.js +0 -176
- package/dist/local/tools/runtime.js.map +1 -1
- package/dist/local/tools/syncDb.d.ts +0 -11
- package/dist/local/tools/syncDb.d.ts.map +1 -1
- package/dist/local/tools/syncDb.js +1 -5
- package/dist/local/tools/syncDb.js.map +1 -1
- package/dist/packageGuidelines.d.ts +3 -2
- package/dist/packageGuidelines.d.ts.map +1 -1
- package/dist/packageGuidelines.js +4 -11
- package/dist/packageGuidelines.js.map +1 -1
- package/dist/prompts.d.ts.map +1 -1
- package/dist/prompts.js +17 -18
- package/dist/prompts.js.map +1 -1
- package/dist/resources.d.ts.map +1 -1
- package/dist/resources.js +1 -8
- package/dist/resources.js.map +1 -1
- package/dist/search/chunker.d.ts +1 -1
- package/dist/search/chunker.d.ts.map +1 -1
- package/dist/search/inferPackages.d.ts +1 -1
- package/dist/search/inferPackages.js +5 -5
- package/dist/search/inferPackages.js.map +1 -1
- package/dist/tools.d.ts +6 -0
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +126 -119
- package/dist/tools.js.map +1 -1
- package/package.json +6 -6
- package/src/docs/guidelines/admin-frontend/guidelines/core.md +6 -5
- package/src/docs/guidelines/ui/guidelines/core.md +1 -1
- package/src/docs/prompts/debug.md +6 -4
- package/src/docs/prompts/screen.md +11 -17
- package/src/docs/resources/overview.md +5 -3
- package/src/docs/resources/patterns.md +157 -238
- package/src/docs/resources/syncdb.md +44 -16
- package/src/docs/templates/bootstrap/CLAUDE.template.md +2 -2
- package/src/docs/templates/bootstrap/cursorrules.template.md +7 -5
- package/src/docs/templates/bootstrap/frontend/app/(tabs)/profile.template.tsx +8 -12
- package/src/docs/templates/bootstrap/frontend/app/_layout.template.tsx +75 -52
- package/src/docs/templates/bootstrap/frontend/app/login.template.tsx +46 -108
- package/src/docs/templates/bootstrap/frontend/app/signup.template.tsx +53 -0
- package/src/docs/templates/bootstrap/frontend/lib/betterAuth.template.md +18 -0
- package/src/docs/templates/bootstrap/frontend/store/apiClient.template.md +33 -0
- package/src/docs/templates/bootstrap/frontend/store/apiSdk.template.md +76 -0
- package/src/docs/templates/bootstrap/frontend/store/syncdb.template.md +56 -0
- package/src/docs/templates/bootstrap/frontend/utils/index.template.md +0 -4
- package/src/docs/upgrades/58.0.0-beta.1.md +504 -0
- package/src/docs/upgrades/58.0.0.md +495 -4
- package/src/docs/versioned/next/README.md +2 -2
- package/src/docs/versioned/next/approval-rubric.md +2 -2
- package/src/docs/versioned/next/explanation/admin-interface.md +6 -5
- package/src/docs/versioned/next/explanation/admin-plugin-frontend.md +1 -1
- package/src/docs/versioned/next/explanation/ai-workflows.md +5 -3
- package/src/docs/versioned/next/explanation/announcements/rtk-deprecation.md +16 -11
- package/src/docs/versioned/next/explanation/authentication.md +62 -43
- package/src/docs/versioned/next/explanation/dependency-management.md +1 -1
- package/src/docs/versioned/next/explanation/deployment-baseline.md +1 -1
- package/src/docs/versioned/next/explanation/example-coverage.md +2 -2
- package/src/docs/versioned/next/explanation/explicit-any-policy.md +5 -6
- package/src/docs/versioned/next/explanation/gcp-hosting-architecture.md +1 -1
- package/src/docs/versioned/next/explanation/local-first-data.md +11 -9
- package/src/docs/versioned/next/explanation/model-router-actions.md +1 -1
- package/src/docs/versioned/next/explanation/modular-api-design.md +2 -2
- package/src/docs/versioned/next/explanation/no-barrel-imports.md +5 -5
- package/src/docs/versioned/next/explanation/roadmap-seed-issues.md +4 -3
- package/src/docs/versioned/next/explanation/source-rules.md +2 -2
- package/src/docs/versioned/next/explanation/typescript-configuration.md +2 -4
- package/src/docs/versioned/next/explanation/versioning-policy.md +3 -3
- package/src/docs/versioned/next/how-to/README.md +3 -2
- package/src/docs/versioned/next/how-to/add-feature-flags.md +50 -25
- package/src/docs/versioned/next/how-to/add-organizations.md +2 -1
- package/src/docs/versioned/next/how-to/admin-custom-home.md +8 -2
- package/src/docs/versioned/next/how-to/admin-custom-widget.md +0 -1
- package/src/docs/versioned/next/how-to/agent-ui-asks.md +6 -6
- package/src/docs/versioned/next/how-to/build-admin-screens.md +10 -8
- package/src/docs/versioned/next/how-to/build-for-web.md +1 -1
- package/src/docs/versioned/next/how-to/call-external-apis.md +2 -2
- package/src/docs/versioned/next/how-to/circleci.md +12 -6
- package/src/docs/versioned/next/how-to/configure-better-auth.md +114 -60
- package/src/docs/versioned/next/how-to/connect-mcp-service-token.md +10 -4
- package/src/docs/versioned/next/how-to/create-a-terreno-app.md +14 -2
- package/src/docs/versioned/next/how-to/debug-with-mcp.md +8 -13
- package/src/docs/versioned/next/how-to/expose-mcp-tools.md +15 -4
- package/src/docs/versioned/next/how-to/generate-the-client-sdk.md +33 -0
- package/src/docs/versioned/next/how-to/github-actions-ci.md +2 -2
- package/src/docs/versioned/next/how-to/in-app-notifications.md +1 -1
- package/src/docs/versioned/next/how-to/migrate-rtk-to-syncdb.md +318 -447
- package/src/docs/versioned/next/how-to/password-reset.md +1 -1
- package/src/docs/versioned/next/how-to/product-announcements.md +6 -3
- package/src/docs/versioned/next/how-to/rate-limiting.md +38 -16
- package/src/docs/versioned/next/how-to/run-mongodb-migrations.md +19 -0
- package/src/docs/versioned/next/how-to/run-tests-locally.md +2 -3
- package/src/docs/versioned/next/how-to/set-up-social-login.md +1 -1
- package/src/docs/versioned/next/how-to/set-up-terreno-mcp.md +2 -2
- package/src/docs/versioned/next/how-to/track-upstream-expo.md +1 -1
- package/src/docs/versioned/next/how-to/upgrade-banner.md +5 -3
- package/src/docs/versioned/next/how-to/upgrade-terreno.md +5 -5
- package/src/docs/versioned/next/how-to/use-syncdb-correctly.md +1 -1
- package/src/docs/versioned/next/how-to/websocket-integration.md +113 -402
- package/src/docs/versioned/next/implementationPlans/b2b-platform-program.md +1 -1
- package/src/docs/versioned/next/implementationPlans/migrate-admin-to-syncdb.md +5 -0
- package/src/docs/versioned/next/implementationPlans/native-module-baseline.md +29 -5
- package/src/docs/versioned/next/implementationPlans/remove-legacy-realtime.md +1 -1
- package/src/docs/versioned/next/implementationPlans/remove-rtk.md +185 -0
- package/src/docs/versioned/next/reference/README.md +1 -1
- package/src/docs/versioned/next/reference/admin-frontend.md +85 -70
- package/src/docs/versioned/next/reference/admin-spa.md +10 -4
- package/src/docs/versioned/next/reference/ai.md +36 -4
- package/src/docs/versioned/next/reference/announcements.md +3 -3
- package/src/docs/versioned/next/reference/api.md +39 -24
- package/src/docs/versioned/next/reference/cli.md +3 -4
- package/src/docs/versioned/next/reference/environment-variables.md +16 -16
- package/src/docs/versioned/next/reference/feature-flags.md +48 -7
- package/src/docs/versioned/next/reference/jobs.md +2 -2
- package/src/docs/versioned/next/reference/legacy/rtk.md +25 -600
- package/src/docs/versioned/next/reference/mcp-server.md +441 -1158
- package/src/docs/versioned/next/reference/syncdb.md +426 -16
- package/src/docs/versioned/next/reference/test.md +28 -3
- package/src/docs/versioned/next/reference/ui.md +132 -19
- package/src/docs/versioned/next/tasks/native-module-baseline.md +8 -4
- package/src/docs/versioned/next/tasks/rate-limiting.md +9 -2
- package/src/docs/versioned/next/tasks/remove-legacy-realtime.md +5 -5
- package/src/docs/versioned/next/tasks/remove-rtk.md +155 -0
- package/dist/docs/guidelines/rtk/guidelines/core.md +0 -26
- package/dist/docs/resources/rtk.md +0 -181
- package/dist/docs/templates/bootstrap/frontend/openapi-config.template.md +0 -16
- package/dist/docs/templates/bootstrap/frontend/scripts/generate-sdk.template.md +0 -49
- package/dist/docs/templates/bootstrap/frontend/store/appState.template.md +0 -45
- package/dist/docs/templates/bootstrap/frontend/store/errors.template.md +0 -51
- package/dist/docs/templates/bootstrap/frontend/store/index.template.md +0 -86
- package/dist/docs/templates/bootstrap/frontend/store/openApiSdk.template.md +0 -49
- package/dist/docs/templates/bootstrap/frontend/store/sdk.template.md +0 -80
- package/src/docs/guidelines/rtk/guidelines/core.md +0 -26
- package/src/docs/resources/rtk.md +0 -181
- package/src/docs/templates/bootstrap/frontend/openapi-config.template.md +0 -16
- package/src/docs/templates/bootstrap/frontend/scripts/generate-sdk.template.md +0 -49
- package/src/docs/templates/bootstrap/frontend/store/appState.template.md +0 -45
- package/src/docs/templates/bootstrap/frontend/store/errors.template.md +0 -51
- package/src/docs/templates/bootstrap/frontend/store/index.template.md +0 -86
- package/src/docs/templates/bootstrap/frontend/store/openApiSdk.template.md +0 -49
- package/src/docs/templates/bootstrap/frontend/store/sdk.template.md +0 -80
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @terreno/syncdb
|
|
2
2
|
|
|
3
|
-
Local-first data layer for Terreno frontends. A TinyBase `MergeableStore` on device is the UI source of truth: reads come from the local store, writes apply optimistically into a durable outbox, and the server reconciles asynchronously over a socket delta protocol with HTTP snapshot catch-up.
|
|
3
|
+
Local-first data layer for Terreno frontends. A TinyBase `MergeableStore` on device is the UI source of truth: reads come from the local store, writes apply optimistically into a durable outbox, and the server reconciles asynchronously over a socket delta protocol with HTTP snapshot catch-up. It is also the frontend's request client, session, feature-flag, and app-utility layer; Terreno 58 removed `@terreno/rtk` and Redux ([upgrade guide](../how-to/migrate-rtk-to-syncdb.md)).
|
|
4
4
|
|
|
5
5
|
## Table of Contents
|
|
6
6
|
|
|
@@ -11,6 +11,11 @@ Local-first data layer for Terreno frontends. A TinyBase `MergeableStore` on dev
|
|
|
11
11
|
- [SyncDb client methods](#syncdb-client-methods)
|
|
12
12
|
- [React hooks](#react-hooks)
|
|
13
13
|
- [Query windows](#query-windows)
|
|
14
|
+
- [API client](#api-client)
|
|
15
|
+
- [Config](#config)
|
|
16
|
+
- [Auth](#auth)
|
|
17
|
+
- [App utilities](#app-utilities)
|
|
18
|
+
- [Feature flags](#feature-flags)
|
|
14
19
|
- [Codegen](#codegen)
|
|
15
20
|
- [Conflict API](#conflict-api)
|
|
16
21
|
- [Sync status API](#sync-status-api)
|
|
@@ -27,7 +32,13 @@ Local-first data layer for Terreno frontends. A TinyBase `MergeableStore` on dev
|
|
|
27
32
|
- `betterAuthAdapter`, `bridgeBetterAuthReactClient`, `AuthProvider`
|
|
28
33
|
- `listConflicts`, `wipeLocalData`, `generateMutationId`
|
|
29
34
|
- Queries: `QueryFieldNotAllowedError`, `ListRequestError`, `WhereFilter`, `SortSpec`, `WindowQuery`, `QueryWindowState`, `QueryWindows`, `compileWhere`, `DEFAULT_WINDOW_PAGE_SIZE`
|
|
30
|
-
- React (`@terreno/syncdb/react`): `SyncDbProvider`, `useEntity`, `useQuery`, `useEntityIds`, `useWindowQuery`, `useMutate`, `useSyncStatus`, `useConflicts`, `useSyncDebugLog`, `createCollectionHooks`
|
|
35
|
+
- React (`@terreno/syncdb/react`): `SyncDbProvider`, `useEntity`, `useQuery`, `useEntityIds`, `useWindowQuery`, `useMutate`, `useSyncStatus`, `useConflicts`, `useSyncDebugLog`, `createCollectionHooks`, `ApiClientProvider`, `useApiClient`, `useApiQuery`, `useLazyApiQuery`, `useApiMutation`, `useSocketEvent`
|
|
36
|
+
- API client (`@terreno/syncdb/api`): `createApiClient`, `ApiClient`, `ApiError`, `isApiError`, `apiFetch`, `unwrapEnvelope`, `serializeParams`
|
|
37
|
+
- Config (`@terreno/syncdb/config`): `baseUrl`, `baseWebsocketsUrl`, `baseTasksUrl`, `appHeaders`, `isWeb`, `resolveBaseUrls`, `resolveDevApiPort`, `SAME_ORIGIN_SENTINEL`
|
|
38
|
+
- Auth (`@terreno/syncdb/auth`): `createBetterAuthClient`, `createStorageAdapter`, `repairCookieJar`, `withCookieJarRepair`, `SessionProvider`, `useSession`, `useSessionStore`, `createSessionStore`, `betterAuthSessionSource`, `betterAuthAdapter`, `bridgeBetterAuthReactClient`, Better Auth types; JWT (legacy): `jwtAuthAdapter`, `useJwtAuth`, `getTokenExpirationTimes`, `shouldShowStillThereModal`, `createJwtTokenStorage`, `readJwtTokens`, `decodeJwtClaims`
|
|
39
|
+
- App (`@terreno/syncdb/app`): `useUpgradeCheck`, `hasPermission`, `canOpenAdminPage`, `ADMIN_PAGE_PERMISSION`, `installDevConsoleLogger`, `useMCPTools`
|
|
40
|
+
- Flags (`@terreno/syncdb/flags`): `useFeatureFlags`, `useTerrenoFeatureFlags`, `TerrenoFlagConfiguration`
|
|
41
|
+
- Build number (`@terreno/syncdb/buildNumber`, node-only): `resolveBuildNumber`, `coerceBuildNumber`
|
|
31
42
|
- CLI: `terreno-syncdb-codegen` (generates `SYNC_COLLECTIONS` + friendly hooks from OpenAPI)
|
|
32
43
|
- Testing (`@terreno/syncdb/testing`): `createFakeTransport`
|
|
33
44
|
- Dev console logger: `installTerrenoDevConsoleLogger({baseUrl})` from `@terreno/syncdb`, also
|
|
@@ -139,7 +150,7 @@ new TerrenoApp({userModel: User})
|
|
|
139
150
|
```
|
|
140
151
|
|
|
141
152
|
- **`SyncApp`** — HTTP sync routes (`/sync/snapshot`, `/sync/mutate`, `/sync/mutate/batch`, `/sync/key`, `/sync/streams`, `/sync/entities`).
|
|
142
|
-
- **`RealtimeApp`** — Socket.io server with change-stream-driven `sync:delta` emission. Requires a **MongoDB replica set** (change streams). `modelRouter` `realtime`
|
|
153
|
+
- **`RealtimeApp`** — Socket.io server with change-stream-driven `sync:delta` emission. Requires a **MongoDB replica set** (change streams). `modelRouter` `realtime` and its RTK `sync` events were removed in Terreno 58; `sync:delta` is the only collection event.
|
|
143
154
|
- **`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.
|
|
144
155
|
|
|
145
156
|
Socket auth requires at least one configured authentication method. It enables legacy JWT
|
|
@@ -190,12 +201,12 @@ be passed to `betterAuthAdapter` directly.
|
|
|
190
201
|
| `setIntervalFn` / `clearIntervalFn` | test hooks | global timers | Inject timers for deterministic tests |
|
|
191
202
|
|
|
192
203
|
For a web app in Better Auth bearer mode (`createBetterAuthClient({webAuth: "bearer"})`), pass
|
|
193
|
-
|
|
194
|
-
`get-session` round trip
|
|
204
|
+
`getBetterAuthBearerToken` from `@terreno/syncdb/auth` so every sync and request-client call
|
|
205
|
+
reads the stored token without a `get-session` round trip:
|
|
195
206
|
|
|
196
207
|
```typescript
|
|
197
|
-
import {getBetterAuthBearerToken} from "@terreno/rtk";
|
|
198
208
|
import {betterAuthAdapter, bridgeBetterAuthReactClient} from "@terreno/syncdb";
|
|
209
|
+
import {getBetterAuthBearerToken} from "@terreno/syncdb/auth";
|
|
199
210
|
|
|
200
211
|
const authProvider = betterAuthAdapter(bridgeBetterAuthReactClient(authClient), {
|
|
201
212
|
getBearerToken: () => getBetterAuthBearerToken({storagePrefix: "myapp"}),
|
|
@@ -261,7 +272,7 @@ before anything else has confirmed one.
|
|
|
261
272
|
| Method | Description |
|
|
262
273
|
|--------|-------------|
|
|
263
274
|
| `start()` | Resolve user, run wipe-on-user-change check, start persistence, connect transport, subscribe collections, start reconcile timer. Resolves even when offline. Idempotent while already started. |
|
|
264
|
-
| `stop()` | Disconnect, stop persistence, clear timers and listeners. Auth changes that fired before the stop but had not run yet are dropped, so a stopped client never reconnects on its own (for example while the app signs out). A stream discovery (`GET /sync/streams`) that was still reading the token when `stop()` ran is not sent, so it never reaches the server with a session the app is signing out of. |
|
|
275
|
+
| `stop()` | Disconnect, flush and stop persistence, clear timers and listeners. Cancels a `start()` that is still running or queued ahead of it, so the transport never connects after logout. Auth changes that fired before the stop but had not run yet are dropped, so a stopped client never reconnects on its own (for example while the app signs out). A stream discovery (`GET /sync/streams`) that was still reading the token when `stop()` ran is not sent, so it never reaches the server with a session the app is signing out of. |
|
|
265
276
|
| `goOffline()` | Simulated outage: disconnect transport, pause replay/reconcile/timer; local mutations keep queueing. |
|
|
266
277
|
| `goOnline()` | End simulated outage; reconnect triggers reconcile + outbox replay. |
|
|
267
278
|
| `mutate({collection, operation, id?, data?})` | Optimistic local write + durable outbox enqueue + fire-and-forget replay. Returns `{mutationId, id}`. |
|
|
@@ -272,9 +283,10 @@ before anything else has confirmed one.
|
|
|
272
283
|
| `replayOutbox()` | Drain queued mutations for the current user now. |
|
|
273
284
|
| `resolveConflict({mutationId, strategy})` | Apply `"useServer"` or `"keepMine"` to a recorded conflict. |
|
|
274
285
|
| `retryFailed({entityId})` | Re-enable an entity's queued successors after a terminal validation failure. |
|
|
275
|
-
| `signOut()` | Explicit teardown like `stop()
|
|
286
|
+
| `signOut()` | Explicit teardown like `stop()` (including cancelling an in-flight `start()`); wipes local data only when `wipeOnSignOut: true`. |
|
|
276
287
|
| `getSyncStatus()` | Snapshot of aggregate sync state (see [Sync status API](#sync-status-api)). |
|
|
277
288
|
| `onStatusChange(callback)` | Subscribe to connectivity/syncing changes; returns unsubscribe. |
|
|
289
|
+
| `onSocketEvent(event, handler)` | Listen for an app-level server event (for example `featureFlagsChanged`) on the sync socket; returns unsubscribe. Listeners survive `stop()`/`start()` and reconnects. `sync:*` and Socket.io lifecycle names throw. A transport without `onEvent` returns a no-op. |
|
|
278
290
|
| `store` | Read surface over the local MergeableStore (`SyncStore`). |
|
|
279
291
|
| `outbox` | Durable mutation outbox (`Outbox`). |
|
|
280
292
|
| `debug` | `SyncDebugLog` when `debug` is enabled; otherwise `undefined`. |
|
|
@@ -442,6 +454,20 @@ Available when the client was created with `debug: true`. `log?.snapshot()` retu
|
|
|
442
454
|
|
|
443
455
|
Returns the `SyncDb` instance from context (escape hatch for imperative calls like `forceResync()`).
|
|
444
456
|
|
|
457
|
+
### `useSocketEvent(event, handler, options?)`
|
|
458
|
+
|
|
459
|
+
Binds `handler` to a server socket event while mounted, through `onSocketEvent` on the
|
|
460
|
+
`SyncDbProvider` client. The latest handler always runs; the listener is rebound only when
|
|
461
|
+
`event`, `client`, or `enabled` changes. Options: `client` (a `SyncDb`, or `null` to listen to
|
|
462
|
+
nothing; required outside a provider) and `enabled` (default `true`). See
|
|
463
|
+
[WebSocket integration](../how-to/websocket-integration.md).
|
|
464
|
+
|
|
465
|
+
```typescript
|
|
466
|
+
useSocketEvent("announcementPublished", () => {
|
|
467
|
+
void refetch();
|
|
468
|
+
});
|
|
469
|
+
```
|
|
470
|
+
|
|
445
471
|
### `createCollectionHooks`
|
|
446
472
|
|
|
447
473
|
Factory used by `terreno-syncdb-codegen` and by hand-written custom collections. Returns six operation hooks (`useListQuery`, `useWindowQuery`, `useReadQuery`, `useCreateMutation`, `useUpdateMutation`, `useDeleteMutation`). Generated SDKs rename them to friendly names (`useTodos`, `useTodosWindow`, `useTodo`, `useCreateTodo`, …). `useWindowQuery` works only for collections listed in `queryCollections`. Mutation hooks return `[trigger]`; triggers apply locally and return `{mutationId, id}` synchronously. Optional `retries` maps to `maxAttempts` (`false` → 1, a number → that many, omitted → engine default).
|
|
@@ -453,6 +479,331 @@ export const {useListQuery: useNotes, useCreateMutation: useCreateNote} =
|
|
|
453
479
|
});
|
|
454
480
|
```
|
|
455
481
|
|
|
482
|
+
## API client
|
|
483
|
+
|
|
484
|
+
Synced collections read and write through the local store. Every other route —
|
|
485
|
+
`/auth/me`, modelRouter actions, AI, consents, announcements, version check — goes
|
|
486
|
+
through the request client in `@terreno/syncdb/api`. It uses the same `AuthProvider` as
|
|
487
|
+
`createSyncDb`, so both send the same token and refresh it the same way.
|
|
488
|
+
|
|
489
|
+
```typescript
|
|
490
|
+
import {createApiClient} from "@terreno/syncdb/api";
|
|
491
|
+
import {appHeaders, baseUrl} from "@terreno/syncdb/config";
|
|
492
|
+
|
|
493
|
+
export const apiClient = createApiClient({
|
|
494
|
+
authProvider, // the provider passed to createSyncDb
|
|
495
|
+
baseUrl,
|
|
496
|
+
credentials: "include", // "same-origin" for cookie SPAs
|
|
497
|
+
headers: appHeaders, // App-Version + App-Platform
|
|
498
|
+
});
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
Wrap the app in `<ApiClientProvider client={apiClient}>`, then:
|
|
502
|
+
|
|
503
|
+
```typescript
|
|
504
|
+
import {useApiMutation, useApiQuery} from "@terreno/syncdb/react";
|
|
505
|
+
|
|
506
|
+
const {data: me, isLoading, refetch} = useApiQuery<Profile>(
|
|
507
|
+
{url: "/auth/me"},
|
|
508
|
+
{providesTags: ["profile"], skip: !userId}
|
|
509
|
+
);
|
|
510
|
+
const [updateMe, {isLoading: isSaving}] = useApiMutation<UpdateProfile, Profile>(
|
|
511
|
+
(body) => ({body, method: "PATCH", url: "/auth/me"}),
|
|
512
|
+
{invalidatesTags: ["profile"]}
|
|
513
|
+
);
|
|
514
|
+
await updateMe({name: "Ada"}).unwrap();
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
| `createApiClient` option | Description |
|
|
518
|
+
|--------------------------|-------------|
|
|
519
|
+
| `baseUrl` | API origin; relative request URLs are prefixed with it |
|
|
520
|
+
| `authProvider` | `getToken` adds `Authorization: Bearer`; on a 401, `refresh` runs once and the request retries |
|
|
521
|
+
| `credentials` | Fetch credentials mode for cookie sessions |
|
|
522
|
+
| `headers` | Object or (async) function merged into every request |
|
|
523
|
+
| `timeoutMs` | Per-request timeout (default 30 000) |
|
|
524
|
+
| `onError` | Called with every `ApiError` after any retry (report to Sentry here) |
|
|
525
|
+
| `keepUnusedDataMs` | How long an unsubscribed cache entry survives (default 60 000) |
|
|
526
|
+
|
|
527
|
+
| Request arg | Description |
|
|
528
|
+
|-------------|-------------|
|
|
529
|
+
| `url`, `method` | Path (or absolute URL) and HTTP method (default `GET`) |
|
|
530
|
+
| `params` | Query params; arrays become repeated keys, nested objects bracket keys (`updated[$gte]`) |
|
|
531
|
+
| `body` | Plain objects are JSON-encoded; `FormData`, `Blob`, and strings pass through |
|
|
532
|
+
| `headers` | Per-request headers (part of the cache key) |
|
|
533
|
+
| `unwrap` | Unwrap `{data}` single-document envelopes (default `true`); list envelopes with `more` stay whole |
|
|
534
|
+
| `parseAs`, `timeoutMs`, `signal` | Response parsing, timeout, and cancellation |
|
|
535
|
+
|
|
536
|
+
| Client method | Description |
|
|
537
|
+
|---------------|-------------|
|
|
538
|
+
| `request(args)` | One uncached request |
|
|
539
|
+
| `fetchQuery(args, {providesTags, force})` | Cached request; identical in-flight calls share one fetch |
|
|
540
|
+
| `getQueryState(args)` / `subscribeQuery(args, listener)` | Read and watch one cache entry |
|
|
541
|
+
| `invalidateTags(tags)` | Refetch mounted queries with those tags; drop unmounted ones |
|
|
542
|
+
| `reset()` | Clear the cache; call when the signed-in user changes |
|
|
543
|
+
|
|
544
|
+
| Hook | Returns |
|
|
545
|
+
|------|---------|
|
|
546
|
+
| `useApiQuery(args \| null, {skip, providesTags, pollingInterval, transformResponse, client})` | `{data, error, isLoading, isFetching, isSuccess, isError, isUninitialized, refetch}` |
|
|
547
|
+
| `useLazyApiQuery({providesTags, client})` | `[trigger(args), state]`; each trigger fetches fresh data |
|
|
548
|
+
| `useApiMutation(toRequest, {invalidatesTags, client})` | `[trigger(arg), {data, error, isLoading, isSuccess, isError, reset}]` |
|
|
549
|
+
|
|
550
|
+
Triggers resolve `{data}` or `{error}`; `.unwrap()` resolves the data or throws.
|
|
551
|
+
Failures are `ApiError` (`status`, `data.title`, `data.detail`, `data.code`, `data.fields`,
|
|
552
|
+
`data.meta`); timeouts and network failures have `status: 0`. `data.disableExternalErrorTracking`
|
|
553
|
+
is `true` when the server marks the error as expected, so an `onError` reporter should skip
|
|
554
|
+
Sentry for it.
|
|
555
|
+
|
|
556
|
+
## Config
|
|
557
|
+
|
|
558
|
+
`@terreno/syncdb/config` resolves the app's API origin once at import (needs
|
|
559
|
+
`expo-constants`):
|
|
560
|
+
|
|
561
|
+
1. `extra.BASE_URL` set to `__SAME_ORIGIN__` resolves to the page origin (same-origin web SPAs).
|
|
562
|
+
2. `EXPO_PUBLIC_API_URL`, in any build.
|
|
563
|
+
3. `extra.BASE_URL` in `app.json` (production/staging builds only).
|
|
564
|
+
4. Expo `hostUri` plus the dev API port (simulator/device).
|
|
565
|
+
5. `http://localhost:<dev API port>`.
|
|
566
|
+
|
|
567
|
+
The dev API port is `4000` unless `EXPO_PUBLIC_DEV_API_PORT` or `extra.DEV_API_PORT` sets
|
|
568
|
+
it. `appHeaders()` returns `App-Version` (Expo config version) and `App-Platform`
|
|
569
|
+
(`web` or `mobile`).
|
|
570
|
+
|
|
571
|
+
## Auth
|
|
572
|
+
|
|
573
|
+
`@terreno/syncdb/auth` holds the Better Auth client and the session state that replace
|
|
574
|
+
`@terreno/rtk`'s `createBetterAuthClient` and Better Auth Redux slice. It needs the
|
|
575
|
+
optional peers `better-auth`, `@better-auth/expo`, `expo-secure-store`,
|
|
576
|
+
`@react-native-async-storage/async-storage`, and `react`.
|
|
577
|
+
|
|
578
|
+
```tsx
|
|
579
|
+
import {createBetterAuthClient, SessionProvider, useSession} from "@terreno/syncdb/auth";
|
|
580
|
+
import {baseUrl} from "@terreno/syncdb/config";
|
|
581
|
+
|
|
582
|
+
export const betterAuthClient = createBetterAuthClient({
|
|
583
|
+
baseURL: baseUrl,
|
|
584
|
+
scheme: "myapp",
|
|
585
|
+
storagePrefix: "myapp",
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
<SessionProvider authClient={betterAuthClient} syncDbs={[syncDb]} onLogout={() => apiClient.reset()}>
|
|
589
|
+
<App />
|
|
590
|
+
</SessionProvider>;
|
|
591
|
+
|
|
592
|
+
const {userId, user, isLoading, isAuthenticated, isSigningOut, error, logout, refresh} =
|
|
593
|
+
useSession();
|
|
594
|
+
```
|
|
595
|
+
|
|
596
|
+
`createBetterAuthClient({baseURL, scheme, storagePrefix?})` returns the Better Auth React
|
|
597
|
+
client with the Expo plugin. Storage is `expo-secure-store` (synchronous API) on native and
|
|
598
|
+
AsyncStorage on web. Reads of the plugin's cookie jar (`<storagePrefix>_cookie`) go through
|
|
599
|
+
`repairCookieJar`, which drops entries the plugin cannot read, so one malformed entry never
|
|
600
|
+
blocks every native request. `storagePrefix` defaults to `"terreno"`.
|
|
601
|
+
|
|
602
|
+
`SessionProvider` props (read once on mount):
|
|
603
|
+
|
|
604
|
+
| Prop | Type | Description |
|
|
605
|
+
|------|------|-------------|
|
|
606
|
+
| `authClient` | Better Auth client | Wrapped with `betterAuthSessionSource`. Use this or `source`. |
|
|
607
|
+
| `source` | `SessionSource` | Any session source (`getSession`, `signOut`, optional `subscribe`). |
|
|
608
|
+
| `store` | `SessionStore` | A store from `createSessionStore`, instead of the props above. Share it with non-React code. |
|
|
609
|
+
| `syncDbs` | `{signOut}[]` | syncdb clients signed out on `logout`, after the auth server confirms the sign-out and while the session still names the user. Create them with `wipeOnSignOut: true` to wipe local data. |
|
|
610
|
+
| `onLogout` | `() => void \| Promise<void>` | Extra cleanup after the auth and syncdb sign-outs. |
|
|
611
|
+
| `signOutTimeoutMs` | `number` | How long `logout` waits for the server's sign-out, and for the session check after a failed one. Default `10000`. |
|
|
612
|
+
| `pollIntervalMs` | `number` | Poll cadence when the source cannot push changes. Default `60000`; `0` turns polling off. |
|
|
613
|
+
|
|
614
|
+
When the session syncs:
|
|
615
|
+
|
|
616
|
+
- **Start:** the provider reads the session on mount. `isLoading` is `true` until it settles.
|
|
617
|
+
- **Sign-in, sign-out, user switch:** Better Auth's session atom (`$store.atoms.session`)
|
|
618
|
+
pushes the change. Only settled emissions apply: while a read is in flight (`isPending` or
|
|
619
|
+
`isRefetching`) the atom still carries the previous user, so a sign-out never briefly
|
|
620
|
+
restores them. Call `refresh()` after a sign-in to resolve the user immediately.
|
|
621
|
+
- **Polling:** only when the client has no session atom.
|
|
622
|
+
- **Logout:** `logout()` signs out of the auth server while the session stays in the state
|
|
623
|
+
(`isSigningOut` is `true`, `isLoading` is untouched), then signs out each `syncDbs` client,
|
|
624
|
+
runs `onLogout`, and clears the session. The session stays until the server answers so a
|
|
625
|
+
login screen that opens at once never finds the live session and signs straight back in, and
|
|
626
|
+
so the sync clients still know the current user for their `wipeOnSignOut` wipe. A sign-out
|
|
627
|
+
that fails or outlasts `signOutTimeoutMs` triggers a session check: when the session is
|
|
628
|
+
still live (or the server cannot be asked and the client may still hold one), `logout()`
|
|
629
|
+
keeps the session, wipes nothing, and sets `error` to `SIGN_OUT_FAILED_MESSAGE`; a session
|
|
630
|
+
that is already gone clears normally. Session pushes during a logout are held, and a
|
|
631
|
+
different user who signed in meanwhile wins when it ends. Concurrent `logout()` calls share
|
|
632
|
+
one run. A failing `syncDbs` or `onLogout` step is logged and the rest still run.
|
|
633
|
+
|
|
634
|
+
A failed startup or `refresh()` read sets `error` to `"Failed to sync session"` and clears the
|
|
635
|
+
session. A failed background read (atom error, poll) keeps the current session, so a network
|
|
636
|
+
blip is never a logout. `useSession()` throws outside a `SessionProvider`.
|
|
637
|
+
|
|
638
|
+
`betterAuthAdapter(bridgeBetterAuthReactClient(betterAuthClient))` turns the same client into
|
|
639
|
+
the `AuthProvider` for `createSyncDb` and `createApiClient`. Both are also exported from the
|
|
640
|
+
package root.
|
|
641
|
+
|
|
642
|
+
### Social login, account linking, and account security
|
|
643
|
+
|
|
644
|
+
`@terreno/syncdb/auth` also exports the pieces behind the example app's login and security
|
|
645
|
+
screens. They take a client from `createBetterAuthClient` (pass `features` to turn on the
|
|
646
|
+
`twoFactor`, `passkey`, `multiSession`, `lastLoginMethod` and `oneTimeToken` client plugins) and
|
|
647
|
+
need no store: session state comes from `useSession()`. Setup: [Configure Better
|
|
648
|
+
Auth](../how-to/configure-better-auth.md) and [Account security](account-security.md).
|
|
649
|
+
|
|
650
|
+
- `BetterAuthClient` — The client type without `features` (also `ReturnType<typeof createBetterAuthClient>`). `BetterAuthClientWithFeatures<Features>` — the client type with them. `BetterAuthClientFor<Features>` — which of the two `createBetterAuthClient` returns. `BetterAuthHandoffClient` — what the OAuth handoff helpers accept: any client from `createBetterAuthClient`
|
|
651
|
+
- `BetterAuthOAuthProvider` — `"google" | "github" | "apple"` or any generic OAuth provider id, such as `"mock-oidc"`
|
|
652
|
+
- `getBetterAuthBearerToken({storagePrefix?, bearerStorage?})` — Reads the token bearer mode stored, or `null`
|
|
653
|
+
- `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))
|
|
654
|
+
- `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
|
|
655
|
+
- `linkSocialWithHandoff({client, provider, callbackURL, errorCallbackURL?}): Promise<{error}>` — Links a provider to the signed-in user. On web in bearer mode it posts the intent to `{basePath}/oauth-handoff/link-intent` with the bearer, then submits the returned single-use token in a hidden POST form to `startURL`, which redirects to the provider; the page comes back to `callbackURL` with no `#ott`. Both URLs may be any page on a web origin (`callbackPaths` do not apply); `errorCallbackURL` defaults to `callbackURL`. In cookie mode and on native it calls `client.linkSocial`. Resolves `{error: null}` once the browser is on its way, or `{error: {status, code?, message?}}` (such as 403 `LINKING_DISABLED`) without submitting anything. `BetterAuthLinkClient` is its client type ([how-to](../how-to/configure-better-auth.md#linking-a-provider-in-bearer-mode))
|
|
656
|
+
- `useAuthMethods(authClient, {skip?}): {methods, isLoading, error, refetch}` — Loads the server's enabled sign-in methods (`BetterAuthMethods`: `providers`, `emailPassword`, `signUp`, `accountLinking`, and feature flags) once per client from `{basePath}/terreno/methods` through the client's `$fetch`, so bearer and cookie modes both work. `methods` is `undefined` while loading and after an error; render no provider buttons until it arrives. `refetch()` requests them again (for Try again); `skip: true` makes no request ([response](api.md#better-auth-authentication))
|
|
657
|
+
- `createRememberedAccountsStore({rememberIdentity, storageKey?, max?, storage?})` — On-device store of accounts that signed in here, for the login screen. Returns `{record(entry), forget(userId), clear(), list(), lastUsedMethod(), isHydrated(), ready, getSnapshot(), subscribe(listener), dispose()}`. Each change re-reads storage first, and on web the store follows `storage` events for its key until `dispose()`, so a stale tab cannot bring back a forgotten account. A failed change rejects to its caller without blocking later changes. Persists `{userId, method, lastUsedAt}` (newest first, at most `max`, default 5, and at most 2048 bytes) under `storageKey` (default `terreno_remembered_accounts`) in SecureStore on native (`WHEN_UNLOCKED_THIS_DEVICE_ONLY` on iOS) or localStorage on web. `rememberIdentity` is required: pass `false` to keep only the method. `name` (in clear), a masked `email`, and an `https://` `image` are kept only with `rememberIdentity: true`. Unreadable or oversized stored data resets to an empty list ([how-to](../how-to/configure-better-auth.md#4-remember-accounts-on-this-device))
|
|
658
|
+
- `useRememberedAccounts(store): {accounts, lastUsedMethod, isHydrated, forget, clear}` — Subscribes a component to the store. `isHydrated` is `false` until native storage has loaded
|
|
659
|
+
- `toLoginAccountOptions(accounts)` — Maps `RememberedAccount[]` to `@terreno/ui` `LoginAccountOption[]` (`id` is the `userId`) for `LoginScreen`'s `rememberedAccounts`
|
|
660
|
+
- `maskEmail(email)` — `josh@mail.flourish.health` → `j***@m***.health`: first character of the local part and domain, subdomains hidden, top-level domain kept; `undefined` for non-emails
|
|
661
|
+
- `getBetterAuthClientWebAuth(client): {webAuth, bearerToken}` — Whether a client from `createBetterAuthClient` is in web bearer mode, and the token it holds; `{webAuth: "cookie", bearerToken: null}` on native, in cookie mode, and for other clients. No request
|
|
662
|
+
- `useLoginController({client, rememberedAccounts, onSignedIn, onTwoFactorRequired?, rememberIdentity?, callbackURL?, errorCallbackURL?, error?, onSignUpPress?, onForgotPasswordPress?, providerLabels?, passkeyAutofill?, skipWhenSignedIn?, webAuth?, getBearerToken?, initialEmail?})` — Drives a login screen from a Better Auth client. Returns `{step, methods, isEmailPasswordEnabled, screenProps, twoFactor, signInEmail, signInSocial, signInPasskey, continueAs, forget}`: `step` is `"checking"`, `"login"`, `"twoFactor"` or `"signedIn"`; `isEmailPasswordEnabled` is `false` when the server has email and password off (render `LoginScreen` with `fields={[]}`); `screenProps` spreads into `@terreno/ui` `LoginScreen`; `twoFactor` feeds `TwoFactorChallengeScreen`. A live session calls `onSignedIn` without showing the screen (in bearer mode it first finishes a pending OAuth handoff; with no stored token it asks the server nothing). If `onSignedIn` throws, `step` returns to `"login"` with an error. The passkey button and autofill are web-only. Device sessions (`multiSession.listDeviceSessions`, never in bearer mode) become session cards and hide remembered entries for the same user. Social sign-in uses `signInSocialWithHandoff` in bearer mode and `signIn.social` otherwise. Every finished sign-in is recorded in the store. Server refusals become notices through `getLoginFeedback` ([how-to](../how-to/configure-better-auth.md#5-drop-in-login-screen))
|
|
663
|
+
- `TerrenoLoginScreen` — `LoginScreen` connected through `useLoginController`; takes its options plus `fields?` (default `DEFAULT_LOGIN_FIELDS`), `title?`, `logo?`, `trustDeviceDays?`, `initialTrustDevice?` (pass `false` to start "Trust this device" unchecked), `onStepChange?` (called with each new step: `checking`, `login`, `twoFactor`, `signedIn`), `testID?` (default `login-screen`; spinner `${testID}-checking`). Renders the inline two-factor challenge unless `onTwoFactorRequired` is set
|
|
664
|
+
- `useAccountSecurity({client, callbackURL, methods?, errorCallbackURL?, linkError?, providerLabels?, passkeySupported?, minPasswordLength?})` — Drives an account security screen from a Better Auth client. Returns `{screenProps, methods, user, refetch}`; `screenProps` (`linkedAccounts`, `password`, `passkeys`, `twoFactor`, `sessions`, `isLoading`, `error`, `onRetry`) spreads into `@terreno/ui` `AccountSecurityScreen`. A card's props are `undefined` when the server's methods (or the client's features) leave it off; passkeys are web-only. Loads `listAccounts`, `passkey.listUserPasskeys`, `getSession({query: {disableCookieCache: true}})` and `listSessions`; each Better Auth `{error}` becomes a thrown `Error` for the card, and after each change only the affected section reloads. Never calls `unlinkAccount` for the only account, or `deletePasskey` for the only sign-in method (`LAST_SIGN_IN_METHOD_MESSAGE`). Links through `linkSocialWithHandoff`; "Set a password" posts `{basePath}/terreno/set-password`. `SESSION_NOT_FRESH` becomes `SIGN_IN_AGAIN_MESSAGE`; a change refused with `CREDENTIAL_ACCOUNT_NOT_FOUND` switches the password card to set mode (`NO_PASSWORD_YET_MESSAGE`). `getLinkErrorMessage(code)` maps a link's `?error=` to safe text ([how-to](../how-to/configure-better-auth.md#6-drop-in-security-screen))
|
|
665
|
+
- `TerrenoAccountSecurityScreen` — `AccountSecurityScreen` connected through `useAccountSecurity`; takes its options plus `title?`, `description?`, `header?`, `children?`, `withPage?`, `maxWidth?`, `testID?`
|
|
666
|
+
- `recordRememberedSignIn({store, user, method, rememberIdentity?})` — Records a sign-in the controller did not finish, such as one completed on the app's own two-factor screen
|
|
667
|
+
- `getLoginFeedback({failure, mode?, originalMethod?}): {notice?, error?, isSignUpClosed}` — Maps `{code?, message?, source: "response" | "url"}` to login screen feedback: `account_not_linked` / `OAUTH_LINK_ERROR` / "account not linked" to guidance for the linking mode, `VERIFY_EMAIL_TO_USE_ACCOUNT` to "verify your email", `signup_disabled` / `EMAIL_PASSWORD_SIGN_UP_DISABLED` to "ask your administrator" (and `isSignUpClosed`), `LINKING_DISABLED`, `provider_not_found`; a response's own message otherwise, and never text from a URL
|
|
668
|
+
|
|
669
|
+
### JWT auth (legacy)
|
|
670
|
+
|
|
671
|
+
For backends on `@terreno/api`'s JWT routes (`/auth/login`, `/auth/refresh_token`). Ported
|
|
672
|
+
from `@terreno/rtk`'s `generateAuthSlice` and `emptyApi` base query; no Redux.
|
|
673
|
+
|
|
674
|
+
```tsx
|
|
675
|
+
import {jwtAuthAdapter, SessionProvider, useJwtAuth, useSession} from "@terreno/syncdb/auth";
|
|
676
|
+
import {createApiClient} from "@terreno/syncdb/api";
|
|
677
|
+
import {appHeaders, baseUrl} from "@terreno/syncdb/config";
|
|
678
|
+
|
|
679
|
+
export const jwtAuth = jwtAuthAdapter({baseUrl, headers: appHeaders});
|
|
680
|
+
export const apiClient = createApiClient({authProvider: jwtAuth, baseUrl, headers: appHeaders});
|
|
681
|
+
export const syncDb = createSyncDb({authProvider: jwtAuth, baseUrl, /* … */});
|
|
682
|
+
|
|
683
|
+
<SessionProvider source={jwtAuth} syncDbs={[syncDb]} onLogout={() => apiClient.reset()}>
|
|
684
|
+
<App />
|
|
685
|
+
</SessionProvider>;
|
|
686
|
+
|
|
687
|
+
// In a screen:
|
|
688
|
+
const {login, signup, googleLogin, resetPassword, logout, isAuthenticating, error} =
|
|
689
|
+
useJwtAuth({adapter: jwtAuth});
|
|
690
|
+
await login({email, password});
|
|
691
|
+
const {userId, isAuthenticated} = useSession();
|
|
692
|
+
```
|
|
693
|
+
|
|
694
|
+
`jwtAuthAdapter(options)` options:
|
|
695
|
+
|
|
696
|
+
| Option | Type | Description |
|
|
697
|
+
|--------|------|-------------|
|
|
698
|
+
| `baseUrl` | `string` | API origin. Required. |
|
|
699
|
+
| `storage` | `JwtTokenStorage` | Token storage. Default: `expo-secure-store` on native, AsyncStorage on web (keys `AUTH_TOKEN`, `REFRESH_TOKEN`; nothing is stored during SSR). |
|
|
700
|
+
| `paths` | `Partial<JwtAuthPaths>` | Route overrides. Defaults: `login` `/auth/login`, `signup` `/auth/signup`, `refreshToken` `/auth/refresh_token`, `googleLogin` `/auth/google`, `resetPassword` `/resetPassword`. |
|
|
701
|
+
| `headers` | object or function | Extra headers on auth requests (e.g. `appHeaders`). |
|
|
702
|
+
| `refreshBeforeExpirySecs` | `number` | Refresh when the access token has fewer seconds left. Default `130`. |
|
|
703
|
+
| `fetchImpl` | `FetchLike` | Custom `fetch`. |
|
|
704
|
+
| `timeoutMs` | `number` | Auth request timeout. Default `30000`. |
|
|
705
|
+
|
|
706
|
+
The adapter is an `AuthProvider` and a `SessionSource`:
|
|
707
|
+
|
|
708
|
+
| Member | Behaviour |
|
|
709
|
+
|--------|-----------|
|
|
710
|
+
| `getToken()` | Stored access token. Under `refreshBeforeExpirySecs` left (or expired), refreshes first. Both tokens expired: signs out and returns `null`. |
|
|
711
|
+
| `refresh()` | `POST /auth/refresh_token` with `{refreshToken}`; stores the new pair and resolves `true`. Concurrent calls share one request. `createApiClient` calls it on a 401 and retries once; `createSyncDb` calls it on an auth pause. |
|
|
712
|
+
| `getUserId()` / `getSession()` | The stored token's `id` claim; `getSession()` returns `{userId, user: {id}}` or `null`. |
|
|
713
|
+
| `onAuthChange(cb)` / `subscribe(listener)` | Fire on login, logout, and token rotation. |
|
|
714
|
+
| `login(body)` | `POST /auth/login` with `{email \| username, password}`; stores the tokens and signs in. |
|
|
715
|
+
| `signup(body)` | `POST /auth/signup` with `{email, password, ...extra}`; stores the tokens and signs in. |
|
|
716
|
+
| `googleLogin({idToken})` | `POST /auth/google`; stores the tokens and signs in. |
|
|
717
|
+
| `resetPassword(body)` | `POST /resetPassword` (`{token, password}`); returns `{token, refreshToken, userId}` without signing in. |
|
|
718
|
+
| `setTokens({token, refreshToken?})` | Store a pair from a custom route and sign in. |
|
|
719
|
+
| `logout()` / `signOut()` | Clear both tokens and notify listeners. JWTs are stateless, so no server call. |
|
|
720
|
+
|
|
721
|
+
Failed requests reject with `ApiError`. Its `message` is the server's `title`, or the
|
|
722
|
+
`message` the JWT routes send on a 401 (for example `"Password or username is incorrect"`).
|
|
723
|
+
Status `0` means the request never got a response (`data.code` `network-error` or
|
|
724
|
+
`request-timeout`).
|
|
725
|
+
|
|
726
|
+
When a refresh fails:
|
|
727
|
+
|
|
728
|
+
- The server rejects it (401, missing tokens in the response, any other HTTP error), or no
|
|
729
|
+
refresh token is stored: both tokens are cleared and the session signs out. syncdb clients
|
|
730
|
+
pause (they never wipe on a sign-out they did not request).
|
|
731
|
+
- Network error or timeout: the session and current token stay; the next request retries.
|
|
732
|
+
|
|
733
|
+
`useJwtAuth({adapter})` must render inside the `SessionProvider` whose `source` is `adapter`.
|
|
734
|
+
`login`, `signup`, `googleLogin`, and `resetPassword` set `isAuthenticating` while pending,
|
|
735
|
+
store the failure message in `error` (cleared when the next request starts), and reject with
|
|
736
|
+
the error. `logout` is the `SessionProvider` logout.
|
|
737
|
+
|
|
738
|
+
Token helpers (all take an optional `{storage, now}`):
|
|
739
|
+
|
|
740
|
+
| Function | Returns |
|
|
741
|
+
|----------|---------|
|
|
742
|
+
| `getTokenExpirationTimes()` | `{authRemainingSecs, refreshRemainingSecs}`; both `undefined` unless both tokens are stored. |
|
|
743
|
+
| `shouldShowStillThereModal()` | `true` when the refresh token has 65 seconds or less left. |
|
|
744
|
+
| `readJwtTokens(storage?)` | `{token, refreshToken}` from storage. |
|
|
745
|
+
| `decodeJwtClaims(token)` | The token's claims (unverified), or `null`. |
|
|
746
|
+
|
|
747
|
+
## App utilities
|
|
748
|
+
|
|
749
|
+
`@terreno/syncdb/app` holds app-shell helpers that replace their `@terreno/rtk`
|
|
750
|
+
versions. It needs `react`, `react-native`, and `expo-constants`; `useMCPTools` also
|
|
751
|
+
needs `@modelcontextprotocol/client`. All are optional peer dependencies.
|
|
752
|
+
|
|
753
|
+
```typescript
|
|
754
|
+
import {
|
|
755
|
+
canOpenAdminPage,
|
|
756
|
+
installDevConsoleLogger,
|
|
757
|
+
useMCPTools,
|
|
758
|
+
useUpgradeCheck,
|
|
759
|
+
} from "@terreno/syncdb/app";
|
|
760
|
+
|
|
761
|
+
installDevConsoleLogger(); // module scope; no-op outside __DEV__
|
|
762
|
+
|
|
763
|
+
const {isRequired, isWarning, onUpdate} = useUpgradeCheck({recheckOnForeground: true});
|
|
764
|
+
const isAdmin = canOpenAdminPage({admin: me?.admin, permissions: me?.permissions});
|
|
765
|
+
const {tools} = useMCPTools({authProvider}); // the provider passed to createSyncDb
|
|
766
|
+
```
|
|
767
|
+
|
|
768
|
+
| Export | Description |
|
|
769
|
+
|--------|-------------|
|
|
770
|
+
| `useUpgradeCheck({pollingIntervalMs, recheckOnForeground, client})` | Calls `GET /version-check` through the request client (`ApiClientProvider` or `client`) with `extra.buildNumber`. Returns `{isRequired, isWarning, requiredMessage, warningMessage, warningCheckCount, canUpdate, onUpdate}`. Skips the check when there is no build number. The server's `pollingIntervalMs` replaces the local fallback after the first response. |
|
|
771
|
+
| `hasPermission(permissions, request)` | True when the `/auth/me` `permissions` map grants every requested action |
|
|
772
|
+
| `canOpenAdminPage({admin, permissions})` | With RBAC, only `admin:access` opens the admin UI; without it, falls back to `user.admin` |
|
|
773
|
+
| `installDevConsoleLogger({baseUrl})` | In `__DEV__`, batches `console.error` / `console.warn` and global errors to `POST /__terreno/browser-logs`. Safe to call more than once. |
|
|
774
|
+
| `useMCPTools({authProvider, baseURL})` | Lists the backend's `/mcp` tools with the official MCP client. The token comes from `authProvider.getToken()` on each fetch, so Better Auth and JWT sessions both work. Returns `{tools, isLoading, error, refetch}`. |
|
|
775
|
+
|
|
776
|
+
`@terreno/syncdb/buildNumber` is node-only (it runs `git`). Import it from
|
|
777
|
+
`app.config.ts`, never from app code:
|
|
778
|
+
|
|
779
|
+
```typescript
|
|
780
|
+
import {resolveBuildNumber} from "@terreno/syncdb/buildNumber";
|
|
781
|
+
|
|
782
|
+
extra: {buildNumber: resolveBuildNumber({configValue: config.extra?.buildNumber})}
|
|
783
|
+
```
|
|
784
|
+
|
|
785
|
+
`resolveBuildNumber` order: `override`, `configValue`, `EXPO_PUBLIC_BUILD_NUMBER` (or
|
|
786
|
+
`envVar`), `git rev-list --count HEAD`, then `undefined` (the check is skipped).
|
|
787
|
+
|
|
788
|
+
## Feature flags
|
|
789
|
+
|
|
790
|
+
`@terreno/syncdb/flags` reads `@terreno/feature-flags` flags with the request client and
|
|
791
|
+
refreshes them when the server emits `featureFlagsChanged` on the sync socket — no second
|
|
792
|
+
socket. Requires the optional peers `@openfeature/web-sdk` (and `@openfeature/react-sdk` for
|
|
793
|
+
OpenFeature React hooks).
|
|
794
|
+
|
|
795
|
+
```typescript
|
|
796
|
+
import {useFeatureFlags, useTerrenoFeatureFlags} from "@terreno/syncdb/flags";
|
|
797
|
+
|
|
798
|
+
// Root, above SyncDbProvider: install flags into OpenFeature and listen for pushes.
|
|
799
|
+
useTerrenoFeatureFlags({skip: !userId, socket: syncDb, userId});
|
|
800
|
+
|
|
801
|
+
// Screens: flat accessors over the same cached query.
|
|
802
|
+
const {getFlag, getVariant, isLoading} = useFeatureFlags({skip: !userId, socket: null, userId});
|
|
803
|
+
```
|
|
804
|
+
|
|
805
|
+
Options and results: [feature flags reference](./feature-flags.md#terrenosyncdbflags).
|
|
806
|
+
|
|
456
807
|
## Query windows
|
|
457
808
|
|
|
458
809
|
Use query windows when a collection is too large to sync whole (messages, events, audit
|
|
@@ -567,6 +918,56 @@ terreno-syncdb-codegen \
|
|
|
567
918
|
|
|
568
919
|
Do not edit the generated file. `--collections` filters when extensions exist. When they do not, it reads list/create/patch schemas from `GET /{name}` (or `/{name}/`). A missing path, a list response without `data.items`, a spec with no extensions and no `--collections` all exit non-zero.
|
|
569
920
|
|
|
921
|
+
A collection name is the route path without its leading slash, so a nested route keeps its
|
|
922
|
+
`/`: `modelRouter("/gpt/histories", …)` syncs as `gpt/histories`, and
|
|
923
|
+
`useQuery("gpt/histories")` reads it. Names are letters, digits, `_`, and `-` in
|
|
924
|
+
`/`-separated segments. Hooks drop the separators and singularize the last segment when the
|
|
925
|
+
list item schema has no `$ref` name: `gpt/histories` → `GptHistory`, `useGptHistories`.
|
|
926
|
+
|
|
927
|
+
### API SDK (`--api-out`)
|
|
928
|
+
|
|
929
|
+
`--api-out <file>` writes typed request hooks for every operation that is **not** synced
|
|
930
|
+
collection CRUD: auth (`/auth/me`), modelRouter actions, AI, consents, announcements,
|
|
931
|
+
version check, and plain REST routes. It replaces `@rtk-query/codegen-openapi`. Pass
|
|
932
|
+
`--out`, `--api-out`, or both in one run.
|
|
933
|
+
|
|
934
|
+
```bash
|
|
935
|
+
terreno-syncdb-codegen \
|
|
936
|
+
--schema http://localhost:4000/openapi.json \
|
|
937
|
+
--out ./store/syncDbSdk.ts \
|
|
938
|
+
--api-out ./store/apiSdk.ts \
|
|
939
|
+
--config ./syncdb-codegen.json
|
|
940
|
+
```
|
|
941
|
+
|
|
942
|
+
| Generated per operation | Example (`GET /gpt/datasets/{id}?historyId=`) |
|
|
943
|
+
|-------------------------|-----------------------------------------------|
|
|
944
|
+
| `<Name>Args` | `{id: string; historyId: string}` |
|
|
945
|
+
| `<Name>Res` | Response after envelope unwrapping (`{data: X}` → `X`; lists keep `more`) |
|
|
946
|
+
| `<name>Request(arg)` | `ApiRequestArgs` for `apiClient.request(...)` |
|
|
947
|
+
| `use<Name>Query` / `useLazy<Name>Query` | GET operations; `providesTags` from the operation's OpenAPI tags |
|
|
948
|
+
| `use<Name>Mutation` | Other methods; `invalidatesTags` from the operation's OpenAPI tags |
|
|
949
|
+
|
|
950
|
+
Names follow the RTK codegen rule, so migrated screens keep their hook names: the
|
|
951
|
+
camelCased `operationId`, otherwise method + path with `{param}` read as `By<Param>`
|
|
952
|
+
(`PATCH /gpt/histories/{id}/rating` → `usePatchGptHistoriesByIdRatingMutation`). Arguments flatten
|
|
953
|
+
the same way: one path param, query param, or body is the argument itself; otherwise an
|
|
954
|
+
object of path params, query params, and `body`. A `multipart/form-data` body is
|
|
955
|
+
`FormData`. Two operations that would share a name exit non-zero; give one an
|
|
956
|
+
`operationId`. `excludePaths` in the config file (path prefixes, or regexes starting
|
|
957
|
+
with `^`) leaves routes out.
|
|
958
|
+
|
|
959
|
+
A paginated custom route must return `more` with its `data` (as `GET /announcements/feed`
|
|
960
|
+
does: `{data, more, page, total}`). Without `more`, the client unwraps the envelope to the
|
|
961
|
+
`data` array and drops `page` and `total`.
|
|
962
|
+
|
|
963
|
+
```typescript
|
|
964
|
+
import {useGetAuthMeQuery, usePatchAuthMeMutation} from "@/store/apiSdk";
|
|
965
|
+
|
|
966
|
+
const {data: me} = useGetAuthMeQuery(undefined, {skip: !userId});
|
|
967
|
+
const [updateMe] = usePatchAuthMeMutation();
|
|
968
|
+
await updateMe({name: "Ada"}).unwrap();
|
|
969
|
+
```
|
|
970
|
+
|
|
570
971
|
## Conflict API
|
|
571
972
|
|
|
572
973
|
A **conflict** occurs when the client's `baseVersion` (last seen `_syncSeq`) does not match the server's current seq. The server responds with a `conflict` nack (HTTP **409** on `POST /sync/mutate`, or `sync:nack` with `code: "conflict"` on the socket) carrying the canonical server document.
|
|
@@ -702,6 +1103,7 @@ Conflict responses on mutate: **409** with `{nack}` body (`code: "conflict"`).
|
|
|
702
1103
|
| `sync:nack` | server → client | `{mutationId, code, serverDoc?, serverSeq?, serverDeleted?, message?, retryAfterMs?}` |
|
|
703
1104
|
| `sync:mutateBatch` | client → server | `{mutations: SyncMutateRequest[], batchId?}` |
|
|
704
1105
|
| `sync:auth-expired` | server → client | `{reason: "expired" \| "disabled"}` — session re-validation sweep; client enters auth-pause |
|
|
1106
|
+
| any other name | server → client | App events (for example `featureFlagsChanged` `{key}`), delivered to `onSocketEvent` / `useSocketEvent` listeners |
|
|
705
1107
|
|
|
706
1108
|
Limits: 50 collection subscriptions per socket; 100 mutations/second per socket (each batch mutation counts); batches capped at 100.
|
|
707
1109
|
|
|
@@ -730,6 +1132,12 @@ const syncDb = createSyncDb({
|
|
|
730
1132
|
});
|
|
731
1133
|
```
|
|
732
1134
|
|
|
1135
|
+
`start()` resolves the key before it loads the store, while the session is still valid.
|
|
1136
|
+
The derived key is cached in IndexedDB, so the flush in `stop()` never fetches
|
|
1137
|
+
`/sync/key`. Logging out while `start()` is still running keeps pending outbox writes.
|
|
1138
|
+
If the key fetch fails (an offline cold start), `start()` still resolves and the next
|
|
1139
|
+
save retries the fetch.
|
|
1140
|
+
|
|
733
1141
|
Saves are debounced (500 ms). The store is serialized once per write, not once per
|
|
734
1142
|
transaction: autosave compares TinyBase's content hashes (O(1)) to skip saves the last
|
|
735
1143
|
write already covered, so a bootstrap of many pages costs a few writes instead of one
|
|
@@ -758,15 +1166,16 @@ transport.respondWithAck();
|
|
|
758
1166
|
transport.deliverDelta({...});
|
|
759
1167
|
```
|
|
760
1168
|
|
|
761
|
-
`createFakeTransport` records sent mutations, simulates connectivity, delivers deltas, and queues ack/nack responders.
|
|
1169
|
+
`createFakeTransport` records sent mutations, simulates connectivity, delivers deltas, and queues ack/nack responders. `transport.emitEvent(name, ...args)` delivers an app event to `onSocketEvent` listeners; `eventListenerCount(name)` reports how many are bound.
|
|
762
1170
|
|
|
763
1171
|
## Environment variables
|
|
764
1172
|
|
|
765
|
-
|
|
1173
|
+
`@terreno/syncdb/config` reads these at import; the core client reads none. Host apps typically set:
|
|
766
1174
|
|
|
767
1175
|
| Variable | Used by | Description |
|
|
768
1176
|
|----------|---------|-------------|
|
|
769
|
-
| `EXPO_PUBLIC_API_URL` | `@terreno/
|
|
1177
|
+
| `EXPO_PUBLIC_API_URL` | `@terreno/syncdb/config` `baseUrl` | Backend origin passed to `createSyncDb({baseUrl})` and `createApiClient({baseUrl})` |
|
|
1178
|
+
| `EXPO_PUBLIC_DEV_API_PORT` | `@terreno/syncdb/config` | Local dev API port (default `4000`) |
|
|
770
1179
|
|
|
771
1180
|
Backend sync requires a MongoDB replica set (`MONGO_URI` with `replicaSet=`) for `RealtimeApp` change streams.
|
|
772
1181
|
|
|
@@ -778,8 +1187,9 @@ Backend sync requires a MongoDB replica set (`MONGO_URI` with `replicaSet=`) for
|
|
|
778
1187
|
- **No `bulkWrite` / `updateMany` / `deleteMany`** on synced models — use per-document loops.
|
|
779
1188
|
- **Do not write reserved tables** (`_outbox`, `_cursors`, `_conflicts`) directly — use `client.mutate`, hooks, and `resolveConflict`.
|
|
780
1189
|
- **User switch wipes local data** — confirmed different-user login clears the previous user's store; bare logout/401 does not (INV-2).
|
|
781
|
-
- **
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
1190
|
+
- **Non-synced routes use the request client** — `/auth/me`, modelRouter actions, AI,
|
|
1191
|
+
and other request/response routes go through generated `store/apiSdk.ts` hooks
|
|
1192
|
+
(`terreno-syncdb-codegen --api-out`) or `useApiQuery` / `useApiMutation`. Eligible
|
|
1193
|
+
admin String-`_id` collection CRUD uses windowed syncdb; other admin requests use
|
|
1194
|
+
admin-frontend's host-bound fetch client. See
|
|
1195
|
+
[How to upgrade from @terreno/rtk to @terreno/syncdb](../how-to/migrate-rtk-to-syncdb.md).
|
|
@@ -282,6 +282,28 @@ per-file, per-rule counts so CI fails only on new violations. Suppress one line
|
|
|
282
282
|
`test-hygiene-disable-next-line <rule>[, <rule>]` comment. Exit codes: 0 clean, 1 new
|
|
283
283
|
violations, 2 usage error.
|
|
284
284
|
|
|
285
|
+
### Documented-response checks (`@terreno/api/testing`)
|
|
286
|
+
|
|
287
|
+
`expectDocumentedResponse({spec, path, method, response})` validates a real response body
|
|
288
|
+
against the schema `/openapi.json` documents for that path, method, and status (AJV, with
|
|
289
|
+
`#/components/...` refs and `nullable` resolved). It throws listing every violation, or when
|
|
290
|
+
the route, status, or media type is not documented. `validateDocumentedResponse` returns
|
|
291
|
+
`{valid, errors}` instead of throwing. `path` is the OpenAPI template
|
|
292
|
+
(`/announcements/{id}/click`); pass the supertest response as `response`.
|
|
293
|
+
|
|
294
|
+
`@terreno/api/testing` also registers api's own test models (including `User`). A package
|
|
295
|
+
that defines its own `User` model imports the same two functions from the side-effect-free
|
|
296
|
+
`@terreno/api/testing/openApi` entry instead, so one test process never compiles `User` twice.
|
|
297
|
+
|
|
298
|
+
```typescript
|
|
299
|
+
import {authAsUser, expectDocumentedResponse} from "@terreno/api/testing";
|
|
300
|
+
|
|
301
|
+
const spec = (await supertest(app).get("/openapi.json")).body;
|
|
302
|
+
const agent = await authAsUser(app, "notAdmin");
|
|
303
|
+
const res = await agent.get("/auth/me").expect(200);
|
|
304
|
+
expectDocumentedResponse({method: "get", path: "/auth/me", response: res, spec});
|
|
305
|
+
```
|
|
306
|
+
|
|
285
307
|
## Smoke helpers
|
|
286
308
|
|
|
287
309
|
A package smoke is a real-process check of the package's built output. It lives in
|
|
@@ -347,7 +369,7 @@ exits 0 on a 2-vCPU worker before it prints the `All files` row. When that
|
|
|
347
369
|
row is still missing after one retry, the script uses `coverage/lcov.info`
|
|
348
370
|
only if it lists every non-test source file under `src/`. Dedicated CircleCI
|
|
349
371
|
jobs (`api-ci`, `ai-ci`, `blocks-ci`,
|
|
350
|
-
`
|
|
372
|
+
`ui-ci`, `syncdb-ci`, `comms-ci`, `mcp-server-ci`, `admin-spa-ci`) run
|
|
351
373
|
that script. Published packages without a dedicated workflow
|
|
352
374
|
(`admin-backend`, `admin-frontend`, `api-health`, `feature-flags`, `@terreno/test`)
|
|
353
375
|
run the same lint, compile, and coverage commands via the parameterized
|
|
@@ -357,7 +379,7 @@ including `.github/workflows/packages-ci.yml`. Those twins run
|
|
|
357
379
|
pass `secrets.CODECOV_TOKEN` into `upload-codecov`.
|
|
358
380
|
|
|
359
381
|
Each of those jobs then uploads `coverage/lcov.info` to Codecov with a distinct
|
|
360
|
-
flag (`api`, `ui`, `
|
|
382
|
+
flag (`api`, `ui`, `syncdb`, …) via `scripts/upload-codecov.sh`. The script
|
|
361
383
|
downloads the linux uploader over HTTPS and refuses to run it unless the
|
|
362
384
|
SHA-256 digest matches the pin in the script (`CODECOV_UPLOADER_SHA256`).
|
|
363
385
|
`codecov.yml` sets per-package flags, `target: auto` with a 1% threshold so
|
|
@@ -394,7 +416,10 @@ registered `DemoConfig` demo and story through `renderWithTheme` from
|
|
|
394
416
|
Pull requests also run the `New file coverage` workflow. Every newly added workspace
|
|
395
417
|
`.ts` or `.tsx` implementation file must have at least 90% function coverage and 90%
|
|
396
418
|
line coverage. Test, spec, story, generated OpenAPI SDK, `dist`, isolated-test,
|
|
397
|
-
`src/types` type modules,
|
|
419
|
+
`src/types` type modules, modules whose only exports are `interface` and `type`
|
|
420
|
+
aliases (comments and string literals do not count; TypeScript erases them, so
|
|
421
|
+
Bun writes no LCOV record), demo
|
|
422
|
+
`story-config/*.config.tsx`, and Expo Router route
|
|
398
423
|
files (`index`, `_layout`, `+not-found`, `[param]`, plus example recovery screens
|
|
399
424
|
`forgotPassword` / `resetPassword` / `verifyEmail`) are excluded. A new implementation
|
|
400
425
|
file that is absent from LCOV is treated as 0% covered, except files listed in
|