@checkstack/backend 0.15.0 → 0.16.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/CHANGELOG.md +167 -0
- package/package.json +16 -16
- package/src/index.ts +102 -6
- package/src/plugin-manager/api-router.ts +47 -8
- package/src/plugin-manager/app-principal-authz.test.ts +178 -0
- package/src/plugin-manager/auth-passthrough.test.ts +259 -0
- package/src/plugin-manager/core-services.ts +78 -0
- package/src/plugin-manager/pg-http-errors.test.ts +65 -0
- package/src/plugin-manager/pg-http-errors.ts +85 -0
- package/src/plugin-manager/plugin-loader.getservice.test.ts +38 -0
- package/src/plugin-manager/plugin-loader.skip-naming.test.ts +115 -0
- package/src/plugin-manager/plugin-loader.ts +64 -5
- package/src/plugin-manager.ts +5 -1
- package/src/services/collector-registry.ts +9 -0
- package/src/services/dev-auth.test.ts +71 -9
- package/src/services/dev-auth.ts +42 -5
- package/src/services/health-check-registry.test.ts +34 -0
- package/src/services/health-check-registry.ts +7 -0
- package/src/services/service-registry.test.ts +60 -0
- package/src/utils/plugin-discovery.test.ts +29 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,172 @@
|
|
|
1
1
|
# @checkstack/backend
|
|
2
2
|
|
|
3
|
+
## 0.16.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 9dcc848: Add the AI platform: a transport-agnostic tool spine, an OAuth Authorization Server + read-only MCP server, a propose/apply flow with audit log, a streaming in-app chat agent, per-conversation permission modes, per-integration spend caps, and user-scoped tool authorization.
|
|
8
|
+
|
|
9
|
+
Two new packages, `@checkstack/ai-common` (the `AiTool` contract, `read`/`mutate`/`destructive` effect classification, the `ai.*` access rules, the OpenAI-compatible connection shape, and the wire contracts) and `@checkstack/ai-backend` (the tool registry, extension points, principal-to-tool resolver, shared zod-to-JSON-Schema serializer, and all transports). The OpenAI-compatible integration provider registers through the existing integration provider extension point, so its API key is stored in the Secrets Vault and configured in the generic Connections UI.
|
|
10
|
+
|
|
11
|
+
What ships:
|
|
12
|
+
|
|
13
|
+
- Tool spine and extension points: `aiToolExtensionPoint.registerTool` (hand-authored composite tools) and `aiToolProjectionExtensionPoint.expose` (opt-in projections of existing oRPC procedures). Authorization mirrors `autoAuthMiddleware` exactly - a tool is surfaced only when every `requiredAccessRules` entry is satisfied, so a scope-narrowed principal can only ever see fewer tools.
|
|
14
|
+
- OAuth + MCP: Checkstack can act as its own OAuth 2.1 Authorization Server (authorization code + PKCE, consent screen, Dynamic Client Registration) and expose a read-only MCP server over Streamable HTTP at `/api/ai/mcp`. Off by default, enabled by the admin `ai.mcp-oauth` setting. A Bearer OAuth-token branch is added to the auth strategy; token scopes are intersected live with the bound user's access rules on every call. A shared-Postgres rate limiter throttles the DCR endpoint per client IP. `getMcpOAuthSettings` / `setMcpOAuthSettings` contracts added to `@checkstack/auth-common`. A minimal OAuth consent page (`/auth/oauth-consent`) renders the requesting client and scopes.
|
|
15
|
+
- Propose/apply + audit: a transport-agnostic two-step service - `propose` re-checks authz, runs the tool's `dryRun` without mutating, and returns a single-use proposal token (the `proposed` audit row IS the token store, 10-minute TTL, atomic single-use); `apply` re-parses the server-stored payload, re-checks authz, and atomically commits. The `ai_tool_calls` audit table records every call across both transports with a SHA-256 args hash (never raw arguments) and stamps who proposed and who applied. An `ai.toolCalled` event carries metadata only.
|
|
16
|
+
- In-app chat: a server-side, provider-agnostic Vercel AI SDK agent loop (OpenAI, Azure, OpenRouter, Ollama, vLLM, LM Studio, ...). The model provider is built on the backend from the integration credentials, so the API key never leaves the backend. The loop offers only resolver-allowed tools, auto-runs read tools (re-entering the live router as the logged-in user) and routes mutating / destructive tools through propose/apply. Durable conversation persistence (`ai_conversations`, `ai_messages`, owner-scoped RPCs) plus a streaming chat UI with a confirm-card component and per-integration model picker.
|
|
17
|
+
- Per-conversation permission mode (Claude-Code-style approve/auto), a durable `permission_mode` column on `ai_conversations` (default `approve`). `read` always auto-runs in both modes; `mutate` inherits the mode (auto-applies server-side in `auto`, confirm-carded in `approve`); `destructive` ALWAYS requires the human `applyTool` in both modes. Security invariant (structural + tested): the mode is consulted only on the `mutate` branch, so no `(effect, mode)` pair routes a destructive tool to auto-apply.
|
|
18
|
+
- Per-integration LLM spend cap (optional `spendCap` = `tokenBudget` + `windowMinutes`, default OFF). Spend is tracked in a shared-Postgres `ai_spend` ledger; enforcement is a rolling-window SUM run before each turn (HTTP 429 over budget). Per-principal tool rate-limit budgets are a rolling COUNT over `ai_tool_calls`, enforced on both transports. An absent / empty / incomplete `spendCap` is treated as "no cap" rather than rejected.
|
|
19
|
+
- Full tool-call replay: `ai_messages.model_messages` (jsonb) persists the canonical AI-SDK `ResponseMessage[]` per turn and replays them verbatim on the next turn; legacy rows fall back to text-only replay.
|
|
20
|
+
- Enforced no-secret-leak scrubbing: `appendMessage` runs `scrubContent` on every write, redacting credential-shaped keys and high-confidence credential values; a canary regression test asserts injected secrets are stripped. A hardening test suite asserts no secret appears in any AI-surface DTO and that handler-side authz holds when the model misbehaves.
|
|
21
|
+
- Provider correctness: the chat provider uses `@ai-sdk/openai-compatible`'s `chatModel` (plain `/chat/completions`), so OpenAI-compatible gateways (OpenRouter, DeepSeek, Ollama, vLLM) no longer reject turns with `invalid_prompt`; `@ai-sdk/openai` is removed.
|
|
22
|
+
|
|
23
|
+
BREAKING CHANGES:
|
|
24
|
+
|
|
25
|
+
- The `AiTool` contract (`@checkstack/ai-common`) gained a `TRpc` type parameter, and both `dryRun` and `execute` now receive a USER-SCOPED `rpcClient` arg bound to the originating user. Every plugin procedure a tool calls re-enters the live router AS THAT USER, so handler-side authorization (access rules AND per-resource/team scope) is enforced exactly as a direct UI/RPC call - closing a prior privilege-escalation where tools captured a trusted service client at construction. A hand-authored tool MUST resolve its plugin client from this per-call arg and MUST NOT capture a trusted service client at factory scope. Tool factories that previously took `{ rpcClient }` should drop that parameter.
|
|
26
|
+
- `AiToolProjectionExtensionPoint.expose` no longer takes a second `pluginMetadata` argument; the owning metadata lives on `input.sourcePluginMetadata`. Callers must drop the second argument.
|
|
27
|
+
|
|
28
|
+
State and scale: conversations, messages, the audit log, proposal tokens, the rate-limit counter, and the spend ledger all live in shared Postgres, so every pod answers identically and the agent loop is resumable on any pod. The only pod-local state is the live MCP connection registry (bookkeeping, never a source of truth). Cross-pod conversation readback, the spend cap, and the tool budget are verified by env-gated two-pod integration tests.
|
|
29
|
+
|
|
30
|
+
This is a beta minor.
|
|
31
|
+
|
|
32
|
+
- 9dcc848: Automations now run as a configured service account, removing implicit god-mode from the dispatch path.
|
|
33
|
+
|
|
34
|
+
BREAKING: every automation must declare a `runAs` application (service account). Previously every automation action ran as the trusted service client, bypassing all access-rule, per-resource, and team-scope checks - so an automation could touch any team's data. Now each automation runs as a bounded `application` principal, and every data-access call an action makes is authorized exactly as that identity. An automation with no `runAs` fails to run with a clear error rather than falling back to the trusted client; legacy automations must be assigned a service account before they run again.
|
|
35
|
+
|
|
36
|
+
What changed:
|
|
37
|
+
|
|
38
|
+
- New top-level field `runAs` on automations (a `run_as_application_id` column + create/update inputs; `AutomationSchema.runAs`). Required on create; GitOps sets it via the `run-as` metadata label.
|
|
39
|
+
- A new `coreServices.rpcClientAs(applicationId)` mints a short-lived, backend-signed app-principal token; the auth service resolves it LIVE to an `application` principal (reusing `enrichApplicationPrincipal`), so it flows through full `autoAuthMiddleware` enforcement. The dispatch engine threads this client into every action's `execute` as the required `context.rpcClient`.
|
|
40
|
+
- Bind authority (anti-escalation): a user may only bind an application whose access rules are a subset of their own (`isApplicationBindable`); `getBindableApplications` lists only bindable apps, and the create/update handlers enforce the check.
|
|
41
|
+
- `notification.sendTransactional` moves from service-only to access-gated (`notification.send`, a new access rule), so an automation's `runAs` can call the built-in `notify_user` / `notification.send` actions; trusted services still bypass via short-circuit.
|
|
42
|
+
- A "Run as (Service Account)" picker in the automation editor, populated from `getBindableApplications` (server-side filtered to bindable apps), seeding from the loaded `runAs` on edit and passing it into create + update. First-class teaching UX: an inline info banner, a blocked Save with an inline hint until one is chosen, and an empty state linking to the Applications admin + docs when none are bindable.
|
|
43
|
+
|
|
44
|
+
State and scale: `runAs` resolution is a pure read over shared tables; the app-principal token is self-contained and verified statelessly, so the per-run client is correct under horizontal scale.
|
|
45
|
+
|
|
46
|
+
This is a beta minor.
|
|
47
|
+
|
|
48
|
+
- 9dcc848: Add environments as a first-class catalog primitive, with per-environment health-check fan-out, config templating, per-environment reactive health, and script run-context exposure.
|
|
49
|
+
|
|
50
|
+
- Catalog primitive: an environment is a sibling of groups - a named, instance-global record carrying free-form custom fields (baseUrl, region, tier, ...) that any system can belong to many-to-many. New `environments` + `systems_environments` tables, `EnvironmentSchema` + create/update schemas, `EntityService` environment CRUD and membership joins, RPC endpoints gated by a new `catalogAccess.environment` access rule, a GitOps `Environment` kind + `System.environments` extension, and frontend management (an `EnvironmentEditor`, an Environments management panel, and a per-system environment picker). The Environments card's Add/Edit/Delete affordances are gated on `catalogAccess.environment.manage`.
|
|
51
|
+
- Per-environment fan-out: run identity becomes `(systemId, configurationId, environmentId)`. Runs, aggregates, and state transitions gain a nullable `environmentId`. The health-check assignment gains an `environmentIds` selector with three modes (All / Specific / None; `null` and `[]` are distinct). The queue executor resolves the effective environment set via the catalog `resolveSystemEnvironments` read and executes one isolated run per environment.
|
|
52
|
+
- Config templating: a new `x-templatable` config-field marker renders a string field through the template engine at execute time, against `{ environment, check, system }`. A shared `renderTemplatableConfig` and a `renderTemplatePreview` helper (re-exported from `@checkstack/template-engine`) keep editor previews identical to the run-time render. The HTTP collector's `url`, `headers[].value`, and `body` are templatable, rendered per environment (the strategy client build moves inside the per-env loop); the `url`'s `.url()` validation moves post-render. Secrets resolve before templating; a field marked both secret and `x-templatable` is rejected at plugin load. `DynamicForm` shows a live "Preview" line, and the catalog `EnvironmentPreviewPicker` ("Preview as: <environment>") drives it in the collector editor (only when the schema has a templatable field).
|
|
53
|
+
- Script run-context: `CollectorRunContext` gains an optional `environment` field (`{ id, name, fields }`, metadata only). Shell collectors receive `CHECKSTACK_ENV_ID` / `_NAME` / `CHECKSTACK_ENV_<FIELD>` vars; inline TS collectors read `globalThis.context.environment`; the editor test panel mirrors both. The env-less path is unchanged.
|
|
54
|
+
- Per-environment reactive health (see BREAKING below), env-keyed read/write paths, env-qualified serialization locks, an optional `trigger.payload.environmentId`, per-environment isolation, and an `ENVIRONMENT_RESOLUTION_FAILED` signal when catalog resolution degrades to a single env-less run.
|
|
55
|
+
|
|
56
|
+
BREAKING CHANGES: the reactive `health` entity's id-shape and cardinality change. It now encodes two views: per-environment (id `"<systemId>::<environmentId>"`) and a system rollup (id `"<systemId>"`, the worst status across environments + env-less runs). The rollup PRESERVES the pre-existing system-level contract - dashboards, status badges, and automations referencing health by `systemId` keep working without re-authoring - but the entity's contract surface changed (new id-shape, higher cardinality, new payload field), so it is flagged breaking. `getBulkHealthState` parses env-qualified ids and keys results by the original id.
|
|
57
|
+
|
|
58
|
+
State and scale: membership and custom fields live only in catalog Postgres and are re-read every tick via the cross-plugin RPC; env-keyed health reads from shared `health_check_runs` / aggregates / transitions (compute-on-read). Every pod resolves the same effective set and the same per-environment health. No pod-local environment state.
|
|
59
|
+
|
|
60
|
+
Also: `unwrapSchema` in `zod-config.ts` loops instead of single-pass-stripping so multi-layer wrappers (`.optional().default()`) still resolve `x-templatable` meta. The env-less `{{ environment.* }}` run notice logs at `debug` (a legitimate recurring configuration), while the post-render HTTP `.url()` check still fails a genuinely-broken empty render with a clear "Rendered URL is invalid" error.
|
|
61
|
+
|
|
62
|
+
This is a beta minor.
|
|
63
|
+
|
|
64
|
+
- 9dcc848: Move primary navigation into a left sidebar, and serve the user guide in-app.
|
|
65
|
+
|
|
66
|
+
Feature navigation (a ~20-item user-menu dropdown) now lives in a persistent left sidebar (a slide-over drawer on mobile), grouped by section with the active route highlighted; the user menu keeps only account actions. A route opts into the sidebar with new `nav` metadata (`{ group, icon, label?, order?, accessRule? }`) on its registration, co-located with path + access + title. The sidebar filters entries with the same access check as page guards. `@checkstack/common` gains `isAccessRuleSatisfied` and a centralized set of in-app doc slugs (`APP_DOC_SLUGS` + `docsPath`, with a test asserting each resolves to a real docs page); `@checkstack/auth-frontend` exports `useAccessRules`.
|
|
67
|
+
|
|
68
|
+
The backend now serves the Astro Starlight docs build same-origin at `/checkstack/*` (the same artifact deployed to GitHub Pages), so the user guide is available inside the app including for self-hosted / air-gapped installs (served verbatim, no rebuild, no link rewriting; from `CHECKSTACK_DOCS_DIST`, before the SPA catch-all, degrading gracefully when absent; the Docker image builds and ships `docs/dist`; Vite proxies `/checkstack` in dev). The "Docs" link is a shell-owned external sidebar entry under the Documentation group (book icon), opening `/checkstack/user-guide/` in a new tab; the group renders even when no plugin route contributes to it.
|
|
69
|
+
|
|
70
|
+
BREAKING (plugin authors): `UserMenuItemsSlot` is no longer the way to add navigation - registering a top user-menu item no longer surfaces it anywhere. Add `nav` to the page's route instead. `UserMenuItemsBottomSlot` (account items) is unchanged. All bundled plugins have been migrated.
|
|
71
|
+
|
|
72
|
+
This is a beta minor.
|
|
73
|
+
|
|
74
|
+
- 9dcc848: Stop the spurious "Plugin unknown is not using new API. Skipping." startup warning.
|
|
75
|
+
|
|
76
|
+
`@checkstack/signal-backend` is a host-consumed library (the backend imports `SignalServiceImpl` and `createWebSocketHandler` directly), but its `package.json` declared `checkstack.type: "backend"`, so plugin discovery inserted it as a runtime backend plugin and the loader tried to read a default `register()` export it does not have - logging the offending package as the literal `unknown`.
|
|
77
|
+
|
|
78
|
+
- Reclassify `@checkstack/signal-backend` to `checkstack.type: "tooling"` (like `@checkstack/backend-api`), so it is no longer discovered or registered as a backend plugin. No runtime behavior change - the SignalService and WebSocket handler are still instantiated and registered directly by the host backend.
|
|
79
|
+
- Harden the loader's skip diagnostic so it can never render `unknown`: it resolves the offending plugin by its database-row package name (falling back to the on-disk path) and tells operators to set `checkstack.type` to `"tooling"` for host-consumed libraries.
|
|
80
|
+
|
|
81
|
+
This is a beta minor.
|
|
82
|
+
|
|
83
|
+
- 9dcc848: Align workspace dependency versions and migrate React Router to v7.
|
|
84
|
+
|
|
85
|
+
BREAKING CHANGES (React Router v7): All frontend packages now depend on `react-router-dom@^7.16.0`. Previously the workspace declared four divergent ranges (`^6.20.0`, `^6.22.0`, `^7.1.1`, `^7.14.2`), which resolved both `react-router@6` and `react-router@7` into a single bundle. Everything is now unified on v7. The public imports the app uses (`BrowserRouter`, `Routes`, `Route`, `Link`, `NavLink`, `MemoryRouter`, `useNavigate`, `useParams`, `useSearchParams`, `useLocation`) are unchanged between v6 and v7, so no source rewrites were required - but any out-of-tree plugin still on react-router v6 should upgrade to v7 (see the React Router v6 -> v7 upgrade guide) to share the host's single router instance via the import map.
|
|
86
|
+
|
|
87
|
+
Other unified ranges (no API change): `react` -> `^18.3.1`, the `@orpc/*` family (`contract`, `server`, `client`, `tanstack-query`, `openapi`, `zod`) -> `^1.14.4`, and `better-auth` -> `^1.6.13`.
|
|
88
|
+
|
|
89
|
+
Removed the pre-rename `@orpc/react-query` leftover from `@checkstack/frontend-api`; its `createRouterUtils` / `RouterUtils` / `ProcedureUtils` now come from `@orpc/tanstack-query` (the package already in use).
|
|
90
|
+
|
|
91
|
+
Stale in-range runtime deps pulled up to current published versions: `hono` `^4.12.23`, `@tanstack/react-query` (+devtools) `^5.100.14`, `date-fns` `^4.4.0`, `jose` `^6.2.3`, `tar` `^7.5.16`, `semver` `^7.8.1`, `@xyflow/react` `^12.11.0`.
|
|
92
|
+
|
|
93
|
+
### Patch Changes
|
|
94
|
+
|
|
95
|
+
- 9dcc848: Assorted bug fixes and small hardening across the platform.
|
|
96
|
+
|
|
97
|
+
- announcement-backend: `updateAnnouncement` now invalidates the active-announcements and admin-list caches (it was missing the `invalidateAllActive` / `invalidateListAll` calls), so an edited announcement no longer stays stale up to the 45s TTL.
|
|
98
|
+
- anomaly-backend: anomaly/drift state transitions (confirmations, recoveries, self-resolutions) now log at `debug` instead of info/warn - they are already surfaced via the `ANOMALY_STATE_CHANGED` signal, so logging them louder just added noise; genuine failure paths stay `warn`.
|
|
99
|
+
- backend: the `/api/:pluginId/*` dispatcher now populates `requestHeaders` on the per-request RPC context, so a handler that re-enters the router as the originating user (e.g. an AI tool's user-scoped client) can forward the caller's session cookie / bearer - previously the loopback failed with "Authentication required". Guarded by a real end-to-end integration test. The HTTP server idle timeout is also raised (default 255s, configurable via `CHECKSTACK_SERVER_IDLE_TIMEOUT_SECONDS`, clamped 0-255, reset on each streamed chunk) so long AI chat SSE turns are not severed mid-stream.
|
|
100
|
+
- backend: a request for an unknown plugin id (`/api/<unknown>/...`) now returns `404 Not Found` instead of `500` (and logs at warn, not error, since it is a client request) - an unknown _procedure_ on a known plugin already 404'd. The in-app docs namespace `/checkstack/*` now serves Starlight's own `404.html` with a real 404 status for a missing doc, instead of falling through to the SPA catch-all and 200-ing the app shell. Both guarded by tests.
|
|
101
|
+
- automation-common: remove polynomial-time backtracking from `toShellEnvKey`'s underscore-trim (CodeQL `js/polynomial-redos`); a negative look-behind anchors the trailing run, keeping the trim linear.
|
|
102
|
+
- common + script-packages-common: the pure transport-safe sandbox-policy schema (`sandboxPolicySchema` and its sub-schemas + inferred types) moved to `@checkstack/common` (the neutral base), removing two inverted deps that existed only to reach the shape; `@checkstack/backend-api` continues to re-export it. The schema is no longer exported from `@checkstack/script-packages-common`. Pure refactor, no behavior change.
|
|
103
|
+
- catalog-backend: reject duplicate system names (a `CONFLICT` on create/rename, enforced by a pre-write check AND a new DB unique index on `systems.name`, migration 0004 which first resolves pre-existing duplicates by suffixing).
|
|
104
|
+
- catalog-frontend: detail-page cleanups (use `<NotFound />` not `<AccessDenied />` on the not-found branch, a readable key/value metadata list via `normalizeMetadata`, runtime locale via `formatDate`); and stop the browse view re-rendering on every health report (adopt a new statuses report only when a value actually changed, via `healthStatusesEqual`, so rows stay stable and interactive).
|
|
105
|
+
- healthcheck-backend: fix the daily-rollup retention step failing with an `ON CONFLICT` mismatch (SQLSTATE 42P10) after `environmentId` joined the `health_check_aggregates` unique constraint - the rollup now groups by (day, environmentId, sourceId) and uses a single exported conflict-target constant (`DAILY_AGGREGATE_CONFLICT_TARGET`) kept in lock-step with the schema by a unit test.
|
|
106
|
+
- automation-frontend: the service-account picker's "Learn more" links are now absolute URLs to the deployed Astro docs site (they 404ed as in-app relative paths). The Monaco script editor double-init crash is fixed (serialized cold init, a guarded `monacoGuard` accessor, theme/type effects gated on `apiReady`).
|
|
107
|
+
- auth-frontend: bound the desktop user-menu popover height (`max-h-[var(--radix-popover-content-available-height)]` + `overflow-y-auto`) so it no longer clips on short viewports, and fold the standalone `Account > Profile` item into a focusable name/email header (`profileHref` on `UserMenu`); the now-empty `Account` group no longer renders.
|
|
108
|
+
- satellite-frontend: picked up via the sidebar-nav migration (account-only user menu).
|
|
109
|
+
|
|
110
|
+
(Related UI fixes - the Monaco editor following the app theme, the `DynamicOptionsField` no-flash fix, the shared `Spinner`, GFM tables, and the user-menu popover bound - land their `@checkstack/ui` bump in the UI/perf changesets where `@checkstack/ui` is already minored.)
|
|
111
|
+
|
|
112
|
+
This is a beta patch.
|
|
113
|
+
|
|
114
|
+
- 9dcc848: Fix the external/published-install dev loop and scaffolded-plugin first-boot (the #251 published-tarball integration lane).
|
|
115
|
+
|
|
116
|
+
Backend:
|
|
117
|
+
|
|
118
|
+
- `CHECKSTACK_DEV_AUTH=true` now actually takes effect for plugin APIs: dev auth is registered as a FACTORY (not a plain instance) so `ServiceRegistry.get()` reaches it instead of resolving the real auth factory first - previously every plugin API request under dev auth 401ed.
|
|
119
|
+
- `CHECKSTACK_DEV_AUTH=true` no longer fatally crashes boot: dev-auth passes through real S2S tokens in `authenticate()` and mints a real plugin-scoped service token in `getCredentials()`, registered per plugin so each carries its own id - so boot-time backend-to-backend calls (e.g. `notification.registerSubscriptionSpec`) are accepted. A `PORT` env override is added to the backend entry point.
|
|
120
|
+
- A dev-loaded plugin's Drizzle migrations now run on boot: `loadPlugins` accepts an optional `manualPluginPaths` map and the dev path supplies `CHECKSTACK_DEV_PLUGIN_PATH` so the plugin's `drizzle/` migrations run (previously manual plugins booted with no tables).
|
|
121
|
+
|
|
122
|
+
dev-server:
|
|
123
|
+
|
|
124
|
+
- The Vite frontend dev server now starts from a published install: `@checkstack/frontend` is resolved from a candidate list (the plugin first, then dev-server's own install), a `checkstack.bundle`-referenced `-frontend` sibling is picked up by scanning sibling dirs, and resolution failures yield a clean `undefined`/`Error` instead of Bun's non-`Error` throw (which had surfaced as "An error occurred").
|
|
125
|
+
- The dev shell is now styled from a published install: `@checkstack/frontend` moves `tailwindcss`, `autoprefixer`, and `tailwindcss-animate` to dependencies and exports a `./tailwind-preset` subpath; the dev server assembles the PostCSS chain from that preset + autoprefixer and injects the plugin-under-dev's source globs into Tailwind's `content` (so a plugin author's custom utility classes compile), degrading gracefully if the toolchain can't be loaded. (`@checkstack/frontend` now declares an `exports` field, a BREAKING change for any consumer importing an undeclared subpath; nothing in the platform imports it as a module, and a `./*` passthrough preserves filesystem-style subpath access. The `@checkstack/frontend` minor bump for this lands in the version-alignment / frontend-bundle-perf changesets.)
|
|
126
|
+
- `--help` output corrected from the stale `checkstack-scripts dev` to `checkstack-dev`, with a note that the binary ships in `@checkstack/dev-server`.
|
|
127
|
+
|
|
128
|
+
scripts (scaffold):
|
|
129
|
+
|
|
130
|
+
- The standalone backend template now ships a generated `drizzle/0000_init` migration creating the example `items` table, and `drizzle.config.ts` `out` points at `./drizzle` (the folder the loader reads), so a scaffolded plugin serves its API on first `bun install && bun run dev` instead of 500ing with "relation \"items\" does not exist".
|
|
131
|
+
- The `common` template's `definePluginMetadata({ pluginId })` now renders the bare base name (not `<base>-common`), matching the backend's `checkstack.pluginId` and `/api/<pluginId>/*` route, fixing "Plugin metadata not found in registry".
|
|
132
|
+
|
|
133
|
+
This is a beta patch.
|
|
134
|
+
|
|
135
|
+
- 9dcc848: Input-validation and error-mapping hardening found by a fuzzing pass against the built container.
|
|
136
|
+
|
|
137
|
+
- backend: a Postgres driver error caused by bad client input no longer surfaces as a `500`. The `/api` and `/rest` dispatchers now map the relevant SQLSTATE classes to the correct status - `22P02`/`22003`/`22001`/`22007` (malformed/out-of-range/over-long/bad-date value), `23502`/`23503`/`23514` (missing/dangling/check-failed) to `400`, and `23505` (unique violation) to `409` - and log them at `warn` (client mistake), not `error`. The client-facing message is generic so column/constraint names are never leaked; genuine unknown faults still log at `error` and 500. Previously a `where id = $1` with a non-uuid `$1` (or an over-long string, or a foreign-key miss in `addSystemToGroup`) reached the driver and 500'd, making routine probing look like a server outage and burying real 500s.
|
|
138
|
+
- slo-common: **fixes a stored cluster-wide DoS.** `windowDays` was accepted up to `2^53`, but the SLO engine derives window boundaries with `Date(now - windowDays * 86_400_000)` - a large value overflows past the max representable `Date` and yields `Invalid Date`. That objective committed fine, then every subsequent read of the system's objectives threw `RangeError: Invalid time value` during serialization (a 500 readable by anyone with SLO read access, on any pod). `windowDays` is now bounded to 1..3650 days at the contract, the GitOps `kind: SLO` spec, and the update path via a single shared `SloWindowDaysSchema`, so the poison row can never be created.
|
|
139
|
+
- slo-common + healthcheck-common: SLO `getDailySnapshots` and the healthcheck history endpoints (`getHistory`, `getDetailedHistory`, `getAggregatedHistory`, `getDetailedAggregatedHistory`, `getRunsForAnalysis`) declared their `startDate`/`endDate` params as `z.date()`, which a `/rest/...` string param can never satisfy - so those endpoints 400'd on the entire REST surface. They now use `z.coerce.date()`, accepting both the REST string shape and the native RPC `Date`.
|
|
140
|
+
- healthcheck-common: `intervalSeconds` was `z.number().min(1)` with no `.int()` and no upper bound, so a fractional or out-of-range value reached the DB and failed at insert (the column is a 32-bit int). It is now `.int().min(1).max(2_592_000)` (1 second .. 30 days), applied to both create and update (the update schema is the create partial).
|
|
141
|
+
- catalog-common: system/group/environment names were bare `z.string()` (environment was `.min(1)` only), so empty, whitespace-only, and 100KB+ names reached the DB - the huge ones surfaced as 500s when parameter binding blew up. Names are now `trim().min(1).max(200)` via a shared schema.
|
|
142
|
+
|
|
143
|
+
**BREAKING:** `getSystemContacts` is now `userType: "authenticated"` (was `"public"`). System contacts carry PII (user id, name, email); the public read leaked them to anonymous status-page visitors. Anonymous callers now receive `401` for this one endpoint; the system detail page already renders "No contacts assigned" for anonymous viewers, so the UI degrades gracefully. All other catalog reads remain public.
|
|
144
|
+
|
|
145
|
+
- catalog-frontend: the system detail page skips the `getSystemContacts` request entirely for anonymous viewers (it would now `401`) and falls back to the empty state.
|
|
146
|
+
|
|
147
|
+
This is a beta release: the breaking contact-visibility change ships as a minor bump per the beta versioning policy, not a major.
|
|
148
|
+
|
|
149
|
+
- Updated dependencies [9dcc848]
|
|
150
|
+
- Updated dependencies [9dcc848]
|
|
151
|
+
- Updated dependencies [9dcc848]
|
|
152
|
+
- Updated dependencies [9dcc848]
|
|
153
|
+
- Updated dependencies [9dcc848]
|
|
154
|
+
- Updated dependencies [9dcc848]
|
|
155
|
+
- Updated dependencies [9dcc848]
|
|
156
|
+
- Updated dependencies [9dcc848]
|
|
157
|
+
- Updated dependencies [9dcc848]
|
|
158
|
+
- Updated dependencies [9dcc848]
|
|
159
|
+
- Updated dependencies [9dcc848]
|
|
160
|
+
- @checkstack/auth-common@0.8.0
|
|
161
|
+
- @checkstack/backend-api@0.21.0
|
|
162
|
+
- @checkstack/signal-backend@0.3.0
|
|
163
|
+
- @checkstack/common@0.13.0
|
|
164
|
+
- @checkstack/cache-api@0.3.9
|
|
165
|
+
- @checkstack/queue-api@0.3.9
|
|
166
|
+
- @checkstack/api-docs-common@0.1.16
|
|
167
|
+
- @checkstack/pluginmanager-common@0.2.5
|
|
168
|
+
- @checkstack/signal-common@0.2.6
|
|
169
|
+
|
|
3
170
|
## 0.15.0
|
|
4
171
|
|
|
5
172
|
### Minor Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@checkstack/backend",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"license": "Elastic-2.0",
|
|
5
5
|
"checkstack": {
|
|
6
6
|
"type": "backend"
|
|
@@ -16,27 +16,27 @@
|
|
|
16
16
|
"dependencies": {
|
|
17
17
|
"@checkstack/api-docs-common": "0.1.15",
|
|
18
18
|
"@checkstack/auth-common": "0.7.2",
|
|
19
|
-
"@checkstack/backend-api": "0.
|
|
19
|
+
"@checkstack/backend-api": "0.20.0",
|
|
20
20
|
"@checkstack/common": "0.12.0",
|
|
21
21
|
"@checkstack/drizzle-helper": "0.0.5",
|
|
22
|
-
"@checkstack/cache-api": "0.3.
|
|
23
|
-
"@checkstack/queue-api": "0.3.
|
|
24
|
-
"@checkstack/signal-backend": "0.2.
|
|
22
|
+
"@checkstack/cache-api": "0.3.8",
|
|
23
|
+
"@checkstack/queue-api": "0.3.8",
|
|
24
|
+
"@checkstack/signal-backend": "0.2.12",
|
|
25
25
|
"@checkstack/signal-common": "0.2.5",
|
|
26
26
|
"@checkstack/pluginmanager-common": "0.2.4",
|
|
27
27
|
"@hono/zod-validator": "^0.7.6",
|
|
28
|
-
"@orpc/client": "^1.
|
|
29
|
-
"@orpc/contract": "^1.
|
|
30
|
-
"@orpc/openapi": "^1.
|
|
31
|
-
"@orpc/server": "^1.
|
|
32
|
-
"@orpc/zod": "^1.
|
|
33
|
-
"better-auth": "^1.
|
|
28
|
+
"@orpc/client": "^1.14.4",
|
|
29
|
+
"@orpc/contract": "^1.14.4",
|
|
30
|
+
"@orpc/openapi": "^1.14.4",
|
|
31
|
+
"@orpc/server": "^1.14.4",
|
|
32
|
+
"@orpc/zod": "^1.14.4",
|
|
33
|
+
"better-auth": "^1.6.13",
|
|
34
34
|
"drizzle-orm": "^0.45.0",
|
|
35
|
-
"hono": "^4.12.
|
|
36
|
-
"jose": "^6.
|
|
35
|
+
"hono": "^4.12.23",
|
|
36
|
+
"jose": "^6.2.3",
|
|
37
37
|
"pg": "^8.11.0",
|
|
38
|
-
"semver": "^7.
|
|
39
|
-
"tar": "^7.
|
|
38
|
+
"semver": "^7.8.1",
|
|
39
|
+
"tar": "^7.5.16",
|
|
40
40
|
"winston": "^3.19.0",
|
|
41
41
|
"zod": "^4.2.1"
|
|
42
42
|
},
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"@types/semver": "^7.5.0",
|
|
47
47
|
"@checkstack/tsconfig": "0.0.7",
|
|
48
48
|
"@checkstack/scripts": "0.3.4",
|
|
49
|
-
"@checkstack/test-utils-backend": "0.1.
|
|
49
|
+
"@checkstack/test-utils-backend": "0.1.33",
|
|
50
50
|
"drizzle-kit": "^0.31.10"
|
|
51
51
|
}
|
|
52
52
|
}
|
package/src/index.ts
CHANGED
|
@@ -35,7 +35,6 @@ import {
|
|
|
35
35
|
type WebSocketData,
|
|
36
36
|
} from "@checkstack/signal-backend";
|
|
37
37
|
import type {
|
|
38
|
-
AuthService,
|
|
39
38
|
BackendPlugin,
|
|
40
39
|
WsConnectionHandlers,
|
|
41
40
|
} from "@checkstack/backend-api";
|
|
@@ -295,6 +294,61 @@ app.get("/.well-known/jwks.json", async (c) => {
|
|
|
295
294
|
return c.json(jwks);
|
|
296
295
|
});
|
|
297
296
|
|
|
297
|
+
// Serve the in-app user guide: the SAME Astro Starlight static build deployed
|
|
298
|
+
// to GitHub Pages, mounted same-origin at `/checkstack/*` (the docs build has
|
|
299
|
+
// `base: "/checkstack"` and all cross-links are `/checkstack/...`, so it works
|
|
300
|
+
// verbatim - no rebuild, no link rewriting). Registered BEFORE the SPA
|
|
301
|
+
// catch-all below so doc paths win; gated on the dist existing so a deployment
|
|
302
|
+
// without docs degrades gracefully (the path 404s as before). Note: this is
|
|
303
|
+
// `/checkstack/*`, distinct from the platform's `/.checkstack/*` (leading dot).
|
|
304
|
+
const docsDistPath =
|
|
305
|
+
process.env.CHECKSTACK_DOCS_DIST ??
|
|
306
|
+
path.resolve(import.meta.dir, "../../../docs/dist");
|
|
307
|
+
if (fs.existsSync(docsDistPath)) {
|
|
308
|
+
rootLogger.info(`📚 Serving in-app user guide from: ${docsDistPath}`);
|
|
309
|
+
const serveDocsFile = async (c: Context, filePath: string) => {
|
|
310
|
+
const file = Bun.file(filePath);
|
|
311
|
+
const content = await file.arrayBuffer();
|
|
312
|
+
c.header("Content-Type", file.type);
|
|
313
|
+
return c.body(content);
|
|
314
|
+
};
|
|
315
|
+
app.get("/checkstack/*", async (c, next) => {
|
|
316
|
+
// Map `/checkstack/<rest>` -> `<docsDist>/<rest>`, resolving a directory or
|
|
317
|
+
// trailing-slash/pretty URL to its `index.html` (Starlight emits
|
|
318
|
+
// `<slug>/index.html`). Fall through on a miss so non-doc `/checkstack/...`
|
|
319
|
+
// paths aren't swallowed.
|
|
320
|
+
const rel = c.req.path.replace(/^\/checkstack\/?/, "");
|
|
321
|
+
if (rel.includes("..")) return next();
|
|
322
|
+
|
|
323
|
+
let filePath = path.join(docsDistPath, rel);
|
|
324
|
+
const isDir =
|
|
325
|
+
rel === "" ||
|
|
326
|
+
rel.endsWith("/") ||
|
|
327
|
+
(fs.existsSync(filePath) && fs.statSync(filePath).isDirectory());
|
|
328
|
+
if (isDir) {
|
|
329
|
+
filePath = path.join(docsDistPath, rel, "index.html");
|
|
330
|
+
} else if (!fs.existsSync(filePath)) {
|
|
331
|
+
// Pretty URL with no trailing slash (e.g. /checkstack/user-guide).
|
|
332
|
+
const asIndex = path.join(docsDistPath, rel, "index.html");
|
|
333
|
+
if (fs.existsSync(asIndex)) filePath = asIndex;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
|
|
337
|
+
return serveDocsFile(c, filePath);
|
|
338
|
+
}
|
|
339
|
+
// Unknown `/checkstack/*` path: this namespace IS the docs site (the
|
|
340
|
+
// platform's own endpoints live under `/.checkstack/`, with a dot), so serve
|
|
341
|
+
// Starlight's own 404 page with a real 404 status instead of falling through
|
|
342
|
+
// to the SPA catch-all (which would 200 the app shell for a missing doc).
|
|
343
|
+
const notFoundPage = path.join(docsDistPath, "404.html");
|
|
344
|
+
if (fs.existsSync(notFoundPage)) {
|
|
345
|
+
c.status(404);
|
|
346
|
+
return serveDocsFile(c, notFoundPage);
|
|
347
|
+
}
|
|
348
|
+
return c.text("Not Found", 404);
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
|
|
298
352
|
// Production: Serve frontend static files when CHECKSTACK_FRONTEND_DIST is set
|
|
299
353
|
// Must be registered at module load time before Hono's router is built
|
|
300
354
|
const frontendDistPath = process.env.CHECKSTACK_FRONTEND_DIST;
|
|
@@ -630,13 +684,35 @@ const init = async () => {
|
|
|
630
684
|
rootLogger.warn(
|
|
631
685
|
"🛠 Dev auth ENABLED — every access rule is auto-granted. Do NOT use in production.",
|
|
632
686
|
);
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
687
|
+
// Register the dev auth service as a FACTORY, not a plain instance.
|
|
688
|
+
// `registerCoreServices` already registered the real (token/strategy-
|
|
689
|
+
// based) auth as a factory for `coreServices.auth`, and
|
|
690
|
+
// `ServiceRegistry.get()` resolves factories BEFORE instances — so a
|
|
691
|
+
// plain `registerService(coreServices.auth, devAuthService)` would be
|
|
692
|
+
// shadowed by the production factory and the dev bypass would never take
|
|
693
|
+
// effect (every plugin API request would 401 with "Authentication
|
|
694
|
+
// required"). Registering the dev auth as a factory makes `get()` reach
|
|
695
|
+
// it. This stays entirely inside the dev-flag-gated path: the production
|
|
696
|
+
// auth factory is left in place and untouched whenever CHECKSTACK_DEV_AUTH
|
|
697
|
+
// is not set.
|
|
698
|
+
// Scoped per plugin (like the production auth factory) so each plugin's
|
|
699
|
+
// S2S credentials are minted as `{ service: <its pluginId> }`.
|
|
700
|
+
pluginManager
|
|
701
|
+
.getRegistry()
|
|
702
|
+
.registerFactory(coreServices.auth, (metadata) =>
|
|
703
|
+
createDevAuthService({
|
|
704
|
+
getAllAccessRules: () => pluginManager.getAllAccessRules(),
|
|
705
|
+
pluginId: metadata.pluginId,
|
|
706
|
+
}),
|
|
707
|
+
);
|
|
637
708
|
}
|
|
638
709
|
|
|
639
710
|
const manualPlugins: BackendPlugin[] = [];
|
|
711
|
+
// Maps a manually-loaded plugin's id to its on-disk dir so the loader can
|
|
712
|
+
// run its Drizzle migrations (in `<dir>/drizzle`). Without this, manual
|
|
713
|
+
// plugins get `pluginPath: ""` and their migrations are skipped — which is
|
|
714
|
+
// why a freshly-scaffolded plugin booted with no `items` table.
|
|
715
|
+
const manualPluginPaths = new Map<string, string>();
|
|
640
716
|
if (devPluginPath) {
|
|
641
717
|
rootLogger.info(`🛠 Dev mode — loading plugin from ${devPluginPath}`);
|
|
642
718
|
|
|
@@ -677,6 +753,9 @@ const init = async () => {
|
|
|
677
753
|
);
|
|
678
754
|
}
|
|
679
755
|
manualPlugins.push(pluginExport);
|
|
756
|
+
// `CHECKSTACK_DEV_PLUGIN_PATH` is the plugin's repo dir (the dev server
|
|
757
|
+
// sets it to the plugin cwd), so its migrations live at `<dir>/drizzle`.
|
|
758
|
+
manualPluginPaths.set(pluginExport.metadata.pluginId, devPluginPath);
|
|
680
759
|
} catch (error) {
|
|
681
760
|
throw new Error(
|
|
682
761
|
`Failed to import dev plugin from ${devPluginPath}: ${extractErrorMessage(error)}`,
|
|
@@ -686,6 +765,7 @@ const init = async () => {
|
|
|
686
765
|
|
|
687
766
|
await pluginManager.loadPlugins(app, manualPlugins, {
|
|
688
767
|
skipDiscovery: !!devPluginPath,
|
|
768
|
+
manualPluginPaths,
|
|
689
769
|
});
|
|
690
770
|
|
|
691
771
|
// 4. Wire up auth client for access-based signal filtering
|
|
@@ -967,8 +1047,24 @@ const fetch = async (
|
|
|
967
1047
|
return app.fetch(req, server);
|
|
968
1048
|
};
|
|
969
1049
|
|
|
1050
|
+
// Bun closes a connection that stays idle (no bytes sent or received) for
|
|
1051
|
+
// `idleTimeout` seconds. The default is 10s, which severs long agentic chat
|
|
1052
|
+
// turns: the AI assistant streams an SSE response that can pause >10s between
|
|
1053
|
+
// chunks while a slow provider "thinks" or a tool runs, surfacing as
|
|
1054
|
+
// "Error in input stream" on the client. Raise it to Bun's maximum (255s) so a
|
|
1055
|
+
// multi-step turn is not killed; each streamed chunk resets the idle timer, so
|
|
1056
|
+
// only a single >255s silent gap would still time out.
|
|
1057
|
+
const IDLE_TIMEOUT_SECONDS = (() => {
|
|
1058
|
+
const raw = Number(process.env.CHECKSTACK_SERVER_IDLE_TIMEOUT_SECONDS);
|
|
1059
|
+
// Bun clamps idleTimeout to [0, 255]; keep within range and fall back to max.
|
|
1060
|
+
return Number.isFinite(raw) && raw >= 0 && raw <= 255 ? raw : 255;
|
|
1061
|
+
})();
|
|
1062
|
+
|
|
970
1063
|
export default {
|
|
971
|
-
port
|
|
1064
|
+
// Listen port. Defaults to 3000; overridable via PORT so a second instance
|
|
1065
|
+
// (e.g. an isolated E2E stack) can run alongside a dev server on another port.
|
|
1066
|
+
port: Number(process.env.PORT) || 3000,
|
|
1067
|
+
idleTimeout: IDLE_TIMEOUT_SECONDS,
|
|
972
1068
|
fetch,
|
|
973
1069
|
websocket: {
|
|
974
1070
|
// Type template for ws.data
|
|
@@ -24,6 +24,7 @@ import type { EventBus } from "@checkstack/backend-api";
|
|
|
24
24
|
import type { PluginMetadata } from "@checkstack/common";
|
|
25
25
|
import { rootLogger } from "../logger";
|
|
26
26
|
import { extractErrorMessage } from "@checkstack/common";
|
|
27
|
+
import { mapPgErrorToHttp } from "./pg-http-errors";
|
|
27
28
|
|
|
28
29
|
interface RouteHandlerDeps {
|
|
29
30
|
registry: ServiceRegistry;
|
|
@@ -139,14 +140,20 @@ async function resolveRequestContext({
|
|
|
139
140
|
deps.pluginMetadataRegistry.get(pluginId);
|
|
140
141
|
|
|
141
142
|
if (!pluginMetadata) {
|
|
142
|
-
|
|
143
|
+
// No metadata for this pluginId. The common cause is a client requesting an
|
|
144
|
+
// unknown plugin (typo / probing) - a 404, not a 500. A genuine
|
|
145
|
+
// misconfiguration (a core router that forgot
|
|
146
|
+
// pluginManager.registerCorePluginMetadata()) surfaces the same way, so we
|
|
147
|
+
// log it for diagnosis, but at warn level since the dominant case is a
|
|
148
|
+
// client error and erroring on every bad path would be noise.
|
|
149
|
+
(logger as Logger).warn(
|
|
143
150
|
`${pathname}: no plugin metadata registered for pluginId='${pluginId}'. ` +
|
|
144
|
-
`
|
|
145
|
-
`pluginManager.registerCorePluginMetadata().`,
|
|
151
|
+
`Either the plugin id is unknown (client typo / probe), or a core ` +
|
|
152
|
+
`router did not call pluginManager.registerCorePluginMetadata().`,
|
|
146
153
|
);
|
|
147
154
|
return {
|
|
148
155
|
ok: false,
|
|
149
|
-
response: c.json({ error: "
|
|
156
|
+
response: c.json({ error: "Not Found" }, 404),
|
|
150
157
|
};
|
|
151
158
|
}
|
|
152
159
|
|
|
@@ -163,6 +170,11 @@ async function resolveRequestContext({
|
|
|
163
170
|
cachePluginRegistry: cachePluginRegistry as CachePluginRegistry,
|
|
164
171
|
cacheManager: cacheManager as CacheManager,
|
|
165
172
|
user,
|
|
173
|
+
// The incoming request's headers, so a handler can forward the caller's OWN
|
|
174
|
+
// auth (session cookie / bearer) when it re-enters the router as the same
|
|
175
|
+
// user - e.g. an AI tool's user-scoped rpcClient (proposeTool/applyTool).
|
|
176
|
+
// Also read by correlationMiddleware for the inbound correlation id.
|
|
177
|
+
requestHeaders: c.req.raw.headers,
|
|
166
178
|
emitHook,
|
|
167
179
|
};
|
|
168
180
|
|
|
@@ -203,6 +215,35 @@ function logHandlerError({
|
|
|
203
215
|
}
|
|
204
216
|
}
|
|
205
217
|
|
|
218
|
+
/**
|
|
219
|
+
* Shared interceptor catch path for both the RPC and REST handlers. A Postgres
|
|
220
|
+
* driver error caused by bad client input (bad uuid, out-of-range int, over-long
|
|
221
|
+
* string, constraint violation) is mapped to the correct 4xx `ORPCError` and
|
|
222
|
+
* logged at warn (it is a client mistake, not a server fault). Everything else
|
|
223
|
+
* keeps the existing error-level logging + rethrow so genuine 500s stay loud.
|
|
224
|
+
*/
|
|
225
|
+
function rethrowAsHttpError({
|
|
226
|
+
error,
|
|
227
|
+
pathname,
|
|
228
|
+
logger,
|
|
229
|
+
protocolLabel,
|
|
230
|
+
}: {
|
|
231
|
+
error: unknown;
|
|
232
|
+
pathname: string;
|
|
233
|
+
logger: Logger | undefined;
|
|
234
|
+
protocolLabel: string;
|
|
235
|
+
}): never {
|
|
236
|
+
const mapped = mapPgErrorToHttp(error);
|
|
237
|
+
if (mapped) {
|
|
238
|
+
(logger ?? rootLogger).warn(
|
|
239
|
+
`${protocolLabel} ${pathname}: ${mapped.code} (${extractErrorMessage(error)})`,
|
|
240
|
+
);
|
|
241
|
+
throw mapped;
|
|
242
|
+
}
|
|
243
|
+
logHandlerError({ error, pathname, logger, protocolLabel });
|
|
244
|
+
throw error;
|
|
245
|
+
}
|
|
246
|
+
|
|
206
247
|
/**
|
|
207
248
|
* Creates the API route handler for Hono.
|
|
208
249
|
* Serves oRPC's native RPC wire protocol at /api/:pluginId/*.
|
|
@@ -242,13 +283,12 @@ export function createApiRouteHandler({
|
|
|
242
283
|
try {
|
|
243
284
|
return await next(rest);
|
|
244
285
|
} catch (error) {
|
|
245
|
-
|
|
286
|
+
rethrowAsHttpError({
|
|
246
287
|
error,
|
|
247
288
|
pathname,
|
|
248
289
|
logger,
|
|
249
290
|
protocolLabel: "RPC",
|
|
250
291
|
});
|
|
251
|
-
throw error;
|
|
252
292
|
}
|
|
253
293
|
},
|
|
254
294
|
],
|
|
@@ -321,13 +361,12 @@ export function createRestRouteHandler({
|
|
|
321
361
|
try {
|
|
322
362
|
return await next(rest);
|
|
323
363
|
} catch (error) {
|
|
324
|
-
|
|
364
|
+
rethrowAsHttpError({
|
|
325
365
|
error,
|
|
326
366
|
pathname,
|
|
327
367
|
logger,
|
|
328
368
|
protocolLabel: "REST",
|
|
329
369
|
});
|
|
330
|
-
throw error;
|
|
331
370
|
}
|
|
332
371
|
},
|
|
333
372
|
],
|