@terreno/agent-plugin 0.0.0-stage → 57.22.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/.claude-plugin/plugin.json +32 -0
- package/CHANGELOG.md +2155 -0
- package/LICENSE +21 -0
- package/README.md +57 -2
- package/agents/pre-commit.md +65 -0
- package/agents/ui-verifier.md +93 -0
- package/package.json +32 -4
- package/references/async-review-bots.md +76 -0
- package/references/documentation-contract.md +61 -0
- package/references/execution-state.schema.json +77 -0
- package/references/github-attention-contract.md +165 -0
- package/references/independent-review.md +31 -0
- package/references/lifecycle-contract.md +211 -0
- package/references/loop-engineering.md +125 -0
- package/references/pick-roast-loop.md +112 -0
- package/references/pr-deployments.md +69 -0
- package/references/product-ci.md +164 -0
- package/references/push-ownership.md +91 -0
- package/references/stage-result.schema.json +82 -0
- package/references/subagent-briefing.md +43 -0
- package/references/testing-plan.md +163 -0
- package/references/user-guide.md +171 -0
- package/skills/1-grow/SKILL.md +145 -0
- package/skills/1-grow/references/grilling.md +243 -0
- package/skills/2-pick/SKILL.md +170 -0
- package/skills/2-pick/references/mocking.md +34 -0
- package/skills/2-pick/references/testing.md +43 -0
- package/skills/3-roast/SKILL.md +168 -0
- package/skills/4-brew/SKILL.md +201 -0
- package/skills/5-taste/SKILL.md +311 -0
- package/skills/ai-prompt-governance/SKILL.md +100 -0
- package/skills/attach-pr-media/SKILL.md +89 -0
- package/skills/attach-pr-media/scripts/upload-pr-media.sh +151 -0
- package/skills/backend-test-env/SKILL.md +64 -0
- package/skills/backfill-regression/SKILL.md +101 -0
- package/skills/backfill-user-guide/SKILL.md +96 -0
- package/skills/bootstrap-mcp-server/SKILL.md +244 -0
- package/skills/building-admin-interfaces/SKILL.md +83 -0
- package/skills/building-admin-interfaces/agents/openai.yaml +4 -0
- package/skills/building-admin-interfaces/references/nav-and-shell.md +42 -0
- package/skills/building-admin-interfaces/references/patterns-and-tools.md +73 -0
- package/skills/building-admin-interfaces/references/screens-and-routes.md +50 -0
- package/skills/building-terreno-apps/SKILL.md +280 -0
- package/skills/building-terreno-apps/agents/openai.yaml +4 -0
- package/skills/building-terreno-apps/references/navigation.md +102 -0
- package/skills/building-terreno-apps/references/route-structure.md +91 -0
- package/skills/building-terreno-apps/references/screen-patterns.md +191 -0
- package/skills/debug-syncdb-with-mcp/SKILL.md +75 -0
- package/skills/deploy-gcp/SKILL.md +73 -0
- package/skills/deploy-gcp/references/troubleshooting.md +29 -0
- package/skills/generate-release-announcements/SKILL.md +145 -0
- package/skills/generate-sdk/SKILL.md +156 -0
- package/skills/mongoose-schema-safety/SKILL.md +164 -0
- package/skills/pick-roast-loop/SKILL.md +152 -0
- package/skills/planning-loop/SKILL.md +161 -0
- package/skills/prd/SKILL.md +138 -0
- package/skills/set-up-social-login/SKILL.md +76 -0
- package/skills/store-assets/SKILL.md +221 -0
- package/skills/store-assets/examples/screenshots.yaml +31 -0
- package/skills/store-assets/examples/storeAssets.config.json +44 -0
- package/skills/store-assets/scripts/banner.ts +165 -0
- package/skills/store-assets/scripts/config.ts +255 -0
- package/skills/store-assets/scripts/devClient.ts +635 -0
- package/skills/store-assets/scripts/devices.ts +286 -0
- package/skills/store-assets/scripts/icon.ts +81 -0
- package/skills/store-assets/scripts/maestro/goHome.yaml +21 -0
- package/skills/store-assets/scripts/maestro/login.yaml +27 -0
- package/skills/store-assets/scripts/maestro/openApp.yaml +26 -0
- package/skills/store-assets/scripts/screenshots.ts +848 -0
- package/skills/taste-sweep/SKILL.md +172 -0
- package/skills/terreno-backend-api/SKILL.md +355 -0
- package/skills/terreno-backend-api/agents/openai.yaml +4 -0
- package/skills/terreno-backend-api/references/custom-routes.md +132 -0
- package/skills/terreno-backend-api/references/model-router.md +116 -0
- package/skills/terreno-backend-api/references/server-setup.md +116 -0
- package/skills/terreno-backend-tests/SKILL.md +176 -0
- package/skills/terreno-data-fetching/SKILL.md +276 -0
- package/skills/terreno-data-fetching/agents/openai.yaml +4 -0
- package/skills/terreno-data-fetching/references/auth-and-tokens.md +93 -0
- package/skills/terreno-data-fetching/references/realtime.md +45 -0
- package/skills/terreno-data-fetching/references/sdk-customization.md +84 -0
- package/skills/terreno-frontend-tests/SKILL.md +190 -0
- package/skills/terreno-mobile-tests/SKILL.md +127 -0
- package/skills/terreno-test-setup/SKILL.md +81 -0
- package/skills/terreno-test-setup/references/backend.md +131 -0
- package/skills/terreno-test-setup/references/frontend.md +169 -0
- package/skills/terreno-test-setup/references/mobile.md +134 -0
- package/skills/terreno-ui/SKILL.md +252 -0
- package/skills/terreno-ui/agents/openai.yaml +4 -0
- package/skills/terreno-ui/references/data-display.md +85 -0
- package/skills/terreno-ui/references/feedback.md +122 -0
- package/skills/terreno-ui/references/forms.md +119 -0
- package/skills/terreno-ui/references/layout.md +124 -0
- package/skills/terreno-ui/references/theming.md +87 -0
- package/skills/update-docs/SKILL.md +82 -0
- package/skills/upgrading-terreno/SKILL.md +86 -0
- package/skills/upgrading-terreno/references/ordering.md +40 -0
- package/skills/verify-ui-changes/SKILL.md +605 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,2155 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented in this file.
|
|
4
|
+
|
|
5
|
+
All `@terreno/*` packages (`api`, `test`, `ui`, `rtk`, `admin-backend`,
|
|
6
|
+
`admin-frontend`, `admin-spa`, `ai`, `announcements`, `api-health`, `comms`,
|
|
7
|
+
`feature-flags`, `jobs`, `mcp`, `syncdb`) and the unscoped `create-terreno-app` CLI are versioned in lockstep
|
|
8
|
+
and published at the same version.
|
|
9
|
+
|
|
10
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
11
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
12
|
+
|
|
13
|
+
Unreleased changes live in [`changelog/unreleased/`](changelog/unreleased/) as one
|
|
14
|
+
file per feature. `bun run changelog:assemble <version>` folds those files into a
|
|
15
|
+
dated section below when cutting a release.
|
|
16
|
+
|
|
17
|
+
Upgrade notes for consumer action live in [`mcp-server/src/docs/upgrades/`](mcp-server/src/docs/upgrades/). Fetch a range with the MCP tool `terreno_get_upgrade_guide`. Format: [`mcp-server/src/docs/upgrades/README.md`](mcp-server/src/docs/upgrades/README.md). The [`58.0.0` stub](mcp-server/src/docs/upgrades/58.0.0.md) is for the next major (not yet a changelog section).
|
|
18
|
+
|
|
19
|
+
## [Unreleased]
|
|
20
|
+
|
|
21
|
+
Unreleased changes live in [`changelog/unreleased/`](changelog/unreleased/). Add one Markdown file per feature (see that directory's README) instead of editing this section.
|
|
22
|
+
|
|
23
|
+
## [57.22.0] - 2026-11-03
|
|
24
|
+
|
|
25
|
+
Upgrade note: [`mcp-server/src/docs/upgrades/57.22.0.md`](mcp-server/src/docs/upgrades/57.22.0.md).
|
|
26
|
+
|
|
27
|
+
### Breaking
|
|
28
|
+
|
|
29
|
+
- `@terreno/ai` no longer integrates with Langfuse. The built-in observability prompt registry, traces, and review queue (`ObservabilityApp` with `createLocalObservabilityPlugin()`) replace it. Removed exports: `LangfuseApp`, `LangfuseCache`, `getCached`, `setCached`, `invalidateCache`, `initLangfuseClient`, `getLangfuseClient`, `isLangfuseInitialized`, `shutdownLangfuseClient`, `compilePrompt`, `createPrompt`, `getPrompt`, `invalidatePromptCache`, `initTracing`, `shutdownTracing`, `createTelemetryConfig`, `preparePromptForAI`, and the types `ChatMessage`, `GetPromptOptions`, `LangfuseAppOptions`, `LangfuseCachedPrompt`, `PaginatedResult`, `PreparePromptResult`, `PromptListItem`, `ScoreSubmission`, `ScoringFunction`, `TelemetrySettings`, `TraceListItem`. The `@terreno/ai/langfuseApp` and `@terreno/ai/langfuseClient` subpath exports are gone, along with the `@langfuse/client`, `@langfuse/otel`, and `@opentelemetry/*` dependencies of `@terreno/ai`.
|
|
30
|
+
- `addGptRoutes` drops `langfuseSystemPromptName`; use `systemPromptName` (a registry prompt) instead. Chat turns no longer set `experimental_telemetry`.
|
|
31
|
+
- `ControlPrimary` is now only `"local"`.
|
|
32
|
+
- The example backend no longer reads `LANGFUSE_SECRET_KEY`, `LANGFUSE_PUBLIC_KEY`, `LANGFUSE_BASE_URL`, `LANGFUSE_ORGANIZATION`, `LANGFUSE_PROJECT`, or `LANGFUSE_PROJECT_ID`. Deploys (`.github/workflows/cd.yml`, `scripts/ci/gcp-deploy.sh`) no longer mount the Langfuse secrets, and `infra/flourish/main.tf` drops the `backend_secret_langfuse_secret_key` and `backend_secret_langfuse_public_key` modules, so the next `terraform apply` deletes those two Secret Manager secrets. Deploy a revision without the mounts before applying.
|
|
33
|
+
|
|
34
|
+
### Added
|
|
35
|
+
|
|
36
|
+
- `addGptRoutes` accepts `systemPromptName` and `systemPromptLabel`. A chat turn prepends that observability registry prompt to the system prompt and records its version on the trace.
|
|
37
|
+
- The example chat reads `chat-assistant` and the `generate_joke` tool reads `joke-generator` from the observability prompt registry.
|
|
38
|
+
- `bun run backend:seed` fills every AI Observability screen: labelled prompt versions for `example-summarize`, `joke-generator`, and `chat-assistant`, golden datasets, finished experiments, traces with scores, and review items in every status. Run it on an existing database with the `seedAiObservabilityDemo` admin script.
|
|
39
|
+
- Review batches: pick a human question set and press **Grab 10 recent traces** to give reviewers a fixed set of inputs and outputs (`POST /ai/observability/review/batches`).
|
|
40
|
+
- The review item screen shows one input, one output, and every question as Yes / No rows. On web, `Y` / `N` answer the highlighted question, `Enter` submits and moves to the next batch item, and `S` skips. Evaluator dimensions accept a reviewer `question`.
|
|
41
|
+
- Run any LLM judge over a batch and see overall and per-question agreement with the human answers, plus a list of disagreements (`POST /ai/observability/review/batches/:id/judgeRuns`).
|
|
42
|
+
- The AI observability seed adds the `summary-review-questions` question set, two judges for it, and a 10-item batch with 8 answers where one judge agrees 97% and the other 78%.
|
|
43
|
+
- Observability registry prompts can reference other prompts with `{{> base-rules}}`, `{{> base-rules label=staging}}`, or `{{> base-rules version=3}}` (default label `production`). Shared instructions live in one prompt; `PromptRegistry.get`, the playground, experiments, and judges use the resolved text.
|
|
44
|
+
- Every reference must resolve when it is saved. Creating a prompt, saving a version, and moving a label return 400 `Invalid prompt reference` for a malformed tag, an unknown prompt or version, or a label the referenced prompt does not have yet, 400 `Prompt reference cycle` when the change would make prompts include each other, and 400 `Prompt references nested too deeply` when it would nest the prompt, or any prompt that includes it, more than 5 levels.
|
|
45
|
+
- Traces stay named after the called prompt. `ObsTrace.prompts` lists it first, then each shared prompt version it pulled in, marked with `includedBy`. `PromptVersionRef.includes` and harness prompt refs carry the same list.
|
|
46
|
+
|
|
47
|
+
### Fixed
|
|
48
|
+
|
|
49
|
+
- Review queue: `Y` and `N` answer the highlighted question when a Yes/No segment is focused. **What the AI was given** and **What the AI wrote** are headings above the field cards.
|
|
50
|
+
- Review batches: **Save batch** sits on the New batch card. Search and **More filters** share a baseline.
|
|
51
|
+
- Prompt hub: description, folder, and tags sit on the Versions screen with the editor. System and template fields explain what to write.
|
|
52
|
+
- Evaluator detail loads the judge prompt `outputSchema` (so helpfulness no longer reports a missing dimension) and experiment rows open that experiment.
|
|
53
|
+
|
|
54
|
+
## [57.21.0] - 2026-11-01
|
|
55
|
+
|
|
56
|
+
Upgrade note: [`mcp-server/src/docs/upgrades/57.21.0.md`](mcp-server/src/docs/upgrades/57.21.0.md).
|
|
57
|
+
|
|
58
|
+
### Breaking
|
|
59
|
+
|
|
60
|
+
- The `terreno-scan` plugin is now `terreno-loop`, and its skills are renamed from `terreno-scan-*` to `terreno-loop-*`. The resident loop `terreno-scan-loop` is now `terreno-loop-run`: `/terreno-loop:run` in Claude Code, `/terreno-loop-run` in Cursor, `$terreno-loop-run` in Codex. Claude Code moves existing installs over through the marketplace `renames` entry. In Cursor and Codex, uninstall `terreno-scan` and install `terreno-loop`. Campaign state stays at `.terreno/scan/<goal>.json`, so campaigns already in progress resume.
|
|
61
|
+
|
|
62
|
+
### Added
|
|
63
|
+
|
|
64
|
+
- `@terreno/admin-spa` serves an MCP OAuth consent screen at `/console/oauth/consent`, so a backend with no product frontend can set `mcpOAuth.consentUrl` to `${issuer}/console/oauth/consent`. Any signed-in user can approve or deny a request with their Better Auth session; signed-out users sign in and return through the login screen's new `returnTo` parameter, which accepts only in-app paths.
|
|
65
|
+
- Claude Code plugins now publish to npm with every release, as `@terreno/agent-plugin` and `@terreno/agent-plugin-loop`. A new `terreno-releases` marketplace installs them at the latest stable release. To pin one release, add `TerrenoLabs/terreno#<version>` instead. See [Install agent skills](docs/how-to/install-agent-skills.md#pick-a-channel).
|
|
66
|
+
- The generated Claude plugin manifest no longer pins `version`. Before this change, plugins installed from the `TerrenoLabs/terreno` marketplace stayed on the `2.12.0` copy, because their version never changed. They now update on every commit. Run `/plugin update terreno@terreno-plugins` once to leave `2.12.0`.
|
|
67
|
+
- The `terreno-planning` lifecycle plugin (2.13.0) plans e2e tests and manual regression cases in every IP:
|
|
68
|
+
|
|
69
|
+
- Grow requires `## E2E tests` and `## Regression cases` sections (either may be `None — <reason>`), read from the e2e guide at `docs/testing/e2e.md` or a `## Testing conventions` override in AGENTS.md or CLAUDE.md.
|
|
70
|
+
- Regression cases stay minimal: most features add none. A case is only for what a developer or QA must check by hand, such as push notifications, and Roast fails one that repeats an existing case.
|
|
71
|
+
- Pick writes each planned spec in the task that delivers it, and Roast runs it; a missing or failing row is `FAIL`.
|
|
72
|
+
- Brew gates on every planned spec and merges regression cases into `docs/regression/<area>.md` with the next free ID.
|
|
73
|
+
- Taste re-checks both sections after behaviour-changing fixes and renumbers regression ID collisions with `master`.
|
|
74
|
+
- `bun run check:lifecycle-skills` enforces these phrases in every stage.
|
|
75
|
+
- New `backfill-regression` skill proposes manual regression cases one area at a time and writes only the sets the human approves.
|
|
76
|
+
- A consumer opts out of manual regression suites with `Regression suites | none` in its `## Testing conventions` table.
|
|
77
|
+
- The `terreno-planning` lifecycle plugin (2.14.0) grows an end-user guide with every feature:
|
|
78
|
+
|
|
79
|
+
- Every IP has a `## User guide` section listing the guide pages it adds, updates, or retires, or `None — <reason>`. Pages live in `docs/user-guide/<area>/<feature>.md`, one feature per page, with `title`, `audience`, `platforms`, and `questions` frontmatter that chat agents use to answer "how do I…" questions.
|
|
80
|
+
- Pick writes each page in the task that ships the feature, Roast checks it against the shipped screens, Brew gates on it and the index, and Taste keeps it current after fixes.
|
|
81
|
+
- A repo opts out with `| User guide | none — <reason> |` in a `## User guide conventions` table in AGENTS.md or CLAUDE.md.
|
|
82
|
+
- New `backfill-user-guide` skill drafts pages for features that shipped without one, one area at a time, and writes only the pages the human approves.
|
|
83
|
+
- `bun run check:user-guide` validates the pages and index (CI `repo-policies`); `--uncovered <app dir>` lists screens no page names.
|
|
84
|
+
- Terreno's own guide covers the example app's main features.
|
|
85
|
+
- `@terreno/api`: opt-in `mcpRequestLog` on `TerrenoApp` records one metadata-only `McpRequestLog` row per MCP tool call (tool, user, credential, service token or OAuth client, duration, outcome, error class; never arguments, results, or error text), expiring after `retentionDays` (default 30).
|
|
86
|
+
- `@terreno/api`: opt-in `mcpToolSettings` on `TerrenoApp` registers every MCP tool in `McpToolSetting` at startup; tools an admin switches off are hidden from `tools/list` and rejected by `tools/call`.
|
|
87
|
+
- `@terreno/api`: deleting an `McpOAuthToken` row revokes its whole connection (token family) and keeps the rows. New `McpOAuthToken.revokeAllForUser` and `McpServiceToken.revokeAllForUser`.
|
|
88
|
+
- `@terreno/admin-backend`: `mcpAdminModels()` adds MCP admin screens (service tokens, OAuth clients, OAuth connections, request log, tool switches), `mcpAdminStatements` for RBAC, and `mcpAdminScripts()` adds `mcp-sign-out-everywhere`. See [Manage MCP from the admin panel](docs/how-to/manage-mcp-in-admin.md).
|
|
89
|
+
- Docs: [Expose an MCP server safely](docs/how-to/expose-mcp-server-safely.md) (OAuth, service tokens, Cloudflare Tunnel, Access) and the `bootstrap-mcp-server` skill for a single-binary MCP server with the admin console embedded.
|
|
90
|
+
- Every workspace package has a real-process smoke test and an e2e page:
|
|
91
|
+
|
|
92
|
+
- Each package's `e2e/smoke.ts` (or `smoke.spec.ts` for browser packages) runs its built output through its public entry: a server on a real port, the compiled binary, or a page in Chromium. `bun run --cwd <package> smoke` runs one; `bun run smoke` runs them all and prints each package's time. The CircleCI `smoke` job runs them on every package change.
|
|
93
|
+
- `@terreno/test` adds the smoke helpers `smokeMongoUri()`, `spawnUntilReady()`, `spawnExampleBackend()`, `runBin()`, and `freePort()`.
|
|
94
|
+
- The [E2E testing guide](docs/testing/e2e.md) explains the test tiers and links one page per package under `docs/testing/e2e/`.
|
|
95
|
+
- Manual regression cases live in `docs/regression/<area>.md`, one file per area, and `bun run check:regression-suites` validates them.
|
|
96
|
+
- example-frontend's smoke serves a web export that `bun run --cwd example-frontend export:smoke` builds once with a placeholder API URL; the smoke swaps in a `freePort()` API URL as it serves the bundle, so the 30 s smoke no longer rebuilds the export. `spawnUntilReady()` takes `stopGraceMs`, and `spawnExampleBackend()` stops example-backend after 0.5 s instead of 5 s.
|
|
97
|
+
- Every workspace package has a `typecheck` script that runs `tsc` over source and test files through a new `tsconfig.typecheck.json`. Root `bun run lint` ends with `bun run typecheck`, and CircleCI runs it in a new `typecheck` job. Test files that had type errors were fixed.
|
|
98
|
+
|
|
99
|
+
### Changed
|
|
100
|
+
|
|
101
|
+
- `@terreno/api`: deleting an `McpOAuthToken` row with document or query `deleteOne` now revokes that row's whole token family (the connection) and keeps the rows instead of removing one pair. `deleteMany` is unchanged.
|
|
102
|
+
- `@terreno/ui`: new `theme.field` tokens (`background`, `backgroundDisabled`, `border`, `borderFocus`, `borderError`, `borderDisabled`) style form inputs. `TextField`, `SelectField`, `DateTimeField`, `HeightField`, and `OtpField` read them instead of `surface.base` and `border.dark`. Defaults equal the old colors, but overriding `surface` or `border` no longer recolors those fields; use `setTheme({field: ...})`.
|
|
103
|
+
|
|
104
|
+
## [57.20.0] - 2026-10-30
|
|
105
|
+
|
|
106
|
+
Upgrade note: [`mcp-server/src/docs/upgrades/57.20.0.md`](mcp-server/src/docs/upgrades/57.20.0.md).
|
|
107
|
+
|
|
108
|
+
### Breaking
|
|
109
|
+
|
|
110
|
+
- `@terreno/api`: the app `User` claim guard moves before the Better Auth user is created, and
|
|
111
|
+
legacy users can prove ownership with their password. 57.17.0 refused to link an unverified
|
|
112
|
+
Better Auth user to an existing `User` by email, but only after the Better Auth user existed, so
|
|
113
|
+
a later email verification could still hand that user the `User`. Now a Better Auth user claims
|
|
114
|
+
an unlinked `User` only when the email is verified (by the provider), or when the sign-up (or
|
|
115
|
+
email sign-in) password verifies against that `User`'s passport-local-mongoose hash. A refused
|
|
116
|
+
sign-up answers 409 with `code: "VERIFY_EMAIL_TO_USE_ACCOUNT"` ("Verify your email to use this
|
|
117
|
+
account"; OAuth callbacks redirect with that `error`) and creates no Better Auth user, account,
|
|
118
|
+
or verification email. A `User` bound to another Better Auth account is refused before
|
|
119
|
+
creation too, with `code: "ACCOUNT_ALREADY_EXISTS"`. `syncBetterAuthUser` throws `APIError`
|
|
120
|
+
409 with the matching title, and never inserts a duplicate `User`. Emails match ignoring case.
|
|
121
|
+
A refusal reveals that an app `User` has that email (an accepted account-existence signal).
|
|
122
|
+
57.17.0's rules still hold: a verified-email link removes a legacy password the `User` never
|
|
123
|
+
proved and bumps `tokenEpoch`, and `trustEmail` seeds skip the checks.
|
|
124
|
+
- `BetterAuthApp` registered without a `userModel` now adopts `TerrenoApp`'s before it creates
|
|
125
|
+
Better Auth (new `adoptUserModel`, called by `TerrenoApp.build()`), so the guard runs whenever
|
|
126
|
+
TerrenoApp's session middleware links app `User`s. The legacy hash check does not touch
|
|
127
|
+
passport's `limitAttempts` counters.
|
|
128
|
+
- With a user model, the `terreno-app-user-sync` plugin syncs the app `User` as each Better Auth
|
|
129
|
+
session is created, not only on the first authenticated request, and the OAuth provider id
|
|
130
|
+
fills `oauthProvider`. A password-proven link that fails to save fails the sign-in with 500
|
|
131
|
+
`APP_USER_LINK_FAILED`. `syncBetterAuthUser`'s options gain `password`;
|
|
132
|
+
`seedBetterAuthUser` passes the seed password beside `trustEmail`.
|
|
133
|
+
- `accountLinking: {mode: "never"}` (or `options.account.accountLinking.enabled: false`) now
|
|
134
|
+
refuses `POST {basePath}/link-social` with 403 `LINKING_DISABLED` instead of failing at the
|
|
135
|
+
provider callback.
|
|
136
|
+
- Migration for JWT apps: users who sign up with their existing password, or sign in with a
|
|
137
|
+
provider that verified their email, link at once. Users with no password hash (GitHub JWT
|
|
138
|
+
sign-in) reset their password through JWT `POST /auth/forgotPassword` first, or sign in with a
|
|
139
|
+
verified provider, so keep the JWT routes mounted during the move. See
|
|
140
|
+
`docs/explanation/account-linking.md`.
|
|
141
|
+
|
|
142
|
+
### Added
|
|
143
|
+
|
|
144
|
+
- `AccountSecurityApp` adds access recovery routes under `/users/:id/security`: `revoke-sessions` deletes every Better Auth session (bearer clients get 401 on their next request; cookie sessions can survive up to Better Auth's 5-minute cookie cache) and bumps `tokenEpoch` when the User model has it; `send-password-reset` mails a reset link (501 without `sendMail` or `publicAppUrl`, 409 for a user with no Better Auth account); `disable` sets `User.disabled` and revokes sessions, and `enable` clears it (both need `isDisabledPlugin`). Each change is audited.
|
|
145
|
+
- `POST /users/:id/security/issue-sign-in-link` creates a Better Auth account for a legacy User with no Better Auth link and no password, links it by `betterAuthId`, and returns a single-use set-password link `{url, expiresAt}` for the administrator to hand over. The email stays unverified. A link that was never used can be issued again (the earlier one stops working). It needs the new RBAC statement `user:issueSignInLink`, which only `superadmin` holds by default (`admin: true` without RBAC).
|
|
146
|
+
- Security route actions can be marked `platformOnly` (`disable`, `enable`, and `issue-sign-in-link` are) and can name a `requiredPermission`. `mountSecurityRoutes(router, authorize, {includePlatformOnly})` leaves platform-only actions out unless asked, so org-admin prefixes never expose them.
|
|
147
|
+
- The security summary reports `emailVerified` and `legacyPassword`, and `AccountSecurityApp` takes a `publicAppUrl` option (defaulting to the registered `BetterAuthApp`'s).
|
|
148
|
+
- The example backend's User applies `isDisabledPlugin`, so its admin and security screens can disable accounts.
|
|
149
|
+
- `AccountSecurityApp` (`@terreno/api`) adds platform account-recovery routes: `GET /users/:id/security` summarizes a user's linked providers, passkeys, 2FA state and remaining backup codes, active sessions, last sign-in, and available actions without exposing any secret, and `POST /users/:id/security/reset-two-factor` turns a locked-out user's 2FA off and forgets their trusted devices. Access needs the new RBAC statement `user:manageSecurity` (held by `superadmin` and `operator`; an existing `operator` role gains it once on the next seed, and a later revocation sticks through the new `RbacRole.appliedGrants`) and the user's own session (JWT or Better Auth session; MCP tokens and any other credential get 403). Acting on yourself, or on someone holding a permission you lack (such as a superadmin), is refused with 403. The plugin refuses to start without `AuditApp` unless you pass `audit: false`. See `docs/reference/account-security.md`.
|
|
150
|
+
- `AuditEvent.source` accepts `security`. `recordSecurityAction` writes one security event per recovery action that changes something, and `onSecurityAction` lets in-process listeners react to it.
|
|
151
|
+
- `mountSecurityRoutes(router, authorize)` mounts the same handlers under another prefix with its own authorizer, and `TerrenoApp.getAccessControl()` returns the app's RBAC access control.
|
|
152
|
+
- `DELETE /users/:id/security/passkeys/:passkeyId` removes one of a user's passkeys (another user's passkey id is a 404), and `DELETE /users/:id/security/accounts/:providerId` unlinks a sign-in provider. The password can't be unlinked. Either action that would leave no sign-in method is refused with 409 unless the user's email is verified, in which case a password-reset link is mailed first (and also afterwards, if a concurrent removal took the last other method). Unlinking doesn't end sessions started with that provider. Both actions are audited and appear in the summary's `actions` when they apply. A custom action that reuses an action id now throws at construction.
|
|
153
|
+
- The example backend registers `AccountSecurityApp`.
|
|
154
|
+
- `MemberSecurityPanel` in `@terreno/admin-frontend` shows a user's sign-in security (providers, password, passkeys, two-factor, sessions, last sign-in, disabled and email-verified state) and runs the recovery actions the server lists. Actions render from the summary's `actions`, so new server actions get a button without a frontend change; destructive ones confirm first, results and server error titles show inline, and a returned sign-in link is shown once with a copy button. It calls `/orgs/:orgId/members/:userId/security` or, without `orgId`, `/users/:id/security` through the host-bound `adminRequest`.
|
|
155
|
+
- `OrgMembersScreen` rows get a **Security** button that opens the panel for active members, and a **Reactivate** button for suspended memberships.
|
|
156
|
+
- `useMemberSecurityApi`, `memberSecurityPath`, and `resolveSecurityActionRequest` are exported for custom security screens.
|
|
157
|
+
- Each entry of the account security summary's `actions` now carries its route: `method` (`POST` or `DELETE`) and `path` relative to the user's security base, with `:param` placeholders such as `/passkeys/:passkeyId`. Clients can run any listed action, including custom ones, from the summary alone.
|
|
158
|
+
- `UserSecurityScreen` in `@terreno/admin-frontend` is the platform staff page for one user's sign-in security: a header with the user's name and email from their admin record, then `MemberSecurityPanel` against `/users/:id/security`, so operators and superadmins can sign a user out everywhere, send a password reset, disable or enable the account, and run every other recovery action the server lists.
|
|
159
|
+
- `UserSecurityLink` and `userSecurityRoute` add a **Security** button to an admin user record (for example in `AdminModelForm` `footerContent`) that opens `{routeBase}/users/:id/security`.
|
|
160
|
+
- The example app mounts the screen at `/admin/users/[id]/security` and links it from the admin `User` record, both only for staff holding `user:manageSecurity`; the link is hidden on the viewer's own record.
|
|
161
|
+
- `@terreno/api`: `BetterAuthConfig` turns auth features on without hand-built plugins:
|
|
162
|
+
`accountLinking: {mode: "verified-email" | "explicit" | "never", trustedProviders?}`,
|
|
163
|
+
`twoFactor: {issuer, backupCodes?, trustDeviceDays?}`, `passkey: {rpID, rpName, origin}`,
|
|
164
|
+
`signUp: {mode: "open" | "closed"}`, `lastLoginMethod`, `multiSession: {maximumSessions?}`,
|
|
165
|
+
`oauthProxy: {productionURL}`, and `nativeApps` (the `@better-auth/expo` server plugin).
|
|
166
|
+
Every feature is off until set; with none set the plugin list stays
|
|
167
|
+
`bearer`, `terreno-methods`, then `config.plugins`. Feature plugins go before
|
|
168
|
+
`config.plugins`, and `options` still overrides what they set.
|
|
169
|
+
- `signUp: {mode: "closed"}` stops new accounts through email and password and OAuth: email
|
|
170
|
+
sign-up answers 400, and a social or generic OAuth sign-in for an unknown email fails with
|
|
171
|
+
`signup_disabled`, even with `requestSignUp: true`. Other sign-in plugins added through
|
|
172
|
+
`plugins` (magic link, email OTP, phone number, anonymous) need their own `disableSignUp`.
|
|
173
|
+
- `passkey` config is checked at startup: each `origin` must be a bare http(s) origin on
|
|
174
|
+
`rpID` or a subdomain of it.
|
|
175
|
+
- `appleOAuth` also accepts `{clientId, teamId, keyId, privateKey, appBundleIdentifier?}` and
|
|
176
|
+
signs Apple's ES256 client secret at startup (valid 180 days).
|
|
177
|
+
- `GET {basePath}/terreno/methods` now reports real `twoFactor`, `passkey`, and
|
|
178
|
+
`multiSession` flags.
|
|
179
|
+
- With `rateLimit` on, `{basePath}/two-factor/*`, `/passkey/*`, `/link-social`,
|
|
180
|
+
`/unlink-account`, `/multi-session/*`, `/oauth-proxy-callback`, and
|
|
181
|
+
`/expo-authorization-proxy` use the `auth` bucket.
|
|
182
|
+
- New dependencies: `@better-auth/passkey` (catalog, api and rtk), `@better-auth/expo` (api),
|
|
183
|
+
`react-native-qrcode-svg` (ui), and `otpauth` (example-frontend dev).
|
|
184
|
+
- `@terreno/api`: every Better Auth server now answers `GET {basePath}/terreno/methods`
|
|
185
|
+
(public, no session) with `{emailPassword, signUp, providers, passkey, twoFactor,
|
|
186
|
+
accountLinking, multiSession}`. `providers` lists the configured Google, GitHub, and Apple
|
|
187
|
+
providers and any `genericOAuth` provider ids. `createBetterAuth` adds the `terreno-methods`
|
|
188
|
+
plugin after `bearer()`, before `config.plugins`.
|
|
189
|
+
- `@terreno/rtk`: `useAuthMethods(authClient)` returns `{methods, isLoading, error}` from that
|
|
190
|
+
endpoint through the client's `$fetch`, so bearer and cookie modes both work.
|
|
191
|
+
- example-frontend: the login screen shows only the social providers the server reports, and
|
|
192
|
+
shows the mock OIDC button when the server lists `mock-oidc`.
|
|
193
|
+
`EXPO_PUBLIC_MOCK_OIDC_SIGN_IN` is no longer read.
|
|
194
|
+
- `@terreno/api` serves `POST {basePath}/terreno/set-password` (`{newPassword}`) so a signed-in user without a password, such as one who signed up with a provider, can add one. It needs a fresh session (403 `SESSION_NOT_FRESH` otherwise) read from the database, refuses with 409 `PASSWORD_ALREADY_SET` when a password exists, follows Better Auth's length rules, and sits in the `auth` rate-limit bucket. With a `userModel`, an app User linked by `betterAuthId` that already has a JWT password gets the new one too, and its `tokenEpoch` is bumped; a social-only account does not gain a JWT password. Two concurrent sets leave one password.
|
|
195
|
+
- A successful set records the new `password-set` security event, which emails the user "A password was added to your account" (template `securityPasswordSet`) when security notifications are on.
|
|
196
|
+
- Two-factor, trusted-device, and passkey sign-in now work for bearer-mode web clients that send no cookies (`createBetterAuthClient({webAuth: "bearer"})`). With `twoFactor` or `passkey` configured, `@terreno/api` copies Better Auth's signed challenge cookies into the `X-Terreno-Auth-Challenge` header for clients that send it and turns the header back into cookies on `/two-factor/*`, `/passkey/*`, and `/sign-in/*`. rtk's bearer mode stores the header beside the bearer token (`${storagePrefix}_auth_challenge`), sends it on those calls, never stores the dead token of a sign-in waiting on a second factor, drops the 2FA challenge after a successful verify and pending challenges on sign-out, and keeps the trusted-device value until it expires. Cookie-mode and native clients are unchanged.
|
|
197
|
+
- create-terreno-app: generated apps get the auth suite. `backend/src/utils/betterAuthConfig.ts` turns on `accountLinking` (`AUTH_ACCOUNT_LINKING`, default `verified-email`), `twoFactor` (issuer = the app's display name, 30 trusted days), `passkey` (origin and rpID from `FRONTEND_URL`, or `PASSKEY_RP_ID`; off in production while `FRONTEND_URL` is a loopback URL), `lastLoginMethod`, `multiSession`, `nativeApps`, and `signUp` (`AUTH_SIGN_UP`, default `open`); `getWebOrigins` now includes the `FRONTEND_URL` origin. `server.ts` registers `AuditApp` and `AccountSecurityApp`; the generated User schema now applies `rbacUserPlugin` (`roles`, default `member`), so the seeded admin's `operator` role is saved and reaches the `/users/:id/security` recovery routes; and the seed keeps sign-up open on its own instance. On the frontend, `lib/betterAuth.ts` passes the matching client `features` and exports a `rememberedAccounts` store that keeps only the sign-in method (`rememberIdentity: false`). `app/login.tsx` renders `TerrenoLoginScreen`, and a new `app/settings/security.tsx` renders `TerrenoAccountSecurityScreen`, opened from the Profile tab's **Security** button. The Profile tab's "New Password" field is gone: it sent `password` to `PATCH /auth/me`, which refuses password fields since 57.17.0. Change passwords on the Security screen. The "Current password" field that an email change needs stays. Generated apps set no `sendMail`, so forgot-password, the `send-password-reset` and `issue-sign-in-link` recovery actions (501), and security notification emails do nothing until the app passes `@terreno/comms` `sendMail` and `renderAuthMail` to the Better Auth config. To move an existing generated app over, copy those files (and `models/user.ts`, `types/models/userTypes.ts`) from a freshly generated app and add `FRONTEND_URL` to the backend env.
|
|
198
|
+
- example-backend turns on every Better Auth feature: account linking (`AUTH_ACCOUNT_LINKING`, default `verified-email`), sign-up mode (`AUTH_SIGN_UP`, default `open`), two-factor (issuer "Terreno Example"), passkeys (origin from `FRONTEND_URL`, rpID from `PASSKEY_RP_ID` or the `FRONTEND_URL` host; off in production while `FRONTEND_URL` is a loopback URL), last-used method, multi-session, and native sign-in. `GET /api/auth/terreno/methods` reports them. Seeded users (`seedBetterAuthUserInProcess` and the e2e setup) get a verified Better Auth email, and seeding still works with `AUTH_SIGN_UP=closed`. See [Auth features in example-backend](docs/how-to/configure-better-auth.md#auth-features-in-example-backend).
|
|
199
|
+
- The e2e mock OIDC provider can sign in any email, verified or not, per browser context with `setMockOidcIdentity(context, {email, emailVerified})` (or a `login_hint`). `playwright.config.ts` lists the cross-site bearer specs in `CROSS_SITE_SPEC_FILES`, and the new `e2e/authMethods.spec.ts` checks the discovery endpoint there.
|
|
200
|
+
- `ListDetailLayout`, `ListDetailIndex`, and `useListDetail` in `@terreno/ui`: a route-driven list/detail layout for an Expo Router group's `_layout.tsx`, like a mail inbox. Phones push the detail route; when the layout is at least 768px wide (tablets, wide browsers, inside a sidebar), the list stays in a left pane and the detail opens beside it. The open item lives in the URL, so deep links and back work, and it stays open across rotation and resize. See `docs/how-to/build-list-detail-screens.md`. `SplitPage` is unchanged; its docs now point new screens to `ListDetailLayout`.
|
|
201
|
+
- `Box` `accessibilityState.selected`. A clickable row sends it as `aria-current="true"` on web; a selectable `accessibilityRole` (`tab`, `option`, `row`, ...) sends `aria-selected`.
|
|
202
|
+
- The demo inbox exports each fixture message (`generateStaticParams`), and the Netlify demo deploy rewrites any other path to `index.html`, so `/demo/list-detail/2` and `/demo/sidebar-navigation/mail/2` load on refresh.
|
|
203
|
+
- The example todo browser shows a spinner while a deep-linked todo is still syncing, and “This todo does not exist” only after sync is idle and the row is absent.
|
|
204
|
+
- A clickable `Box` with `selected` sends only `aria-current` on web. `selected` is left off the host `accessibilityState`, so react-native-web does not also set `aria-selected` on the button.
|
|
205
|
+
- The hosted Terreno MCP server rate limits JSON-RPC calls per client IP (default 120 requests per 60 seconds). Excess calls get HTTP 429 with a JSON-RPC error naming the limit and retry window. Configure with `TERRENO_MCP_RATE_LIMIT_MAX` (`0` disables), `TERRENO_MCP_RATE_LIMIT_WINDOW_MS`, and `TERRENO_MCP_TRUST_PROXY_HOPS`.
|
|
206
|
+
- `@terreno/api` MCP OAuth accepts `grant_type=client_credentials` for a confidential client created with `McpOAuthClient.issueConfidential`. The helper binds the client to a user, stores the secret as a SHA-256 hash, and returns the secret once. The token endpoint accepts `client_secret_basic` or `client_secret_post`, whichever method the client was created with, and returns an `mcpat_` access token with no refresh token. The token acts as that user (`credential: "oauth"`). Revoking the client ends every token it holds, and a revoked client is rejected at `/oauth/authorize` and `/oauth/token`. An authorization-code exchange, a refresh, or a client_credentials grant that loses a race with that revoke is discarded. `deleteOne`, `findOneAndDelete`, and `deleteMany` do the same. A query `deleteOne` or `findOneAndDelete` whose filter matches more than one client pins the delete to one document and revokes that client's tokens. Dynamic client registration stays public-only. Discovery advertises `client_credentials`, `client_secret_basic`, and `client_secret_post`.
|
|
207
|
+
- `@terreno/api`: with `oauthHandoff`, a bearer-mode web client on another site can link a
|
|
208
|
+
social or generic OAuth provider to the signed-in user. `POST {basePath}/oauth-handoff/link-intent`
|
|
209
|
+
(bearer- or cookie-authenticated, `{provider, callbackURL, errorCallbackURL?}`) returns
|
|
210
|
+
`{token, startURL}`: a single-use, three-minute link token, stored hashed and bound to the
|
|
211
|
+
caller's session. Both URLs must be absolute pages on one of `oauthHandoff.webOrigins` that
|
|
212
|
+
`trustedOrigins` allows (400 otherwise); `callbackPaths` do not apply, since no token travels
|
|
213
|
+
on a link's return. The intent answers 403 `LINKING_DISABLED` when account linking is off. The web app posts the token
|
|
214
|
+
from a hidden form to `POST {basePath}/oauth-handoff/link-start`, which refuses a missing, `null`, or
|
|
215
|
+
foreign `Origin` with 403 (so web origins must not serve `Referrer-Policy: no-referrer` or
|
|
216
|
+
`same-origin`), then uses the token up (400 with no
|
|
217
|
+
redirect when it is unknown, used, or expired), sets only the OAuth state cookie, first-party on the
|
|
218
|
+
API's origin, and redirects to the provider. The provider callback links the account and
|
|
219
|
+
redirects to `callbackURL` with no one-time token, since no session is created. A session
|
|
220
|
+
revoked after the intent comes back with `error=session_expired`. Both routes are in the auth
|
|
221
|
+
rate-limit bucket, and Better Auth's limiter allows three of each per ten seconds per IP.
|
|
222
|
+
- `@terreno/rtk`: new `linkSocialWithHandoff({client, provider, callbackURL, errorCallbackURL?})`.
|
|
223
|
+
On web in bearer mode it posts the intent and submits the hidden form; in cookie mode and on
|
|
224
|
+
native it calls `client.linkSocial`. It resolves `{error}` (`null` once the browser is on its
|
|
225
|
+
way to the provider). New type `BetterAuthLinkClient`.
|
|
226
|
+
- Org-admins can run account recovery on members of their organization. When the app registers `AccountSecurityApp`, `OrgsApp` mounts the security routes at `/orgs/:orgId/members/:userId/security/*` (`TerrenoApp` passes the registered plugin; pass `accountSecurity` to a hand-registered `OrgsApp`). An org-admin may act on any active member of the org, even one in other orgs. Suspended members and non-members answer `404`; platform staff (`superadmin`, `operator`) and `admin: true` targets answer `403`; disabled orgs answer `403`. Platform staff holding `user:manageSecurity` pass the org checks. Every event records the org.
|
|
227
|
+
- New org-only action `suspend-membership` replaces `disable` under the org prefix: it suspends the membership in that org only, so the user still signs in and keeps their other orgs. `disable`, `enable`, and `issue-sign-in-link` stay platform-only. Security route actions can be marked `orgOnly`, mounted only with `mountSecurityRoutes(..., {includeOrgOnly: true})`. `mountSecurityRoutes` also takes `pathPrefix` (such as `/:orgId/members`) so a prefix's params appear in OpenAPI.
|
|
228
|
+
- `OrgsApp` takes `orgRecovery: {minMembershipDays}` (default `0`) to refuse org-admin recovery on members who joined too recently.
|
|
229
|
+
- The recovery routes' "target holds permissions you lack" check resolves each user in their own context: outside any organization on the platform routes, and under each user's own membership in `:orgId` on the org routes (authorizers return it as `orgScope`). A target is never judged by the acting org-admin's membership grants.
|
|
230
|
+
- `OrgsApp` throws `OrgsApp accountSecurity is not registered` when handed an `AccountSecurityApp` that is not registered (`AccountSecurityApp.isRegistered()`), so the org routes never run without the audit check, access control, or user model.
|
|
231
|
+
- `suspend-membership` keeps the last-org-admin rule under concurrency (suspend, recount, undo), so two org-admins suspending each other never leave the org without one.
|
|
232
|
+
- `oauthProxy` on the production server itself (`productionURL` on the `baseURL` origin) now sets oauth-proxy's `currentURL`, so that server's own sign-ins, including server-side starts such as `GET {basePath}/oauth-handoff/start` and requests on another host name, skip the proxy and keep Better Auth's state-cookie check. Its callback still forwards previews' returns.
|
|
233
|
+
- The OAuth one-time-token handoff also runs when oauth-proxy finishes a preview's sign-in (`GET {basePath}/callback/:id/oauth-proxy`, and the deprecated `GET {basePath}/oauth-proxy-callback`), so a bearer-mode web app on a preview gets `#ott=<token>` after the stable deploy forwards the provider's return.
|
|
234
|
+
- example-backend turns on `oauthProxy` from `OAUTH_PROXY_PRODUCTION_URL`, reads the Sign in with Apple key form (`APPLE_CLIENT_ID`, `APPLE_TEAM_ID`, `APPLE_KEY_ID`, `APPLE_PRIVATE_KEY`; `APPLE_CLIENT_SECRET` still works), and trusts `https://appleid.apple.com` when Apple is configured.
|
|
235
|
+
- The example deploy sends every preview's social sign-in through the stable backend's registered callbacks. `STABLE_BACKEND_URL` and `STABLE_FRONTEND_URL` (defaulting to today's Cloud Run and Netlify hosts) set the stable `BETTER_AUTH_URL`, `FRONTEND_URL`, `OAUTH_PROXY_PRODUCTION_URL`, and, when both are subdomains of one domain, `PASSKEY_RP_ID`. Stable mounts the `GOOGLE_*`, `GITHUB_*` and `APPLE_*` secrets that have a version; previews mount only the `*_CLIENT_ID` ones and configure providers with the placeholder secret `OAUTH_PROXY_PREVIEW_CLIENT_SECRET`, since stable exchanges every code. `STABLE_PASSKEY_RP_ID` overrides the derived rpID, terraform creates the secret containers, and the stable web build defaults to the same untagged Cloud Run URL as the deploy. New guide: [Set up social login](docs/how-to/set-up-social-login.md).
|
|
236
|
+
- `OAUTH_CALLBACK_TAG=pr-<n>` makes that example preview own the registered OAuth callbacks: it mounts every provider secret and exchanges codes itself, other previews proxy through it, and stable gets only client IDs. It lets the providers be tried before stable runs a release with this setup. See [Register on a preview first](docs/how-to/set-up-social-login.md#register-on-a-preview-first).
|
|
237
|
+
- New `set-up-social-login` skill (planning plugin): the order of work for Google, GitHub, and Sign in with Apple in a Terreno app, from server config and callback URLs to secrets, preview-first registration, and the `/terreno/methods` check. [Set up social login](docs/how-to/set-up-social-login.md) now covers the whole setup end to end: setting the gcloud project, choosing same-site hosts, the server config, a callback URL table for local dev, previews, and stable, and how to check the result.
|
|
238
|
+
- **Provisional consent signing.** Staff can record consent on a user's behalf with
|
|
239
|
+
`POST /consent-responses/provisional` and a required note. Pass `accessControl` to
|
|
240
|
+
`ConsentApp` and grant the new `consentResponse:signOnBehalf` RBAC action (the built-in
|
|
241
|
+
`admin` role gets it on the next role seed); without RBAC only `user.admin` may sign. A
|
|
242
|
+
provisional signature counts as consent but leaves the form in the user's consent gate,
|
|
243
|
+
which now shows "Signed on your behalf by <name> on <date>". The user's own answer always
|
|
244
|
+
wins.
|
|
245
|
+
- `getConsentStatuses({userId})` (`@terreno/api`) and `GET /consent-responses/status` return
|
|
246
|
+
each form's `pending`, `provisional`, `signed`, or `declined` status. `POST
|
|
247
|
+
/consent-responses/:id/void` voids a provisional signature and keeps the record. With
|
|
248
|
+
`AuditApp`, signing, voiding, and status reads write audit events; audit events gain a
|
|
249
|
+
`read` operation and a `viewed` verb.
|
|
250
|
+
- `ProvisionalConsentSigner` and `useProvisionalConsent` (`@terreno/ui`) list a user's forms,
|
|
251
|
+
sign with a note, print, and void. `printConsentHtml` prints consent HTML on native and web,
|
|
252
|
+
and `buildConsentPdfHtml` renders a "Provisional signature" section.
|
|
253
|
+
- `ConsentResponseViewer` and `ConsentHistory` (`@terreno/admin-frontend`) show provisional
|
|
254
|
+
signatures and gain Print buttons, and their PDFs include the provisional signature.
|
|
255
|
+
- `@terreno/rtk`: `createRememberedAccountsStore({rememberIdentity})` remembers who signed in on
|
|
256
|
+
this device and how, for the login screen's "last used" badge and "Continue as" cards. It stores
|
|
257
|
+
`{userId, method, lastUsedAt}` in SecureStore on native and localStorage on web, keeps the newest
|
|
258
|
+
5, re-reads storage before each change (and follows `storage` events on web) so a stale tab
|
|
259
|
+
cannot bring back a forgotten account, and resets on unreadable data. `rememberIdentity` is
|
|
260
|
+
required: pass `false` to keep only the method. With `true`, the name, a masked email
|
|
261
|
+
(`j***@n***.io`) and an `https://` avatar are stored too, and switching back to `false` scrubs
|
|
262
|
+
them on the next load. `useRememberedAccounts(store)` returns `{accounts, lastUsedMethod,
|
|
263
|
+
isHydrated, forget, clear}`, and `toLoginAccountOptions()` maps accounts to `@terreno/ui`
|
|
264
|
+
`LoginScreen`'s `rememberedAccounts`.
|
|
265
|
+
- `@terreno/rtk`: `createBetterAuthClient({features})` adds the Better Auth client plugins for the
|
|
266
|
+
server's auth features: `twoFactor` (`twoFactorClient`, with `onTwoFactorRedirect`), `passkey`
|
|
267
|
+
(`passkeyClient` from `@better-auth/passkey/client`), `multiSession`, `lastLoginMethod` and
|
|
268
|
+
`oneTimeToken`. Each enabled feature's namespace is typed without casts (`client.twoFactor.enable`,
|
|
269
|
+
`client.passkey.addPasskey`, `client.multiSession.listDeviceSessions`, ...), and the plugins run
|
|
270
|
+
after the Expo plugin on native. Without `features` (or with a config whose type does not name
|
|
271
|
+
them) the client and its type are unchanged. In web bearer mode `multiSession` lists no
|
|
272
|
+
sessions, and 2FA and passkey challenges still need cookies. New types:
|
|
273
|
+
`BetterAuthClientFeatures`, `BetterAuthClientWithFeatures`, `BetterAuthClientFor`,
|
|
274
|
+
`BetterAuthHandoffClient`.
|
|
275
|
+
`BetterAuthOAuthProvider` now accepts any generic OAuth provider id, the OAuth handoff helpers
|
|
276
|
+
accept clients with features, and `BetterAuthClientInterface` accepts the stricter session types
|
|
277
|
+
those clients return, so they work with `generateBetterAuthSlice`, which now stores a missing
|
|
278
|
+
user `image` as `null`.
|
|
279
|
+
- `AccountSecurityApp` emails the affected user about every security action, admin and
|
|
280
|
+
self-service.
|
|
281
|
+
- Each email says who acted ("you" or "an administrator", never the admin's name), when, and how
|
|
282
|
+
to get help.
|
|
283
|
+
- It names the org only with `notifications.includeOrgName: true` (default `false`), and never
|
|
284
|
+
includes tokens, links, passkey names, or other secrets.
|
|
285
|
+
- Mail goes out only when `sendMail` is configured. `notifications: {enabled: false}` turns it
|
|
286
|
+
off, and `notifications.sendMail` supplies a mailer outside `TerrenoApp`.
|
|
287
|
+
- `notifications.throttleMinutes` (default 10) sends at most one email per user, template,
|
|
288
|
+
origin (self or admin), and org per window, so an admin action is never hidden behind the
|
|
289
|
+
user's own change.
|
|
290
|
+
- A failed delivery is logged and never fails the action.
|
|
291
|
+
- `send-password-reset` sends no extra email, because its reset mail is the notice.
|
|
292
|
+
`issue-sign-in-link` tells the user a link was created, without the link.
|
|
293
|
+
- The newest registered `AccountSecurityApp` owns the emails, so two apps in one process never
|
|
294
|
+
email twice. `unsubscribeNotifications()` stops them.
|
|
295
|
+
- `createBetterAuth` records the user's own security changes as security actions with
|
|
296
|
+
`origin: "self"`, written as `AuditEvent`s when `AuditApp` is registered.
|
|
297
|
+
- Recorded: a provider linked or unlinked, a passkey added or removed, two-factor turned on or
|
|
298
|
+
off, a password changed or reset with a link, and other sessions signed out.
|
|
299
|
+
- Two-factor changes are read from the database, not the cookie-cached session.
|
|
300
|
+
- The new action ids are `password-reset` and `other-sessions-revoked`.
|
|
301
|
+
- The emails render with a built-in renderer. `BetterAuthConfig.renderAuthMail` is not used for
|
|
302
|
+
them; opt in to a custom renderer with `notifications.render`. `BetterAuthConfig.authMailTemplates`
|
|
303
|
+
accepts the new `security*` ids as overrides.
|
|
304
|
+
- `@terreno/comms` `renderAuthMail` renders the `security*` template ids (subject, text, and HTML),
|
|
305
|
+
so it can be passed as `notifications.render`. It takes optional `data`, `token` is now optional,
|
|
306
|
+
and HTML bodies get HTML-escaped values.
|
|
307
|
+
- `@terreno/rtk` adds `TerrenoAccountSecurityScreen`, a drop-in account security screen, and `useAccountSecurity`, the hook behind it. It renders `@terreno/ui`'s `AccountSecurityScreen` with the cards the server's `/terreno/methods` turns on: linked providers (linking through the OAuth handoff in bearer mode), the password (change, or set a first one), passkeys (web), two-factor (enrolment returns the backup codes to the card), and signed-in sessions ("Sign out everywhere else" calls `revokeOtherSessions`). Every Better Auth `{error}` becomes the calling card's error, each change reloads only the affected section, the screen never asks the server to remove a user's only sign-in method, `SESSION_NOT_FRESH` reads "Sign in again to manage this.", and a credential row without a password switches the password card to set mode.
|
|
308
|
+
- `useAuthMethods` returns `refetch()` for a Try again button and takes `{skip: true}` for callers that already have the methods.
|
|
309
|
+
- A link's `?error=`, an error code, or a provider id that matches an `Object.prototype` key (`__proto__`, `constructor`, `toString`) reads as the generic link error or goes unlabelled, instead of crashing the screen with a non-string value.
|
|
310
|
+
- `@terreno/rtk` adds `TerrenoLoginScreen`, a drop-in login screen, and `useLoginController`, the hook behind it. They read the server's `/terreno/methods` and skip the screen when a session is live. They offer "Continue as" cards for this device's signed-in sessions (`multiSession`, not in bearer mode) and its remembered accounts, a session card hiding the same user's remembered card. They badge the last used method from the remembered-accounts store, offer passkeys (button and web autofill), and sign in socially through the OAuth handoff in bearer mode or `signIn.social` otherwise. The two-factor step runs inline unless `onTwoFactorRequired` routes to the app's own screen. Each finished sign-in is recorded in the remembered-accounts store.
|
|
311
|
+
- `getLoginFeedback` turns server refusals into login guidance: `account_not_linked` worded for the linking mode, `VERIFY_EMAIL_TO_USE_ACCOUNT` as "verify your email", and `signup_disabled` (and the ID-token "signup disabled") as "ask your administrator for access", with the sign-up link hidden. Text from a URL is never shown.
|
|
312
|
+
- `getBetterAuthClientWebAuth(client)` reports whether a client is in web bearer mode and the token it holds. `recordRememberedSignIn` records a sign-in finished on an app's own screen.
|
|
313
|
+
- `@terreno/ui` `LoginScreen` takes `initialValues`, which prefill form fields; a new object later replaces those fields and keeps what the user typed in the others. With `fields={[]}` it leaves out the form block (fields, Log In, forgot-password, and the email dividers), and `TerrenoLoginScreen` does that when the server has email and password off. `OAuthButtons` takes `dividerPosition: "none"`.
|
|
314
|
+
- `TerrenoLoginScreen` takes `onStepChange`, called with each step the screen moves to, so an app that already holds a session can leave a login route the screen could not skip.
|
|
315
|
+
- A remembered method or a provider id that matches an `Object.prototype` key (`__proto__`, `constructor`) is named from the id ("Proto", "Constructor") instead of an inherited value.
|
|
316
|
+
- `@terreno/ui` adds `LinkedAccountsCard` and `PasswordCard`, the first account security cards.
|
|
317
|
+
`LinkedAccountsCard` shows one row per linked account (so two Google accounts are each
|
|
318
|
+
unlinkable) with its email and linked date, and a Link row for each offered provider without
|
|
319
|
+
one. Unlink asks for confirmation and calls `onUnlink(providerId, account)`; pass `account.id`
|
|
320
|
+
(Better Auth's account row id) to `unlinkAccount({accountId})`. The acting row shows a spinner
|
|
321
|
+
while `onLink` or `onUnlink` runs, and Unlink is disabled with an explanation when it would
|
|
322
|
+
remove the user's last sign-in method (the `credential` password account and
|
|
323
|
+
`otherMethodCount` count; `canUnlinkLast` opts out). `PasswordCard` shows "Change password"
|
|
324
|
+
(current, new, confirm, optional "Sign out of other devices" sent as `revokeOtherSessions`) or
|
|
325
|
+
"Set a password" for provider-only users, with inline length and confirmation errors, focus on
|
|
326
|
+
the first invalid field plus an alert summary, Enter to submit, and a promise-based `onSubmit`.
|
|
327
|
+
Also exported: the `LinkedAccount` (`linkedAt` accepts a `Date` or ISO string),
|
|
328
|
+
`LinkableProvider` and `PasswordCardSubmission` types, `CREDENTIAL_PROVIDER_ID`, and
|
|
329
|
+
`hasPasswordCredential(accounts)`.
|
|
330
|
+
A provider id that matches an `Object.prototype` key (`__proto__`, `constructor`) is labelled
|
|
331
|
+
with its id instead of an inherited value.
|
|
332
|
+
- `@terreno/ui` adds `AccountSecurityScreen`, a security settings screen composed from the account
|
|
333
|
+
security cards. Pass each card's props as one object (`linkedAccounts`, `password`, `passkeys`,
|
|
334
|
+
`twoFactor`, `sessions`); a card renders only when its object is given, so a feature the server
|
|
335
|
+
has off hides its card. Cards appear in a fixed order (sign-in methods, then passkeys, two-factor
|
|
336
|
+
and sessions) in one centered column up to `maxWidth` (720px by default), under a "Security"
|
|
337
|
+
heading with optional `description` and `header`. `isLoading` shows a spinner, `error` shows a
|
|
338
|
+
page-level alert with Try again (`onRetry`), and `children` add app-specific content after the
|
|
339
|
+
cards. The screen wraps itself in a scrolling `Page`; pass `withPage={false}` to place it inside
|
|
340
|
+
your own `Page` or `ScrollView`. Also exported: the `AccountSecurityScreenProps` type.
|
|
341
|
+
|
|
342
|
+
`Heading` gains an optional `level` (1–6): with it, the heading renders `role="heading"` and
|
|
343
|
+
`aria-level` (an `<h1>`–`<h6>` on web, the header accessibility role on native); without it,
|
|
344
|
+
output is unchanged. The account security cards' titles are now level 2 headings and the screen
|
|
345
|
+
title level 1. Below the desktop floor (`isSupportedDesktopWidth`), `Card` and
|
|
346
|
+
`AccountSecurityScreen` draw edge to edge.
|
|
347
|
+
- `@terreno/ui` adds `FormContainer`: a real `<form>` on web (`display: contents`, `noValidate`, page reload always prevented) and a `Box` on native. `ref.requestSubmit()` submits through the form element on web so password managers see a real submit and offer "Save password"; `username` adds a hidden `autocomplete="username"` input for change-password forms. Chrome no longer logs "Password field is not contained in a form" on `LoginScreen`, `SignUpScreen`, `PasswordCard`, the `TwoFactorSettingsCard` password step, `GPTChat`'s API key modal, the admin configuration screen, or the example app's reset-password, profile, and admin password forms.
|
|
348
|
+
- `TextField` sends `autoComplete` to native too: React Native's `autoComplete` (Android's autofill hint) and the matching iOS `textContentType` (`username`, `emailAddress`, `password`, `newPassword`, `oneTimeCode`, `none`), so iOS and Android offer saved passwords, strong new passwords, and one-time codes. It also accepts `"one-time-code"`.
|
|
349
|
+
- `LoginScreen` and `SignUpScreen` default the email field to `autoComplete="username"`, name each input by its field label, and handle Enter: it moves to the next field and submits from the last one once, only when the submit button is enabled. `PasswordCard` now moves from current to new to confirm on Enter and submits from confirm. `OtpField` gains `onEnter`, and `TwoFactorChallengeScreen` submits a complete code on Enter.
|
|
350
|
+
- `PasswordCard` and `TwoFactorSettingsCard` take a `username`, which `useAccountSecurity` fills with the session user's email.
|
|
351
|
+
- `LoginScreen` (`@terreno/ui`) gains optional props for returning users: `lastUsedMethod` badges the
|
|
352
|
+
method this device used last and moves a provider or passkey above the form; `currentSessions` and
|
|
353
|
+
`rememberedAccounts` render one-tap "Continue" cards (`onContinue`, `onForget`); `onPasskeyPress`
|
|
354
|
+
adds a "Sign in with a passkey" button and `passkeyAutofill` asks web browsers to offer saved
|
|
355
|
+
passkeys in the email field; `notice` shows guidance such as an `account_not_linked` hint; and
|
|
356
|
+
`genericProviders` renders providers without a branded button (for example a generic OIDC
|
|
357
|
+
provider). `OAuthButtons` accepts `genericProviders`, `lastUsed`, and `dividerPosition`. Without the
|
|
358
|
+
new props the screen renders exactly as before. A method or provider id that matches an
|
|
359
|
+
`Object.prototype` key (`__proto__`, `constructor`) is named by its id, and the new
|
|
360
|
+
`getSocialProviderName(providerId)` returns a branded provider's name or `undefined`.
|
|
361
|
+
- `@terreno/ui` adds `PasskeysCard` and `SessionsCard` to the account security cards.
|
|
362
|
+
`PasskeysCard` lists passkeys with their added and last-used dates and a "Synced" badge for
|
|
363
|
+
backed-up ones (an unnamed passkey shows `authenticatorName`, for example from
|
|
364
|
+
`getAuthenticatorName(aaguid)`, else "Unnamed passkey"), and adds (with an optional name, `onAdd(name?)`), renames (`onRename(id, name)`)
|
|
365
|
+
and removes (confirmed, `onRemove(passkey)`) them; `supported={false}` hides Add and explains that
|
|
366
|
+
the device can't create passkeys. `SessionsCard` lists sessions with the browser and OS parsed
|
|
367
|
+
from the user agent, location and IP when known, "Signed in" (`createdAt`) and "Last refreshed"
|
|
368
|
+
(`updatedAt`, which Better Auth moves only once per `session.updateAge`) as Luxon relative times
|
|
369
|
+
that recompute every minute (inject `now` for deterministic output). The current session, matched by `currentSessionId` or
|
|
370
|
+
`currentSessionToken`, comes first, is marked "This device", and can't be revoked on its own;
|
|
371
|
+
other sessions have Sign out (`onRevoke(session)`), and "Sign out everywhere else"
|
|
372
|
+
(`onRevokeOthers()`) asks for confirmation and is disabled when no other session exists. Better
|
|
373
|
+
Auth's `listUserPasskeys()` and `listSessions()` items fit the new `AccountPasskey` and
|
|
374
|
+
`AccountSession` types as is. Also exported: `parseUserAgent` and `describeUserAgent`, a small
|
|
375
|
+
dependency-free user agent parser that also recognizes React Native app clients. The account
|
|
376
|
+
security docs and examples now unwrap Better Auth's `{data, error}` results and throw, since the
|
|
377
|
+
client does not reject and the cards only show errors from rejected callbacks.
|
|
378
|
+
- `@terreno/ui` adds `OtpField` and `TwoFactorChallengeScreen`. `OtpField` is a one-time code input
|
|
379
|
+
with a cell per digit: paste or one-time-code autofill fills every cell, backspace steps back a
|
|
380
|
+
cell, each cell is labelled "Digit k of N", and `onComplete(code)` fires when the code is full.
|
|
381
|
+
`TwoFactorChallengeScreen` is the second sign-in step: an authenticator code that submits itself
|
|
382
|
+
once complete, a toggle to a backup code, an optional "Don't ask again on this device for N days"
|
|
383
|
+
checkbox (`trustDeviceDays`), an alert error that clears the code, and
|
|
384
|
+
`onSubmit({code, kind: "totp" | "backup", trustDevice})`. `TextField` accepts
|
|
385
|
+
`accessibilityLabel` to set its input's accessible name.
|
|
386
|
+
- `@terreno/ui` adds `TwoFactorSettingsCard` to the account security cards. Turning on two-factor
|
|
387
|
+
authentication walks through four steps: confirm the password (`onStartEnroll(password)` resolves
|
|
388
|
+
`{totpURI, backupCodes}`, narrowed from the TOTP branch of Better Auth's `twoFactor.enable()`
|
|
389
|
+
data, which is `{method: "otp"}` or `{method: "totp", totpURI, backupCodes}`), scan a QR code drawn with
|
|
390
|
+
`react-native-qrcode-svg` or copy the setup key under it, enter a code in `OtpField`
|
|
391
|
+
(`onVerify(code)`), then save the backup codes with Copy and Download (a `.txt` file on web) or
|
|
392
|
+
Share (native), with Finish gated on "I saved these backup codes". Once on, the card offers new
|
|
393
|
+
backup codes (`onRegenerateBackupCodes(password)`, shown with the same gate) and Turn off
|
|
394
|
+
(`onDisable(password)`, after a destructive confirm), and warns when `backupCodesRemaining` drops
|
|
395
|
+
below 3. Better Auth requires the password unless its two-factor plugin sets `allowPasswordless`,
|
|
396
|
+
so `hasPassword={false}` disables the card's actions and asks an OAuth-only user to set a
|
|
397
|
+
password first, and
|
|
398
|
+
`requirePassword={false}` skips the password steps on a passwordless server. Also exported:
|
|
399
|
+
`parseTotpSecret(totpURI)`, `parseTotpDigits(totpURI)` (the code length defaults to it) and the
|
|
400
|
+
`TwoFactorEnrollment` type. Step changes move focus to the step on web and are announced on native.
|
|
401
|
+
|
|
402
|
+
### Changed
|
|
403
|
+
|
|
404
|
+
- `@terreno/admin-frontend` renders a nested (non-array) object field that has `items` in its admin config as a titled group with one control per subfield, instead of a JSON text box. Groups recurse to any depth and also render inside array-of-subdocument items. `Mixed`, `Map`, and objects without `items` keep the JSON editor, and a field `widget` still takes precedence. Subfield test IDs use the dotted path (`admin-field-tasks.enabled`, group `admin-field-group-tasks`); inside array items, nested groups are prefixed with the array key and item index (`admin-field-servers.0.media.port`). New array items and create-mode nested objects start from subfield defaults and leave out system fields and enums, refs, dates, and `Mixed` subfields that have no default. Save still sends the whole object for the field.
|
|
405
|
+
- The example app's login route is now `TerrenoLoginScreen` with `rememberIdentity: true`. It shows a badge on the last used method and "Continue as" cards after sign-out, offers a passkey button on web, and runs the two-factor challenge inline. Its Better Auth client turns on the `multiSession`, `passkey` and `twoFactor` features. The mock OIDC provider's button is now the login screen's generic provider button, `login-screen-oauth-mock-oidc`, which replaces `login-mock-oidc-button`. The root layout leaves a signed-in user on `/login`, so the screen can record a returning social sign-in before it moves on. The example's `logout` stops syncdb before it dispatches rtk's `logout()`, and the profile screen shows the sign-out in progress on its button, and the error when a sign-out could not reach the server. The root layout shows its loading spinner only while no user is loaded, so a session refresh or a sign-out keeps the current screen.
|
|
406
|
+
- e2e: `loginAs` answers the two-factor challenge for a user with a `totpSecret`. `crossSiteChallenges.spec.ts` now runs (its `test.fixme` is removed). New cross-site specs `crossSiteLogin.spec.ts`, `two-factor.spec.ts` and `account-linking.spec.ts` cover the last used method, "Continue as", passkey sign-in, TOTP and backup codes, and email and mock-OAuth sign-ins that end up in one account. `E2E_ACCOUNT_LINKING=explicit` starts the backend in explicit linking mode for that spec's explicit-mode case.
|
|
407
|
+
- The example app has a Security screen at `/settings/security` (`TerrenoAccountSecurityScreen`), opened from the profile's **Security** button (`profile-security-link`). It links and unlinks providers, changes the password through Better Auth's `changePassword`, adds and removes passkeys, turns two-factor on with backup codes, and signs out other sessions. The profile's "New Password" field, which sent `password` to `PATCH /auth/me` (refused since 57.17.0), is gone; the "Current password" field an email change needs stays.
|
|
408
|
+
- e2e: `account-security.spec.ts` joins the cross-site bearer run. It links the mock OIDC provider and unlinks it (the password still signs in), turns on two-factor and signs in with a saved backup code, adds and removes a passkey through the virtual authenticator, signs out a second browser's session (which then gets 401), and changes the password (the old one is refused, the new one signs in). `profile.spec.ts` opens Security from the profile in the cookie-mode run.
|
|
409
|
+
- The hosted Terreno MCP server moves to `https://mcp.terreno.app/mcp`. The README, docs, and `create-terreno-app` now use it. `https://mcp.terreno.flourish.health/mcp` keeps serving for at least one more release; update `.cursor/mcp.json`, `.mcp.json`, and `claude mcp add` registrations as described in `docs/how-to/set-up-terreno-mcp.md`.
|
|
410
|
+
- Card, MarkdownEditor, the admin shell, and the admin filter drawer switch to the narrow layout below the desktop floor (1024pt) instead of 768pt. Admin home and the example sync debugger stack their side-by-side panes below that floor. Demo headers and stories read the live window width instead of a one-time viewport check. `isSupportedDesktopWidth` reports that floor. `ADMIN_FILTER_MOBILE_BREAKPOINT` is 1024.
|
|
411
|
+
- `useLoginController` maps a cancelled sign-in (`AUTH_CANCELLED`, or a message of "auth cancelled"
|
|
412
|
+
/ "authentication cancelled") to an info notice. A wrong email or password
|
|
413
|
+
(`INVALID_EMAIL_OR_PASSWORD`, `INVALID_PASSWORD`, `INVALID_EMAIL`, or those messages on a response)
|
|
414
|
+
is `passwordError` on the login screen instead of the error banner. Continuing a remembered email
|
|
415
|
+
account that has a usable address sets `continuingAccount` and opens the password step.
|
|
416
|
+
Canceling that step, or canceling two-factor, clears `continuingAccount` and sets the form
|
|
417
|
+
password back to empty so the next account does not keep the previous password.
|
|
418
|
+
`account_not_linked` notices name the original method and, when that method is known, offer
|
|
419
|
+
"Sign in with {method}". The two-factor failure message tells the user to enter the code their
|
|
420
|
+
app shows now. `TerrenoLoginScreen` passes the email that started the challenge as
|
|
421
|
+
`accountLabel`. Pass `initialTrustDevice={false}` to start "Trust this device" unchecked;
|
|
422
|
+
otherwise the inline challenge keeps the authenticator default (checked).
|
|
423
|
+
- `@terreno/rtk` `generateBetterAuthSlice`: `logout()` no longer clears the session before the server answers. The session stays in the state until `authClient.signOut()` finishes (`signOutTimeoutMs`, default 10 seconds), and then it is cleared. Meanwhile the new `isSigningOut` state (`selectBetterAuthIsSigningOut`) is true; `isLoading` is left alone, so a navigator gated on it keeps its screen. Before, a login screen that opened at once could find the still-live session and sign the user straight back in. A sign-out that fails or times out checks the session first. A session that is still live, or a cookie or native session the server cannot be asked about, stays signed in, and `error` is set to `SIGN_OUT_FAILED_MESSAGE` ("Couldn't sign out. Check your connection and try again."). A web bearer client, which drops its token before the request leaves, ends signed out. The global `auth/logout` action (a refused session) now ends signed out even when the sign-out request fails. With `apisToReset` (57.17.0), `logout()` purges the cache when the session clears, not when it is dispatched: a purge at dispatch refetched every mounted screen mid-sign-out, and a refetch that could not reach the server answered 401 and signed the user out even though the sign-out failed. A failed sign-out keeps both the session and the cache. The global `auth/logout` action still purges at once (only the first of several in flight) and again when the session clears. A sign-in for another user that lands while a sign-out is in flight is kept; a sign-out that started signed out asks `getSession` first, so a restore of the session it revoked is cleared. Show `selectBetterAuthIsSigningOut` on the sign-out control and `selectBetterAuthError` after a failed sign-out.
|
|
424
|
+
- `@terreno/ui`: `SyncStatusBanner` no longer flashes "Offline" while the sync socket first connects or during brief drops. The badge appears only after `isOnline` stays false for `offlineGraceMs` (default 3000 ms) and hides as soon as the client reconnects. Pass `offlineGraceMs={0}` to show it immediately, as before.
|
|
425
|
+
- `LoginScreen` matches the login redesign. Method pills sit above the email form. Returning users
|
|
426
|
+
see a continue-as list, with other methods behind "Sign in another way". Edit removes a remembered
|
|
427
|
+
account from this device; a signed-in session cannot be removed there. A remembered email opens a
|
|
428
|
+
password step (`continuingAccount`). A wrong password is `passwordError` on the password field.
|
|
429
|
+
`error` is a banner, and one `notice` (info, warning, or error) replaces it. The submit button
|
|
430
|
+
reads "Sign in". `TwoFactorChallengeScreen` titles the authenticator step "Enter your code" and
|
|
431
|
+
the backup step "Use a backup code", checks "Trust this device" by default for an authenticator
|
|
432
|
+
code (`initialTrustDevice={false}` starts it unchecked), and shows the `error` string it was
|
|
433
|
+
given. `OAuthButtons` `appearance="pill"` and
|
|
434
|
+
`SocialLoginButton` `variant="outline"` render the same neutral pill.
|
|
435
|
+
|
|
436
|
+
### Deprecated
|
|
437
|
+
|
|
438
|
+
- `installTerrenoDevConsoleLogger` moves to `@terreno/syncdb` (also `@terreno/syncdb/devConsoleLogger`) and takes `{baseUrl}`. The `@terreno/rtk` export is a deprecated wrapper that keeps working with no arguments.
|
|
439
|
+
- `terreno-mcp-local` adds `get_client_state`, which inspects either the RTK Query store or a SyncDB client and returns a `layer` field (`rtk`, `syncdb`, `syncdb+rtk`, or `none`). `get_rtk_state` is now a deprecated alias for one release and returns the same wrapped response. `terreno state` uses the new tool.
|
|
440
|
+
|
|
441
|
+
### Fixed
|
|
442
|
+
|
|
443
|
+
- `@terreno/admin-frontend`: an admin record no longer stays on its loading spinner when the organization switcher settles on a default organization after the record's request started. `useAdminApi`'s `useListQuery` and `useReadQuery` now carry the organization in the query argument, so the switch fetches the new organization's entry instead of waiting on one that was never requested. A read-only user record's `roles` field also no longer requests `GET /rbac/roles`, which staff without `rbac:read` (such as an operator viewing a user) were refused with 403; it lists the assigned role names instead.
|
|
444
|
+
- The admin user security header clears the previous user's name and email while the next user's record loads, so a recovery action is not confirmed under the wrong identity.
|
|
445
|
+
- `@terreno/api` answers a malformed JSON request body with 400 `Malformed request body` instead of 500 `Internal server error`. `apiErrorMiddleware` now converts body-parser client errors with the new `bodyParserErrorToAPIError` helper and keeps the parser's status: an oversized body returns 413 `Request body too large`, and an unsupported charset returns 415 `Unsupported request body encoding`. The parser's message is in `detail`. These errors are logged at `warn` and are not sent to Sentry.
|
|
446
|
+
- `@terreno/api`: a Better Auth password reset no longer gives a social-only account a JWT password. `onPasswordReset` now copies the new password to the app User's passport hash only when that User already has one (the same rule as `terreno/set-password` and `change-password`), so an account that signed in only with a provider cannot then sign in at `POST /auth/login`, which has no second factor. A dual-enrolled account still gets its JWT password replaced, and every matching app User's `tokenEpoch` is still bumped, so outstanding JWT refresh tokens and MCP credentials stop working after any reset.
|
|
447
|
+
- Terreno's Better Auth password reset sync to the app User's JWT password now also reads the new password from the parsed body of the Better Auth endpoint being served, so direct `auth.api.resetPassword` calls and the email-OTP and phone-number resets (which send `password`) update the passport hash and bump `tokenEpoch` the same way `POST {basePath}/reset-password` does.
|
|
448
|
+
- OpenAPI specs and model metadata from `@terreno/api` no longer mark a field as required when
|
|
449
|
+
its Mongoose `required` is a function. Such a field is only required on some documents, so
|
|
450
|
+
generated SDKs now type it as optional (for example `ConsentResponse.provisionalNote` and
|
|
451
|
+
`GptHistory.text`). Saving still enforces the condition.
|
|
452
|
+
- Apps generated by `create-terreno-app` now send web console errors and warnings to the backend, so `read_logs` and `last_error` with the `browser` source see them. The generated `frontend/app/_layout.tsx` calls `installTerrenoDevConsoleLogger({baseUrl})` from `@terreno/syncdb` at startup, and the generated `backend/.env` sets `NODE_ENV=development` so `TerrenoApp` registers `POST /__terreno/browser-logs`. In an existing app, add both lines; see `docs/how-to/set-up-terreno-mcp.md`.
|
|
453
|
+
- `create-terreno-app` MCP configs now start `terreno-mcp-local` with `bunx -p @terreno/mcp terreno-mcp-local`. The old `bunx terreno-mcp-local` entry failed in generated apps because no npm package has that name. Generated apps also get a root `.mcp.json` for Claude Code with the same servers as `.cursor/mcp.json`. In an existing app, add `"-p", "@terreno/mcp"` before `"terreno-mcp-local"` in the `terreno-local` args; see `docs/how-to/set-up-terreno-mcp.md`.
|
|
454
|
+
- `installTerrenoDevConsoleLogger` no longer logs `Requiring unknown module "<id>"` on the first lazy component load on web (for example `ConsentNavigator` after login). Metro reports the missing split-bundle module to `ErrorUtils` while Expo's web `import()` probes it, and Expo then catches the rethrown error and loads the bundle. The logger now records an `ErrorUtils` error only after the previous handler consumes it. An error that is rethrown and never caught is still logged by the window listeners, and unhandled rejections now include the `Error` stack.
|
|
455
|
+
- `@terreno/api`: with `BetterAuthApp` registered, the JWT recovery routes no longer give an account without a JWT password one. `POST /auth/forgotPassword` mails no legacy reset link to such an account (it still answers 202, so the response does not reveal the account), and `POST /auth/resetPassword` (and the `POST /resetPassword` alias) refuses its token with 400 `legacy-password-reset-unavailable` and issues no tokens. Before, a social-only account could reset through these routes and then sign in at `POST /auth/login`, which has no second factor. That account resets through Better Auth instead. Accounts that already have a JWT password reset here as before, and apps without Better Auth are unchanged.
|
|
456
|
+
- `terreno_bootstrap_ai_rules` now returns files that `rulesync generate` accepts with rulesync 9. `rulesync.jsonc` and the root rule target `cursor`, `claudecode`, `copilot`, and `agentsmd` (rulesync 9 has no `windsurf` target; Windsurf reads `AGENTS.md`). The single `.rulesync/rules/00-root.md` root rule now covers Claude Code, so the tool no longer returns the `01-claudecode-root.md` `localRoot` rule that rulesync rejected; delete it if an earlier bootstrap left it behind. The instructions run `bunx rulesync@9 generate` instead of the removed `rulesync sync`. The root rule now includes everything `create-terreno-app` writes to `CLAUDE.md` and `.cursorrules` (including the "Adding Features" steps), and the instructions say to delete the superseded `.cursorrules` and to move hand edits to `CLAUDE.md`, `AGENTS.md`, `.claude/rules/`, `.cursor/rules/`, and `.github/instructions/` into `.rulesync/rules/10-project.md` before generating, because `rulesync generate` overwrites or deletes them.
|
|
457
|
+
- The MCP server's `terreno_bootstrap` frontend template drops the profile screen's "New Password" field. It sent `password` to `PATCH /auth/me`, which refuses password fields since 57.17.0; change passwords through Better Auth (`changePassword`, or `TerrenoAccountSecurityScreen` from `@terreno/rtk`). The "Current password" field that an email change needs stays.
|
|
458
|
+
- `terreno_generate_form_fields` now emits code that compiles against `@terreno/ui`: fields use `title`, `onChange`, and `errorText` (not `label`, `onChangeText`/`onChangeValue`, `error`), dates use `type` (not `mode`), `NumberField` gets `type="number"` and a string value, passwords use `TextField type="password"`, and required fields declare the `errors` state they read. A test type-checks the output against the `@terreno/ui` declarations so prop drift fails CI. The `terreno://docs/ui` form-field example uses the same props.
|
|
459
|
+
- `terreno_validate_model_schema` no longer reports "Missing TypeScript interfaces" when the model imports its `Document` type from `src/types/models/` or `src/modelInterfaces.ts`; it still reports a model with no `Document` type.
|
|
460
|
+
- The hosted Terreno MCP image now builds the full `@terreno/ui` props data and the versioned docs search corpus, so `terreno_get_component_docs` knows every component and `terreno_search_docs` searches versioned docs instead of only the bundled resources. The `terreno://docs/patterns` resource now shows SyncDB collection CRUD instead of the deprecated RTK pattern.
|
|
461
|
+
- `terreno-mcp-local` CDP tools (`read_logs` source `app`, `last_error`, `get_client_state`, `evaluate`, `navigate`) connect to Metro again on Expo 57. The debugger socket now sends the `Origin` header React Native dev-middleware requires, and when another debugger such as React Native DevTools takes the session the status names Metro's close reason.
|
|
462
|
+
- `terreno-mcp-local` `database_schema` and `database_query` work under Bun again. They previously failed with `undefined is not an object (evaluating 'mongoose.connection.readyState')` whenever a Mongo URI was found; each call now uses its own short-lived connection.
|
|
463
|
+
- `GET /oauth/authorize` accepts a loopback `redirect_uri` (`localhost`, `127.0.0.1`, or `[::1]`) on any port when a registered URI has the same scheme, host, path, and query (RFC 8252 §7.3). The authorization code is returned to the port the client sent. Those three hosts do not match each other, and every other redirect URI must still match exactly.
|
|
464
|
+
- Prevented modelRouter array-item PATCH requests with nested subdocument arrays from failing after the first update.
|
|
465
|
+
- The example app's native (Android) sign-in now works with the local mock OIDC provider: Better
|
|
466
|
+
Auth's Expo authorization proxy refuses plain-http provider URLs, so example-backend serves that
|
|
467
|
+
proxy for the mock issuer's origin itself while `MOCK_OIDC_ISSUER` is set. The mock provider takes
|
|
468
|
+
`MOCK_OIDC_PORT`, `MOCK_OIDC_ISSUER` and `MOCK_OIDC_REDIRECT_URI`, and
|
|
469
|
+
`example-frontend/maestro/native-social-login.yaml` drives the sign-in on an emulator. See the new
|
|
470
|
+
"Native" section of `docs/how-to/set-up-social-login.md`.
|
|
471
|
+
- The `operator` role can find users in the admin panel and open a user's record, and from there their Security screen, in apps that declare an `adminUser` statement (as the example app does). Seeding gives the operator `adminUser:read` (never `write`), and an operator row seeded earlier gains it once through `appliedGrants`. Before this fix, `/admin/config` left User out for operators and `/admin/User/:id` showed a spinner that never finished.
|
|
472
|
+
- `@terreno/api`: `OrgsApp` now clears a user's cached permissions when their membership is added, changed, or removed through `POST` / `PATCH` / `DELETE /orgs/:id/members`. Before, a newly attached org-admin could be refused the admin page (no `admin:access`) for up to the 30-second permission cache TTL, and a demoted, suspended, or removed org-admin kept it as long. Disabling (`PATCH /orgs/:id` with `disabled: true`) or deleting (`DELETE /orgs/:id`) an organization, which suspends all of its memberships, now clears the cached permissions of every member too, so the organization's org-admins lose `admin:access` on their next request.
|
|
473
|
+
- A server bundled with `bun build --compile` no longer crashes at startup with "tsyringe requires a reflect polyfill". `@terreno/api` now depends on `reflect-metadata` and loads it before the passkey plugin, whose `@peculiar/x509` dependency needs it.
|
|
474
|
+
- `@terreno/api`: the account-security reach check (an org-admin or platform operator acting on another user) and the RBAC user-write guard no longer let an actor through because a permission source was down. A failed source drops its grants from the target's resolved permissions, which made the target look less privileged than it is. Both now resolve the target with the new `access.getPermissions({user, requireComplete: true})`, which answers 503 `Permission source unavailable` when a failed source's grants are unknown (grants served from a `"use-stale"` cache still count).
|
|
475
|
+
- `@terreno/api`: the RoleManager target-privilege rule (`access.roles.assign`, `unassign`, `previewAssignment`, and `assertCanModifyUser`, used by `PUT /rbac/users/:id/roles`, `POST /rbac/users/:id/roles/preview`, and `@terreno/admin-backend` user writes) now resolves the target user with `requireComplete`. Before, when an additive permission source was down, the target's grants from it were dropped, so the target looked less privileged and an actor without those permissions could change its roles. The check now answers 503 `Permission source unavailable` in that case. `createRoleManager` takes an optional `getTargetPermissions` for this.
|
|
476
|
+
- `SocialLoginButton` now follows later changes to its `loading` prop instead of reading it only on
|
|
477
|
+
the first render, so a parent can start or clear the spinner after mount.
|
|
478
|
+
- `@terreno/api`: a recovery action that changes something (such as `suspend-membership` under `/orgs/:orgId/members/:userId/security`) now clears the target's cached permissions, so a suspended org-admin loses `admin:access` at once instead of keeping it for up to the 30-second permission cache TTL.
|
|
479
|
+
- @terreno/syncdb no longer sends a stream discovery (`GET /sync/streams`) that a `stop()` overtook while it was reading the token. An app that stops syncdb and then signs out (as the example's `logout` does) used to see that request go out with the revoked session and fail with a 401. `createHttpChannel`'s `fetchStreams` takes an optional `{isSuperseded}` checked before the request is sent; a superseded request rejects with the new exported `RequestSupersededError` and reaches nothing.
|
|
480
|
+
- `@terreno/ui`: `TextField` now sets its `autoComplete` prop as the input's `autocomplete`
|
|
481
|
+
attribute on web (it was ignored before). Native rendering is unchanged. The prop also accepts
|
|
482
|
+
`"email"`, `"new-password"`, and `"username webauthn"` (passkey autofill).
|
|
483
|
+
- Because the hint now reaches the browser, `autoComplete="off"` takes effect on web and turns
|
|
484
|
+
autofill off for that field. The example app, `create-terreno-app`, and the MCP bootstrap
|
|
485
|
+
templates switch their email fields from `"off"` to `"username"` (login) and `"email"`
|
|
486
|
+
(sign-up).
|
|
487
|
+
- `Field type="password"` still defaults to `"current-password"`; pass `autoComplete="new-password"`
|
|
488
|
+
(also through `TapToEdit`) on fields that set a new password. The example admin set-password
|
|
489
|
+
fields now do.
|
|
490
|
+
`SignUpScreen` password fields default to `"new-password"` and `LoginScreen` password fields to
|
|
491
|
+
`"current-password"` unless the field config sets its own.
|
|
492
|
+
- `LoginScreen` names each email and password input by its field label for screen readers,
|
|
493
|
+
instead of the generic "Text input field".
|
|
494
|
+
- `Avatar` takes keyboard focus only when it is editable (`status="imagePicker"`), so a display
|
|
495
|
+
avatar, such as the one on a remembered-account card, is no longer an empty tab stop.
|
|
496
|
+
|
|
497
|
+
## [57.19.2] - 2026-10-26
|
|
498
|
+
|
|
499
|
+
Upgrade note: [`mcp-server/src/docs/upgrades/57.19.2.md`](mcp-server/src/docs/upgrades/57.19.2.md).
|
|
500
|
+
|
|
501
|
+
57.19.0 and 57.19.1 were tagged but never published to npm (the publish gate failed before the release notes had merged). This release contains everything from both.
|
|
502
|
+
|
|
503
|
+
### Added
|
|
504
|
+
|
|
505
|
+
- `@terreno/test` gains the test helpers Flourish built and proved in its own suites:
|
|
506
|
+
|
|
507
|
+
- `waitUntil` / `waitForCalls` to poll a condition instead of sleeping, and `freezeTime` to
|
|
508
|
+
freeze `Date.now()`, `new Date()`, and Luxon together.
|
|
509
|
+
- `authWithToken` for a supertest agent with a pre-minted token (no login round-trip),
|
|
510
|
+
`expectAPIError` for `APIError` bodies, and `assertStrictResponseProperties` to fail on any
|
|
511
|
+
response field outside an allowlist.
|
|
512
|
+
- Opt-in `mock.module` registrars for Cloud Tasks (with `drainTasks`), SendGrid, Twilio, Expo
|
|
513
|
+
push, GCS, Google auth, and the `@terreno/api` notifiers, plus `createMockLanguageModel` for
|
|
514
|
+
the AI SDK.
|
|
515
|
+
- `@terreno/test/expo/*`: `registerExpoTestEnvironment`, `renderWithTerreno`, in-memory auth
|
|
516
|
+
token storage, and a module-alias mocker for Bun + Expo tests. `example-frontend` now uses it.
|
|
517
|
+
- `terreno-test-shard` (weighted test sharding) and `terreno-test-hygiene` (AST lint for leaky
|
|
518
|
+
or flaky Bun test patterns, with a baseline) CLIs.
|
|
519
|
+
- Testing skills for Terreno apps, installable with the other agent skills: `terreno-test-setup`
|
|
520
|
+
bootstraps backend (Bun + in-memory Mongo), frontend (Bun + React Native Testing Library,
|
|
521
|
+
Playwright), and mobile (Maestro) test setups; `terreno-backend-tests`,
|
|
522
|
+
`terreno-frontend-tests`, and `terreno-mobile-tests` cover writing tests for each. New how-to:
|
|
523
|
+
[Set up tests in a Terreno app](docs/how-to/set-up-app-tests.md).
|
|
524
|
+
- `@terreno/ui` loading skeletons: `Box` and the container components (`Card`, `EditableCard`, `ChartCard`, `Scorecard`, `Page`, `SplitPage`, and the charts) take `loading`, `loadingComponent`, `loadingAnimation`, and `loadingLabel` props. New `Skeleton`, `SkeletonText`, `SkeletonCircle`, `SkeletonCard`, `SkeletonList`, and `SkeletonGroup` components support `pulse`, `shimmer`, or no animation and respect reduce-motion. `withLoading(Component, options)` adds the props to custom components. See [Show loading skeletons](docs/how-to/show-loading-skeletons.md). (#1571)
|
|
525
|
+
- Published packages now include the repository `CHANGELOG.md` in their tarballs. (#1632)
|
|
526
|
+
|
|
527
|
+
### Changed
|
|
528
|
+
|
|
529
|
+
- `@terreno/cli` and `@terreno/mcp` require Bun `>=1.4.4`. 1.4.4 patches the Brotli decoder (CVE-2026-111497). CI, `example-backend`, and `mcp-server` images pin that release. `oven/bun:1` and `oven/bun:1-slim` can still resolve to an affected release, and `bun build --compile` embeds the builder's Bun binary. New apps from `create-terreno-app` get `oven/bun:1.4.4-slim`. Existing apps should set that tag in their Dockerfile and rebuild. See [Upgrade Terreno](docs/how-to/upgrade-terreno.md).
|
|
530
|
+
|
|
531
|
+
EAS build profiles stay on Bun 1.3.10. That pin is part of the Expo fingerprint, so changing it forces a native rebuild. Those profiles remain on an affected Bun release until that rebuild is scheduled.
|
|
532
|
+
|
|
533
|
+
### Fixed
|
|
534
|
+
|
|
535
|
+
- `create-terreno-app` did nothing when run through its package bin (`bunx create-terreno-app`,
|
|
536
|
+
`npx create-terreno-app`): the entry check compared the module URL with the unresolved
|
|
537
|
+
`node_modules/.bin` symlink. It now resolves the path first.
|
|
538
|
+
- `@terreno/test` now installs and imports outside the monorepo. `supertest`,
|
|
539
|
+
`@types/supertest`, and the `mongodb-memory-server` packages moved from
|
|
540
|
+
`devDependencies` to `dependencies` (57.17.0 failed with `Cannot find package
|
|
541
|
+
'supertest'` on import), and the published tarball no longer ships test files.
|
|
542
|
+
- `registerSimpleMongoPreload` now passes `TERRENO_TEST_MONGO_LAUNCH_TIMEOUT_MS` (default 60s) to
|
|
543
|
+
`mongodb-memory-server`, so a cold `mongod` start no longer fails at the library's 10s default.
|
|
544
|
+
|
|
545
|
+
## [57.18.0] - 2026-10-25
|
|
546
|
+
|
|
547
|
+
### Added
|
|
548
|
+
|
|
549
|
+
- Vercel hosting: `example-backend` runs on Vercel Functions (exported `http.Server` with Socket.IO, full Bun bundle), and `example-frontend` and `demo` deploy as static Expo exports. CircleCI deploys all three to the `terrenolabs` Vercel team alongside Cloud Run and Netlify. See `docs/how-to/deploy-to-vercel.md` and `docs/how-to/vercel-function-websockets.md`.
|
|
550
|
+
- `@terreno/rtk`: `useSocketConnection({transports})` chooses the Socket.IO transports (default unchanged: polling, then websocket). Use `["websocket"]` on hosts without session affinity, such as Vercel Functions.
|
|
551
|
+
- `@terreno/syncdb`: `createSyncDb({socketTransports})` does the same for the default socket transport.
|
|
552
|
+
- New `deploy-vercel` agent skill.
|
|
553
|
+
|
|
554
|
+
### Fixed
|
|
555
|
+
|
|
556
|
+
- `@terreno/syncdb`'s tests now cover the debug-recorder paths (replay failures, retries, rate-limit and auth nacks, batch sends, stream join/leave/skip, cursor migration, tombstone compaction, snapshot-batch fallback, `forceResync`, and subscribe catch-up), so the 95% coverage gate passes again on Bun 1.4.4, which counts the lines of debug event objects that never run without a recorder.
|
|
557
|
+
|
|
558
|
+
## [57.17.0] - 2026-10-23
|
|
559
|
+
|
|
560
|
+
Upgrade note: [`mcp-server/src/docs/upgrades/57.17.0.md`](mcp-server/src/docs/upgrades/57.17.0.md).
|
|
561
|
+
|
|
562
|
+
### Breaking
|
|
563
|
+
|
|
564
|
+
- `PATCH /auth/me` now requires `currentPassword` when the body changes the email to a different mailbox (casing-only changes and an unchanged email echoed back still need none). A missing or wrong password gets 403 and nothing is saved; a user with no password (for example GitHub-only) gets 400 and must set one through the reset flow first. The password is checked against the passport hash, which counts toward its `limitAttempts` lockout, or against the Better Auth credential when `BetterAuthApp` is registered (new `authOptions.verifyBetterAuthPassword`, wired by TerrenoApp, and `verifyBetterAuthCredentialPassword`). After a change the old address gets a `sensitive: true` notice through `authOptions.sendMail` with the new address partially masked and no links. Clients that let users change their email must collect and send `currentPassword`; the example frontend, `create-terreno-app`, and the MCP bootstrap profile screens now show a "Current password" field when the email is edited. See [Changing the email on PATCH /auth/me](docs/reference/api.md#changing-the-email-on-patch-authme).
|
|
565
|
+
- `PermissionSource.failClosedWhenDenyUnknown` now defaults to `true`. When a source's `getGrants` fails for a user this process has no last-known deny posture for (a new instance, or the posture was evicted), permission checks throw 503 `Permission source unavailable` instead of resolving local roles without the source's `deny`. Behavior after a successful fetch is unchanged. Add `failClosedWhenDenyUnknown: false` to app sources that never return `deny` (additive-only role or permission mappings) to keep their old outage behavior. The built-in organization membership source already sets it. See [RBAC permission sources](docs/reference/api.md#rbac-permission-sources).
|
|
566
|
+
- With RBAC (`accessControl`), changing another user's `email`, `disabled`, `betterAuthId`, `githubId`, or `emailVerified` now requires the new `user:updateIdentity` permission in addition to `user:update`. This applies to modelRouter, sync, MCP, instance actions, and AdminApp `PATCH` and bulk-patch. Writes without it get 403 `Missing user:updateIdentity permission`. Newly seeded `admin` roles include it and `superadmin` gets it through `*`. `seedDefaults` does not update an `admin` role that is already stored, so existing deployments must add `updateIdentity` to that role's `user` actions (for example with `PATCH /rbac/roles/admin`) to keep RBAC admins able to edit user emails and disable users. Legacy `admin: true` users and self-writes are not affected. See [RBAC user identity writes](docs/reference/api.md#rbac-user-identity-writes).
|
|
567
|
+
|
|
568
|
+
### Added
|
|
569
|
+
|
|
570
|
+
- `attach-pr-media` agent skill. It uploads local screenshots and recordings to one shared secret gist with only the `gh` token and posts Markdown that embeds them in the pull request, for sandboxes where `/opt/cursor/artifacts/` is unavailable. `verify-ui-changes` now points to it. Install with `npx skills add TerrenoLabs/terreno --skill attach-pr-media` or from the `terreno` Claude plugin.
|
|
571
|
+
- `BetterAuthConfig` takes `options` and `plugins`. `options` passes any other Better Auth option (for example `emailAndPassword.requireEmailVerification`, legacy `password.{hash, verify}`, or `databaseHooks`), deep-merged over Terreno's defaults with arrays replaced and explicit values winning. `plugins` are appended after `bearer()` and mount under `basePath`. Apps no longer need to replace `BetterAuthApp` to reach them. (#1578)
|
|
572
|
+
- `BetterAuthConfig.oauthHandoff: {webOrigins, callbackPaths?}` lets a web app on another site finish a social or generic OAuth sign-in in bearer mode without cookies. When the OAuth callback creates a session and redirects to one of `callbackPaths` (exact paths written in normal form, default `["/"]`; the query is free) on one of `webOrigins` (exact `http(s)` origins), the redirect carries `#ott=<token>` in its URL fragment: a single-use token that expires in three minutes and is stored hashed. `POST {basePath}/one-time-token/verify` exchanges it once for the session and the `set-auth-token` header. Neither the handed-off callback redirect nor verify sets a session cookie on the API's origin. App schemes, relative paths, the API's own origin, and origins that only `trustedOrigins` allows get no token. Clients cannot mint one (`GET /one-time-token/generate` is refused), and no response sends a `set-ott` header. Start the sign-in with a top-level navigation to the new `GET {basePath}/oauth-handoff/start?provider=<id>&callbackURL=<url>` (optional `errorCallbackURL`): it sets the OAuth state cookie first-party on the API's origin and redirects to the provider, so the callback works when the browser blocks third-party cookies. It answers 400 when `Sec-Fetch-Dest` is set and is not `document`, so only a page navigation can start a sign-in. It works for social and generic OAuth providers, and answers 400 with no redirect unless `callbackURL` is one of `callbackPaths` on one of `webOrigins`, `errorCallbackURL` (optional) is on one of `webOrigins`, and `trustedOrigins` allows both. With the default `["/"]`, any other return page is refused. An unknown provider or one that cannot start sends the page back with `error=provider_not_found` or `error=sign_in_failed`. Terreno's opt-in `rateLimit` puts `{basePath}/oauth-handoff/*` and `{basePath}/one-time-token/*` in the auth bucket. Unset by default, which adds no `/one-time-token` or `/oauth-handoff` routes.
|
|
573
|
+
- `signInSocialWithHandoff({client, provider, callbackURL?, errorCallbackURL?})` starts a social or generic OAuth sign-in for a `createBetterAuthClient({webAuth: "bearer"})` web client. It stores a random nonce in `sessionStorage` and navigates the page to the server's `GET {basePath}/oauth-handoff/start` with `handoff=<nonce>` on the callback URL. It throws on native and in cookie mode.
|
|
574
|
+
- `completeBetterAuthOAuthHandoff({client})` finishes that sign-in on the return page. When the URL has `#ott=<token>` and a `handoff` equal to the stored nonce, it removes both from the URL with `history.replaceState`, then exchanges the token at `{basePath}/one-time-token/verify`, and bearer mode stores the `set-auth-token` that comes back. A token without a matching nonce is removed and never sent. Other query and fragment parts stay, and `?error=` is left for the app. It resolves `true` only when a token was stored. Await it at startup before session reads, other query handling, and Sentry. The server needs `oauthHandoff: {webOrigins, callbackPaths?}`. See [Social sign-in in bearer mode](docs/how-to/configure-better-auth.md#social-sign-in-in-bearer-mode).
|
|
575
|
+
- `createBetterAuthClient` takes `webAuth: "bearer"` for web apps whose frontend and API are on different sites. On web it stores the `set-auth-token` header from sign-in, sends `Authorization: Bearer` on every Better Auth call including `getSession`, and sends no cookies. `signOut()` clears the token before its request, and a successful `getSession` with no session clears it unless a newer token was stored meanwhile. The token lives in `localStorage` under `${storagePrefix}_bearer_token`, or in the `bearerStorage` you pass (for example `window.sessionStorage` on shared devices). The default stays `"cookie"`, and native is unchanged. The web origin must be in the server's Better Auth `trustedOrigins`. See [Frontend and API on different sites](docs/how-to/configure-better-auth.md#frontend-and-api-on-different-sites).
|
|
576
|
+
- `getBetterAuthBearerToken({storagePrefix?, bearerStorage?})` reads that stored token without a network call.
|
|
577
|
+
- `GPTChat` takes `showHistory` (default `true`). Pass `false` to leave out the history sidebar so the chat panel uses the full width on narrow and phone layouts.
|
|
578
|
+
- modelRouter actions can opt into MCP with `mcp: {name, description?}`. The action becomes a tool on `/mcp` that takes `{id, ...body}` (instance) or the body alone (collection), runs the action's own permission chain including RBAC `access`, and gets `req.isMCPRequest === true`. Actions also accept `unwrapResponse: true` to send the handler's return value as the REST body instead of `{data: …}`. See [modelRouter actions → MCP tools](docs/explanation/model-router-actions.md#mcp-tools).
|
|
579
|
+
- `TerrenoApp` `mcpOAuth: {enabled: true}` turns on an OAuth 2.1 authorization server for `/mcp`, so Claude.ai connectors and ChatGPT apps can connect to apps that sign in with Terreno JWTs. It is off by default. It serves RFC 9728 and RFC 8414 discovery, the authorization-code flow with PKCE S256 and an app-hosted consent screen, rotating refresh tokens with reuse detection, and revocation. Revoking a token family writes an `McpOAuthRevokedFamily` tombstone that `McpOAuthToken.issue` honors, so concurrent refreshes and code replays cannot leave a live token. Dynamic client registration (public clients, redirect hosts allowlisted) is a separate opt-in, `dynamicClientRegistration: true`; without it, the app creates `McpOAuthClient` documents itself. With `mcpOAuth` on, `/mcp` answers unauthenticated requests with a `401` `WWW-Authenticate` challenge. See [Enable MCP OAuth for a JWT app](docs/how-to/enable-mcp-oauth.md).
|
|
580
|
+
- `getCurrentRequestContext()?.credential` records how the caller authenticated: `"jwt"`, `"better-auth-session"`, `"mcp-service-token"`, or `"oauth"`. Permission functions and hooks can use it to keep broad access (such as an admin bypass) to the user's own session and away from delegated agent tokens. `extractAuthFromHeaders` returns the same `{user, credential}` pair for MCP mounts. See [Request credential](docs/reference/api.md#request-credential).
|
|
581
|
+
- `useTerrenoChat` takes a `tokenSource` option. Each chat request reads its `Authorization: Bearer` token from that source, else the one registered with `setTokenSource`, else the built-in JWT source, and sends no `Authorization` header when there is no token. JWT apps behave as before.
|
|
582
|
+
- `useMCPTools` takes a `tokenSource` option. The MCP transport reads its `Authorization: Bearer` token from that source, else the one registered with `setTokenSource`, else the built-in JWT source, before every MCP request instead of capturing one token per fetch. When the source has no token as a fetch starts, the hook connects without authentication, as JWT apps do today.
|
|
583
|
+
- `createOfflineMiddleware` takes a `tokenSource` option. With a non-JWT source (that option, else the one registered with `setTokenSource`), queued mutations are tagged with the source's `getUserId()` instead of `state.auth.userId`, so the middleware works without an `auth` slice. They replay only while that same user is signed in, with `Authorization: Bearer` from the source's `getToken()` (omitted when there is no token). The offline slice gains an optional `lastUserId` (with `setLastUserId` and `selectLastUserId`): the last user id the source confirmed, refreshed on the first action, on rehydrate, on `onAuthChange` and on reconnect. A session read that fails while offline therefore still tags new mutations, and when the `offline` slice is persisted with redux-persist, mutations made after an offline launch are tagged with the persisted user. Until a read in the running app confirms that persisted id, those mutations carry `userIdConfirmed: false` (new optional `QueuedMutation` field, with the new `resolveUnconfirmedMutations` and `dropUnconfirmedMutations` actions): a read that returns a user keeps them only if it is the same user and no auth change or source swap happened since, and any auth change drops them (even for the same user), so they survive only when the reconnect read confirms them first. syncdb's `betterAuthAdapter` fires `onAuthChange` on the session's first successful load, which after an offline launch happens once the network returns, so that event drops offline-launch writes the reconnect read has not confirmed yet. The one remaining risk needs the signed-in user to change while no running instance of the middleware observes it (for example in another browser tab sharing the persisted `offline` slice) between an unconfirmed write and the next launch's first read. On the token-source path, a sign-out reported by the source, `auth/logout`, or the Better Auth slice's `betterAuth/logout` clears the queue, conflicts and `lastUserId`, and discards writes still waiting on a user id read and any replay in progress. A reconnect with no readable user, or a user switch during a replay, keeps the remaining queue for a later reconnect, and a mutation still waiting on a user id read gets no user (so it never replays) if the user changes first. JWT apps behave as before.
|
|
584
|
+
- `useSocketConnection` no longer requires `getAuthToken`. Without it, the hook reads the token source: the new `tokenSource` option, else the one registered with `setTokenSource`, else the built-in JWT source. The handshake re-reads the token on every connect and reconnect, and when the source's `onAuthChange` reports a different token the hook reconnects with it (with no token it disconnects). When the server rejects the handshake token, the hook calls `refresh()` once and reconnects; a second rejection before a successful connect stops reconnecting and calls `onConnectError`. Passing `getAuthToken` keeps the previous behavior.
|
|
585
|
+
- `@terreno/rtk` reads request tokens from one registered token source. Call `setTokenSource(authProvider)` once at startup with the same syncdb `AuthProvider` the app builds for `createSyncDb` (for example `betterAuthAdapter(betterAuthClient)`). `getTokenSource()`, `jwtTokenSource`, and `isJwtTokenSource()` are also exported. Without a registered source, rtk keeps using the JWT tokens in storage, so JWT apps change nothing.
|
|
586
|
+
- `emptySplitApi` sends the registered source's token.
|
|
587
|
+
- `betterAuthAdapter` takes a `getBearerToken` option. When it returns a token, `getToken` uses it without calling `getSession()`; a null, empty or throwing getter falls back to `getSession()` as before. Pair it with `getBetterAuthBearerToken` from `@terreno/rtk` for web apps in Better Auth bearer mode. `getUserId`, `onAuthChange` and `refresh` are unchanged. See [createSyncDb configuration](docs/reference/syncdb.md#createsyncdb-configuration).
|
|
588
|
+
- `WhatsNewBanner` in `@terreno/ui` shows non-blocking release notes, such as "What's new
|
|
589
|
+
in 57.16.2", until the viewer opens or dismisses it, then hidden until a newer release. See
|
|
590
|
+
[Show release notes with WhatsNewBanner](docs/how-to/whats-new-banner.md).
|
|
591
|
+
- The example app's what's new banner lists the 10 most recent `CHANGELOG.md` releases.
|
|
592
|
+
`bun run whats-new:generate` writes them, `changelog:assemble` refreshes them on each
|
|
593
|
+
release, and CI fails through `bun run check:whats-new` when they are stale.
|
|
594
|
+
- `BannerButton` accepts an optional `testID`.
|
|
595
|
+
|
|
596
|
+
### Changed
|
|
597
|
+
|
|
598
|
+
- `RealtimeApp` uses the Better Auth instance of a `BetterAuthApp` registered on the same `TerrenoApp`, so Better Auth socket sessions work without building a second instance and passing `betterAuth`. An explicit `betterAuth` option still wins. (#1576)
|
|
599
|
+
- An explicit `betterAuth` option without a `userModel` now gets the `RealtimeApp`'s (else the `TerrenoApp`'s), so its sockets resolve to the app user instead of the Better Auth id. (#1576)
|
|
600
|
+
- The Better Auth socket handshake creates the app `User` on first contact, as the REST session middleware does, and refuses disabled users. (#1576)
|
|
601
|
+
- A `RealtimeApp` registered without its own `userModel` uses the `TerrenoApp`'s, for JWT and Better Auth sockets alike. Socket authorization then reads the user from the database instead of the token's claims, and the revalidation sweep disconnects disabled or deleted users, as `TerrenoApp({realtime})` already did. (#1576)
|
|
602
|
+
- create-terreno-app: generated backends no longer build a second Better Auth instance for `RealtimeApp`; it uses the registered `BetterAuthApp`, so `BetterAuthConfig` `options` and `plugins` apply to sockets too. (#1594)
|
|
603
|
+
- create-terreno-app: generated frontends register the syncdb Better Auth adapter from `store/syncdb.ts` with `setTokenSource` before the store is built, and the generated SDK uses `@terreno/rtk`'s `emptySplitApi` (`openapi-config.ts` `apiFile: "@terreno/rtk"`). `store/betterAuthApi.ts` and its forked base query are no longer generated. Generated web clients still use Better Auth's session cookie. RTK requests now get rtk's retries, and a 401 that survives one session refresh signs the user out (`auth/logout`); the fork never did. To move an existing generated app over: export `authProvider` from `store/syncdb.ts`, call `setTokenSource(authProvider)` in `store/index.ts`, set `apiFile: "@terreno/rtk"` in `openapi-config.ts`, run `bun run sdk`, delete `store/betterAuthApi.ts`, and import `useGetVersionCheckQuery` from `@terreno/rtk` if the app used it.
|
|
604
|
+
- The example backend seeds its staff modal and patient banner announcements as drafts, so a
|
|
605
|
+
fresh database (local dev or a PR preview) no longer interrupts sign-in. Publish them from
|
|
606
|
+
**Admin → Announcements** to try them.
|
|
607
|
+
- `@terreno/feature-flags` evaluation diagnostics are off unless `FEATURE_FLAGS_DEBUG=true` (they were on unless set to `"false"`), are written at `debug` level instead of `info`, and no longer include the requesting user's value for a field rule.
|
|
608
|
+
- Lifecycle skills now own a PR after every push: Brew, Taste, the planning loop, taste-sweep, and the repository skills that push (fix-conflicts, respond-to-review, update-dependencies, release, track-upstream-expo, implement-ready-for-dev) keep fixing red CI and answer every review-bot comment, then escalate to the human as `BLOCKED` after three failed fix rounds instead of leaving the PR red. See `references/push-ownership.md`.
|
|
609
|
+
|
|
610
|
+
### Fixed
|
|
611
|
+
|
|
612
|
+
- `BetterAuthApp` stores Better Auth users, accounts, sessions, and verifications in the mongoose connection's database (the `dbName` passed to `mongoose.connect`) instead of the connection string's default database. Apps that connect with a `dbName` different from the URI's database should copy the `user`, `account`, `session`, and `verification` collections into the `dbName` database when upgrading.
|
|
613
|
+
- A Better Auth session whose app `User` has `disabled: true` no longer authenticates on REST: `req.user` stays unset, so protected routes answer 401, matching JWT and MCP. Better Auth's own routes, such as sign-out, still work. (#1577)
|
|
614
|
+
- The `TerrenoApp` rate limiter resolves `.`/`..` path segments (including `%2e` forms) before choosing a bucket, so dot-segment variants of Better Auth sign-in, sign-up, and password-reset paths are charged to the auth bucket, and dot-segment paths no longer get the health/openapi/swagger skip.
|
|
615
|
+
- Request logging redacts credential fields under any key name and at any depth in request bodies and query strings (for example `currentPassword`, `idToken.token`, `client_secret`, `code_verifier`, and OAuth `code`), not only five top-level keys. `TerrenoApp` now honors `loggingOptions.logRequests: false`, which was previously ignored.
|
|
616
|
+
- Better Auth apps are no longer logged out on their first RTK Query request (#1217). With a registered non-JWT token source, `emptySplitApi` sends no `authorization` header when the token is null, and dispatches `auth/logout` only when a 401 survives one `refresh()` and retry.
|
|
617
|
+
- `betterAuthAdapter` no longer reports a failed session read as a sign-out. When `getSession()` rejects or returns a non-401 `{error}` envelope, `getUserId()` answers with the user id the server last confirmed instead of null. Before, going offline between a session-atom emission and the syncdb client's follow-up read cleared the client's current user, paused sync, and made `mutate()` throw "requires start() to have resolved an authenticated user" until the next sign-in.
|
|
618
|
+
- `betterAuthAdapter` keeps the newest confirmed user id. A session read that started before a sign-out or user switch and resolves after it no longer restores the previous user, and neither does a session-atom emission that repeats the cached session (a refetch start, or the value a new `onAuthChange` subscriber receives on subscribing). A later failed read therefore cannot revive a signed-out identity or report the wrong user.
|
|
619
|
+
- `betterAuthAdapter` treats a 401 session-atom emission as a sign-out, the same as a 401 from `getSession()`: it clears the confirmed user id and fires `onAuthChange`. Before, the subscribe path ignored it like a network failure and kept the previous user.
|
|
620
|
+
|
|
621
|
+
## [57.16.2] - 2026-10-13
|
|
622
|
+
|
|
623
|
+
### Fixed
|
|
624
|
+
|
|
625
|
+
- Light-mode primary button labels, icons, and loading indicators are white again,
|
|
626
|
+
including on disabled primary buttons. (#1567)
|
|
627
|
+
|
|
628
|
+
## [57.16.1] - 2026-10-12
|
|
629
|
+
|
|
630
|
+
Upgrade note: [`mcp-server/src/docs/upgrades/57.16.1.md`](mcp-server/src/docs/upgrades/57.16.1.md).
|
|
631
|
+
|
|
632
|
+
### Added
|
|
633
|
+
|
|
634
|
+
- `terraform/modules/terreno-backend` provisions one Terreno backend on Cloud Run: Artifact Registry, a least-privilege runtime service account, empty Secret Manager secrets, and a websocket-safe service (session affinity, 3600s timeout, one minimum instance). Set `github_deploy` for a keyless GitHub Actions deployer. See [Deploy backend to Cloud Run](docs/how-to/deploy-backend-to-cloud-run.md#manage-with-terraform).
|
|
635
|
+
|
|
636
|
+
### Changed
|
|
637
|
+
|
|
638
|
+
- `terraform/` now holds only reusable modules. Terreno's own GCP deployment (example backend, jobs worker, hosted MCP server) moved to `infra/flourish/`, which uses the shared modules through a symlink; CI stages a dereferenced copy for Infra Manager. No infrastructure changes: resource addresses are unchanged.
|
|
639
|
+
|
|
640
|
+
### Fixed
|
|
641
|
+
|
|
642
|
+
- `TextField` and `TextArea` on web no longer drop a typo fix made within half a second. The
|
|
643
|
+
oscillation guard used to suppress any change that undid the previous one within 500ms, so
|
|
644
|
+
typing a character and backspacing it right away put the character back. Changes the user
|
|
645
|
+
types, deletes, pastes, drops or undoes (judged by the DOM input event's `inputType`) now
|
|
646
|
+
bypass the guard. Spellcheck replacements, IME composition and changes with no input event
|
|
647
|
+
stay guarded.
|
|
648
|
+
|
|
649
|
+
## [57.15.0] - 2026-10-11
|
|
650
|
+
|
|
651
|
+
Upgrade note: [`mcp-server/src/docs/upgrades/57.15.0.md`](mcp-server/src/docs/upgrades/57.15.0.md).
|
|
652
|
+
|
|
653
|
+
### Changed
|
|
654
|
+
|
|
655
|
+
- SyncDB bootstraps and reconciles every stream through one `POST /sync/snapshot/batch` request
|
|
656
|
+
per round instead of one `GET /sync/snapshot` per stream. A reconnect with nothing new now costs
|
|
657
|
+
2 requests however many collections the app syncs. The client sends the next round while it
|
|
658
|
+
writes the current one and writes rows in chunks of 500 so the UI keeps responding. Clients fall
|
|
659
|
+
back to per-stream paging against servers without the batch route. The web IndexedDB persister
|
|
660
|
+
no longer serializes the whole store once per transaction: it serializes once per write.
|
|
661
|
+
|
|
662
|
+
## [57.14.0] - 2026-10-11
|
|
663
|
+
|
|
664
|
+
Upgrade note: [`mcp-server/src/docs/upgrades/57.14.0.md`](mcp-server/src/docs/upgrades/57.14.0.md).
|
|
665
|
+
|
|
666
|
+
### Changed
|
|
667
|
+
|
|
668
|
+
- `@terreno/syncdb` now pages snapshot streams side by side instead of one at a time, so cold
|
|
669
|
+
start no longer costs one sequential round trip per stream. Reconcile, subscribe catch-up,
|
|
670
|
+
`forceResync` and wipe re-bootstrap run up to `snapshotConcurrency` streams at once (new
|
|
671
|
+
`createSyncDb` option, default 6; `1` restores the old behavior). Each stream now runs one pass
|
|
672
|
+
at a time: a subscribe catch-up queues behind a pass that started before the room join, and a
|
|
673
|
+
force resync waits for the stream's in-flight pass before purging it, so a concurrent pass can
|
|
674
|
+
no longer mark a purged stream bootstrapped over rows it never paged. A new how-to,
|
|
675
|
+
[Use syncdb correctly](docs/how-to/use-syncdb-correctly.md), lists the library behavior
|
|
676
|
+
apps have to handle.
|
|
677
|
+
|
|
678
|
+
## [57.13.0] - 2026-10-10
|
|
679
|
+
|
|
680
|
+
Upgrade note: [`mcp-server/src/docs/upgrades/57.13.0.md`](mcp-server/src/docs/upgrades/57.13.0.md).
|
|
681
|
+
|
|
682
|
+
### Added
|
|
683
|
+
|
|
684
|
+
- Agent UI rich replies: a block document can now hold a `stepper` (− value + that calls a host action and gets the replacement stepper back), a `checklist` (ticks with an "n of m" counter, local or sent to a host action), a `gallery` (2–6 photos), a `list` (titled rows with portrait thumbnails), a card `eyebrow`, and a `copy` button action that writes a block's text to the clipboard on the device. See [Add a stepper callback](docs/how-to/agent-ui-blocks.md#add-a-stepper-callback), [UI blocks](docs/reference/blocks.md), and [Agent UI blocks](docs/explanation/agent-ui-blocks.md).
|
|
685
|
+
- `@terreno/blocks` exports the stepper, checklist, gallery, list, and copy action types, `blockPlainText`, `stepperElementIds`, `checklistElementId`, `COPY_TARGET_TYPES`, and the error codes `OUT_OF_RANGE` and `COPY_TARGET_INVALID`. `blocksPromptSection` takes `richBlocks`, `stepperActions`, and `checklistActions`; `validateBlocks` takes the two action lists. The Sunday roast golden document is `blocks/src/fixtures/golden/sunday-roast.yaml`.
|
|
686
|
+
- `@terreno/ai` adds the opt-in host actions `scaleStepperHostAction` and `toggleChecklistHostAction`, `findAgentBlock` for reading the block the agent wrote from the stored history, and `runBufferedChatTurn` for one chat turn without HTTP. A host action can set `handles: "stepper" | "checklist"` and `logResponse: false` (the returned block stays out of the `ui_action` log). `uiBlocks.richBlocks` controls which blocks the prompt offers.
|
|
687
|
+
- `@terreno/ui` `BlocksView` renders the new blocks with Terreno components. `DataTable` has right-aligned `number` and Luxon `date` cells, and the `table` block uses them. `MarkdownView` tables get theme borders, a bold header, and sideways scroll. `Box` takes `accessibilityState` and `accessibilityLiveRegion`. `Image` uses `alt` as its accessible label.
|
|
688
|
+
- The example app generates a stored photo library once (`bun run photos:generate`, Vertex Imagen), gives the agent a `findPhotos` tool, serves `GET /photoLibrary/:id/url`, and registers `scaleStepper` and `toggleChecklist`. `bun run blocks:smoke` checks that a real model writes the roast reply, and skips without a model key.
|
|
689
|
+
- `@terreno/api` exports `createRouteRateLimitMiddleware({name, max, windowMs, store?, keyBy?})`, a fixed-window limit for one route that works without `TerrenoApp` `rateLimit`. See [Rate limiting](docs/how-to/rate-limiting.md). (#1499)
|
|
690
|
+
- `@terreno/admin-backend` `DocumentStorageApp` takes `access: "authenticated"` to open document storage to every signed-in user. Non-admins only see their own `users/<userId>/` folder under `folderPrefix`; admins keep the whole prefix. `uploadRateLimit` limits uploads per client IP (or `keyBy`). A `folderPrefix` without a trailing slash gets one, and a missing bucket lists as empty. (#1499)
|
|
691
|
+
- `@terreno/admin-frontend` `DocumentStorageBrowser` takes `backButton` and shows upload errors inline, including a 429 "Upload limit reached" message. (#1499)
|
|
692
|
+
- `@terreno/ui` `SegmentedControl` takes `size="sm"` (28px, sized to its labels). (#1499)
|
|
693
|
+
|
|
694
|
+
### Changed
|
|
695
|
+
|
|
696
|
+
- The admin Roles screen shows one row per resource, with that resource's actions as badges, instead of one badge per `resource:action`. Custom permissions list before model permissions. Badges are green for read actions, amber for writes, and red for destructive actions (`delete`, `destroy`, `remove`). Saved role data is unchanged. (#1540)
|
|
697
|
+
- Behaviour change: hosts with `uiBlocks` on get the new rich blocks (`checklist`, `gallery`, `list`, card `eyebrow`, and `copy`, plus `stepper` when a host action has `handles: "stepper"`) in the chat system prompt by default. A native build released before them shows an error banner for a reply that uses them. Set `uiBlocks.richBlocks: false` to keep the old catalog until those builds update; the prompt is then unchanged from before, even with a stepper action registered. Validation accepts the new blocks either way. See [Keep the old catalog for shipped native builds](docs/how-to/agent-ui-blocks.md#keep-the-old-catalog-for-shipped-native-builds).
|
|
698
|
+
- `DataTable` columns with `columnType: "number"` now right-align their cells and header, and `columnType: "date"` cells format an ISO string or `Date` as Luxon `DATE_MED` (date only, local zone). Values that are already formatted strings show as before. Use `customColumnComponentMap` to keep the old look.
|
|
699
|
+
- `Image` with `alt` now sets it as the accessible label and makes the image its own screen-reader element.
|
|
700
|
+
- `table` blocks show column names with underscores as spaces, size to their rows, and show at most 10 rows before the body scrolls.
|
|
701
|
+
- The `neutral500` theme primitive is now `#949494` (was `#9A9A9A`), so extra-light text meets contrast guidelines. Disabled surfaces, dark borders, the away status, and new `create-terreno-app` projects pick up the new gray. Apps that set their own `neutral500` with `setPrimitives` are unaffected. (#1544)
|
|
702
|
+
|
|
703
|
+
### Fixed
|
|
704
|
+
|
|
705
|
+
- On web, an open toast no longer blocks clicks on the rest of the page at the same height.
|
|
706
|
+
The toast's `SafeAreaView` and the `Toast` layout wrapper span the full width (up to 900px)
|
|
707
|
+
around the visible toast and swallowed clicks there. Both now use `pointerEvents: "box-none"`,
|
|
708
|
+
so only the visible toast is interactive.
|
|
709
|
+
- `Tooltip` no longer shows up in the top-left corner and blocks the screen when its trigger measures as zero-sized or off-screen. When the trigger measurement is invalid or the ref cannot be measured, the tooltip hides and logs `console.error`. Measurements left over from an earlier show are ignored. (#1490)
|
|
710
|
+
- `Tooltip` on web no longer mounts a full-screen layer over the app while it is open.
|
|
711
|
+
The bubble now renders into `document.body` with `position: fixed` instead of through
|
|
712
|
+
the `PortalHost` layer, so nothing covers the page and the content below stays
|
|
713
|
+
clickable. Web tooltips also use `zIndex: 9999`, matching the body-level overlays
|
|
714
|
+
(`Modal`, `DropdownPanel`), so tooltips inside a modal show on top of it. Native keeps
|
|
715
|
+
the `PortalHost` path.
|
|
716
|
+
|
|
717
|
+
## [57.12.0] - 2026-10-07
|
|
718
|
+
|
|
719
|
+
Upgrade note: [`mcp-server/src/docs/upgrades/57.12.0.md`](mcp-server/src/docs/upgrades/57.12.0.md).
|
|
720
|
+
|
|
721
|
+
### Added
|
|
722
|
+
|
|
723
|
+
- `@terreno/ai` harness: `harness.createTask(def, input, {trace: {scope, tags}, prompts})` writes a scope (tenant, workspace), tags, and prompt versions onto the task's `ObsTrace`; child tasks and subagents share it. `ObsTrace` gains `scope` (indexed with `created`) and `tags`, and the admin trace list filters by `scope`. `rt.runAgent` takes `prompts` and accepts a registry `PromptVersionRef` as `instructions` (its `body` runs, its version is recorded). New subpath exports replace `dist/` deep imports: `@terreno/ai/admin`, `@terreno/ai/harness/{agentLoop,commit,events,internalRuntime}`, and `@terreno/ai/observability/{observabilityApp,localPlugin,promptStore}`. `getProjectModel()` returns the chat `Project` model, and `new AiApp({projects: false})` skips the `/gpt/projects` routes so an app can own a model named `Project`. See [AI harness reference](docs/reference/ai-harness.md#trace-scope-and-prompt-versions). Closes #1533.
|
|
724
|
+
|
|
725
|
+
### Changed
|
|
726
|
+
|
|
727
|
+
- Harness task HTTP responses and the task OpenAPI schema omit `lease`. The fencing token stays on the server. Approve, reject, submit, and abort invalidate the same cache tags as the task, approval, and conversation lists (`harnesstasks`, `harnessapprovals`, `harnessconversations`).
|
|
728
|
+
- Importing `@terreno/ai` no longer registers the mongoose `Project` model. The `Project` export is a lazy proxy and is deprecated: the first use of `Project` or `getProjectModel()` registers the model. Statics, `new Project(...)`, and `instanceof Project` still work. The proxy is not the model (`Project !== mongoose.models.Project`, `Object.keys(Project)` is empty, and reading `Project.prototype` throws). Use `getProjectModel()`.
|
|
729
|
+
|
|
730
|
+
### Fixed
|
|
731
|
+
|
|
732
|
+
- Web `SplitPage` desktop child columns scroll vertically again. The corner radius and clip sit on the pane, so `overflow: hidden` no longer blocks the column `ScrollView`. A child with `height: "100%"` stays within the visible pane and can scroll internally. Narrow web pager pages use the measured pane height, so that same full-height child can scroll internally while the page still clips its rounded corners. Labeled pager previous and next controls sit in fixed slots in a row under the column, and the column uses the height left after that row and its spacing. `desktopChildrenMinWidth` horizontal scrolling, the list column, and native `SplitPage` are unchanged.
|
|
733
|
+
|
|
734
|
+
## [57.11.0] - 2026-10-05
|
|
735
|
+
|
|
736
|
+
Upgrade note: [`mcp-server/src/docs/upgrades/57.11.0.md`](mcp-server/src/docs/upgrades/57.11.0.md).
|
|
737
|
+
|
|
738
|
+
### Added
|
|
739
|
+
|
|
740
|
+
- `@terreno/ai/harness/jobsRunner`: `JobsRunner` runs durable harness phases as `@terreno/jobs` jobs (`terreno.harness.phase`) on every instance, for multi-instance deploys such as Cloud Run. The task lease decides who commits, so duplicate or late jobs are harmless. `@terreno/jobs` is an optional peer; importing `@terreno/ai` never loads it. Adds `HarnessTask.claims` (default 0, no migration). See [Run the harness on multiple instances](docs/how-to/run-harness-on-multiple-instances.md).
|
|
741
|
+
- `@terreno/api` can manage one Slack workspace with `SLACK_BOT_TOKEN`: `createSlackPrivateChannel` creates a private channel, invites member ids, and posts an optional first message; `inviteSlackUsersToChannel` adds staff to a channel the bot already belongs to; `postSlackMessage` posts as the bot; `findSlackChannelByName` resolves a visible channel name to its id.
|
|
742
|
+
- `@terreno/syncdb` query windows: `queryCollections` config, `client.queryWindows`, and the `useWindowQuery` hook (plus generated `use{Collection}Window` hooks) run server-filtered, paged list queries (`where`, `sort`, `pageSize`, `fetchNextPage`, `refetch`, `hasMore`, `total`) over a shared, deduplicated entity table. Overlapping windows store each row once and update it once; each window lists only its own members. `fullSync: true` adds windows to a collection that still snapshot-syncs.
|
|
743
|
+
- `useQuery` / `useEntityIds` accept the same Mongo-style `where`, string sort specs, and `limit` for local queries.
|
|
744
|
+
- Window refetches learn the route's `maxLimit` and pipeline page requests with a `console.warn` instead of silently truncating.
|
|
745
|
+
- `modelRouter` list requests that filter on a field missing from `queryFields` now return a 400 naming the field, model, and allowed fields (`meta.allowedQueryFields`), checked before query validation so the filter can never be silently stripped. syncdb raises it as `QueryFieldNotAllowedError`.
|
|
746
|
+
- `terreno-prd` interviews the app owner and writes a product PRD with measurable success metrics before Grow. Invoke `/terreno-prd` in Cursor, `/terreno:prd` in Claude Code, or `$terreno-prd` in Codex.
|
|
747
|
+
|
|
748
|
+
### Changed
|
|
749
|
+
|
|
750
|
+
- `@terreno/ai/harness`: `InProcessRunner` now runs up to 8 claimed tasks at once (new `concurrency` option, default 8), each under its own task lease, and `stop()` waits for all of them. Expect parallel model calls (watch provider rate limits and cost) and more stream-counter transaction retries. Turns of one conversation stay serial. Pass `new InProcessRunner({concurrency: 1})` to keep the previous one-at-a-time behavior. See [AI harness reference → InProcessRunner](docs/reference/ai-harness.md#inprocessrunner).
|
|
751
|
+
|
|
752
|
+
### Fixed
|
|
753
|
+
|
|
754
|
+
- Outline `Button`s match solid buttons in width and height. The border width is subtracted from horizontal padding as well as vertical, at the default and `sm` sizes.
|
|
755
|
+
- `@terreno/ui` date and table components import `FontAwesome6` from `@expo/vector-icons/FontAwesome6`, so the named export resolves on iOS. The public component API is unchanged.
|
|
756
|
+
- Web `SplitPage` navigation IconButtons use the default variant, and detail child columns get a default `md` corner radius with `overflow: hidden`. Pass `childColumnRounding` to override that radius. The list column is unchanged. Native `SplitPage` is unchanged.
|
|
757
|
+
|
|
758
|
+
## [57.10.1] - 2026-10-04
|
|
759
|
+
|
|
760
|
+
Upgrade note: [`mcp-server/src/docs/upgrades/57.10.1.md`](mcp-server/src/docs/upgrades/57.10.1.md).
|
|
761
|
+
|
|
762
|
+
### Breaking
|
|
763
|
+
|
|
764
|
+
- Security: `GET /files/*gcsKey` (`addFileRoutes`, `AiApp` with `gcsBucket`) now requires authentication and returns a signed URL only for the caller's own upload. Before, anyone could get a signed URL for any stored key, and keys are guessable (`uploads/<userId>/<ms>-<name>`). Another user's file, including for an admin, returns 404 like a missing file, so keys cannot be probed. Send the user's session token with the request; an unauthenticated request now returns 401.
|
|
765
|
+
|
|
766
|
+
### Added
|
|
767
|
+
|
|
768
|
+
- Agent UI Asks: the chat agent can pause a turn and ask the user a typed question, then continue with the exact answer. Turn it on with `asks: true` (or `{kinds, approvals, maxFileSizeBytes}`) on `AiApp` or `addGptRoutes`. Asks are off by default: with `asks` unset, tools, the system prompt, SSE events, and endpoints are unchanged. See `docs/how-to/agent-ui-asks.md`, `docs/reference/agent-ui-asks.md`, and `docs/explanation/agent-ui-asks.md`.
|
|
769
|
+
- New package `@terreno/blocks`: the shared Zod contracts for asks (input and answer schemas, `validateAskInput`, `validateAskResponse`, stable error codes, `ASK_LIMITS`), simple cards (`toSimpleCard`, `resolveButtonAnswer`), the compact surface, the headless endpoint bodies, and JSON Schema documents plus fixtures (`@terreno/blocks/schemas/*`, `@terreno/blocks/fixtures/*`) for native clients such as a Swift watch app.
|
|
770
|
+
- Five ask kinds, each a client-side `ask_<kind>` tool: `choice` (select one, or select many with an optional Other text answer), `confirm`, `markdown` (edit a draft), `form` (a few typed fields, one submit), and `files` (upload images or documents the model reads in the same turn, as data URLs or `fileId`s from `POST /files/upload`).
|
|
771
|
+
- Server-enforced approval: with asks on, a host tool marked `needsApproval: true` runs only after the user approves a server-made `confirm` ask. `asks.approvals` sets that ask's text per tool name. With asks off, such a tool never runs.
|
|
772
|
+
- `/gpt/prompt` accepts `askResponse` and `surface: "full" | "compact"`, and streams `{ask}` and `{askResolved}` events plus `pendingAsk` on `{done}`. `GptHistory` stores the pending ask. With `chat` options that turn asks on, `addGptHistoryRoutes` (and `AiApp`) adds `GET /gpt/histories/pendingAsks` and `POST /gpt/histories/:id/turn`, which run a turn to completion and return JSON for clients that do not read server-sent events.
|
|
773
|
+
- `@terreno/ui`: `GPTChat` shows asks as an `AskCard` in the transcript (`GPTChatMessage.ask`, `onAskSubmit`, `askErrors`, `resolveAskFiles`). New `AskCard` and `SimpleAskCard` components. `Button` has a new `wrapText` prop that wraps a long label onto centered lines instead of overflowing its container.
|
|
774
|
+
- The example app's AI screen answers asks end to end, and the example backend runs a scripted keyless demo agent (`terreno-demo-agent`) when no model is configured, so asks can be tried without an API key.
|
|
775
|
+
- Agent UI blocks: a chat reply can be one YAML document of headings, metrics, charts, tables, and actions. Turn it on with `uiBlocks` on `addGptRoutes`. The model is told its whole reply is that document. After the text, the route checks it and sends `{blocks}` before `{done}`. `repair: true` runs one repair call. See `docs/how-to/agent-ui-blocks.md`, `docs/reference/blocks.md`, and `docs/explanation/agent-ui-blocks.md`.
|
|
776
|
+
- `@terreno/blocks` adds `parseBlocks`, `validateBlocks`, `blocksJsonSchema`, and `terreno-blocks validate`. Charts and tables read inline rows or a `ref` dataset. `AIService.generateBlocks` returns one validated document at temperature 0.
|
|
777
|
+
- `@terreno/ai` stores chart rows on `AIDataset` via `registerAiDataset`. `GET /gpt/datasets/:id` is owner-only and can bucket, downsample, or paginate them. `POST /gpt/actions` runs a named host callback after an owner check and a payload check.
|
|
778
|
+
- `@terreno/ui` `BlocksView` paints a document. `GPTChat` `uiBlocks` renders assistant messages through that view, including a spinner while the reply is still streaming.
|
|
779
|
+
- The example backend registers `exportDataset` and a `todoStats` tool that stores open and completed todo counts. The example AI tab loads `ref` charts and posts callbacks.
|
|
780
|
+
- `html` blocks are opt-in (`uiBlocks.html` on the server, `allowHtml` on `GPTChat` or `BlocksView`). The server strips scripts, event handlers, forms, frames, and links, then stores the cleaned document. The client draws it in a sandboxed frame only after the reply finishes.
|
|
781
|
+
- `callout`, `image`, and `details` render as a banner, an image, and an accordion. An `https` image loads only when its host is in `uiBlocks.imageHosts`.
|
|
782
|
+
- A reply that omits an actions block `id` gets one before it is stored. A block document written in the same step as a tool call is kept when the turn would otherwise have no text. The chat receives the document after that fill, repair, and HTML sanitizing, so an invalid draft is not shown. `{replace: "text"}` is sent only when text was already streamed and then changed. A validation banner names the field path once the reply has finished.
|
|
783
|
+
- Example-backend registers `accessControl: access` on `ObservabilityApp` and seeds optional `aiObservabilityViewer` and `aiObservabilityOperator` RBAC roles that demonstrate read-only and full observability access without auto-assigning them to users.
|
|
784
|
+
- `@terreno/ai` ships local prompt versions, nested traces, evaluators, datasets, experiments, and a review queue through `ObservabilityApp`, with the admin screens in `@terreno/admin-frontend`. Prompts, datasets, and experiments have one writer. Traces and scores fan out to every registered sink (local Mongo, Langfuse, or OTLP). Six RBAC resources cover read-only, reviewer, and operator access. See `docs/how-to/observe-llm-calls.md`.
|
|
785
|
+
- `@terreno/ai/harness` runs phased tasks on Mongo that survive a process crash. Each checkpoint and its trace span commit in one transaction. Tasks can wait for a person, retry, abort from the bottom up, and stay pinned to the definition version they started with. `defineTask`, `defineAgent`, and `defineTool` describe the work; approvals have their own permissions and an admin inbox. See `docs/how-to/build-a-durable-workflow.md`.
|
|
786
|
+
|
|
787
|
+
### Fixed
|
|
788
|
+
|
|
789
|
+
- `GET` and `DELETE /files/*gcsKey` find uploads by their full key. Express 5 passes the wildcard as path segments, so a key with slashes, such as the `uploads/<userId>/<ms>-<name>` key every upload gets, returned 404.
|
|
790
|
+
|
|
791
|
+
## [57.9.0] - 2026-10-03
|
|
792
|
+
|
|
793
|
+
Upgrade note: [`mcp-server/src/docs/upgrades/57.9.0.md`](mcp-server/src/docs/upgrades/57.9.0.md).
|
|
794
|
+
|
|
795
|
+
### Added
|
|
796
|
+
|
|
797
|
+
- `@terreno/ui` can compose operations dashboards with scorecards, comparison sparklines, multi-series line and area charts, donut center labels, previous-period bar overlays, chart-card headers, and spanning dashboard items.
|
|
798
|
+
- File uploads can be turned off with the `file-uploads` feature flag. `POST /files/upload`, document storage uploads, and chat attachments return 403 when the flag is off. The example app hides the upload controls. A missing flag leaves uploads enabled.
|
|
799
|
+
- `/gpt/prompt` rejects client-only attachment URLs (`blob:`, `file:`, `content:`, `ph:`) with `400` before streaming. With `fileStorageService` configured, `data:` attachments are uploaded and history stores the storage `url` plus `gcsKey`; later turns send the model signed URLs.
|
|
800
|
+
- `/gpt/prompt` saves the turn before streaming, sends a first `{historyId, started: true, streamId}` event, and persists partial reply text about every second with `status: "streaming"`. The finished reply is saved with `status: "complete"`; a failed one keeps its partial text with `status: "error"`.
|
|
801
|
+
- New `GET /gpt/histories/:id/stream` re-attaches to an in-flight reply after a reload or remount (`offset` skips text the client already shows). The example app reopens the last chat after a reload and resumes its live reply.
|
|
802
|
+
- `store-assets` agent skill for Expo apps. It keeps a simulator/emulator dev client in sync with the native fingerprint (downloading a matching EAS build or starting one), captures App Store and Google Play screenshots on store-sized devices with Maestro flows that log in through the `@terreno/ui` LoginScreen, and renders the Play feature graphic and 512×512 icon from a repo's `storeAssets.config.json`. Install with `npx skills add TerrenoLabs/terreno --skill store-assets` or from the `terreno` Claude plugin.
|
|
803
|
+
|
|
804
|
+
### Changed
|
|
805
|
+
|
|
806
|
+
- `GPTChat` history rows move rename and delete into a three-dot overflow menu. Rename uses an outlined pencil in the dark secondary color; titles truncate before the menu trigger at any sidebar width.
|
|
807
|
+
- `GPTChat` shows "Scroll to bottom" only when content sits below the viewport, never in an empty chat.
|
|
808
|
+
- The `GPTChat` composer grows with long text up to 200px, then scrolls. `TextField` / `TextArea` accept `maxHeight` to cap `grow`.
|
|
809
|
+
- Generated images in `GPTChat` offer copy-image and (web) download. Copying an image-only reply copies the image instead of text.
|
|
810
|
+
- `FilePickerButton` opens an anchored dropdown instead of a modal. On web, **Document** now opens the file picker, and both options return `data:` URLs instead of `blob:` URLs.
|
|
811
|
+
|
|
812
|
+
### Fixed
|
|
813
|
+
|
|
814
|
+
- Web toasts now render above open modals and remain correctly centered in statically exported apps instead of inheriting a zero-width server-rendered viewport.
|
|
815
|
+
|
|
816
|
+
## [57.8.0] - 2026-10-02
|
|
817
|
+
|
|
818
|
+
Upgrade note: [`mcp-server/src/docs/upgrades/57.8.0.md`](mcp-server/src/docs/upgrades/57.8.0.md).
|
|
819
|
+
|
|
820
|
+
### Added
|
|
821
|
+
|
|
822
|
+
- `@terreno/announcements` imports a versioned release pack (`pack.yaml` plus one Markdown file per announcement) through idempotent `POST /announcements/import-release`. Imports default to drafts; pass `publish: true` to publish on import. Re-importing the same product, release version, channel, and slug updates the existing row, and a matching soft-deleted row is restored. Admin auth or `ANNOUNCEMENTS_UPLOAD_TOKEN` can call the route. Request bodies for the import and for impression and click events are strict Zod schemas.
|
|
823
|
+
- `SplitPage` accepts opt-in web layout props. `desktopChildrenMinWidth` keeps one or two desktop children at least that many pixels wide and scrolls horizontally when the row is tighter. `narrowViewportChildLabels`, `narrowViewportSelectionActive`, `narrowViewportSelectionKey`, and `narrowViewportListButtonLabel` replace the dotted narrow swiper with a labeled pager. `narrowBelowWidth` uses the narrow layout at or below that pixel width; omit it to keep `isNarrowViewport()`. Native `SplitPage` ignores these props. `IconButton` accepts `backgroundOpacity` to tint only the button background.
|
|
824
|
+
|
|
825
|
+
### Changed
|
|
826
|
+
|
|
827
|
+
- CircleCI `publish-release` compiles every lockstep package once and publishes them in parallel instead of recompiling and retesting each package in turn. Tagged commits already passed CI on master. Releases drop from 25+ minutes to about 4.
|
|
828
|
+
- `deploy-demo` runs alongside `publish-release` instead of after it.
|
|
829
|
+
- `scripts/ci/netlify-deploy.sh` passes `--no-build`, so Netlify deploys no longer rerun the root `netlify.toml` docs build (about 3.5 minutes per demo and frontend deploy).
|
|
830
|
+
|
|
831
|
+
### Deprecated
|
|
832
|
+
|
|
833
|
+
- `isMobileDevice` is deprecated. Call `isNarrowViewport` instead. The old function stays exported and returns the same result until it is removed.
|
|
834
|
+
|
|
835
|
+
### Fixed
|
|
836
|
+
|
|
837
|
+
- `DropdownPanel` now anchors to its trigger rather than to the row around it. The measured wrapper stretched to fill its container, so the panel opened at the start of that row — visible whenever the trigger is not at the start edge, such as a right-aligned button or a right-to-left layout, where the panel appeared on the opposite side of the screen.
|
|
838
|
+
- `/gpt/prompt` sends each generated image once. Image-output models no longer duplicate the SSE `image` event and saved content part, and image-only responses save empty text instead of the `(image)` placeholder. `GptHistory` prompt `text` is now required only when the prompt has no `content` parts.
|
|
839
|
+
|
|
840
|
+
## [57.7.0] - 2026-09-28
|
|
841
|
+
|
|
842
|
+
Upgrade note: [`mcp-server/src/docs/upgrades/57.7.0.md`](mcp-server/src/docs/upgrades/57.7.0.md).
|
|
843
|
+
|
|
844
|
+
### Changed
|
|
845
|
+
|
|
846
|
+
- CircleCI is now the only continuous deployer for the Netlify demo, example frontend, and docs sites and for GCP terraform, Cloud Run backend/tasks, and MCP. The GitHub Actions deploy workflows are disabled for rollback only, and CircleCI deploy jobs fail instead of skipping when `terreno-netlify` or `terreno-gcp` is missing a value. CircleCI deploys record GitHub Deployments (same environment names as before, plus `docs` and `mcp-production`) when the `terreno-github-deployments` context holds `GITHUB_DEPLOYMENTS_TOKEN`.
|
|
847
|
+
- `@terreno/ui`'s `Filter` is now `DropdownPanel`, reflecting that it is a general compositional dropdown (trigger + anchored panel + optional Apply/Clear/Cancel footer), not a filter-only control. `Filter` and `FilterProps` stay exported as deprecated aliases until Terreno 58; the `FilterSelectMenu` / `FilterBoolean` / `FilterAccordion` / `FilterChangesBadge` controls keep their names.
|
|
848
|
+
- `DropdownPanel` no longer runs off screen. The panel right-aligns to its trigger when a left-aligned panel would cross the right viewport edge, clamps to an 8px screen margin, flips above the trigger when there is no usable room below, and scrolls its body (footer pinned) when the content is taller than the space available. `align` and `maxPanelHeight` override the automatic behavior, and `computeDropdownPanelLayout` is exported for callers that position their own panels.
|
|
849
|
+
- `DropdownPanel` renders through the `TerrenoProvider` portal host on native, so the panel is no longer clipped inside scroll views or cards. Without a host it falls back to the previous inline overlay.
|
|
850
|
+
- `DropdownPanel` triggers are now styleable: `triggerVariant` (any `Button` variant), `applyButtonVariant`, `triggerSize` on labeled triggers, `fullWidth`, and `renderTrigger` for a fully custom trigger.
|
|
851
|
+
- `bun run prepush` now runs the local mirror of every CI job the branch triggers, using the CircleCI path-filter mapping. That covers package `test:coverage` plus the new-file LCOV gate, `ui` types, example-backend and example-frontend tests, every `repo-policies` step, rulesync drift, and the typedoc API reference. It runs every step even after a failure and prints a summary. Use `--dry-run` to see the plan and `--all` to run everything.
|
|
852
|
+
- New `bun run check:test-isolation` blocks new `mock.module` calls in shared Bun test suites (see `docs/explanation/test-isolation.md`).
|
|
853
|
+
- The lifecycle plugin now makes one commit per task after Roast `PASS` with no bookkeeping commits, keeps pushes out of Pick and Roast, has Pick list edge cases before coding, has Roast run the CI-equivalent package gate, and has Taste merge `master` only when the PR conflicts, a failure traces to base drift, or the branch is merge-ready.
|
|
854
|
+
- Taste invoked directly by a human now repeats bounded reactions (at most 3 fix pushes and 3 hours of waiting) until `PASS`, `BLOCKED`, or `FAIL` instead of returning `PENDING`, and a directly invoked Brew starts Taste as its next stage.
|
|
855
|
+
|
|
856
|
+
## [57.6.1] - 2026-09-25
|
|
857
|
+
|
|
858
|
+
### Fixed
|
|
859
|
+
|
|
860
|
+
- `modelRouter` array subroutes now return a structured `400` error when the requested field is not an array, after preserving consumer update authorization hooks, instead of throwing an iterable `TypeError`.
|
|
861
|
+
|
|
862
|
+
## [57.6.0] - 2026-09-24
|
|
863
|
+
|
|
864
|
+
Upgrade note: [`mcp-server/src/docs/upgrades/57.6.0.md`](mcp-server/src/docs/upgrades/57.6.0.md).
|
|
865
|
+
|
|
866
|
+
### Added
|
|
867
|
+
|
|
868
|
+
- `sendToSlack` accepts `mentionUserIds` (Slack member IDs) so incoming webhooks can @-mention people. Names and emails in the message text do not notify anyone. New helpers: `formatSlackUserMention`, `normalizeSlackUserId`, and `lookupSlackUserIdByEmail` (optional `SLACK_BOT_TOKEN` with `users:read.email` to resolve an id from email and store it on the staff/user record).
|
|
869
|
+
- Add full SyncDB support to `terreno-mcp-local`: inspect debugger and local-store
|
|
870
|
+
state, capture/compare/merge snapshots, mutate or directly edit local entities,
|
|
871
|
+
flush the outbox, reconcile/resync, resolve conflicts, retry failures, and
|
|
872
|
+
exercise offline transitions. State-changing operations require
|
|
873
|
+
`TERRENO_MCP_EVAL=1`; returned state redacts sensitive fields.
|
|
874
|
+
- Add `@terreno/cli` (`terreno`) for docs search, codegen (syncdb, RTK SDK, models, routes, screens, forms, admin), bootstrap, unified backend/browser/Metro/app logs, Redux/RTK state inspection, opt-in CDP evaluation/navigation, Bun 1.4 WebView automation and screenshot proof, read-only database tools, OpenAPI `api list|call|request`, and `generate rest-cli` to scaffold an app CLI from a backend spec.
|
|
875
|
+
|
|
876
|
+
### Changed
|
|
877
|
+
|
|
878
|
+
- Terreno moved to the `TerrenoLabs` GitHub organization. The repository is now `TerrenoLabs/terreno`, the roadmap board is `github.com/orgs/TerrenoLabs/projects/1`, and package `repository` URLs, docs links, and plugin/skill install commands (`/plugin marketplace add TerrenoLabs/terreno`, `npx skills add TerrenoLabs/terreno`) point at the new location. Old `FlourishHealth/terreno` URLs redirect.
|
|
879
|
+
- Every tsconfig builds on TypeScript 6 without `ignoreDeprecations`. Node packages (`@terreno/api`, `ai`, `admin-backend`, `announcements`, `api-health`, `comms`, `feature-flags`, `jobs`, `test`, and the `admin-spa` server plugin) now compile with `module: nodenext` to ES2023 CommonJS instead of ES5. Their dynamic `import()` stays a real ESM import, so `loadMigrations` can load migration files by `file://` URL from compiled `dist/`. Frontend libraries (`ui`, `admin-frontend`, `rtk`, `syncdb`) emit ES2022 with bundler resolution and ship declaration maps. `@terreno/test` no longer publishes its compiled Bun tests. `create-terreno-app` scaffolds drop `ignoreDeprecations` and `moduleResolution: node`, and the generated backend moves to TypeScript 6. See `docs/explanation/typescript-configuration.md`.
|
|
880
|
+
|
|
881
|
+
### Fixed
|
|
882
|
+
|
|
883
|
+
- Framework Mongoose models added in 57.3 and 57.4 now use `Terreno*` model names
|
|
884
|
+
without changing their MongoDB collections, preventing collisions with consumer
|
|
885
|
+
models such as `Notification`, `Membership`, and `Job`. Admin UI keys and
|
|
886
|
+
`admin<ModelName>` RBAC resources keep the pre-namespace names. JWT recovery
|
|
887
|
+
routes can be disabled with `authOptions.passwordReset`, `emailVerification`,
|
|
888
|
+
and `legacyResetPasswordRoute`.
|
|
889
|
+
|
|
890
|
+
## [57.4.0] - 2026-09-22
|
|
891
|
+
|
|
892
|
+
Upgrade note: [`mcp-server/src/docs/upgrades/57.4.0.md`](mcp-server/src/docs/upgrades/57.4.0.md).
|
|
893
|
+
|
|
894
|
+
### Added
|
|
895
|
+
|
|
896
|
+
- CircleCI Playwright now shards `admin-home`, `admin-form`, `admin-table-search-filter`, `admin-table-bulk-actions`, `admin-custom-screens`, and `admin-comms-back` alongside `admin`.
|
|
897
|
+
- CircleCI now runs `admin-backend-ci` and `admin-frontend-ci` (lint, compile, 95% coverage) when those packages change. Use `bun run admin-frontend:test` and `bun run admin-backend:test` from the repo root.
|
|
898
|
+
- `@terreno/admin-frontend` now ships `adminRequest`, a native `fetch` helper (timeout, JSON/`FormData`, credentials) for admin RPC that is leaving RTK `injectEndpoints`.
|
|
899
|
+
- Example admin Todos now allow admin delete, and Playwright covers Todo create → edit → delete on embedded `/admin` and SPA `/console`.
|
|
900
|
+
- Built-in admin String-`_id` model CRUD can now run windowed local-first through
|
|
901
|
+
`@terreno/syncdb`: REST supplies membership, TinyBase supplies live rows,
|
|
902
|
+
create/update/delete use the durable outbox, bulk patch stays server-side, and
|
|
903
|
+
`ConflictSheet` resolves admin-loaded records. The embedded example and
|
|
904
|
+
standalone admin SPA demonstrate Bearer and same-origin cookie hosts.
|
|
905
|
+
- `GET /admin/config` model meta includes `adminBroadcast` (always) and `syncCollection` when the app sync registry has `adminBroadcast: true`. `AdminProvider` accepts optional `syncDb`. `AdminModelTable` then uses REST membership + a live TinyBase overlay with a visible Refresh control; hosts that pass only `api` keep the RTK list. Refresh treats RTK `refetch` error envelopes as failures, drops in-flight Refresh when list params change, and toasts rejected `hydrateWindow` calls. Fetch-RPC mutations invalidate mounted comms, scripts, and configuration queries just like their RTK counterparts.
|
|
906
|
+
- `BetterAuthConfig.disableRateLimit` turns off Better Auth's built-in limiter on a throwaway in-process seed instance. Preview container seeds that create several users no longer 429 on the fourth signup when `NODE_ENV=production`.
|
|
907
|
+
- `@terreno/ui` now ships owned-SVG `LineChart`, `BarChart`, `AreaChart`, and `DonutChart`
|
|
908
|
+
plus an eager `DashboardGrid` for wrapping caller `Card`s. Charts use `react-native-svg`
|
|
909
|
+
and private d3 helpers — not `victory-native`. Each chart sizes itself to its container
|
|
910
|
+
and clips to it, so a chart never widens the card it sits in, and `height` covers the whole
|
|
911
|
+
chart including its tick and tooltip rows. `bun run ui:charts:compare` diffs actually
|
|
912
|
+
rendered gallery PNGs against `demo/rendered-snapshots/` (not JSON snapshots).
|
|
913
|
+
Conditional `DashboardGrid` children no longer reserve blank cells, and an open chart
|
|
914
|
+
tooltip now updates or closes when live data changes.
|
|
915
|
+
- `create-terreno-app` — unscoped npm CLI (`bunx create-terreno-app` /
|
|
916
|
+
`npm create terreno-app`) that scaffolds a deployable full-stack Terreno app.
|
|
917
|
+
Published in lockstep with `@terreno/*`; generated scaffolds pin
|
|
918
|
+
`^<create-terreno-app version>` on Terreno dependencies.
|
|
919
|
+
- **@terreno/ui `DataTable`**: optional server-side search and column filters emit
|
|
920
|
+
modelRouter-shaped query params via `onQueryChange` and `buildDataTableListQuery`
|
|
921
|
+
(web per-column `Filter` popovers; native Filters sheet).
|
|
922
|
+
- **@terreno/admin-frontend `AdminModelTable`**: adopts DataTable filter/search UI;
|
|
923
|
+
list search still uses `q`; choice filters support multi-value `$in`. Admin list
|
|
924
|
+
URLs use `qs` bracket serialization so nested filters work with any RTK base query.
|
|
925
|
+
- **@terreno/admin-backend**: `parseAdminListFilters` accepts choice `{$in: string[]}`
|
|
926
|
+
and escaped-literal text `{$regex, $options: "i"}` while rejecting extra operators.
|
|
927
|
+
Optional choice filters auto-enable an **Empty** option (`__empty__` wire sentinel → Mongo
|
|
928
|
+
`null`) when the Mongoose field is not required; explicit `allowEmpty` overrides.
|
|
929
|
+
Empty and Empty-plus-value filters remain normalized when combined with toolbar search.
|
|
930
|
+
- **@terreno/api**: OpenAPI query validation accepts choice scalars, `{$in: [...]}`, and
|
|
931
|
+
the empty sentinel alongside enum values. Nested query operators reject extra keys and
|
|
932
|
+
executable regex patterns; `queryFilter` can remove consumed wire keys with `undefined`.
|
|
933
|
+
- **@terreno/ui `buildDataTableListQuery`**: single concrete choice values emit scalar
|
|
934
|
+
equality (validator-friendly); empty-only and empty+concrete use `$in` with `__empty__`.
|
|
935
|
+
- **@terreno/ui `Filter`**: new `iconOnly` and `triggerSize` props render a compact
|
|
936
|
+
icon trigger for dense chrome; DataTable column headers use it.
|
|
937
|
+
- **@terreno/ui `DataTable`**: single-column filter popovers no longer duplicate the
|
|
938
|
+
popover's own Clear with a per-field **Clear filter**; the boolean per-field clear
|
|
939
|
+
now appears only where one surface hosts several filters.
|
|
940
|
+
- **@terreno/ui `DataTable`**: date range filters use date inputs, so entering a
|
|
941
|
+
calendar date filters immediately and either bound works on its own. Previously the
|
|
942
|
+
datetime input emitted nothing until an hour and minute were also entered. The **to**
|
|
943
|
+
bound now closes the chosen UTC day instead of landing on midnight, so rows recorded
|
|
944
|
+
later that day stay in range.
|
|
945
|
+
- Opt-in append-only `AuditEvent` log. Register `AuditApp` and set `audit: true` (or
|
|
946
|
+
`{redact: ["ssn"]}`) on `modelRouter` so successful HTTP CRUD and array mutations
|
|
947
|
+
persist redacted changed-field diffs. Default redaction includes compound keys such as
|
|
948
|
+
`tokenHash`. AdminApp writes the same collection when the plugin is registered and omits
|
|
949
|
+
each model's `hiddenFields` / `excludeFields`; RBAC uses
|
|
950
|
+
`persistRbacAuditToAuditEvent`. List/read is admin-only at `GET /audit-events` and
|
|
951
|
+
`/admin/audit-events`; create/update/delete over HTTP return 405. Persist is
|
|
952
|
+
fire-and-forget; a non-24-hex `actorId` (including 12-character strings mongoose would
|
|
953
|
+
accept) is dropped from the event instead of dropping the event. Update/delete snapshots
|
|
954
|
+
run only when `audit` is on, and a throwing `toJSON` does not fail the mutation. Set `GCP_TASKS_AUDIT_QUEUE` and `AUDIT_TASKS_URL` to enqueue Cloud Tasks
|
|
955
|
+
instead of writing Mongo on the request. Default retention is forever; `retentionDays`
|
|
956
|
+
adds a Mongo TTL index on `created`. See `docs/how-to/audit-log.md`.
|
|
957
|
+
- # GCP durable-jobs worker infrastructure
|
|
958
|
+
|
|
959
|
+
Infra Manager now owns the example Cloud Tasks queue `terreno-example-jobs-v2`, the
|
|
960
|
+
private `terreno-backend-example-tasks` Cloud Run service, the
|
|
961
|
+
`terreno-backend-runtime` API identity (sole queue enqueuer / jobs-invoker
|
|
962
|
+
`actAs`), and the `terreno-jobs-invoker` OIDC callback identity. CD deploys
|
|
963
|
+
matching PR tags for the API and tasks services. The example API still uses
|
|
964
|
+
`MongoJobRunner` until a follow-up selects `JOBS_RUNNER=gcp-cloud-tasks`. The
|
|
965
|
+
example image compiles `@terreno/jobs` before `@terreno/admin-backend`.
|
|
966
|
+
- `GPTChat` accepts an optional `mascot` React node from the consumer. Terreno does not
|
|
967
|
+
ship a default character. The node renders on an empty chat and hides once messages
|
|
968
|
+
exist. The empty state (mascot plus suggested prompts) stays centered in the chat
|
|
969
|
+
panel inside a scrollable message area, and the composer row centers the
|
|
970
|
+
attachment, tools, and Send controls vertically against the input. The example
|
|
971
|
+
consumer bundles four plant-robot mascots and chooses one on each AI screen mount.
|
|
972
|
+
- Unattended agents can claim one unstarted GitHub issue labeled
|
|
973
|
+
`status:ready-for-dev`, Pick ⇄ Roast a posted plan, and Brew a draft PR via the
|
|
974
|
+
`implement-ready-for-dev` skill. Pickup skips open linked PRs via GraphQL (not
|
|
975
|
+
`linked:<number>`) and aborts if an untrusted author edits the issue body after
|
|
976
|
+
the label. Dashboard paste lives in that skill's `references/cursor-automation.md`.
|
|
977
|
+
Interactive `/work-github-issues` still requires chat confirmation.
|
|
978
|
+
- `@terreno/jobs` adds durable background work for Terreno backends: MongoDB-persisted jobs
|
|
979
|
+
with retries, dead-lettering, cron schedules (IANA timezones), admin list/detail/retry/requeue/cancel,
|
|
980
|
+
and pluggable runners (`MongoJobRunner`, GCP Cloud Tasks, Vercel Queues, custom). Workers start
|
|
981
|
+
only via explicit `startWorker()` — register `JobsApp` on `TerrenoApp`, define handlers, enqueue
|
|
982
|
+
with `getJobsService()`, and choose in-process or standalone worker processes. Admin UI widgets
|
|
983
|
+
ship in `@terreno/admin-frontend`. See `docs/how-to/background-jobs.md` and
|
|
984
|
+
`docs/reference/jobs.md`.
|
|
985
|
+
- MongoDB migrations tooling: `terreno-migrate` (`check` / `generate` / `status` / `up` / `down`), `TerrenoApp` `migrations.runOnStart`, admin **Migrations** page, and `example-backend/migrations/`. Admin HTTP is `modelRouter` collection actions `GET /admin/migrations/status` and `POST /admin/migrations/run` (`{data: ...}`), documented in `/openapi.json` under the `adminMigrations` tag. Production wet apply requires `ALLOW_MIGRATIONS=true`. History lives in `terreno_migrations` and is not wiped by seed `--reset`.
|
|
986
|
+
- The in-app notification center adds `NotificationsApp` in `@terreno/api` with
|
|
987
|
+
owner-scoped `Notification` and `NotificationPreference`
|
|
988
|
+
models, `getNotificationService().notify()`, `notificationsBeforeSend`, mark-all-read, and
|
|
989
|
+
optional `retainDays` tombstone sweep. `@terreno/ui` provides presentational
|
|
990
|
+
`NotificationBell`, `NotificationInbox`, and `NotificationPreferences` components.
|
|
991
|
+
The example app demonstrates syncdb collections, a todos-header bell that toggles a
|
|
992
|
+
right-side drawer, a full active and archived history page, notification preferences,
|
|
993
|
+
seeded notification examples, todo activity notifications, and a development notify
|
|
994
|
+
route. Inbox list, mark-read, and dismiss (archive via `archivedAt`) use syncdb only;
|
|
995
|
+
`POST /notifications/dev-notify` remains the development send path. `terreno-syncdb-codegen` now accepts hyphenated collection names such as
|
|
996
|
+
`notification-preferences`. Consumers can replace the bell icon and unread bubble with
|
|
997
|
+
`NotificationBell` render props while retaining its layout and accessibility behavior.
|
|
998
|
+
- Optional organization management: `OrgsApp` adds `Organization` / `Membership`, org context, isolation filters, and membership-scoped `org-admin` plus operator RBAC. Existing single-tenant apps stay unchanged. Admin hosts get an operator directory, org switcher, settings, and members screens. Newly bootstrapped apps enable an org by default.
|
|
999
|
+
- `terreno-pick-roast-loop` drives an approved plan through all actionable Pick/Roast
|
|
1000
|
+
cycles without stopping on ordinary engineering failures. It accumulates task, attempt,
|
|
1001
|
+
verification, artifact, docs, and risk evidence into one final report. When a genuine
|
|
1002
|
+
human decision is required, it first explains the overall plan state, completed work,
|
|
1003
|
+
decisive evidence, options and impact, and its recommendation, then asks one exact
|
|
1004
|
+
question. The loop excludes Grow, Brew, Taste, and product-CI monitoring. Cursor,
|
|
1005
|
+
Codex, Claude Code, and `npx skills` expose the loop.
|
|
1006
|
+
- `@terreno/announcements` ships `AnnouncementsApp` for admin-managed modal, banner, and feed product updates. Targeting covers staff, patients, and all users, plus custom `audience` segments. Acknowledgements, impressions, frequency caps, minimum-build gates, and CTA click tracking are first-class. Admin screens include a guided overview with aggregate and per-row analytics. Frontend surfaces are `AnnouncementNavigator`, `AnnouncementBanner`, and `AnnouncementScreen` in `@terreno/ui`. See `docs/how-to/product-announcements.md`.
|
|
1007
|
+
- CircleCI `publish-release` and the GitHub `publish-on-tag.yml` fallback now publish `@terreno/announcements` in lockstep with the other `@terreno/*` packages.
|
|
1008
|
+
- `SyncConfig.adminBroadcast` (default `false`) is stored on collection registration. When `true`, change-stream `sync:delta` emission also targets `{collection}|admin`. Admin clients join that stream with `sync:subscribe {mode: "window"}` (`createSyncDb({windowCollections})`) and skip `GET /sync/snapshot` paging for those collections. Window subscribe requires admin panel and model-list access. `hydrateWindow` renders REST rows immediately, then refreshes canonical seq/deleted metadata for every requested id through `GET /sync/entities` (unknown ids ignored). AdminApp list/read permissions and `queryFilter` apply to hydrate and live deltas. `{collection}|admin` deltas apply only to ids already in the local window; Refresh is REST + `hydrateWindow`.
|
|
1009
|
+
- Admin-window sync writes use an explicit `mutationMode: "adminWindow"` marker on `POST /sync/mutate`, `POST /sync/mutate/batch`, `sync:mutate`, and `sync:mutateBatch`. `createSyncDb({windowCollections})` tags matching outbox rows automatically. The server validates the marker together with `adminBroadcast`, admin-window access, and a registered AdminApp write scope, then runs the shared sync executor with AdminApp pre/post hooks (not product `modelRouter` hooks), applying the same permission, stripping, User admin-flag/role gates, and audit semantics as REST. Product clients without the marker keep existing product sync permissions and hooks.
|
|
1010
|
+
- `@terreno/syncdb` exports `bridgeBetterAuthReactClient`, which adapts a Better Auth
|
|
1011
|
+
**react** client to the session-subscription surface `betterAuthAdapter` watches. Without
|
|
1012
|
+
it the adapter cannot see the client's nanostore session atom and falls back to polling
|
|
1013
|
+
`getSession()`. The example app and the admin SPA both use it.
|
|
1014
|
+
- `TextField` with `type="password"` now renders a show/hide eye control so users can check what
|
|
1015
|
+
they typed. The value starts masked, a disabled field cannot be revealed, and
|
|
1016
|
+
`showVisibilityToggle={false}` removes the control. The toggle is reachable in tests at
|
|
1017
|
+
`{testID}.visibility-toggle` (override with `testIDs.visibilityToggle`). `Field`, `LoginScreen`,
|
|
1018
|
+
and `SignUpScreen` password fields inherit it.
|
|
1019
|
+
- Dependency updates go through the daily `update-dependencies` skill: one trusted,
|
|
1020
|
+
same-repository rolling PR with a landed/failed ledger, a test that imports each
|
|
1021
|
+
bumped package, and a freeze on Expo fingerprint changes until a release.
|
|
1022
|
+
|
|
1023
|
+
### Changed
|
|
1024
|
+
|
|
1025
|
+
- `AdminProvider` accepts host-injected `credentials` and `getAuthHeaders` for native `adminRequest`. The admin SPA uses cookie `same-origin` credentials; the example app sends a Bearer token.
|
|
1026
|
+
- Admin RPC hooks dual-run: hosts that inject `credentials` / `getAuthHeaders` on `AdminProvider` use native `adminRequest`; hosts that pass only `api` keep RTK `injectEndpoints`.
|
|
1027
|
+
- Admin panel HTTP script runs enqueue durable job `admin/script` when `JobsApp` is registered.
|
|
1028
|
+
The Scripts UI still polls `BackgroundTask`. If runner enqueue throws, the HTTP handler
|
|
1029
|
+
marks that `BackgroundTask` failed and returns 500. CLI script runs stay in-process. See
|
|
1030
|
+
`docs/how-to/background-jobs.md`.
|
|
1031
|
+
- CircleCI starts fewer concurrent jobs: repository policies share one `repo-policies` check, Playwright runs five shards instead of one container per spec, and path filters no longer start example-backend / new-file-coverage for unrelated UI, RTK, or e2e-spec-only changes. `repo-policies` uses Node 22.14 for Knip. Require `repo-policies`; treat `e2e-*` shard names as path-filtered (config-only PRs post `e2e-auth`).
|
|
1032
|
+
- CircleCI uses smaller Docker classes for Playwright e2e shards, skips empty
|
|
1033
|
+
Netlify/GCP contexts before `bun install`, and turns off Docker Layer Caching.
|
|
1034
|
+
- The `terreno-planning` plugin now includes reusable Terreno backend/API, UI, data,
|
|
1035
|
+
schema, SDK, admin, prompt-governance, documentation, upgrade, deployment, and UI
|
|
1036
|
+
verification skills alongside the lifecycle. It also ships `pre-commit` and
|
|
1037
|
+
`ui-verifier` agents. The redundant `commit` and `create-pr` skills are removed in
|
|
1038
|
+
favor of Brew. Conflicting or unused Expo skills (`native-data-fetching`,
|
|
1039
|
+
`building-native-ui`, `expo-ui`, App Clip, brownfield, Observe, Tailwind setup,
|
|
1040
|
+
EAS update insights, and Expo module authoring) are no longer distributed. Taste's
|
|
1041
|
+
pre-push gate runs lint, typecheck, and affected tests for every supported host.
|
|
1042
|
+
Rulesync now also generates native stop hooks for Cursor, Claude Code, GitHub Copilot,
|
|
1043
|
+
and Devin that run repository lint and typecheck, emit each host's blocking JSON
|
|
1044
|
+
protocol, and avoid repeated checks on structured Stop-hook retries. Plugin
|
|
1045
|
+
`terreno-planning` is `2.8.0`.
|
|
1046
|
+
- CircleCI Playwright shards now run only when a changed file can reach them. `e2e-prepare` resolves the affected shards with `bun run check:e2e-affected` (import graph with barrel, lazy-registry, and lockfile resolution) and each shard halts before `bun install` when it is unaffected. The gate fails open on anything it cannot resolve. See [circleci.md](../../docs/how-to/circleci.md#e2e-affected-gate).
|
|
1047
|
+
- The deployed example backend selects `JOBS_RUNNER=gcp-cloud-tasks`, verifies Cloud
|
|
1048
|
+
Tasks OIDC on `POST /jobs/execute`, and points each PR preview at its matching
|
|
1049
|
+
tasks-service tag. GitHub Actions and CircleCI both deploy that tag, and production
|
|
1050
|
+
deploys overwrite env vars so preview `MONGO_DB_NAME` / `PR_NUMBER` cannot stick.
|
|
1051
|
+
The API
|
|
1052
|
+
process starts the schedule ticker; the tasks service does not. The compiled Cloud Run binary enqueues through the Cloud Tasks REST
|
|
1053
|
+
API (`google-auth-library`) because `@google-cloud/tasks` cannot load its JSON
|
|
1054
|
+
config from a `bun build --compile` image. Enqueued HTTP tasks set
|
|
1055
|
+
`dispatchDeadline` to protobuf `{seconds: 1800}` (REST JSON `"1800s"`) so Cloud
|
|
1056
|
+
Tasks does not retry a still-running 30-minute handler at the 10-minute HTTP
|
|
1057
|
+
default. The REST client encodes that Duration; the optional `@google-cloud/tasks`
|
|
1058
|
+
peer receives the protobuf object. Queue and worker service resources live in Infra
|
|
1059
|
+
Manager.
|
|
1060
|
+
- Grow now ends with a standalone approval brief instead of a 15-line index. It opens
|
|
1061
|
+
with a paragraph on where the repository is and where the change takes it, adds
|
|
1062
|
+
background on current state when a reviewer needs it, then lays out the idea and the
|
|
1063
|
+
plan (tasks, tracer, verification, out of scope, risks). The Decisions table follows
|
|
1064
|
+
the plan and now pairs every settled human decision with the question that prompted
|
|
1065
|
+
it, so a reviewer can see what was asked as well as what was chosen; it is still
|
|
1066
|
+
omitted when grilling settled none. A reviewer should be able to approve from the
|
|
1067
|
+
brief alone, without opening the IP, the ticket, or the interview history.
|
|
1068
|
+
- `implement-ready-for-dev` now Grow-shapes the claimed issue without a chat pause
|
|
1069
|
+
(assume answers unless a genuine human gate), then Pick ⇄ Roast, Brew, and Taste
|
|
1070
|
+
to a mergeable PR. Dashboard paste is in that skill's `references/cursor-automation.md`.
|
|
1071
|
+
There is no separate `autobot-ready-for-dev` skill.
|
|
1072
|
+
- `new-file-coverage` no longer reruns a package's full test suite when that package's CI already produced LCOV. The 90% new-file gate runs against the package report instead. Remaining reruns use colocated tests, compile `@terreno/*` dist deps only when imported, and coverage-script unit tests run in a separate `coverage-scripts` job.
|
|
1073
|
+
- Planning Brew titles are `[ticket] Short feature title`. The ticket uses the attached
|
|
1074
|
+
Linear id (`[FH-1632]`) or GitHub issue (`[#412]`); the rest names the feature only.
|
|
1075
|
+
Titles must not use `feat:` / `docs:` prefixes or lifecycle labels such as
|
|
1076
|
+
`IP Approved` and `Task list`.
|
|
1077
|
+
|
|
1078
|
+
PR bodies now preserve the IP's initial justification and a brief overview of its
|
|
1079
|
+
intended outcomes. Verification always includes reproducible testing instructions;
|
|
1080
|
+
Brew updates that section as testing changes while keeping the rationale and overview
|
|
1081
|
+
stable.
|
|
1082
|
+
- Planning stages and outer loops now close every wait-for-human or done chat with the
|
|
1083
|
+
current PR's GitHub Deployment demo URLs as the last visible section when those URLs
|
|
1084
|
+
exist. Plugin `terreno-planning` is `2.11.0`.
|
|
1085
|
+
- Removed the `terreno_bootstrap_app` MCP tool. The only supported scaffold path
|
|
1086
|
+
is `bunx create-terreno-app` / `npm create terreno-app`. MCP still offers
|
|
1087
|
+
`terreno_bootstrap_ai_rules` for editor rules after the CLI runs.
|
|
1088
|
+
- Roast no longer asks two unconstrained subagents to each rediscover the repository
|
|
1089
|
+
(full-branch diff plus skill catalog). Pick, Roast, and Brew pass a task-scoped
|
|
1090
|
+
briefing (`plugins/terreno-planning/references/subagent-briefing.md`): this task's
|
|
1091
|
+
criteria, file list, and patch. Roast may spawn at most one UI/runtime verifier when
|
|
1092
|
+
this task lists UI files, and must not spawn a conventions reviewer. Installable
|
|
1093
|
+
lifecycle skills copy only the plugin references they link.
|
|
1094
|
+
- Taste records failed tests from the last product-CI run and re-verifies those exact
|
|
1095
|
+
local commands before any GitHub push. `prepush` does not substitute for that
|
|
1096
|
+
re-verify. Example-frontend now exposes `test:ci` so root `bun run test` includes it.
|
|
1097
|
+
Cursor Cloud test mapping lives in `docs/how-to/run-tests-locally.md`. Plugin
|
|
1098
|
+
`terreno-planning` is `2.12.0`.
|
|
1099
|
+
- Taste now runs a repository root's `prepush` package script, when present, in its fresh
|
|
1100
|
+
no-context subagent before pushing. Repositories own the exact local gate; when the
|
|
1101
|
+
script is absent, Taste retains its affected-package lint, typecheck, and test fallback.
|
|
1102
|
+
Terreno's root `prepush` gate runs workspace lint, TypeScript compilation, and full
|
|
1103
|
+
Knip/dependency-cruiser static analysis. Plugin `terreno-planning` is `2.10.0`.
|
|
1104
|
+
- The component demo and documentation site now use distinct Terreno Garden app
|
|
1105
|
+
icons. The demo home screen also shows a Terreno Garden banner that flows inline
|
|
1106
|
+
with the component grid, filling the first two card slots.
|
|
1107
|
+
|
|
1108
|
+
### Deprecated
|
|
1109
|
+
|
|
1110
|
+
- `@terreno/admin-frontend`'s `useAdminApi` RTK `injectEndpoints` path and required
|
|
1111
|
+
`api` prop are deprecated. Terreno 57 retains them for ObjectId/API-only
|
|
1112
|
+
compatibility; Terreno 58 removes them. New admin RPC uses the host-bound fetch
|
|
1113
|
+
client, and eligible String-`_id` model CRUD uses windowed syncdb.
|
|
1114
|
+
|
|
1115
|
+
### Removed
|
|
1116
|
+
|
|
1117
|
+
- The UI component demo no longer includes announcement or in-app notification
|
|
1118
|
+
stories. Exercise those surfaces in `example-frontend` instead.
|
|
1119
|
+
|
|
1120
|
+
### Fixed
|
|
1121
|
+
|
|
1122
|
+
- The `cloud_run_service` Terraform module no longer ignores `template.revision`.
|
|
1123
|
+
Ignoring it pinned the live revision name in state, so any structural change
|
|
1124
|
+
failed with Cloud Run 409 `Revision named '<name>' with different configuration
|
|
1125
|
+
already exists`. The Cloud Tasks queue now also waits on the IAM module so the
|
|
1126
|
+
first apply cannot 403 on `cloudtasks.queues.create`.
|
|
1127
|
+
- `compile-workspace-deps` uses `tsconfig.server.json` when present so
|
|
1128
|
+
`@terreno/admin-spa` emits `src/dist` for example-backend coverage.
|
|
1129
|
+
- Compiled example-backend images (`bun build --compile`) no longer point admin migrations at `$bunfs`. The Docker image copies `migrations/` to `/app/migrations` and sets `MIGRATIONS_DIR`. A missing directory returns 404 `Migration directory not found` instead of a generic 500.
|
|
1130
|
+
- `test:coverage` and new-file coverage no longer fail when Bun 1.4.2+ exits 1 for bunfig `coverageThreshold` after every test passed. Isolated LCOV merges still enforce the 95% / 90% gates.
|
|
1131
|
+
- Terraform preview CI now prints Infra Manager `errorCode` / `errorLogs` when
|
|
1132
|
+
`previews create` fails, and `terraform-admin` gains `roles/logging.logWriter`
|
|
1133
|
+
so Cloud Build can write regional logs.
|
|
1134
|
+
- Admin Migrations re-enables Dry run / Apply when task polling fails. `terreno-migrate generate` keeps safe index operations as comments in fail-closed stubs that also include unsafe ops.
|
|
1135
|
+
- `terreno-pick-roast-loop` resumes only Pick or Roast. A stored `next: brew` finishes
|
|
1136
|
+
the loop without launching Brew, Taste, or Grow.
|
|
1137
|
+
- example-backend listens on `PORT` before connecting to MongoDB and attaching
|
|
1138
|
+
the Terreno Express app. If that boot later throws, the process closes the
|
|
1139
|
+
holder and exits so Cloud Run does not keep a 503 listener. GitHub Actions
|
|
1140
|
+
preview deploys overwrite Cloud Run revision secrets, smoke-test the same
|
|
1141
|
+
`JOBS_*` env as the revision, rebuild traffic from Ready revisions (omitting
|
|
1142
|
+
failed tags), deploy `--no-traffic` without `--tag`, then tag the new revision.
|
|
1143
|
+
- RBAC permission resolution now keys the in-memory grant cache by user id and
|
|
1144
|
+
sorted role names, so promoting a user (for example e2e `setUserAdmin`) takes
|
|
1145
|
+
effect on the next `/auth/me` instead of serving a 30s stale set.
|
|
1146
|
+
- Conflict resolution with **Keep mine** now preserves the admin-window mutation marker
|
|
1147
|
+
when cloning a durable outbox row, so its retry retains AdminApp authorization,
|
|
1148
|
+
protected-field stripping, hooks, and audit handling.
|
|
1149
|
+
- `Tooltip` no longer disappears the instant it opens on web. The portal layer that
|
|
1150
|
+
renders overlays (tooltips, modals, toasts) declared `pointerEvents: "box-none"` in an
|
|
1151
|
+
inline style, which react-native-web drops, so every mounted portal covered the app with
|
|
1152
|
+
a full-screen overlay that swallowed hover and press events. The portal layer now uses a
|
|
1153
|
+
registered `StyleSheet` style, and `Tooltip` stays off screen until its trigger has been
|
|
1154
|
+
measured instead of flashing in the top-left corner.
|
|
1155
|
+
- `TextField` on React Native Web no longer enters an update-depth loop when
|
|
1156
|
+
browser autocorrect alternates controlled values. Web autocorrect/spellcheck is
|
|
1157
|
+
disabled, the input handler stays stable, and rapid synthetic A→B→A reversals
|
|
1158
|
+
are suppressed without blocking normal typing or delayed backspace/retype.
|
|
1159
|
+
|
|
1160
|
+
## [57.3.0] - 2026-09-02
|
|
1161
|
+
|
|
1162
|
+
Upgrade note: [`mcp-server/src/docs/upgrades/57.3.0.md`](mcp-server/src/docs/upgrades/57.3.0.md).
|
|
1163
|
+
|
|
1164
|
+
### Added
|
|
1165
|
+
|
|
1166
|
+
- The lifecycle stages now ship as a Claude Code plugin. Add the marketplace with
|
|
1167
|
+
`/plugin marketplace add TerrenoLabs/terreno`, install with
|
|
1168
|
+
`/plugin install terreno@terreno-plugins`, then invoke `/terreno:1-grow`. The marketplace
|
|
1169
|
+
name is `terreno-plugins` so it does not collide with the plugin name `terreno` (Claude
|
|
1170
|
+
Code's installer breaks when those names match). Claude Code takes a
|
|
1171
|
+
plugin skill's command from the frontmatter `name`, so its shortened stage names
|
|
1172
|
+
(`1-grow` … `5-taste`) ship as a generated copy at `plugins/terreno-claude/`
|
|
1173
|
+
(`bun run skills:sync`). Cursor and `npx skills` are unchanged: plugin
|
|
1174
|
+
`terreno-planning`, stages `terreno-1-grow` … `terreno-5-taste`.
|
|
1175
|
+
- The lifecycle stages now ship as a Codex plugin. Add the marketplace with
|
|
1176
|
+
`codex plugin marketplace add TerrenoLabs/terreno`, install with
|
|
1177
|
+
`codex plugin install terreno-planning --source terreno-plugins`, then invoke
|
|
1178
|
+
`$terreno-1-grow`. Codex uses the canonical `plugins/terreno-planning/` tree
|
|
1179
|
+
(`.codex-plugin/plugin.json`) and the repo marketplace at
|
|
1180
|
+
`.agents/plugins/marketplace.json`. Stage names match Cursor and `npx skills`.
|
|
1181
|
+
- `TwilioSmsProvider` at `@terreno/comms/adapters/twilioSms` (optional peer `twilio`). Sends
|
|
1182
|
+
prefer a messaging service SID over a from-number, require valid E.164 destinations, classify
|
|
1183
|
+
Twilio error codes, and store a console deep link on accepted sends. The example backend
|
|
1184
|
+
registers the adapter when Twilio env vars are complete.
|
|
1185
|
+
- `TwilioVerifyProvider` at `@terreno/comms/adapters/twilioVerify` (optional peer `twilio`).
|
|
1186
|
+
Starts and checks SMS/email OTP via a Verify service SID, classifies Twilio errors, redacts
|
|
1187
|
+
destinations, never stores codes, and marks verification rows non-retryable. The example
|
|
1188
|
+
backend registers the adapter when `TWILIO_VERIFY_SERVICE_SID` is set with account
|
|
1189
|
+
credentials.
|
|
1190
|
+
- Admin comms dashboard: filter and inspect delivery logs, retry failed sends (including bulk retry
|
|
1191
|
+
with a cap), and view per-provider failure rates. Created and attempt timestamps print in the
|
|
1192
|
+
operator locale. List, stats, and bulk retry share a trailing 7-day window when dates are omitted.
|
|
1193
|
+
Editing a filter while that window is implicit keeps both date bounds. Push
|
|
1194
|
+
`beforeSend` cancel returns `loggedMessageId`. Retry returns the log row created by that send. Routes live on `@terreno/comms`; screens ship in
|
|
1195
|
+
`@terreno/admin-frontend` as the `comms` custom screen.
|
|
1196
|
+
- `describeModel()` and `describeModelForRouter()` in `@terreno/api` walk Mongoose schemas once and expose a canonical `ModelDescription` field graph. OpenAPI (`getOpenApiSpecForModel`), admin `/admin/config` field metadata, and MCP Zod tool schemas now format that graph instead of independently walking `schema.paths` or mongoose-to-swagger. Map fields take their value kind from Mongoose `of` / `getEmbeddedSchemaType()` (not a date fallback).
|
|
1197
|
+
|
|
1198
|
+
Exports include `modelDescriptionToOpenApiSpec`, `modelDescriptionToAdminFields`, and `fieldDescriptionToZodType`. See `docs/explanation/schema-metadata.md`.
|
|
1199
|
+
- `ExpoPushProvider` at `@terreno/comms/adapters/expoPush` (optional peer
|
|
1200
|
+
`expo-server-sdk`). `sendPush` returns one `SendResult` per token, chunks Expo
|
|
1201
|
+
payloads, classifies ticket/receipt errors, and polls receipts. `DeviceNotRegistered`
|
|
1202
|
+
deactivates `PushToken` rows via `CommsService.deactivatePushToken`. `MessageTooBig` is
|
|
1203
|
+
`errorClass: config` and does not deactivate the token. `expo-server-sdk`
|
|
1204
|
+
moved off `@terreno/api`. The example app requests notification permission, registers the device token after
|
|
1205
|
+
login, and exposes a profile-screen test send (`POST /comms/dev/testPush`) in
|
|
1206
|
+
non-production.
|
|
1207
|
+
- Added `create-github-issue` and `work-github-issues` skills plus a lifecycle
|
|
1208
|
+
work-item GitHub form so agents can file pick-ready issues, confirm a plan,
|
|
1209
|
+
post it as the Roast contract, then implement with Pick ⇄ Roast.
|
|
1210
|
+
- Opt-in HTTP rate limiting on `TerrenoApp` via `rateLimit: {}` (memory default; `redis` or `mongo` for shared buckets). Login and related auth routes use 20 requests / 15 minutes; other framework HTTP uses 600 / 15 minutes. Credential-exchange JWT routes ignore a stale access token. Trailing slashes and Express `req.path` drive the auth/api bucket. `trustProxy` defaults off (set `1` on Cloud Run). Omitted `rateLimit` is a no-op until Terreno 58. 429 is `APIError` `code: "rate-limit-exceeded"`. See `docs/how-to/rate-limiting.md`.
|
|
1211
|
+
- Inbound webhooks on `WebhooksApp`: raw-body capture, HMAC/Stripe/Twilio/SendGrid
|
|
1212
|
+
verifiers (SendGrid ECDSA uses the same 300s timestamp window as Stripe), memory or
|
|
1213
|
+
Mongo `webhookReceipts` idempotency. `CommsApp` mounts Twilio status/inbound and
|
|
1214
|
+
SendGrid Event Webhook routes when passed the same plugin. `recordDeliveryEvent`
|
|
1215
|
+
rethrows a failed `CommsMessage` save so webhook claims release. See
|
|
1216
|
+
`docs/how-to/inbound-webhooks.md`.
|
|
1217
|
+
- `terreno_search_docs` and `terreno_get_component_docs` accept an optional
|
|
1218
|
+
`version` so agents can search retained docs snapshots for the consumer's
|
|
1219
|
+
`@terreno/*` lockstep version. Snapshot component pages match both hyphenated
|
|
1220
|
+
camelCase filenames and concatenated generator slugs.
|
|
1221
|
+
- Pull requests now fail when a newly added workspace `.ts` or `.tsx` implementation
|
|
1222
|
+
file is below 90% function coverage or 90% line coverage. Run
|
|
1223
|
+
`bun run check:new-file-coverage --base=origin/master --threshold=90` locally.
|
|
1224
|
+
The gate reuses each package's `bun test` file arguments so Playwright `*.spec.ts`
|
|
1225
|
+
files are not collected. Glob arguments are expanded before spawn so packages such
|
|
1226
|
+
as `example-frontend` still collect `*.test.ts` files. Globs that match no files
|
|
1227
|
+
are omitted.
|
|
1228
|
+
- JWT and Better Auth password reset plus email verification: `POST /auth/forgotPassword` (always 202), `POST /auth/resetPassword`, `POST /auth/sendVerification`, and `POST /auth/verifyEmail`. Opt in with `emailVerificationPlugin`, `tokenEpoch`, `authOptions.publicAppUrl` / `sendMail`, and `@terreno/comms` `renderAuthMail`. Better Auth uses the same `publicAppUrl` and `sendMail`. `LoginScreen` accepts `onForgotPassword`. See `docs/how-to/password-reset.md`.
|
|
1229
|
+
- The `terreno-planning` Cursor plugin (`2.3.0`) adds two outer-loop skills beside the
|
|
1230
|
+
five stages: `terreno-planning-loop` walks the approved task list (default Grow once,
|
|
1231
|
+
then Pick once — Pick owns the pick-roast inner loop; pass `phases=` to restrict to
|
|
1232
|
+
`grow`, `pick`, `roast`, `brew`, and/or `taste`), and `terreno-taste-sweep` finds the
|
|
1233
|
+
author's open non-draft PRs that are conflicting or failing and reinvokes Taste until
|
|
1234
|
+
each is mergeable or blocked.
|
|
1235
|
+
- `Popover` in `@terreno/ui` for previewing a document with loading, loaded, and error
|
|
1236
|
+
states, an open action, and optional thumbs up/down feedback.
|
|
1237
|
+
- CI now fails production TypeScript that uses `function` declarations, `Date`/`Date.now()`, `throw new Error`, `console.log`, Mongoose `findOne`, or unsuppressed `as any`. Run `bun run check:source-rules`.
|
|
1238
|
+
- Upgrade notes format, 0.21.0–0.30.0 backfill, `terreno_get_upgrade_guide` coverage headers, versioning policy, `upgrading-terreno` skill, and changelog links to `mcp-server/src/docs/upgrades/`.
|
|
1239
|
+
|
|
1240
|
+
### Changed
|
|
1241
|
+
|
|
1242
|
+
- Brew and Taste now discover and observe product CI on every configured host, including
|
|
1243
|
+
CircleCI and Buildkite, not only GitHub check runs. Waits use provider-native hooks such
|
|
1244
|
+
as `gh pr checks --watch`, `circleci run watch`, and `bk build watch` where available,
|
|
1245
|
+
with bounded polling only as fallback. Plugin `terreno-planning` is `2.4.0`.
|
|
1246
|
+
- Brew and Taste now sleep until async review bots such as Bugbot and CodeQL finish on
|
|
1247
|
+
the current head, then continue so they can react in the same invocation. Ordinary
|
|
1248
|
+
product CI still uses Taste `PENDING` and the outer loop. Plugin `terreno-planning` is
|
|
1249
|
+
`2.2.0`.
|
|
1250
|
+
- PR GitHub Actions spend fewer minutes on docs, Playwright, Expo fingerprints,
|
|
1251
|
+
Maestro, and the example-backend Docker check. Docs previews build only the
|
|
1252
|
+
current version (unminified, no local search index) and reuse generated
|
|
1253
|
+
TypeDoc/component MDX when the source hashes match. Docusaurus Faster
|
|
1254
|
+
(Rspack) is on for PR and `master`; production still builds every versioned
|
|
1255
|
+
tree.
|
|
1256
|
+
|
|
1257
|
+
CI pins Bun `1.4.0` instead of `latest`. Playwright e2e compiles the workspace
|
|
1258
|
+
once per run and shares `dist/` with the spec shards. Example-backend CI
|
|
1259
|
+
compiles `@terreno/*` deps in one process and watches `api/**`. The backend
|
|
1260
|
+
Docker check rebuilds only when the image recipe changes; CD still builds
|
|
1261
|
+
preview images from source.
|
|
1262
|
+
|
|
1263
|
+
Backend startup now defers sync index creation until after MongoDB connects.
|
|
1264
|
+
This prevents import-time Mongoose buffering timeouts from blocking Cloud Run
|
|
1265
|
+
containers before they begin listening. The example backend also avoids the
|
|
1266
|
+
OpenTelemetry Mongoose patch that deadlocked index creation in Bun-compiled
|
|
1267
|
+
binaries, keeps startup logging on Cloud Run's captured stdout instead of a
|
|
1268
|
+
blocking network transport, and lets Better Auth-only realtime run without a
|
|
1269
|
+
legacy JWT secret.
|
|
1270
|
+
- Architectural PR review and Maestro web E2E now run on CircleCI. Matching GitHub
|
|
1271
|
+
workflows are retained with `on: []` for rollback. Cursor Approval Agent, Bugbot,
|
|
1272
|
+
and Security Agent stay on GitHub (Cursor GitHub App automations, not repo
|
|
1273
|
+
workflows).
|
|
1274
|
+
- CircleCI path filters start Netlify and GCP production and PR preview jobs.
|
|
1275
|
+
Those jobs skip (exit 0) until `terreno-netlify` and `terreno-gcp` are filled.
|
|
1276
|
+
GitHub Actions remains the live deployer in that window. After CircleCI
|
|
1277
|
+
secrets exist and a deploy succeeds, set the GHA deploy workflows back to
|
|
1278
|
+
`on: []` so terraform is not applied twice. Fork PRs skip CircleCI previews.
|
|
1279
|
+
Preview cleanup on PR close uses GitHub `preview-cleanup.yml`.
|
|
1280
|
+
- CircleCI provides Netlify and GCP deploy jobs, semver-tag npm releases, and
|
|
1281
|
+
manual preview cleanup, preview deploy, EAS development build, and
|
|
1282
|
+
single-package publish operations. Automatic production deploy path triggers
|
|
1283
|
+
stay paused until the Netlify contexts and GCP OIDC bootstrap pass manual
|
|
1284
|
+
verification. Matching GitHub CI/CD workflows are retained with `on: []` for
|
|
1285
|
+
rollback. CircleCI uses OIDC for GCP; no service-account JSON key is required.
|
|
1286
|
+
- `modelRouter` registers collections in one in-memory catalog keyed by route path. MCP, realtime, and sync read shared `ModelRouterOptions` from that catalog; `replaceCollectionOptions` updates every surface at once. Registry `clear*` helpers clear the whole catalog in tests.
|
|
1287
|
+
- `@terreno/api` RBAC routes now narrow path parameters at runtime instead of
|
|
1288
|
+
relying on implicit `any`. `@terreno/ui` Hyperlink props (`linkify`, styles,
|
|
1289
|
+
`injectViewProps`) and the Google Maps `window.google` global use concrete
|
|
1290
|
+
types instead of `any`.
|
|
1291
|
+
- Root lint now rejects explicit `any` suppressions without a `noExplicitAny:` rationale.
|
|
1292
|
+
- `improve-rulesync` now keeps user-facing guidance in `docs/` and agent-facing
|
|
1293
|
+
guidance in `.ai/` or rules, with cross-links instead of duplicated architecture.
|
|
1294
|
+
- Heavy optional `@terreno/ui` screens (`GPTChat`, `MarkdownEditor`, `ConsentFormScreen`, `AIRequestExplorer`, and the other named exports from `lazyBoundaries/heavyOptionalExports`) load through lazy boundaries so a root `@terreno/ui` import stays smaller. Named exports are unchanged.
|
|
1295
|
+
- MCP create, update, and delete tools run through the same `executeCreate` /
|
|
1296
|
+
`executeUpdate` / `executeDelete` pipeline as REST and Sync. Permission denials
|
|
1297
|
+
and hook failures use `APIError` titles in the MCP error envelope. User-role
|
|
1298
|
+
stripping happens after hooks in the executor (MCP uses the registry model
|
|
1299
|
+
name). `loadDocOr404` maps invalid document `_id` values to 404, not populate
|
|
1300
|
+
`CastError`s.
|
|
1301
|
+
- Pick and Roast now run as an automated inner loop: implement one unblocked task, roast
|
|
1302
|
+
it, then pick the next until the approved list is done. Roast never invokes Pick; Pick
|
|
1303
|
+
owns continuation. Exactly one driver continues after each Roast. Later tasks rediscover
|
|
1304
|
+
docs and skills. Brew starts only after every in-scope task has Roast `PASS`. Plugin
|
|
1305
|
+
`terreno-planning` is `2.3.0`.
|
|
1306
|
+
- Planning plugin skills (five stages plus planning-loop and taste-sweep) are model-invocable. The lifecycle checker requires those skills omit `disable-model-invocation`.
|
|
1307
|
+
- Aligned README, docs landing, docs site tagline, agent context, MCP overview, and npm package descriptions on the canonical "Django/Rails for TypeScript — with universal app support" positioning.
|
|
1308
|
+
- CircleCI is enabled again for package CI, repo policies, and Playwright e2e
|
|
1309
|
+
(`.circleci/config.yml` setup + path-filtering). Config-only PRs run a small
|
|
1310
|
+
smoke slice; mixed PRs skip that slice so path-filtered jobs are not doubled.
|
|
1311
|
+
E2E shards share one compile + `expo export`. `rulesync-check` runs only when
|
|
1312
|
+
rule sources change. CircleCI deploy jobs are available through manual
|
|
1313
|
+
parameters while automatic production path triggers remain paused. Matching
|
|
1314
|
+
GitHub deploy workflows are retained with `on: []`. See `docs/how-to/circleci.md`.
|
|
1315
|
+
- Taste waits in-process for product CI with a GitHub CLI or CircleCI CLI watch loop.
|
|
1316
|
+
Before any push it always pulls latest `master`, then runs `bun lint` (and affected
|
|
1317
|
+
tests) in a fresh subagent with no parent conversation, then pushes and watches CI.
|
|
1318
|
+
Plugin `terreno-planning` is `2.5.0`.
|
|
1319
|
+
|
|
1320
|
+
### Deprecated
|
|
1321
|
+
|
|
1322
|
+
- `modelRouter` `realtime` and `@terreno/rtk` cache-patching helpers (`realtimeList`,
|
|
1323
|
+
`realtimeDocument`, `setRealtimeSocket`, `getRealtimeSocket`) are deprecated and will
|
|
1324
|
+
be removed in Terreno **58**. Migrate collection live updates to `sync` +
|
|
1325
|
+
`@terreno/syncdb`. `RealtimeApp` remains required for sync sockets. See
|
|
1326
|
+
[migrate-rtk-to-syncdb.md](docs/how-to/migrate-rtk-to-syncdb.md) and
|
|
1327
|
+
[remove-legacy-realtime.md](docs/tasks/remove-legacy-realtime.md).
|
|
1328
|
+
|
|
1329
|
+
### Fixed
|
|
1330
|
+
|
|
1331
|
+
- Admin custom screens now show a clickable back arrow by default. The shared
|
|
1332
|
+
`AdminScreenPage` routes back to admin home reliably on web instead of depending on
|
|
1333
|
+
browser history, and `Page` supports an explicit `onBack` handler.
|
|
1334
|
+
- Better Auth lazy User create no longer sets `oauthProvider: null` on email/password sign-up, so `strict: "throw"` User schemas without that field get `req.user` on the first authenticated request instead of 401.
|
|
1335
|
+
- CircleCI Playwright chaos e2e no longer force-restarts the sync client after
|
|
1336
|
+
flaps. `goOnline` waiting for the Offline banner to hide is the reconnect
|
|
1337
|
+
signal; `client.stop()` hung for 30s on the production static export.
|
|
1338
|
+
- CircleCI automatic Netlify and GCP production path triggers are paused until
|
|
1339
|
+
their contexts and OIDC bootstrap pass manual verification. Netlify jobs now
|
|
1340
|
+
validate credentials before expensive builds, docs builds avoid minification to
|
|
1341
|
+
fit the available executor memory, and the Terraform configuration is formatted.
|
|
1342
|
+
- CircleCI no longer deploys the production demo from prerelease tags, Zoom
|
|
1343
|
+
release notify reports failure when publish fails, Maestro keeps Xvfb alive as
|
|
1344
|
+
a background step, and CircleCI OIDC can require the `terreno-gcp` context UUID
|
|
1345
|
+
before GCP impersonation.
|
|
1346
|
+
- Taste can query CircleCI with `CIRCLECI_TOKEN` on the GitHub App project slug. The
|
|
1347
|
+
new-file coverage job compiles the same workspace packages as example-backend CI.
|
|
1348
|
+
The example profile form no longer resets the name field when `/auth/me` is refetched.
|
|
1349
|
+
- Example backend Cloud Run images include `expo-server-sdk` in the compiled
|
|
1350
|
+
binary by injecting an `Expo` client into `ExpoPushProvider`. Preview deploys
|
|
1351
|
+
no longer crash at boot with a missing optional peer.
|
|
1352
|
+
- GitHub `cd.yml` GCP preview jobs (`terraform-preview`,
|
|
1353
|
+
`backend-deploy-preview`) and `preview-cleanup.yml` skip fork pull requests.
|
|
1354
|
+
OIDC `id-token` is granted only on jobs that authenticate to GCP, not the
|
|
1355
|
+
whole workflow. Fork PRs keep `repository: TerrenoLabs/terreno` on the
|
|
1356
|
+
token, so WIF would otherwise accept them.
|
|
1357
|
+
- Sync mutation date equality now parses date-only ISO strings as UTC and rejects invalid input instead of throwing. The unused datetime NumberPicker stores UTC ISO so Luxon can round-trip the picker value.
|
|
1358
|
+
- The `maestro-e2e` CI job now exports the example-frontend web bundle before starting the
|
|
1359
|
+
example backend. That executor shares its memory with the mongo service container, and
|
|
1360
|
+
bundling alongside a running backend got the export OOM-killed (`SIGKILL`) even though the
|
|
1361
|
+
same export succeeds in `e2e-prepare`.
|
|
1362
|
+
- `bun run check:new-file-coverage` now expands the glob arguments it reads from a package's
|
|
1363
|
+
`test` script before spawning `bun`, so packages without a `src/` directory (such as
|
|
1364
|
+
`example-frontend`) no longer fail with "filters did not match any test files". Expo Router
|
|
1365
|
+
route-structural entry files under `app/` (`index`, `_layout`, `+not-found`, and dynamic
|
|
1366
|
+
segments) are exempt from the gate; the router mounts them by file path and the screens they
|
|
1367
|
+
render are covered in their owning package.
|
|
1368
|
+
- JWT password reset now updates Better Auth credentials and sessions when both stacks are mounted, Better Auth password reset updates the JWT password and `tokenEpoch`, mailbox changes invalidate unused reset tokens even without `emailVerified`, authenticated verification resend returns 501 without a `publicAppUrl`, and Better Auth recovery hooks refuse to send relative links.
|
|
1369
|
+
- Select dropdown chevrons stay inside the field border in narrow selects on web. The `am`/`pm`
|
|
1370
|
+
and timezone pickers in `DateTimeField` no longer render their chevrons far to the right of
|
|
1371
|
+
their boxes.
|
|
1372
|
+
- Example backend Cloud Run images include `twilio` in the compiled binary by injecting
|
|
1373
|
+
a Twilio client into the SMS and Verify adapters. Invalid SMS destinations return a
|
|
1374
|
+
permanent `SendResult` instead of throwing, so the facade does not retry them. Partial
|
|
1375
|
+
Twilio env in the example backend throws `APIError` at boot.
|
|
1376
|
+
|
|
1377
|
+
## [57.2.0] - 2026-08-24
|
|
1378
|
+
|
|
1379
|
+
Upgrade note: [`mcp-server/src/docs/upgrades/57.2.0.md`](mcp-server/src/docs/upgrades/57.2.0.md).
|
|
1380
|
+
|
|
1381
|
+
### Breaking
|
|
1382
|
+
|
|
1383
|
+
- Lifecycle stage handoff YAML is now compact `v: 2` (`v`, `stage`, `status`, `next`,
|
|
1384
|
+
`action`; omit empty keys). Outer loops that parse `schema_version` / `recommended_next_stage`
|
|
1385
|
+
must switch keys. Chat and PRs keep the YAML in a Details toggle. Grow lists every grilled
|
|
1386
|
+
decision in an unbounded table after the 15-line index, or omits that table when there were
|
|
1387
|
+
none, and stays on a question until the answer is executable.
|
|
1388
|
+
|
|
1389
|
+
### Added
|
|
1390
|
+
|
|
1391
|
+
- Admin RBAC is now first-class: `admin:access` is the only permission that opens the admin
|
|
1392
|
+
page. Per-model access is `read` / `write` / `writeOwned` (with `isOwned` helpers and custom
|
|
1393
|
+
`authorize` callbacks). The shell hides models, custom screens, scripts, and Platform tools
|
|
1394
|
+
the caller cannot use; row update/delete follow per-record capabilities; roles use a
|
|
1395
|
+
one-select access level; denied forms are read-only.
|
|
1396
|
+
|
|
1397
|
+
### Changed
|
|
1398
|
+
|
|
1399
|
+
- Upgraded the toolchain to Bun 1.4. The GitHub Actions jobs that pinned an exact
|
|
1400
|
+
Bun version now use `1.4.0`, `@types/bun` and `bun-types` move to `^1.4.0`, and
|
|
1401
|
+
apps scaffolded by `@terreno/mcp` get `@types/bun@^1.4.0`. Contributors should
|
|
1402
|
+
run Bun 1.4 or newer locally. EAS build profiles keep their existing Bun pin:
|
|
1403
|
+
`eas.json` is an Expo fingerprint input, so bumping it would change the native
|
|
1404
|
+
runtime version and force a rebuild for a toolchain-only change.
|
|
1405
|
+
|
|
1406
|
+
## [57.1.0] - 2026-08-23
|
|
1407
|
+
|
|
1408
|
+
Upgrade note: [`mcp-server/src/docs/upgrades/57.1.0.md`](mcp-server/src/docs/upgrades/57.1.0.md).
|
|
1409
|
+
|
|
1410
|
+
### Added
|
|
1411
|
+
|
|
1412
|
+
- Admin configuration can now live beside `modelRouter` registrations or in plugin
|
|
1413
|
+
`adminContribution()` metadata. `@terreno/admin-frontend` adds a provider-backed widget registry,
|
|
1414
|
+
screen router, filter drawer, bulk actions, autocomplete references, and first-party Feature Flags,
|
|
1415
|
+
Consent, Documents, and AI admin integrations.
|
|
1416
|
+
- `terreno_bootstrap_app` scaffolds Better Auth + `@terreno/syncdb` (replica-set MongoDB,
|
|
1417
|
+
`SyncApp`/`RealtimeApp`, `SyncDbProvider`) instead of JWT `generateAuthSlice`
|
|
1418
|
+
- CircleCI dual-run for package CI, repo policies, and Playwright e2e (`.circleci/`;
|
|
1419
|
+
deploys still on GitHub Actions). See `docs/how-to/circleci.md`.
|
|
1420
|
+
- `@terreno/comms` Phase 1 gap-fill: `beforeSend` mutate/cancel, `recordDeliveryEvent` /
|
|
1421
|
+
`recordOptOut`, attempt history on `CommsMessage`, payload retention
|
|
1422
|
+
(`retainPayloadDays`, `redactPayload`), and channel-wide transient retry (SMS,
|
|
1423
|
+
verification start, per-token push). `onRetry` stays `(context, result)` with
|
|
1424
|
+
`context.attempt`. Push prune honors `errorClass: "permanent"` as well as
|
|
1425
|
+
`isPermanentFailure`.
|
|
1426
|
+
- `createCollectionHooks` in `@terreno/syncdb/react` and optional per-mutation
|
|
1427
|
+
`maxAttempts` on syncdb writes.
|
|
1428
|
+
- Model Context Protocol support in `modelRouter` via an `mcp` option: opted-in
|
|
1429
|
+
models expose their CRUD operations as MCP tools at `POST /mcp`, reusing the
|
|
1430
|
+
same permissions, query filters, population, and lifecycle hooks as REST
|
|
1431
|
+
([#358](https://github.com/TerrenoLabs/terreno/pull/358)). `getMCPTools(user)`
|
|
1432
|
+
in `@terreno/ai` returns those tools as Vercel AI SDK objects for in-process
|
|
1433
|
+
chat. MCP list filters accept Mongo comparison operators (`$in`, `$gte`, `$ne`,
|
|
1434
|
+
and friends) and top-level `$and` / `$or` on `queryFields`; operators that can
|
|
1435
|
+
execute code (`$where`, `$expr`, `$function`) are rejected. `@terreno/rtk`
|
|
1436
|
+
adds `useMCPTools()` and `useTerrenoChat()`. MCP HTTP/stdio servers use the
|
|
1437
|
+
TypeScript SDK v2 (`2026-07-28`) with a how-to guide and structured tool-call
|
|
1438
|
+
logs. Lifecycle hooks and REST `responseHandler` receive a stub Express-shaped
|
|
1439
|
+
request from `createMCPRequest` (authenticated user, tool args as `body`, empty
|
|
1440
|
+
`headers`/`query`/`params`, `isMCPRequest: true`) rather than forwarded HTTP
|
|
1441
|
+
headers. `registerMCPTool` adds custom tools alongside modelRouter CRUD. The
|
|
1442
|
+
example backend exposes `users_todo_statuses` (admin-only: every user and their
|
|
1443
|
+
todo completed flags).
|
|
1444
|
+
- Example app profiles list every assigned role and link superadmins to role editing. The admin
|
|
1445
|
+
script runner includes a guarded `resetDatabase` action, and seed data includes
|
|
1446
|
+
`superadmin@example.com`. The admin roles page supports creating and editing roles by selecting
|
|
1447
|
+
from the server's available permissions. Admin script execution requires `admin:runScripts` when
|
|
1448
|
+
RBAC is configured, and live production resets require `ALLOW_ADMIN_DB_RESET=true`.
|
|
1449
|
+
- `SendGridMailProvider` at `@terreno/comms/adapters/sendgrid` (optional peer
|
|
1450
|
+
`@sendgrid/mail`) with sandbox mode, `errorCode`/`errorClass` taxonomy, Email Activity
|
|
1451
|
+
deep links, and one transient retry via `CommsService` hooks (`onError` / `onRetry` /
|
|
1452
|
+
`onSend`).
|
|
1453
|
+
- `terreno-syncdb-codegen` CLI in `@terreno/syncdb` generates typed collection hooks
|
|
1454
|
+
(`store/syncDbSdk.ts`) from OpenAPI `x-terreno-sync` list operations. The CLI is a
|
|
1455
|
+
bin of `@terreno/syncdb`, not a separate `@terreno/syncdb-codegen` package.
|
|
1456
|
+
- `@terreno/syncdb` documentation: reference (`docs/reference/syncdb.md`), migration guide
|
|
1457
|
+
(`docs/how-to/migrate-rtk-to-syncdb.md`), and local-first explainer
|
|
1458
|
+
(`docs/explanation/local-first-data.md`)
|
|
1459
|
+
|
|
1460
|
+
### Changed
|
|
1461
|
+
|
|
1462
|
+
- User admin forms now load existing RBAC roles into an **Add role** dropdown. Assigned roles
|
|
1463
|
+
remain visible and can be removed without entering raw array values. Blank optional enum
|
|
1464
|
+
fields no longer block otherwise valid saves, and role-update responses include the new roles.
|
|
1465
|
+
- The admin sidebar groups Scripts, Roles, Version, Audit Log, Feature Flags, and Configuration
|
|
1466
|
+
under a consistently spaced **Platform** section at the bottom. Empty navigation sections are
|
|
1467
|
+
hidden, and Audit Log / Feature Flags no longer appear twice under Models.
|
|
1468
|
+
- The Terreno planning plugin 2.0 is a fresh-invocation loop-engineering lifecycle:
|
|
1469
|
+
Grow → Pick → Roast → Brew → Taste. Every bounded stage discovers repository skills,
|
|
1470
|
+
reads/writes a shared evidence-oriented result/state contract, and exits. Brew no longer
|
|
1471
|
+
executes Taste; Taste reacts once to current CI/mergeability/review state while the outer
|
|
1472
|
+
loop owns waiting and reinvocation. Terreno package commands and domain rules remain in
|
|
1473
|
+
repo-local skills. Grow retains interactive grilling and a 15-line approval summary; Pick
|
|
1474
|
+
retains vertical-slice TDD and independent implementation/test-quality review; Roast is
|
|
1475
|
+
the authoritative verifier. The former Grind behavior remains an outer-loop feature
|
|
1476
|
+
profile, and static validation enforces names, transitions, portability, and loop bounds.
|
|
1477
|
+
Deprecated repo-local routers (`ip`, `implement`, `submit`, `autobot`, `check-watcher`)
|
|
1478
|
+
are removed. Quiet package tests (`test:agent`) run with `AGENT=1`. Brew, Taste, and
|
|
1479
|
+
repository PR skills now enforce an attention-budgeted GitHub format: only `Why`, `What
|
|
1480
|
+
changed`, and `Verification` remain visible; optional detail is expandable; comments are
|
|
1481
|
+
reserved for blocked decisions or non-obvious review resolutions.
|
|
1482
|
+
- Unreleased notes are one file per feature in `changelog/unreleased/` instead of a shared
|
|
1483
|
+
`CHANGELOG.md` `## [Unreleased]` section. `docs/implementationPlans/PLAN_INDEX.md` is
|
|
1484
|
+
removed for the same reason — status lives on each IP's `**Status:**` header. Parallel
|
|
1485
|
+
PRs no longer conflict on those shared files.
|
|
1486
|
+
- GitHub Actions package CI (API, AI, UI, RTK, comms, syncdb, examples, E2E, Maestro, admin SPA) now runs on `pull_request` with path filters, and on `push` only to `master`. This stops the first push of a new branch from ignoring path filters and running unrelated jobs (the 13 Playwright shards were the main cost). Docs-only and rules-only PRs skip package CI, Rulesync, CD, and frontend/demo deploy workflows. Example Frontend Deploy PR path filters match the frontend build gate (backend-only PRs skip the workflow; mixed PRs still retarget preview `BASE_URL`).
|
|
1487
|
+
- CircleCI pipelines are disabled (`.circleci/config.yml` no-op). GitHub Actions
|
|
1488
|
+
remains the CI of record. Restore with `.circleci/config.setup.yml`. See
|
|
1489
|
+
`docs/how-to/circleci.md`.
|
|
1490
|
+
- Lifecycle stages now follow a documentation contract: read architecture docs before
|
|
1491
|
+
acting, update them in the same slice, and fail user-visible or architectural work that
|
|
1492
|
+
ships without matching docs. All agent skills are installable with
|
|
1493
|
+
`npx skills add TerrenoLabs/terreno`. The committed `skills/` tree is generated from
|
|
1494
|
+
`.rulesync/skills/`, the planning plugin stages, and `<package>/.ai/skills/` overlays via
|
|
1495
|
+
`bun run skills:sync`.
|
|
1496
|
+
- Role assignment **preview** no longer writes `RbacAudit` denied-assign rows. Denied
|
|
1497
|
+
mutation audits are recorded only for the escalation `403` (`Cannot grant permissions
|
|
1498
|
+
you do not hold`); other failures are not stored as denials, and a failed audit write
|
|
1499
|
+
does not replace that original `403`.
|
|
1500
|
+
- When a `PermissionSource` refresh fails with `staleOnFailure: "deny"` (the default),
|
|
1501
|
+
last-cached `deny` grants stay in force so IdP/ABAC restrictions do not lift. Additive
|
|
1502
|
+
`roles` / `permissions` from that source are still omitted.
|
|
1503
|
+
- RBAC `createView: "deny"` and unknown field-view names fail closed instead of granting a
|
|
1504
|
+
full mask. Nested field omits clone documents; write masks honor dotted paths. Bulk create
|
|
1505
|
+
and array mutations apply the same write mask as single-document writes.
|
|
1506
|
+
- AdminApp model CRUD requires resource actions in addition to `admin:access`. Self-service
|
|
1507
|
+
still cannot write User `admin`/`roles`. Without RBAC, admin CRUD may set `admin`.
|
|
1508
|
+
With RBAC, `roles` go through `RoleManager.assign`. Changing `admin` requires
|
|
1509
|
+
`rbac:assignRoles` plus an actor who already holds the legacy admin flag;
|
|
1510
|
+
`rbac:manageRoles` is not a substitute. Unchanged echoed `admin`
|
|
1511
|
+
values are allowed. If `assign` fails after an admin User update or bulk-patch,
|
|
1512
|
+
non-role fields are restored so the request does not keep a partial write.
|
|
1513
|
+
Role update requires the actor to hold the union of current and incoming
|
|
1514
|
+
permissions, and delete requires holding the role's current permissions, so
|
|
1515
|
+
`manageRoles` cannot empty or remove grants the actor lacks.
|
|
1516
|
+
assign/unassign require the actor to already hold the
|
|
1517
|
+
target user's current permissions. The seeded `auditor` role no longer receives
|
|
1518
|
+
`admin:access` via read-only expansion. Admin CRUD for a resource missing
|
|
1519
|
+
from statements fails closed for list/read, search, and writes. Framework statements now
|
|
1520
|
+
include `featureFlag`, `consentForm`, and `consentResponse` so superadmin `*`
|
|
1521
|
+
can list those admin models; example-backend adds `adminAuditLog` list/read.
|
|
1522
|
+
`POST /admin/background-tasks`
|
|
1523
|
+
requires `admin:runScripts`; version-config GET/PUT require `configuration:read` /
|
|
1524
|
+
`configuration:update`.
|
|
1525
|
+
- MCP and SyncDB registries use resolved RBAC options (TerrenoApp-injected and the
|
|
1526
|
+
documented `access` + `accessControl` path, including pathless `modelRouter`)
|
|
1527
|
+
instead of the pre-build legacy permission arrays. Create/update also apply
|
|
1528
|
+
`validateAccessWritePayload` (field views / `createView: "deny"`).
|
|
1529
|
+
User `roles` on modelRouter writes (HTTP, sync, and MCP) are dropped when
|
|
1530
|
+
`accessControl` is set.
|
|
1531
|
+
Example-backend `backfillAdmins` is dry-run unless `RBAC_BACKFILL_ADMINS=true`.
|
|
1532
|
+
- `runActionPermissions` combines legacy `action.permissions` with RBAC instead of replacing
|
|
1533
|
+
them. Actions without `access` inherit the router's `access.resource` and the mapped CRUD
|
|
1534
|
+
verb (`instance` POST → `update`, `collection` POST → `create`). Example todo
|
|
1535
|
+
`bulkComplete` / `markComplete` set `access: {resource: "todo", action: "update"}`
|
|
1536
|
+
so they cannot bypass `todo:update`. Empty action permissions and inherited CRUD actions
|
|
1537
|
+
mapped to `null` remain disabled. Assignment previews use uncached permission resolution
|
|
1538
|
+
on both the current and proposed sets, honor source `staleOnFailure` without writing
|
|
1539
|
+
caches, and reject role permissions the actor does not already hold. The admin role
|
|
1540
|
+
editor keeps Create/Save in the
|
|
1541
|
+
modal footer and scrolls the permission grid so a larger statement vocabulary cannot
|
|
1542
|
+
hide the save control. Create/list responses always apply the **read** field
|
|
1543
|
+
mask. Per-router `access.scope`
|
|
1544
|
+
extra PermissionSets are evaluated on HTTP and realtime reads. Sync and realtime
|
|
1545
|
+
serializers accept change-stream BSON post-images (no Mongoose `toObject`) so
|
|
1546
|
+
`sync:delta` is not dropped after RBAC wraps `defaultResponseHandler`. Invalid permission sets
|
|
1547
|
+
use a stable `APIError.title`.
|
|
1548
|
+
- Self-service signup and `PATCH /auth/me` strip `organizationIds` alongside `admin` and `roles`,
|
|
1549
|
+
preventing callers from assigning themselves tenant membership. Administrative organization
|
|
1550
|
+
membership changes must use a privileged server-side path.
|
|
1551
|
+
- Ordinary RBAC-enabled User modelRouter, sync, and MCP writes strip `admin`, `roles`, and
|
|
1552
|
+
`organizationIds`. AdminApp strips `organizationIds` the same way, and marks legacy
|
|
1553
|
+
`admin` writes only after `assignRoles` and an existing-admin check succeed.
|
|
1554
|
+
- `RoleManager` writes `RbacAudit` (with `permissionDelta`) on create, update, remove, assign,
|
|
1555
|
+
and unassign. Denied escalation attempts are stored with `denied: true`. HTTP `rbacRouter`
|
|
1556
|
+
no longer writes a second audit row. Apps can pass `auditSink` (one function or an array)
|
|
1557
|
+
to fan records into a consuming-app log; set `persistAudit: false` to skip the built-in
|
|
1558
|
+
collection (at least one sink is then required).
|
|
1559
|
+
- `RbacRole.seedDefaults` accepts `extraRoles` and shares `upsertSeededRole` with
|
|
1560
|
+
`RoleManager.seedDefaults`. `previewRoleChange` reports a real `affectedUserCount`.
|
|
1561
|
+
- Importing `@terreno/api` no longer registers `RbacRole` / `RbacAudit` on the default
|
|
1562
|
+
mongoose connection. Use `createAccess({connection})` or `createRbacRoleModel` /
|
|
1563
|
+
`createRbacAuditModel`. The `RbacRoleModel` / `RbacAuditModel` singletons are removed;
|
|
1564
|
+
the `RbacRoleModel` type remains.
|
|
1565
|
+
- The RBAC implementation plan is Complete (phases 1–6 shipped).
|
|
1566
|
+
|
|
1567
|
+
### Deprecated
|
|
1568
|
+
|
|
1569
|
+
- **`@terreno/rtk` for data synchronization** — deprecated as of **56.0.0**. Still published
|
|
1570
|
+
through the current major line; will not ship in the next major. Migrate collection CRUD to
|
|
1571
|
+
[`@terreno/syncdb`](docs/reference/syncdb.md) using
|
|
1572
|
+
[migrate-rtk-to-syncdb.md](docs/how-to/migrate-rtk-to-syncdb.md). Continue using `@terreno/rtk`
|
|
1573
|
+
for the OpenAPI SDK, Better Auth Redux, feature flags, and sockets.
|
|
1574
|
+
|
|
1575
|
+
### Fixed
|
|
1576
|
+
|
|
1577
|
+
- Admin lists no longer inherit public `queryFilter` scoping, and `adminFilter` Mongo operators
|
|
1578
|
+
are not rejected as client filters.
|
|
1579
|
+
- Admin mutations and responses consistently scrub excluded fields, including populated refs.
|
|
1580
|
+
- Plugin admin contributions forward `populatePaths`.
|
|
1581
|
+
- Document Storage clients use the contributed `/documents` API path.
|
|
1582
|
+
- Admin search applies the same `queryFilter`/`adminFilter` as list CRUD.
|
|
1583
|
+
- AI Request Explorer multi-type filters use `$in` instead of dropping the filter.
|
|
1584
|
+
- Admin list search (`q`) is a case-insensitive partial match across `searchFields`.
|
|
1585
|
+
- The filter drawer can clear all filters and disables Apply when the draft is unchanged.
|
|
1586
|
+
- Admin config `name` is unique when the same Mongoose model is mounted at more than one
|
|
1587
|
+
`routePath`, and list search/bulk-patch metadata is looked up by path.
|
|
1588
|
+
- Document download failures log a status code only — not storage paths or provider payloads.
|
|
1589
|
+
- The admin Roles screen scrolls its role cards and the "Available permissions" list instead
|
|
1590
|
+
of clipping them, and keeps the heading and "Add role" button pinned above the scroll area.
|
|
1591
|
+
- `@terreno/ui` no longer triggers the react-native-web warning `"shadow*" style props are
|
|
1592
|
+
deprecated. Use "boxShadow".` — `Filter`, `WebDropdownMenu` (select fields, timezone picker,
|
|
1593
|
+
address field), and `DraggableList` now use `boxShadow`, and the new `createBoxShadow` /
|
|
1594
|
+
`applyColorOpacity` helpers build the shadow value from a color plus opacity.
|
|
1595
|
+
- Patched `react-native-modalize` and `react-native-actions-sheet` to use `boxShadow` on
|
|
1596
|
+
iOS/web and `elevation` only on Android, so the two APIs do not stack. The modalize
|
|
1597
|
+
stylesheet ran at import time, so the deprecation warning appeared on every web page that
|
|
1598
|
+
imported `@terreno/ui`.
|
|
1599
|
+
- Android centered dropdowns (`WebDropdownMenu` `presentation="centered"`) keep `elevation` and
|
|
1600
|
+
omit `boxShadow`, so the two APIs do not stack.
|
|
1601
|
+
- Push hooks and retries are per-token; provider results are zipped to token strings
|
|
1602
|
+
after `beforeSend`. Mail payloads retain `replyTo` and `dynamicTemplateData`.
|
|
1603
|
+
`DeliveryEvent.errorClass` is persisted on the log row. `defaultFrom` is reapplied
|
|
1604
|
+
after `beforeSend`. Payload cleanup is best-effort and cannot fail `logSend` /
|
|
1605
|
+
`appendAttempt`. Hook exception text is logged only; `metadata.hookErrors` stores
|
|
1606
|
+
`hook-threw`, not `String(error)`.
|
|
1607
|
+
- Conflict `requeue` copies per-mutation `maxAttempts` onto the cloned outbox
|
|
1608
|
+
row so `retries: false` stays fail-fast after keepMine.
|
|
1609
|
+
- Expo native fingerprints for the demo and example frontend now exclude `package.json`
|
|
1610
|
+
scripts. Development and test command changes no longer trigger unnecessary native build
|
|
1611
|
+
acknowledgements or rebuilds.
|
|
1612
|
+
- MCP create/update apply REST `validation.excludeFromCreate` /
|
|
1613
|
+
`excludeFromUpdate` and MCP `excludeFields` as a write denylist on persist and
|
|
1614
|
+
hook request bodies, including nested/dot paths and literal dotted keys
|
|
1615
|
+
(`"metadata.nested.token"`). Invalid ObjectIds return a structured not-found
|
|
1616
|
+
instead of crashing on `CastError`; mixed-case 24-hex ids still work. List
|
|
1617
|
+
returns a structured error when `queryFilter` throws. A throwing
|
|
1618
|
+
`responseHandler` / `mcpResponseHandler` becomes a structured tool error instead
|
|
1619
|
+
of a protocol crash. List filters ignore queryFields that sit under an
|
|
1620
|
+
`excludeFields` parent path, matching tool schema generation. Lifecycle hooks
|
|
1621
|
+
that throw `APIError` return `error.title` to the MCP client, matching
|
|
1622
|
+
`queryFilter` handling.
|
|
1623
|
+
- `@terreno/ui` no longer triggers the React Native Web warning
|
|
1624
|
+
`props.pointerEvents is deprecated. Use style.pointerEvents`. Every component that set
|
|
1625
|
+
`pointerEvents` as a prop (`DateTimeField`, `Filter`, `SidebarNavigation`, `ToastNotifications`,
|
|
1626
|
+
`WebDropdownMenu`) now sets it in `style`.
|
|
1627
|
+
- The `react-native-portalize` dependency, which set the deprecated prop on every screen through
|
|
1628
|
+
`TerrenoProvider`, is replaced by an internal portal host. `Host` and `Portal` are now exported
|
|
1629
|
+
from `@terreno/ui` with the same API.
|
|
1630
|
+
- `seedDefaults` no longer overwrites customized unsealed roles on restart; sealed defaults still refresh from code.
|
|
1631
|
+
API tests clear `RbacRole` / `RbacAudit` in `setupDb` so leftover unsealed names cannot leak across cases.
|
|
1632
|
+
- Admin `/bulk-patch` authorizes each target document, so scoped `update` cannot patch out-of-scope ids.
|
|
1633
|
+
- Admin User CRUD can set the `admin` flag when RBAC is off. With RBAC, changing `admin` requires `rbac:assignRoles`; echoed unchanged `admin` values on create/update do not.
|
|
1634
|
+
- Permission resolver caches evict expired and overflow entries so distinct identities cannot grow unbounded.
|
|
1635
|
+
- Clearing a role description in the admin UI sends `null` so PATCH removes the field.
|
|
1636
|
+
- Admin User create rolls back the new row if `RoleManager.assign` fails after insert.
|
|
1637
|
+
- `assign` / `unassign` refuse to change a user whose current permissions the actor does not hold.
|
|
1638
|
+
- MCP model tools pick up TerrenoApp-injected `accessControl` (permissions, query filters, write masks) instead of keeping the pre-build legacy checks.
|
|
1639
|
+
- Example todos Sync Lab panel starts collapsed so the first list row stays
|
|
1640
|
+
above the tab bar on short web viewports.
|
|
1641
|
+
- `terreno-syncdb-codegen` rejects non-identifier collection/type names and
|
|
1642
|
+
JSON-escapes generated strings so a remote OpenAPI document cannot inject
|
|
1643
|
+
TypeScript.
|
|
1644
|
+
- `@terreno/syncdb` no longer misses server changes that land between its startup snapshot
|
|
1645
|
+
and the socket joining a stream's room: each `sync:subscribed` confirmation now pages the
|
|
1646
|
+
streams it names from their cursor, instead of leaving the client stale until the next
|
|
1647
|
+
periodic reconcile
|
|
1648
|
+
|
|
1649
|
+
## [57.0.0] - 2026-08-20
|
|
1650
|
+
|
|
1651
|
+
Upgrade note: [`mcp-server/src/docs/upgrades/57.0.0.md`](mcp-server/src/docs/upgrades/57.0.0.md).
|
|
1652
|
+
|
|
1653
|
+
First stable release of the Expo SDK 57 line, cut from `master` after
|
|
1654
|
+
[`#1065`](https://github.com/TerrenoLabs/terreno/pull/1065). Publishes to the npm
|
|
1655
|
+
`latest` dist-tag. Apps on `0.x` should stay pinned until they upgrade Expo.
|
|
1656
|
+
|
|
1657
|
+
### Added
|
|
1658
|
+
|
|
1659
|
+
- `Filter` and its composable select, boolean, accordion, and change-badge controls in
|
|
1660
|
+
`@terreno/ui` for desktop web filtering flows
|
|
1661
|
+
([#972](https://github.com/TerrenoLabs/terreno/pull/972))
|
|
1662
|
+
|
|
1663
|
+
### Changed
|
|
1664
|
+
|
|
1665
|
+
- Terreno's version major now tracks Expo SDK 57 (`57.x.y`). `npm install @terreno/ui`
|
|
1666
|
+
resolves to this line; pin `0.x` if the app has not upgraded Expo yet
|
|
1667
|
+
([#1065](https://github.com/TerrenoLabs/terreno/pull/1065))
|
|
1668
|
+
- Frontend peer/catalog stack moves to Expo SDK 57 / React Native 0.86.2
|
|
1669
|
+
(`expo ~57.0.14`, matching React Native DevTools and Hermes V1 fixes from
|
|
1670
|
+
`expo@57.0.9+`). React stays at `19.2.3`. Consuming apps should run
|
|
1671
|
+
`npx expo install expo@latest --fix` then rebuild native binaries
|
|
1672
|
+
([#1065](https://github.com/TerrenoLabs/terreno/pull/1065))
|
|
1673
|
+
- `@terreno/syncdb` bumps `tinybase` to `^9.5.1` for Expo SDK 57 /
|
|
1674
|
+
`expo-sqlite` type compatibility ([#1065](https://github.com/TerrenoLabs/terreno/pull/1065))
|
|
1675
|
+
|
|
1676
|
+
## [57.0.0-beta.1] - 2026-08-20
|
|
1677
|
+
|
|
1678
|
+
Upgrade note: [`mcp-server/src/docs/upgrades/57.0.0-beta.1.md`](mcp-server/src/docs/upgrades/57.0.0-beta.1.md).
|
|
1679
|
+
|
|
1680
|
+
First beta of the Expo SDK 57 line, cut from `master` after merging
|
|
1681
|
+
[`#1065`](https://github.com/TerrenoLabs/terreno/pull/1065). Publishes under the npm
|
|
1682
|
+
`beta` dist-tag; `npm install @terreno/ui` still resolves to the stable `0.x` line.
|
|
1683
|
+
|
|
1684
|
+
### Added
|
|
1685
|
+
|
|
1686
|
+
- `Filter` and its composable select, boolean, accordion, and change-badge controls in
|
|
1687
|
+
`@terreno/ui` for desktop web filtering flows
|
|
1688
|
+
([#972](https://github.com/TerrenoLabs/terreno/pull/972))
|
|
1689
|
+
|
|
1690
|
+
### Changed
|
|
1691
|
+
|
|
1692
|
+
- Terreno's version major now tracks Expo SDK 57 (`57.x.y`). The stable `0.x` packages are
|
|
1693
|
+
unaffected; this beta does not move `latest`
|
|
1694
|
+
([#1065](https://github.com/TerrenoLabs/terreno/pull/1065))
|
|
1695
|
+
- Frontend peer/catalog stack moves to Expo SDK 57 / React Native 0.86.2
|
|
1696
|
+
(`expo ~57.0.14`, matching React Native DevTools and Hermes V1 fixes from
|
|
1697
|
+
`expo@57.0.9+`). React stays at `19.2.3`. Consuming apps should run
|
|
1698
|
+
`npx expo install expo@latest --fix` then rebuild native binaries
|
|
1699
|
+
([#1065](https://github.com/TerrenoLabs/terreno/pull/1065))
|
|
1700
|
+
- `@terreno/syncdb` bumps `tinybase` to `^9.5.1` for Expo SDK 57 /
|
|
1701
|
+
`expo-sqlite` type compatibility ([#1065](https://github.com/TerrenoLabs/terreno/pull/1065))
|
|
1702
|
+
|
|
1703
|
+
## [56.0.0-beta.2] - 2026-08-17
|
|
1704
|
+
|
|
1705
|
+
Upgrade note: [`mcp-server/src/docs/upgrades/56.0.0-beta.2.md`](mcp-server/src/docs/upgrades/56.0.0-beta.2.md).
|
|
1706
|
+
|
|
1707
|
+
Second beta of the Expo SDK 56 line, cut from `master` after merging
|
|
1708
|
+
[`#976`](https://github.com/TerrenoLabs/terreno/pull/976). Publishes under the npm
|
|
1709
|
+
`beta` dist-tag; `npm install @terreno/ui` still resolves to the stable `0.x` line.
|
|
1710
|
+
|
|
1711
|
+
### Added
|
|
1712
|
+
|
|
1713
|
+
- Expo SDK 56 target for frontend packages: `expo ~56.0.12`, `react-native 0.85.3`,
|
|
1714
|
+
`react 19.2.3`, TypeScript 6
|
|
1715
|
+
([#976](https://github.com/TerrenoLabs/terreno/pull/976))
|
|
1716
|
+
- `@terreno/syncdb` local-first data layer (TinyBase MergeableStore, durable outbox,
|
|
1717
|
+
websocket delta sync, encrypted web persistence) plus `SyncApp` / sync protocol support
|
|
1718
|
+
in `@terreno/api`
|
|
1719
|
+
([#976](https://github.com/TerrenoLabs/terreno/pull/976))
|
|
1720
|
+
- `SyncStatusBanner` and `ConflictSheet` in `@terreno/ui` for sync UX
|
|
1721
|
+
([#976](https://github.com/TerrenoLabs/terreno/pull/976))
|
|
1722
|
+
- `@terreno/comms` with pluggable mail, SMS, push, and verification contracts, console
|
|
1723
|
+
development providers, delivery logging, owner-scoped push-token routes, an admin delivery
|
|
1724
|
+
explorer, and generated RTK Query hooks
|
|
1725
|
+
([#1037](https://github.com/TerrenoLabs/terreno/pull/1037))
|
|
1726
|
+
|
|
1727
|
+
### Changed
|
|
1728
|
+
|
|
1729
|
+
- Terreno's version major now tracks the Expo SDK major it targets (`56.x.y` for Expo 56).
|
|
1730
|
+
The stable `0.x` packages are unaffected; this beta does not move `latest`
|
|
1731
|
+
([#976](https://github.com/TerrenoLabs/terreno/pull/976))
|
|
1732
|
+
- Frontend peer/catalog stack moves to Expo 56 / React Native 0.85 / React 19.2 /
|
|
1733
|
+
TypeScript 6 — consuming apps must upgrade Expo before installing this beta
|
|
1734
|
+
([#976](https://github.com/TerrenoLabs/terreno/pull/976))
|
|
1735
|
+
- `excludeArchivedPlugin` now filters `findOne` the same way as `find`, matching
|
|
1736
|
+
`isDeletedPlugin`
|
|
1737
|
+
- Sync/REST CRUD executors preserve kebab-case error `code` values
|
|
1738
|
+
(`create-not-allowed`, `update-not-allowed`, `delete-not-allowed`,
|
|
1739
|
+
`invalid-request-body`) and status-specific error names on hook-rejection paths
|
|
1740
|
+
- Publish workflow pins `@terreno/ui` to the release version for prerelease
|
|
1741
|
+
`admin-frontend` / `admin-spa` publishes (same as `ui` / `rtk`)
|
|
1742
|
+
- Catalog `@shopify/react-native-skia` bumped to `2.6.5` (drops the postinstall that
|
|
1743
|
+
could hang `bun install --frozen-lockfile`); removed unused payment/native deps from
|
|
1744
|
+
`example-frontend`
|
|
1745
|
+
- The `terreno-5-dialin` plugin skill now owns merge conflicts that appear after the
|
|
1746
|
+
Pour handoff: it checks PR mergeability each cycle, resolves conflicts by merging the
|
|
1747
|
+
base branch without rewriting pushed history, re-runs checks and frontend
|
|
1748
|
+
verification, and treats a conflicted PR as broken rather than mergeable
|
|
1749
|
+
([#1039](https://github.com/TerrenoLabs/terreno/pull/1039))
|
|
1750
|
+
|
|
1751
|
+
### Fixed
|
|
1752
|
+
|
|
1753
|
+
- `generateTokens` in `@terreno/api` now logs and falls back to its default expiration when
|
|
1754
|
+
`TOKEN_EXPIRES_IN` or `REFRESH_TOKEN_EXPIRES_IN` is not a valid duration, instead of throwing
|
|
1755
|
+
from `jwt.sign`
|
|
1756
|
+
- Sync seq claims under Mongoose 9: `claimSyncSeqs` passes `updatePipeline: true` for
|
|
1757
|
+
its aggregation-pipeline `findOneAndUpdate` (required after the master mongoose 9 merge)
|
|
1758
|
+
- `RealtimeApp` accepts injectable `SocketServer` / `startChangeStreamWatcher` so setup
|
|
1759
|
+
tests no longer use process-wide `mock.module` (which broke socketAuth + sync integration)
|
|
1760
|
+
- Mongoose 9 `insertMany` probe in `syncFrontier.test.ts` no longer calls removed `next()`
|
|
1761
|
+
- Sync save error middleware: `throw` instead of `next(error)` under Mongoose 9/Kareem 3,
|
|
1762
|
+
and release pending seq claims on any failed claimed save (not only VersionError)
|
|
1763
|
+
- Drop orphaned `ui` consent-history PDF test left after the move to `admin-frontend`
|
|
1764
|
+
|
|
1765
|
+
## [0.31.0] - 2026-08-11
|
|
1766
|
+
|
|
1767
|
+
Upgrade note: [`mcp-server/src/docs/upgrades/0.31.0.md`](mcp-server/src/docs/upgrades/0.31.0.md).
|
|
1768
|
+
|
|
1769
|
+
### Added
|
|
1770
|
+
|
|
1771
|
+
- `EditableCard` in `@terreno/ui`: a summary card with an optional icon, badge,
|
|
1772
|
+
description, helper text, edit button, and attention state
|
|
1773
|
+
([#981](https://github.com/TerrenoLabs/terreno/pull/981))
|
|
1774
|
+
- MIT `LICENSE` file in every published package, plus contribution guide, changelog,
|
|
1775
|
+
and GitHub issue/PR templates ([#985](https://github.com/TerrenoLabs/terreno/pull/985))
|
|
1776
|
+
- Public roadmap generated from `docs/` into GitHub, with `roadmap:generate` /
|
|
1777
|
+
`roadmap:check` scripts and roadmap discussion setup
|
|
1778
|
+
([#986](https://github.com/TerrenoLabs/terreno/pull/986), [#991](https://github.com/TerrenoLabs/terreno/pull/991))
|
|
1779
|
+
- `check:upgrade-docs` release gate that fails a tagged publish when a release
|
|
1780
|
+
documents breaking, changed, deprecated, or removed behavior without an
|
|
1781
|
+
`mcp-server/src/docs/upgrades/<version>.md` note ([#987](https://github.com/TerrenoLabs/terreno/pull/987))
|
|
1782
|
+
- Deployment foundation for GCP, deploy guides, and agentic SDLC plugin phase 1
|
|
1783
|
+
([#988](https://github.com/TerrenoLabs/terreno/pull/988))
|
|
1784
|
+
- Reference documentation for `@terreno/ai`, `@terreno/admin-spa`, and `@terreno/test`,
|
|
1785
|
+
plus full READMEs for `admin-backend`, `admin-frontend`, `ai`, and `api-health`
|
|
1786
|
+
([#995](https://github.com/TerrenoLabs/terreno/pull/995))
|
|
1787
|
+
|
|
1788
|
+
### Changed
|
|
1789
|
+
|
|
1790
|
+
- Mongoose 9 support: the workspace runs on Mongoose 9.7.4 and every published
|
|
1791
|
+
package widens its `mongoose` peer dependency to `^8.0.0 || ^9.0.0`, so Mongoose 8
|
|
1792
|
+
consumers keep working ([#984](https://github.com/TerrenoLabs/terreno/pull/984))
|
|
1793
|
+
- All published packages are MIT licensed; they were previously Apache-2.0
|
|
1794
|
+
([#985](https://github.com/TerrenoLabs/terreno/pull/985))
|
|
1795
|
+
- `modelRouter` accepts a wider Mongoose model generic, so models carrying custom
|
|
1796
|
+
query helpers, methods, or virtuals no longer need a cast ([#984](https://github.com/TerrenoLabs/terreno/pull/984))
|
|
1797
|
+
- `findOneOrNoneFor` takes `ModelQuery<T>` instead of Mongoose's `FilterQuery<T>`
|
|
1798
|
+
([#984](https://github.com/TerrenoLabs/terreno/pull/984))
|
|
1799
|
+
- `@terreno/admin-backend` reads array field metadata through Mongoose's public
|
|
1800
|
+
`getEmbeddedSchemaType()` (falling back to the Mongoose 8 `caster`) and only emits
|
|
1801
|
+
`itemEnum` when the embedded enum is an array ([#984](https://github.com/TerrenoLabs/terreno/pull/984))
|
|
1802
|
+
- Local development requires Node >= 20.19.0 ([#984](https://github.com/TerrenoLabs/terreno/pull/984))
|
|
1803
|
+
- Positioning copy blocks and an honest framework comparison table
|
|
1804
|
+
([#993](https://github.com/TerrenoLabs/terreno/pull/993))
|
|
1805
|
+
- Implementation plans and program docs for the infrastructure MCP server, the B2B
|
|
1806
|
+
platform program, the IP + roadmap flow, and the RTK-to-syncdb migration strategy,
|
|
1807
|
+
with merged plans linked to their roadmap tracking issues
|
|
1808
|
+
([#990](https://github.com/TerrenoLabs/terreno/pull/990), [#996](https://github.com/TerrenoLabs/terreno/pull/996), [#998](https://github.com/TerrenoLabs/terreno/pull/998), [#999](https://github.com/TerrenoLabs/terreno/pull/999), [#1028](https://github.com/TerrenoLabs/terreno/pull/1028))
|
|
1809
|
+
- Test coverage, rule alignment, and explicit-any remediation across `api`, `ui`, and `rtk`
|
|
1810
|
+
([#946](https://github.com/TerrenoLabs/terreno/pull/946), [#977](https://github.com/TerrenoLabs/terreno/pull/977), [#978](https://github.com/TerrenoLabs/terreno/pull/978), [#979](https://github.com/TerrenoLabs/terreno/pull/979), [#980](https://github.com/TerrenoLabs/terreno/pull/980), [#982](https://github.com/TerrenoLabs/terreno/pull/982), [#983](https://github.com/TerrenoLabs/terreno/pull/983), [#1000](https://github.com/TerrenoLabs/terreno/pull/1000), [#1001](https://github.com/TerrenoLabs/terreno/pull/1001), [#1002](https://github.com/TerrenoLabs/terreno/pull/1002))
|
|
1811
|
+
|
|
1812
|
+
### Fixed
|
|
1813
|
+
|
|
1814
|
+
- CI queues `terreno-example` EAS native builds only for new fingerprints
|
|
1815
|
+
([#965](https://github.com/TerrenoLabs/terreno/pull/965))
|
|
1816
|
+
- Keep the Dial In PR loop active through slow or pending CI, and only return
|
|
1817
|
+
broken checks when no autonomous action can advance them or user direction is
|
|
1818
|
+
required ([#1027](https://github.com/TerrenoLabs/terreno/pull/1027))
|
|
1819
|
+
- Dial In preserves existing PR descriptions instead of overwriting them
|
|
1820
|
+
([#1030](https://github.com/TerrenoLabs/terreno/pull/1030))
|
|
1821
|
+
|
|
1822
|
+
## [0.30.0] - 2026-08-03
|
|
1823
|
+
|
|
1824
|
+
Upgrade note: [`mcp-server/src/docs/upgrades/0.30.0.md`](mcp-server/src/docs/upgrades/0.30.0.md) (consolidated 0.21.0 → 0.30.0).
|
|
1825
|
+
|
|
1826
|
+
### Added
|
|
1827
|
+
|
|
1828
|
+
- `ThumbsUpDownFeedback` UI component ([#948](https://github.com/TerrenoLabs/terreno/pull/948))
|
|
1829
|
+
|
|
1830
|
+
### Fixed
|
|
1831
|
+
|
|
1832
|
+
- Pass `APIError` context through `modelRouter` without re-wrapping errors raised
|
|
1833
|
+
inside hooks or Mongoose middleware ([#967](https://github.com/TerrenoLabs/terreno/pull/967))
|
|
1834
|
+
- `modelRouter` no longer drops `status`, `title`, `detail`, `code`, and `meta`
|
|
1835
|
+
when re-throwing an `APIError` from create/update/delete handlers, populate,
|
|
1836
|
+
`queryFilter`, list serialization, or array-operation paths
|
|
1837
|
+
- Mongoose validation and cast errors from `modelRouter` writes surface per-field
|
|
1838
|
+
messages via `meta.fields` again instead of generic wrapper titles
|
|
1839
|
+
- `queryFilter` wrapper details use readable message extraction instead of
|
|
1840
|
+
`String(error)`
|
|
1841
|
+
- Framework-thrown errors follow the 0.28.0 contract: stable `title`, per-occurrence
|
|
1842
|
+
text in `detail`, kebab-case `code`, and status subclasses where applicable
|
|
1843
|
+
- `apiUnauthorizedMiddleware` only matches plain `Error("Unauthorized")`, so a
|
|
1844
|
+
`ForbiddenError` titled `"Unauthorized"` stays 403
|
|
1845
|
+
|
|
1846
|
+
### Changed
|
|
1847
|
+
|
|
1848
|
+
- Add `errorDetail(error)` helper for nested `APIError` detail in framework wrappers
|
|
1849
|
+
- Test coverage, rule alignment, and explicit-any remediation commits ([#959](https://github.com/TerrenoLabs/terreno/pull/959), [#963](https://github.com/TerrenoLabs/terreno/pull/963), [#968](https://github.com/TerrenoLabs/terreno/pull/968), [#969](https://github.com/TerrenoLabs/terreno/pull/969))
|
|
1850
|
+
|
|
1851
|
+
## [0.29.0] - 2026-07-31
|
|
1852
|
+
|
|
1853
|
+
### Added
|
|
1854
|
+
|
|
1855
|
+
- Typed and draw/type signature capture fields in `@terreno/ui` ([#958](https://github.com/TerrenoLabs/terreno/pull/958))
|
|
1856
|
+
- Short-attention-span skill for action-first AI output ([#956](https://github.com/TerrenoLabs/terreno/pull/956))
|
|
1857
|
+
|
|
1858
|
+
## [0.28.0] - 2026-07-30
|
|
1859
|
+
|
|
1860
|
+
### Added
|
|
1861
|
+
|
|
1862
|
+
- Open source launch program: 15 implementation plans, task lists, and the
|
|
1863
|
+
`build-terreno-app` dogfooding skill ([#942](https://github.com/TerrenoLabs/terreno/pull/942))
|
|
1864
|
+
|
|
1865
|
+
### Changed
|
|
1866
|
+
|
|
1867
|
+
- Redesign `APIError` to use standard `Error` fields for Sentry grouping ([#949](https://github.com/TerrenoLabs/terreno/pull/949))
|
|
1868
|
+
- Rename `SelectField` `searchable` prop to `disableSearch` ([#954](https://github.com/TerrenoLabs/terreno/pull/954))
|
|
1869
|
+
|
|
1870
|
+
### Fixed
|
|
1871
|
+
|
|
1872
|
+
- Queue only missing platform dev builds on EAS PR slow path ([#955](https://github.com/TerrenoLabs/terreno/pull/955))
|
|
1873
|
+
|
|
1874
|
+
## [0.27.0] - 2026-07-29
|
|
1875
|
+
|
|
1876
|
+
### Added
|
|
1877
|
+
|
|
1878
|
+
- Terreno-native agent skills (`building-terreno-apps`, `terreno-data-fetching`,
|
|
1879
|
+
`terreno-backend-api`, `terreno-ui`) ([#941](https://github.com/TerrenoLabs/terreno/pull/941))
|
|
1880
|
+
- Explicit-any audit script for tracking `any` usage across the monorepo ([#927](https://github.com/TerrenoLabs/terreno/pull/927))
|
|
1881
|
+
- RBAC permissions API design doc ([#887](https://github.com/TerrenoLabs/terreno/pull/887))
|
|
1882
|
+
|
|
1883
|
+
### Changed
|
|
1884
|
+
|
|
1885
|
+
- Document custom icon registration in `@terreno/ui` ([#910](https://github.com/TerrenoLabs/terreno/pull/910))
|
|
1886
|
+
- Change example-backend seed admin user to `admin@example.com` (password
|
|
1887
|
+
unchanged) ([#935](https://github.com/TerrenoLabs/terreno/pull/935))
|
|
1888
|
+
- Require frontend app login, feature exercise, and PR evidence in agent
|
|
1889
|
+
workflows ([#915](https://github.com/TerrenoLabs/terreno/pull/915))
|
|
1890
|
+
|
|
1891
|
+
### Fixed
|
|
1892
|
+
|
|
1893
|
+
- Fix `Modal` Confirm button on native Android tablets ([#952](https://github.com/TerrenoLabs/terreno/pull/952))
|
|
1894
|
+
- Guard `@terreno/rtk` web tests against `IsWeb` platform mock leakage ([#934](https://github.com/TerrenoLabs/terreno/pull/934))
|
|
1895
|
+
|
|
1896
|
+
## [0.26.0] - 2026-07-15
|
|
1897
|
+
|
|
1898
|
+
### Added
|
|
1899
|
+
|
|
1900
|
+
- `IconButton` gains an `active` interaction state via `state?: "default" | "active"`
|
|
1901
|
+
([#885](https://github.com/TerrenoLabs/terreno/pull/885))
|
|
1902
|
+
|
|
1903
|
+
### Changed
|
|
1904
|
+
|
|
1905
|
+
- Remove internal barrel imports; ban new internal barrel `index.ts` files via
|
|
1906
|
+
Biome lint and `check:no-barrel-imports` ([#907](https://github.com/TerrenoLabs/terreno/pull/907))
|
|
1907
|
+
- Order Terreno planning skills by workflow step (`terreno-1-blend` through
|
|
1908
|
+
`terreno-5-dialin`) ([#900](https://github.com/TerrenoLabs/terreno/pull/900))
|
|
1909
|
+
|
|
1910
|
+
### Fixed
|
|
1911
|
+
|
|
1912
|
+
- Fix `Table`/`DataTable` preview cards on the demo home page rendering as a
|
|
1913
|
+
floating overlay ([#911](https://github.com/TerrenoLabs/terreno/pull/911))
|
|
1914
|
+
- Fix demo, docs, and example-frontend production Netlify deploys silently
|
|
1915
|
+
no-opping on push to `master` ([#903](https://github.com/TerrenoLabs/terreno/pull/903))
|
|
1916
|
+
- Fix duplicate `if` keys in example-app E2E and Admin SPA integration workflows
|
|
1917
|
+
([#901](https://github.com/TerrenoLabs/terreno/pull/901))
|
|
1918
|
+
- Stabilize AI and admin frontend test suites ([#902](https://github.com/TerrenoLabs/terreno/pull/902))
|
|
1919
|
+
|
|
1920
|
+
## [0.25.0] - 2026-07-12
|
|
1921
|
+
|
|
1922
|
+
### Added
|
|
1923
|
+
|
|
1924
|
+
- `@terreno/api`: HTTP client layer — `createAuthenticatedClient`,
|
|
1925
|
+
`normalizeApiError`, and `withApiErrorHandling` ([#870](https://github.com/TerrenoLabs/terreno/pull/870))
|
|
1926
|
+
- `SelectField` / `WebDropdownMenu`: type-to-filter searchable dropdown
|
|
1927
|
+
([#615](https://github.com/TerrenoLabs/terreno/pull/615))
|
|
1928
|
+
- Demo AI palette generator (`/palette`) with WCAG contrast checks ([#863](https://github.com/TerrenoLabs/terreno/pull/863))
|
|
1929
|
+
|
|
1930
|
+
### Changed
|
|
1931
|
+
|
|
1932
|
+
- Add repo subagents and upgrade rulesync to v9 ([#892](https://github.com/TerrenoLabs/terreno/pull/892))
|
|
1933
|
+
- Include run evidence (screenshots/videos) in PRs via submit and pour skills
|
|
1934
|
+
([#871](https://github.com/TerrenoLabs/terreno/pull/871))
|
|
1935
|
+
|
|
1936
|
+
### Fixed
|
|
1937
|
+
|
|
1938
|
+
- `@terreno/api`: return `401` (not `500`) for auth failures in `bun build --compile`
|
|
1939
|
+
binaries ([#894](https://github.com/TerrenoLabs/terreno/pull/894))
|
|
1940
|
+
- Website: prune docs versions correctly so `versions.json` stays valid ([#866](https://github.com/TerrenoLabs/terreno/pull/866))
|
|
1941
|
+
|
|
1942
|
+
## [0.24.0] - 2026-07-03
|
|
1943
|
+
|
|
1944
|
+
### Added
|
|
1945
|
+
|
|
1946
|
+
- `AiSuggestionBox`: `hidden` suggestion status, condensed collapsed states, race-safe
|
|
1947
|
+
expansion, and refreshed sparkles/thumbs visuals ([#865](https://github.com/TerrenoLabs/terreno/pull/865))
|
|
1948
|
+
|
|
1949
|
+
### Changed
|
|
1950
|
+
|
|
1951
|
+
- Test coverage and explicit-any remediation commits ([#852](https://github.com/TerrenoLabs/terreno/pull/852), [#851](https://github.com/TerrenoLabs/terreno/pull/851), [#861](https://github.com/TerrenoLabs/terreno/pull/861), [#862](https://github.com/TerrenoLabs/terreno/pull/862))
|
|
1952
|
+
|
|
1953
|
+
## [0.23.1] - 2026-07-02
|
|
1954
|
+
|
|
1955
|
+
### Added
|
|
1956
|
+
|
|
1957
|
+
- Architectural PR review workflow ([#844](https://github.com/TerrenoLabs/terreno/pull/844))
|
|
1958
|
+
|
|
1959
|
+
### Changed
|
|
1960
|
+
|
|
1961
|
+
- Document Cursor Cloud dev-environment setup for example-backend MongoDB ([#845](https://github.com/TerrenoLabs/terreno/pull/845))
|
|
1962
|
+
- Demo Appium CI non-blocking unless a mobile build feature changes ([#846](https://github.com/TerrenoLabs/terreno/pull/846))
|
|
1963
|
+
- Consolidate shared dependencies into Bun catalog ([#848](https://github.com/TerrenoLabs/terreno/pull/848))
|
|
1964
|
+
- Align dev API port with docs; clear required on warning ([#842](https://github.com/TerrenoLabs/terreno/pull/842))
|
|
1965
|
+
|
|
1966
|
+
### Fixed
|
|
1967
|
+
|
|
1968
|
+
- Correct example-backend consent enum snapshot ordering ([#628](https://github.com/TerrenoLabs/terreno/pull/628))
|
|
1969
|
+
- Fix lint regression and iOS Appium smoke timeout ([#843](https://github.com/TerrenoLabs/terreno/pull/843))
|
|
1970
|
+
- Fix E2E todos web server startup in CI ([#811](https://github.com/TerrenoLabs/terreno/pull/811))
|
|
1971
|
+
- Resolve UI and demo formatting CI failures ([#789](https://github.com/TerrenoLabs/terreno/pull/789))
|
|
1972
|
+
- Stop Expo fingerprint churn forcing a native build on every PR ([#849](https://github.com/TerrenoLabs/terreno/pull/849))
|
|
1973
|
+
- `Button`: use transparent background for ghost variant ([#858](https://github.com/TerrenoLabs/terreno/pull/858))
|
|
1974
|
+
|
|
1975
|
+
## [0.23.0] - 2026-06-26
|
|
1976
|
+
|
|
1977
|
+
### Added
|
|
1978
|
+
|
|
1979
|
+
- `@terreno/ui` compound components expose predictable dot-suffixed test IDs ([#832](https://github.com/TerrenoLabs/terreno/pull/832), [#840](https://github.com/TerrenoLabs/terreno/pull/840))
|
|
1980
|
+
- Admin Script Runner CLI (`runScriptCli`, declared `args` on scripts) ([#828](https://github.com/TerrenoLabs/terreno/pull/828))
|
|
1981
|
+
- Consent response list/read populate `userId`; `ConsentResponseViewer` user section
|
|
1982
|
+
([#833](https://github.com/TerrenoLabs/terreno/pull/833))
|
|
1983
|
+
|
|
1984
|
+
### Changed
|
|
1985
|
+
|
|
1986
|
+
- Document `GCP_SA_*` service account secrets in cloud agent instructions ([#829](https://github.com/TerrenoLabs/terreno/pull/829))
|
|
1987
|
+
- Add example-backend Script Runner CI workflow and CLI docs ([#828](https://github.com/TerrenoLabs/terreno/pull/828))
|
|
1988
|
+
|
|
1989
|
+
### Fixed
|
|
1990
|
+
|
|
1991
|
+
- Equalize `MarkdownEditor` edit and preview pane heights ([#831](https://github.com/TerrenoLabs/terreno/pull/831))
|
|
1992
|
+
|
|
1993
|
+
## [0.22.2] - 2026-06-24
|
|
1994
|
+
|
|
1995
|
+
### Fixed
|
|
1996
|
+
|
|
1997
|
+
- `@terreno/api`: declare `@terreno/test` as a runtime dependency so
|
|
1998
|
+
`@terreno/api/testing` resolves under isolated installs ([#827](https://github.com/TerrenoLabs/terreno/pull/827))
|
|
1999
|
+
|
|
2000
|
+
### Changed
|
|
2001
|
+
|
|
2002
|
+
- Publish `@terreno/feature-flags` for the first time since `0.21.0`; all packages
|
|
2003
|
+
move to `0.22.2` to stay in lockstep
|
|
2004
|
+
|
|
2005
|
+
## [0.22.1] - 2026-06-24
|
|
2006
|
+
|
|
2007
|
+
### Fixed
|
|
2008
|
+
|
|
2009
|
+
- `@terreno/test`: pin `qs` to `^6.14.1` instead of a missing `catalog:` entry so
|
|
2010
|
+
the publish workflow succeeds ([#824](https://github.com/TerrenoLabs/terreno/pull/824))
|
|
2011
|
+
|
|
2012
|
+
### Changed
|
|
2013
|
+
|
|
2014
|
+
- Publish `@terreno/test`, `@terreno/ai`, `@terreno/admin-backend`, and
|
|
2015
|
+
`@terreno/feature-flags` for the first time at `0.22.1`; all packages move to
|
|
2016
|
+
`0.22.1` to stay in lockstep
|
|
2017
|
+
|
|
2018
|
+
## [0.22.0] - 2026-06-24
|
|
2019
|
+
|
|
2020
|
+
### Added
|
|
2021
|
+
|
|
2022
|
+
- New `@terreno/test` package: shared Bun test helpers and in-memory MongoDB fixtures
|
|
2023
|
+
([#822](https://github.com/TerrenoLabs/terreno/pull/822), [#823](https://github.com/TerrenoLabs/terreno/pull/823))
|
|
2024
|
+
- `Button`: `ghost` variant and `sm` size; fix `outline` button height ([#816](https://github.com/TerrenoLabs/terreno/pull/816))
|
|
2025
|
+
- `OpenApiMiddlewareBuilder.withOperationId()` for custom OpenAPI `operationId` ([#815](https://github.com/TerrenoLabs/terreno/pull/815))
|
|
2026
|
+
|
|
2027
|
+
### Changed
|
|
2028
|
+
|
|
2029
|
+
- Add SyncDB local-first data layer plan and tasks ([#739](https://github.com/TerrenoLabs/terreno/pull/739))
|
|
2030
|
+
|
|
2031
|
+
## [0.21.0] - 2026-06-22
|
|
2032
|
+
|
|
2033
|
+
Upgrade note: [`mcp-server/src/docs/upgrades/0.21.0.md`](mcp-server/src/docs/upgrades/0.21.0.md).
|
|
2034
|
+
|
|
2035
|
+
### Changed
|
|
2036
|
+
|
|
2037
|
+
- **Breaking:** `setupServer` removed — use `TerrenoApp` instead ([#795](https://github.com/TerrenoLabs/terreno/pull/795))
|
|
2038
|
+
- **Breaking:** JSON object responses include `requestId` in the body ([#793](https://github.com/TerrenoLabs/terreno/pull/793))
|
|
2039
|
+
|
|
2040
|
+
### Added
|
|
2041
|
+
|
|
2042
|
+
- Admin UI v2 backend: `schemaVersion: 2` config, bulk-patch, background tasks ([#782](https://github.com/TerrenoLabs/terreno/pull/782))
|
|
2043
|
+
- Traceable API logging: `createScopedLogger`, `createFeatureFlaggedLogger` ([#799](https://github.com/TerrenoLabs/terreno/pull/799))
|
|
2044
|
+
- `Card` redesign: `container` and `display` variants ([#375](https://github.com/TerrenoLabs/terreno/pull/375))
|
|
2045
|
+
- MCP documentation search: `terreno_search_docs`, `terreno_get_component_docs` ([#796](https://github.com/TerrenoLabs/terreno/pull/796))
|
|
2046
|
+
- `@terreno/rtk` exports `devStore` utilities ([#782](https://github.com/TerrenoLabs/terreno/pull/782))
|
|
2047
|
+
|
|
2048
|
+
### Fixed
|
|
2049
|
+
|
|
2050
|
+
- Unblock Dependabot by upgrading `@terreno/ai` multer dependency ([#803](https://github.com/TerrenoLabs/terreno/pull/803))
|
|
2051
|
+
- Fix CI: install workspace deps before Trigger EAS Workflow dispatch ([#805](https://github.com/TerrenoLabs/terreno/pull/805))
|
|
2052
|
+
|
|
2053
|
+
## [0.20.2] - 2026-06-17
|
|
2054
|
+
|
|
2055
|
+
### Changed
|
|
2056
|
+
|
|
2057
|
+
- Normalize `Badge` height to 20px; add Badge vs SelectBadge demo ([#751](https://github.com/TerrenoLabs/terreno/pull/751))
|
|
2058
|
+
|
|
2059
|
+
## [0.20.1] - 2026-06-16
|
|
2060
|
+
|
|
2061
|
+
### Added
|
|
2062
|
+
|
|
2063
|
+
- `@terreno/ui`: support registering custom icons ([#771](https://github.com/TerrenoLabs/terreno/pull/771))
|
|
2064
|
+
- Expo skills for AI agents ([#777](https://github.com/TerrenoLabs/terreno/pull/777))
|
|
2065
|
+
- Admin UI v2 IP with Django-style `home.slots` ([#775](https://github.com/TerrenoLabs/terreno/pull/775))
|
|
2066
|
+
- `@terreno/ai`: export `./parseAiJson` subpath ([#783](https://github.com/TerrenoLabs/terreno/pull/783))
|
|
2067
|
+
|
|
2068
|
+
### Changed
|
|
2069
|
+
|
|
2070
|
+
- Normalize skill descriptions to single-line UI summaries ([#772](https://github.com/TerrenoLabs/terreno/pull/772))
|
|
2071
|
+
|
|
2072
|
+
### Fixed
|
|
2073
|
+
|
|
2074
|
+
- Restore demo CI ordering checks ([#770](https://github.com/TerrenoLabs/terreno/pull/770))
|
|
2075
|
+
- Fix Appium dev-client smoke tests on Android and iOS ([#773](https://github.com/TerrenoLabs/terreno/pull/773))
|
|
2076
|
+
|
|
2077
|
+
## [0.16.0] - 2026-06-02
|
|
2078
|
+
|
|
2079
|
+
Historical `@terreno/api` release notes preserved from the former
|
|
2080
|
+
`api/CHANGELOG.md` (packages now version in lockstep from 0.20.0 onward).
|
|
2081
|
+
|
|
2082
|
+
### Added
|
|
2083
|
+
|
|
2084
|
+
- **`modelRouter` actions** — `instanceActions` and `collectionActions` on
|
|
2085
|
+
`ModelRouterOptions` for named operations at `/resource/:id/action` and
|
|
2086
|
+
`/resource/action`
|
|
2087
|
+
- **`loadDocOr404`** — shared document loader used by permission middleware and
|
|
2088
|
+
instance actions (soft-delete-aware 404 metadata preserved)
|
|
2089
|
+
|
|
2090
|
+
### Changed
|
|
2091
|
+
|
|
2092
|
+
- Permission middleware doc loading delegates to `loadDocOr404` (behavior-preserving)
|
|
2093
|
+
|
|
2094
|
+
### Dependencies
|
|
2095
|
+
|
|
2096
|
+
- Added `@asteasolutions/zod-to-openapi` ^8.5.0
|
|
2097
|
+
- Added **`zod` ^4.3.6 as a peer dependency** for backends defining action Zod
|
|
2098
|
+
schemas
|
|
2099
|
+
|
|
2100
|
+
### Migration
|
|
2101
|
+
|
|
2102
|
+
- Regenerate frontend SDKs after adding actions; `operationId` values follow
|
|
2103
|
+
`{tag}_{actionName}` (e.g. `todos_markComplete`)
|
|
2104
|
+
|
|
2105
|
+
## [0.20.0] - 2026-06-14
|
|
2106
|
+
|
|
2107
|
+
Upgrade note: [`mcp-server/src/docs/upgrades/0.20.0.md`](mcp-server/src/docs/upgrades/0.20.0.md).
|
|
2108
|
+
|
|
2109
|
+
### Changed
|
|
2110
|
+
|
|
2111
|
+
- **Breaking:** `@terreno/api` `ConfigurationApp`: `POST {basePath}/list-secrets` is
|
|
2112
|
+
now read-only validation/status — it no longer resolves or returns secret values;
|
|
2113
|
+
`PATCH {basePath}` strips `secret: true` fields ([#768](https://github.com/TerrenoLabs/terreno/pull/768))
|
|
2114
|
+
- **`configurationPlugin` no longer adds the `_singleton` unique index by default**
|
|
2115
|
+
— opt in via `enforceSingletonIndex: true`
|
|
2116
|
+
- **`configurationPlugin` singleton semantics are soft-delete aware**
|
|
2117
|
+
- **`configurationPlugin.updateConfig` applies updates via `findOneAndUpdate({$set})`
|
|
2118
|
+
with dotted paths** instead of `Object.assign` + `doc.save()`
|
|
2119
|
+
- Prefix `terreno-planning` Cursor plugin skills with `terreno-` ([#764](https://github.com/TerrenoLabs/terreno/pull/764))
|
|
2120
|
+
- Submit skill: merge-first PR body updates ([#765](https://github.com/TerrenoLabs/terreno/pull/765))
|
|
2121
|
+
|
|
2122
|
+
### Added
|
|
2123
|
+
|
|
2124
|
+
- `@terreno/api` configuration and secret upgrades: `CompositeSecretProvider`,
|
|
2125
|
+
`CachingSecretProvider`, pluggable `permissions`, `preUpdate`/`postUpdate` hooks,
|
|
2126
|
+
optional `version` on secret resolution ([#768](https://github.com/TerrenoLabs/terreno/pull/768))
|
|
2127
|
+
- OpenFeature migration for feature flags: `MongoFeatureFlagProvider`,
|
|
2128
|
+
`GET …/flagConfiguration`, `useTerrenoFeatureFlags` hook ([#761](https://github.com/TerrenoLabs/terreno/pull/761))
|
|
2129
|
+
- `SecretProvider.getSecret(secretName, version?)` and `flattenToDotPaths` export
|
|
2130
|
+
|
|
2131
|
+
### Deprecated
|
|
2132
|
+
|
|
2133
|
+
- Legacy `GET …/evaluate` feature-flag endpoint (sends `Deprecation`/`Sunset` headers)
|
|
2134
|
+
([#761](https://github.com/TerrenoLabs/terreno/pull/761))
|
|
2135
|
+
|
|
2136
|
+
[57.0.0]: https://github.com/TerrenoLabs/terreno/releases/tag/57.0.0
|
|
2137
|
+
[57.0.0-beta.1]: https://github.com/TerrenoLabs/terreno/releases/tag/57.0.0-beta.1
|
|
2138
|
+
[56.0.0-beta.2]: https://github.com/TerrenoLabs/terreno/releases/tag/56.0.0-beta.2
|
|
2139
|
+
[0.30.0]: https://github.com/TerrenoLabs/terreno/releases/tag/0.30.0
|
|
2140
|
+
[0.29.0]: https://github.com/TerrenoLabs/terreno/releases/tag/0.29.0
|
|
2141
|
+
[0.28.0]: https://github.com/TerrenoLabs/terreno/releases/tag/0.28.0
|
|
2142
|
+
[0.27.0]: https://github.com/TerrenoLabs/terreno/releases/tag/0.27.0
|
|
2143
|
+
[0.26.0]: https://github.com/TerrenoLabs/terreno/releases/tag/0.26.0
|
|
2144
|
+
[0.25.0]: https://github.com/TerrenoLabs/terreno/releases/tag/0.25.0
|
|
2145
|
+
[0.24.0]: https://github.com/TerrenoLabs/terreno/releases/tag/0.24.0
|
|
2146
|
+
[0.23.1]: https://github.com/TerrenoLabs/terreno/releases/tag/0.23.1
|
|
2147
|
+
[0.23.0]: https://github.com/TerrenoLabs/terreno/releases/tag/0.23.0
|
|
2148
|
+
[0.22.2]: https://github.com/TerrenoLabs/terreno/releases/tag/0.22.2
|
|
2149
|
+
[0.22.1]: https://github.com/TerrenoLabs/terreno/releases/tag/0.22.1
|
|
2150
|
+
[0.22.0]: https://github.com/TerrenoLabs/terreno/releases/tag/0.22.0
|
|
2151
|
+
[0.21.0]: https://github.com/TerrenoLabs/terreno/releases/tag/0.21.0
|
|
2152
|
+
[0.20.2]: https://github.com/TerrenoLabs/terreno/releases/tag/0.20.2
|
|
2153
|
+
[0.20.1]: https://github.com/TerrenoLabs/terreno/releases/tag/0.20.1
|
|
2154
|
+
[0.20.0]: https://github.com/TerrenoLabs/terreno/releases/tag/0.20.0
|
|
2155
|
+
[0.16.0]: https://github.com/TerrenoLabs/terreno/releases/tag/0.16.0
|