@checkstack/common 0.12.0 → 0.14.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 +69 -0
- package/package.json +4 -4
- package/src/access-utils.ts +20 -0
- package/src/docs-links.test.ts +44 -0
- package/src/docs-links.ts +27 -0
- package/src/icons.ts +19 -4
- package/src/index.ts +4 -0
- package/src/logger.ts +46 -0
- package/src/migration.ts +21 -0
- package/src/sandbox-policy.ts +189 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,74 @@
|
|
|
1
1
|
# @checkstack/common
|
|
2
2
|
|
|
3
|
+
## 0.14.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 13373ce: Break the publish-time dependency cycle between `@checkstack/backend-api` and `@checkstack/cache-api` / `@checkstack/queue-api`.
|
|
8
|
+
|
|
9
|
+
`cache-api` and `queue-api` only ever used `Logger` and `Migration` from `backend-api` as `import type`, yet declared `@checkstack/backend-api` as a runtime dependency. In the monorepo this is harmless (everything resolves via `workspace:*`), but once published, `bun publish` freezes each `workspace:*` into a concrete pin of the _other_ package's then-current version. Because the dependency is mutual, a consumer installing these packages from the registry must resolve `backend-api -> cache-api -> backend-api -> ...` backward through release history until it reaches ancient versions that shipped raw `workspace:*` ranges and a long-removed `@checkstack/cache-api@0.1.0` pin - which fail to resolve. This surfaced as `bun install` errors (and a missing `checkstack-dev` binary) in freshly scaffolded standalone plugins.
|
|
10
|
+
|
|
11
|
+
`Logger` and `Migration` now live in `@checkstack/common` (a dependency-free leaf package). `@checkstack/backend-api` re-exports both for backward compatibility, so existing `import type { Logger, Migration } from "@checkstack/backend-api"` call sites are unchanged. `cache-api` and `queue-api` now depend on `@checkstack/common` instead of `@checkstack/backend-api`, removing the cycle.
|
|
12
|
+
|
|
13
|
+
## 0.13.0
|
|
14
|
+
|
|
15
|
+
### Minor Changes
|
|
16
|
+
|
|
17
|
+
- 9dcc848: Cut initial-load JS: lazy plugin contributions, a hardened lazy-by-default contribution contract, on-demand Monaco, and a lighter icon/chart load.
|
|
18
|
+
|
|
19
|
+
- Lazy plugin route pages: each plugin's route `element` references a `React.lazy`-wrapped page rendered inside a shared `<Suspense>` boundary. Plugins still register synchronously, so nav, slots, commands, API factories, and `foreignSignals` are available on first paint. This moves ~37 route-page chunks (~600 KB) out of the entry; the entry chunk drops from ~2.4 MB to ~190 KB. Auth flow pages stay eager. The `@checkstack/scripts` scaffold template generates lazy route pages too.
|
|
20
|
+
- Hardened contribution contract (BREAKING, frontend plugin contract): plugins declare contributions lazily and let the framework own code-splitting, Suspense, and per-plugin error isolation. Routes use `load: () => import("./Page").then((m) => ({ default: m.Page }))` instead of `element: <Page />` (`element` is still accepted for the rare page that must paint without a chunk fetch; provide exactly one). Slot extensions accept either an eager `component` or a lazy `load`; new `getLazyContribution` + `ExtensionComponent` exports from `@checkstack/frontend-api` render either kind. This also fixes runtime-installed plugins: `ExtensionSlot` subscribes to the plugin registry, and the API registry rebuilds when the plugin set changes (`getPlugins()` returns an immutable snapshot via `useSyncExternalStore`). A per-plugin error boundary contains a bad contribution.
|
|
21
|
+
- On-demand Monaco: the `@checkstack/ui` barrel no longer pulls the `@codingame/*` / `monaco-languageclient` stack into the initial load. `CodeEditor` lazy-loads its Monaco-backed editor behind `React.lazy` + Suspense, `validateTypeScriptSources` imports the editor API via in-body `await import(...)`, and the "vscode services ready" signal moved to a Monaco-free module. The ~10 MB editor body loads only when a `CodeEditor` mounts. A `react-vendor` `manualChunks` split was added for stable vendor caching.
|
|
22
|
+
- lucide-react 1.x + lighter icons/charts (BREAKING for icon consumers): lucide-react unified from three drifting ranges to `^1.17.0`. lucide v1 removed brand icons, so the GitHub/GitLab marks are vendored in `@checkstack/ui` (`GithubIcon`, `GitlabIcon`, `brandIcons`); a new `IconName` type (`LucideIconName | BrandIconName`) in `@checkstack/common` is canonical, accepted by `AuthStrategy.icon` and the card components, so data-driven brand names keep working. `DynamicIcon` no longer eagerly imports lucide's ~1600-icon map (~1 MB) - it lives in a `React.lazy` `iconRegistry` chunk fetched on first data-driven render, while statically named-imported icons tree-shake normally. The recharts-backed health-check charts (~300 KB) and the `HealthCheckSystemOverview` drawer leave the initial load.
|
|
23
|
+
|
|
24
|
+
BREAKING CHANGES:
|
|
25
|
+
|
|
26
|
+
- Frontend plugin contract: routes/slot contributions are lazy-by-default (`load` instead of `element`/eager elements) as described above.
|
|
27
|
+
- Any external consumer importing a brand icon from `lucide-react` (e.g. `import { Github } from "lucide-react"`) must switch to the vendored `@checkstack/ui` brand icons or a custom SVG.
|
|
28
|
+
|
|
29
|
+
This is a beta minor.
|
|
30
|
+
|
|
31
|
+
- 9dcc848: Move primary navigation into a left sidebar, and serve the user guide in-app.
|
|
32
|
+
|
|
33
|
+
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`.
|
|
34
|
+
|
|
35
|
+
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.
|
|
36
|
+
|
|
37
|
+
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.
|
|
38
|
+
|
|
39
|
+
This is a beta minor.
|
|
40
|
+
|
|
41
|
+
- 9dcc848: Align workspace dependency versions and migrate React Router to v7.
|
|
42
|
+
|
|
43
|
+
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.
|
|
44
|
+
|
|
45
|
+
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`.
|
|
46
|
+
|
|
47
|
+
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).
|
|
48
|
+
|
|
49
|
+
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`.
|
|
50
|
+
|
|
51
|
+
### Patch Changes
|
|
52
|
+
|
|
53
|
+
- 9dcc848: Assorted bug fixes and small hardening across the platform.
|
|
54
|
+
|
|
55
|
+
- 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.
|
|
56
|
+
- 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`.
|
|
57
|
+
- 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.
|
|
58
|
+
- 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.
|
|
59
|
+
- 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.
|
|
60
|
+
- 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.
|
|
61
|
+
- 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).
|
|
62
|
+
- 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).
|
|
63
|
+
- 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.
|
|
64
|
+
- 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`).
|
|
65
|
+
- 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.
|
|
66
|
+
- satellite-frontend: picked up via the sidebar-nav migration (account-only user menu).
|
|
67
|
+
|
|
68
|
+
(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.)
|
|
69
|
+
|
|
70
|
+
This is a beta patch.
|
|
71
|
+
|
|
3
72
|
## 0.12.0
|
|
4
73
|
|
|
5
74
|
### Minor Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@checkstack/common",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"license": "Elastic-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -13,14 +13,14 @@
|
|
|
13
13
|
}
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@orpc/contract": "^1.
|
|
17
|
-
"lucide-react": "
|
|
16
|
+
"@orpc/contract": "^1.14.4",
|
|
17
|
+
"lucide-react": "^1.17.0",
|
|
18
18
|
"zod": "^4.0.0"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
21
21
|
"typescript": "^5.7.2",
|
|
22
22
|
"@checkstack/tsconfig": "0.0.7",
|
|
23
|
-
"@checkstack/scripts": "0.
|
|
23
|
+
"@checkstack/scripts": "0.4.0"
|
|
24
24
|
},
|
|
25
25
|
"scripts": {
|
|
26
26
|
"typecheck": "tsgo -b",
|
package/src/access-utils.ts
CHANGED
|
@@ -125,6 +125,26 @@ export function qualifyAccessRuleId(
|
|
|
125
125
|
return `${pluginMetadata.pluginId}.${rule.id}`;
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
+
/**
|
|
129
|
+
* Pure predicate: does a set of granted access-rule ids satisfy a rule?
|
|
130
|
+
*
|
|
131
|
+
* This is the single source of truth for client-side access checks - the
|
|
132
|
+
* `useAccess` API and the sidebar both delegate here so nav visibility always
|
|
133
|
+
* matches page accessibility. A rule is satisfied by the wildcard `*`, an exact
|
|
134
|
+
* id match, or (for `read` rules) a `manage` grant on the same resource.
|
|
135
|
+
*/
|
|
136
|
+
export function isAccessRuleSatisfied(
|
|
137
|
+
grantedRuleIds: readonly string[],
|
|
138
|
+
rule: Pick<AccessRule, "id" | "resource" | "level">,
|
|
139
|
+
): boolean {
|
|
140
|
+
if (grantedRuleIds.includes("*")) return true;
|
|
141
|
+
if (grantedRuleIds.includes(rule.id)) return true;
|
|
142
|
+
if (rule.level === "read") {
|
|
143
|
+
return grantedRuleIds.includes(`${rule.resource}.manage`);
|
|
144
|
+
}
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
|
|
128
148
|
/**
|
|
129
149
|
* Creates an access rule for a resource.
|
|
130
150
|
*
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { APP_DOC_SLUGS, docsPath } from "./docs-links";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Guards the docs<->app contract: every user-guide page the app deep-links to
|
|
8
|
+
* (navbar + "Learn more" links) MUST exist in the docs. If a page is renamed or
|
|
9
|
+
* moved, the in-app links would silently 404; this test fails instead.
|
|
10
|
+
*
|
|
11
|
+
* Source of truth is the actual docs content (`docs/src/content/docs`), resolved
|
|
12
|
+
* relative to this file (core/common/src -> repo root).
|
|
13
|
+
*/
|
|
14
|
+
const DOCS_ROOT = resolve(import.meta.dir, "../../../docs/src/content/docs");
|
|
15
|
+
|
|
16
|
+
/** A slug resolves if `<slug>.md(x)` or `<slug>/index.md(x)` exists. */
|
|
17
|
+
function slugExists(slug: string): boolean {
|
|
18
|
+
return [
|
|
19
|
+
`${slug}.md`,
|
|
20
|
+
`${slug}.mdx`,
|
|
21
|
+
`${slug}/index.md`,
|
|
22
|
+
`${slug}/index.mdx`,
|
|
23
|
+
].some((candidate) => existsSync(resolve(DOCS_ROOT, candidate)));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe("app doc links resolve to real docs pages", () => {
|
|
27
|
+
test("the docs content root is found (guards the relative path)", () => {
|
|
28
|
+
// If this fails the path is wrong, which would otherwise mask real
|
|
29
|
+
// missing-page failures below.
|
|
30
|
+
expect(existsSync(DOCS_ROOT)).toBe(true);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
for (const [key, slug] of Object.entries(APP_DOC_SLUGS)) {
|
|
34
|
+
test(`${key} -> "${slug}" exists in the docs`, () => {
|
|
35
|
+
expect(slugExists(slug)).toBe(true);
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
test("docsPath builds a /checkstack-based trailing-slash URL", () => {
|
|
40
|
+
expect(docsPath(APP_DOC_SLUGS.teamsAndAccess)).toBe(
|
|
41
|
+
"/checkstack/user-guide/concepts/teams-and-access/",
|
|
42
|
+
);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User-guide pages the APP deep-links to (the navbar "Docs" entry and in-context
|
|
3
|
+
* "Learn more" links). Centralised as a SINGLE source of truth and guarded by
|
|
4
|
+
* `docs-links.test.ts`, which asserts each slug resolves to a real docs page -
|
|
5
|
+
* so renaming/moving a doc fails the test instead of silently 404-ing the
|
|
6
|
+
* in-app links.
|
|
7
|
+
*
|
|
8
|
+
* A slug is the path under the docs content root WITHOUT the `/checkstack` base
|
|
9
|
+
* and WITHOUT a trailing slash, e.g. `user-guide/concepts/teams-and-access`.
|
|
10
|
+
*/
|
|
11
|
+
export const APP_DOC_SLUGS = {
|
|
12
|
+
userGuideHome: "user-guide",
|
|
13
|
+
teamsAndAccess: "user-guide/concepts/teams-and-access",
|
|
14
|
+
apiKeys: "user-guide/reference/api-keys",
|
|
15
|
+
} as const;
|
|
16
|
+
|
|
17
|
+
export type AppDocSlugKey = keyof typeof APP_DOC_SLUGS;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Build the same-origin in-app URL for a docs slug. The backend serves the Astro
|
|
21
|
+
* Starlight build at `/checkstack/*` (base path `/checkstack`), and Starlight
|
|
22
|
+
* pages are directory routes, so the URL is `/checkstack/<slug>/` (trailing
|
|
23
|
+
* slash). Pass a value from {@link APP_DOC_SLUGS}.
|
|
24
|
+
*/
|
|
25
|
+
export function docsPath(slug: string): string {
|
|
26
|
+
return `/checkstack/${slug}/`;
|
|
27
|
+
}
|
package/src/icons.ts
CHANGED
|
@@ -15,12 +15,27 @@ import { z } from "zod";
|
|
|
15
15
|
export type LucideIconName = keyof typeof icons;
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
|
-
*
|
|
19
|
-
*
|
|
18
|
+
* Brand icon names that lucide-react v1 removed (all brand marks were dropped
|
|
19
|
+
* for trademark reasons) but that we still support as data-driven icon names.
|
|
20
|
+
* The frontend's `DynamicIcon` resolves these to vendored brand SVGs in
|
|
21
|
+
* `@checkstack/ui`. Keep this in sync with the `brandIcons` map there.
|
|
22
|
+
*/
|
|
23
|
+
export type BrandIconName = "Github" | "Gitlab";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Any icon name accepted across the platform: a lucide icon or a vendored
|
|
27
|
+
* brand icon.
|
|
28
|
+
* @example "CircleAlert", "Settings", "Github"
|
|
29
|
+
*/
|
|
30
|
+
export type IconName = LucideIconName | BrandIconName;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Zod schema for {@link IconName}.
|
|
34
|
+
* Uses string at runtime but infers `IconName` for compile-time safety.
|
|
20
35
|
* Use this in RPC contracts to get proper type inference.
|
|
21
36
|
*
|
|
22
37
|
* @example
|
|
23
38
|
* const schema = z.object({ icon: lucideIconSchema.optional() });
|
|
24
|
-
* // Infers: { icon?:
|
|
39
|
+
* // Infers: { icon?: IconName }
|
|
25
40
|
*/
|
|
26
|
-
export const lucideIconSchema = z.string() as z.ZodType<
|
|
41
|
+
export const lucideIconSchema = z.string() as z.ZodType<IconName>;
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export * from "./types";
|
|
2
|
+
export * from "./logger";
|
|
3
|
+
export * from "./migration";
|
|
2
4
|
export * from "./actor";
|
|
3
5
|
export * from "./pagination";
|
|
4
6
|
export * from "./routes";
|
|
@@ -12,3 +14,5 @@ export * from "./json-schema";
|
|
|
12
14
|
export * from "./chart-types";
|
|
13
15
|
export * from "./procedure-builder";
|
|
14
16
|
export * from "./error-utils";
|
|
17
|
+
export * from "./sandbox-policy";
|
|
18
|
+
export * from "./docs-links";
|
package/src/logger.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Backend logger interface used everywhere in the platform via `RpcContext.logger`
|
|
3
|
+
* and the various `coreServices.logger` accessors.
|
|
4
|
+
*
|
|
5
|
+
* Each method accepts a free-form trailing argument list (`...args: unknown[]`)
|
|
6
|
+
* so the long-standing varargs callsites - `logger.error("…", err)` where `err`
|
|
7
|
+
* is an `Error`, or `logger.info("…", value1, value2)` - keep working unchanged.
|
|
8
|
+
*
|
|
9
|
+
* For NEW code, prefer the structured-metadata shape:
|
|
10
|
+
*
|
|
11
|
+
* logger.info("did something", { userId, durationMs });
|
|
12
|
+
*
|
|
13
|
+
* Winston's `splat` handling treats a single trailing plain object as
|
|
14
|
+
* structured metadata (merged into the log entry), and an `Error` instance as
|
|
15
|
+
* a special-cased error (with stack). Either shape lands in the same vararg
|
|
16
|
+
* slot here, so this signature covers both without overload churn.
|
|
17
|
+
*
|
|
18
|
+
* Auto-injected metadata (when the request flows through
|
|
19
|
+
* `correlationMiddleware`): `{ correlationId, pluginId, userId? }`. Do NOT
|
|
20
|
+
* include secrets in the structured-metadata object - it is forwarded
|
|
21
|
+
* verbatim to the log destination.
|
|
22
|
+
*
|
|
23
|
+
* Lives in `@checkstack/common` (rather than `@checkstack/backend-api`) so that
|
|
24
|
+
* low-level packages such as `@checkstack/cache-api` and `@checkstack/queue-api`
|
|
25
|
+
* can reference it without taking a dependency on `backend-api` - which would
|
|
26
|
+
* create a publish-time dependency cycle. `@checkstack/backend-api` re-exports
|
|
27
|
+
* it for backward compatibility.
|
|
28
|
+
*/
|
|
29
|
+
export interface Logger {
|
|
30
|
+
info(message: string, ...args: unknown[]): void;
|
|
31
|
+
error(message: string, ...args: unknown[]): void;
|
|
32
|
+
warn(message: string, ...args: unknown[]): void;
|
|
33
|
+
debug(message: string, ...args: unknown[]): void;
|
|
34
|
+
/**
|
|
35
|
+
* Returns a derived logger with the supplied metadata bound to every
|
|
36
|
+
* subsequent log entry. Used by `correlationMiddleware` to attach
|
|
37
|
+
* `{ correlationId, pluginId, userId? }`, and available to handlers that
|
|
38
|
+
* want a tighter scope (e.g. `ctx.logger.child({ jobId })`).
|
|
39
|
+
*
|
|
40
|
+
* Optional only to keep minimal test-mock logger objects compatible with
|
|
41
|
+
* this interface - production loggers (Winston via `core/backend`) always
|
|
42
|
+
* implement it. Call sites that rely on metadata binding should branch
|
|
43
|
+
* on presence and fall back to the base logger when it is not available.
|
|
44
|
+
*/
|
|
45
|
+
child?(meta: Record<string, unknown>): Logger;
|
|
46
|
+
}
|
package/src/migration.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type-safe migration from one data version to another.
|
|
3
|
+
* Used for backward-compatible schema evolution (see `Versioned` /
|
|
4
|
+
* `MigrationBuilder` in `@checkstack/backend-api`).
|
|
5
|
+
*
|
|
6
|
+
* Lives in `@checkstack/common` (rather than `@checkstack/backend-api`) so that
|
|
7
|
+
* low-level packages such as `@checkstack/cache-api` and `@checkstack/queue-api`
|
|
8
|
+
* can reference it without taking a dependency on `backend-api` - which would
|
|
9
|
+
* create a publish-time dependency cycle. `@checkstack/backend-api` re-exports
|
|
10
|
+
* it for backward compatibility.
|
|
11
|
+
*/
|
|
12
|
+
export interface Migration<TFrom = unknown, TTo = unknown> {
|
|
13
|
+
/** Version number migrating from */
|
|
14
|
+
fromVersion: number;
|
|
15
|
+
/** Version number migrating to (must be fromVersion + 1) */
|
|
16
|
+
toVersion: number;
|
|
17
|
+
/** Human-readable description of what this migration does */
|
|
18
|
+
description: string;
|
|
19
|
+
/** Migration function that transforms old data to new format */
|
|
20
|
+
migrate(data: TFrom): TTo | Promise<TTo>;
|
|
21
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Canonical, transport-safe sandbox policy schema (the single source of truth
|
|
5
|
+
* for the policy SHAPE).
|
|
6
|
+
*
|
|
7
|
+
* This lives in `@checkstack/common` - the neutral base every tier already
|
|
8
|
+
* depends on - NOT in `@checkstack/backend-api` and NOT in a plugin's
|
|
9
|
+
* `*-common` package, so it can be imported by:
|
|
10
|
+
*
|
|
11
|
+
* - the script-packages RPC contract (`@checkstack/script-packages-common`),
|
|
12
|
+
* which exposes the admin read / write endpoints for the global policy;
|
|
13
|
+
* - the satellite WS protocol (`@checkstack/satellite-common`), which relays
|
|
14
|
+
* the resolved global policy to satellites on connect and on change;
|
|
15
|
+
* - `@checkstack/backend-api`, whose `script-sandbox/policy.ts` re-exports this
|
|
16
|
+
* schema and layers the runtime-only helpers (the shipped default profile,
|
|
17
|
+
* the env-seeded UID/GID resolution, and `mergeSandboxPolicy`) on top.
|
|
18
|
+
*
|
|
19
|
+
* Common packages cannot depend on `backend-api`, and putting the pure schema
|
|
20
|
+
* in a single plugin's `*-common` forced backward dependencies from
|
|
21
|
+
* `backend-api` and `satellite-common` onto that plugin. Hosting it in the
|
|
22
|
+
* neutral base keeps it shareable across the contract + protocol + platform
|
|
23
|
+
* without an inverted dependency or a duplicated definition (which would risk
|
|
24
|
+
* drift).
|
|
25
|
+
*
|
|
26
|
+
* Phase 1 implemented: resource caps (rlimits + output truncation), privilege
|
|
27
|
+
* dropping (uid/gid), and the env-key denylist. Filesystem and network
|
|
28
|
+
* isolation are enforced in later phases; the schema scaffolds every layer.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/** What to do when a requested layer cannot be enforced on this host. */
|
|
32
|
+
export const onUnavailableSchema = z
|
|
33
|
+
.enum(["degrade", "fail"])
|
|
34
|
+
.describe(
|
|
35
|
+
"degrade = drop this layer to the portable subset and surface a downgrade; " +
|
|
36
|
+
"fail = refuse to run when the layer can't be enforced.",
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
export type OnUnavailable = z.infer<typeof onUnavailableSchema>;
|
|
40
|
+
|
|
41
|
+
/** Per-run resource caps. All optional; unset = not capped by this layer. */
|
|
42
|
+
export const resourceLimitsSchema = z
|
|
43
|
+
.object({
|
|
44
|
+
cpuSeconds: z
|
|
45
|
+
.number()
|
|
46
|
+
.int()
|
|
47
|
+
.positive()
|
|
48
|
+
.max(3600)
|
|
49
|
+
.optional()
|
|
50
|
+
.describe(
|
|
51
|
+
"Max CPU time (RLIMIT_CPU). Distinct from the wall-clock timeout.",
|
|
52
|
+
),
|
|
53
|
+
memoryBytes: z
|
|
54
|
+
.number()
|
|
55
|
+
.int()
|
|
56
|
+
.positive()
|
|
57
|
+
.optional()
|
|
58
|
+
.describe(
|
|
59
|
+
"Max address space (RLIMIT_AS). On the ESM runner also derives " +
|
|
60
|
+
"--smol / --max-old-space-size as the portable fallback.",
|
|
61
|
+
),
|
|
62
|
+
maxOpenFiles: z
|
|
63
|
+
.number()
|
|
64
|
+
.int()
|
|
65
|
+
.positive()
|
|
66
|
+
.max(1_048_576)
|
|
67
|
+
.optional()
|
|
68
|
+
.describe("Max open file descriptors (RLIMIT_NOFILE)."),
|
|
69
|
+
maxProcesses: z
|
|
70
|
+
.number()
|
|
71
|
+
.int()
|
|
72
|
+
.positive()
|
|
73
|
+
.max(65_536)
|
|
74
|
+
.optional()
|
|
75
|
+
.describe(
|
|
76
|
+
"Max processes/threads for the run's UID (RLIMIT_NPROC). Fork-bomb guard.",
|
|
77
|
+
),
|
|
78
|
+
maxOutputBytes: z
|
|
79
|
+
.number()
|
|
80
|
+
.int()
|
|
81
|
+
.positive()
|
|
82
|
+
.optional()
|
|
83
|
+
.describe(
|
|
84
|
+
"Hard cap on captured stdout+stderr; the runner truncates and flags overflow.",
|
|
85
|
+
),
|
|
86
|
+
maxFileSizeBytes: z
|
|
87
|
+
.number()
|
|
88
|
+
.int()
|
|
89
|
+
.positive()
|
|
90
|
+
.optional()
|
|
91
|
+
.describe(
|
|
92
|
+
"Max single-file write size (RLIMIT_FSIZE). Disk-filler guard.",
|
|
93
|
+
),
|
|
94
|
+
})
|
|
95
|
+
.strict();
|
|
96
|
+
|
|
97
|
+
export type ResourceLimits = z.infer<typeof resourceLimitsSchema>;
|
|
98
|
+
|
|
99
|
+
export const filesystemPolicySchema = z
|
|
100
|
+
.object({
|
|
101
|
+
mode: z
|
|
102
|
+
.enum(["off", "scratch-only", "scratch-plus-ro"])
|
|
103
|
+
.default("off")
|
|
104
|
+
.describe(
|
|
105
|
+
"off = current behavior (full host FS). " +
|
|
106
|
+
"scratch-only = child sees only its per-run scratch dir (writable) + a minimal /usr,/bin,/lib read-only. " +
|
|
107
|
+
"scratch-plus-ro = scratch-only PLUS a read-only bind of resolutionRoot/node_modules for managed packages.",
|
|
108
|
+
),
|
|
109
|
+
scratchBytes: z
|
|
110
|
+
.number()
|
|
111
|
+
.int()
|
|
112
|
+
.positive()
|
|
113
|
+
.optional()
|
|
114
|
+
.describe(
|
|
115
|
+
"Optional tmpfs size cap for the scratch dir when the mechanism supports it.",
|
|
116
|
+
),
|
|
117
|
+
})
|
|
118
|
+
.strict();
|
|
119
|
+
|
|
120
|
+
export type FilesystemPolicy = z.infer<typeof filesystemPolicySchema>;
|
|
121
|
+
|
|
122
|
+
export const networkPolicySchema = z
|
|
123
|
+
.object({
|
|
124
|
+
mode: z
|
|
125
|
+
.enum(["unrestricted", "deny", "allowlist"])
|
|
126
|
+
.default("unrestricted")
|
|
127
|
+
.describe(
|
|
128
|
+
"unrestricted = current behavior. " +
|
|
129
|
+
"deny = no egress (loopback only). " +
|
|
130
|
+
"allowlist = only the listed destinations are reachable.",
|
|
131
|
+
),
|
|
132
|
+
/** v1: IP / CIDR only. Domains are a v2 extension. */
|
|
133
|
+
allow: z
|
|
134
|
+
.array(z.string())
|
|
135
|
+
.default([])
|
|
136
|
+
.describe(
|
|
137
|
+
"IPv4/IPv6 addresses or CIDR blocks reachable when mode=allowlist.",
|
|
138
|
+
),
|
|
139
|
+
denyLinkLocalAndMetadata: z
|
|
140
|
+
.boolean()
|
|
141
|
+
.default(true)
|
|
142
|
+
.describe(
|
|
143
|
+
"Always block 169.254.0.0/16, fc00::/7 link-local, and cloud metadata IPs, even under unrestricted, when a network layer is active.",
|
|
144
|
+
),
|
|
145
|
+
})
|
|
146
|
+
.strict();
|
|
147
|
+
|
|
148
|
+
export type NetworkPolicy = z.infer<typeof networkPolicySchema>;
|
|
149
|
+
|
|
150
|
+
export const privilegePolicySchema = z
|
|
151
|
+
.object({
|
|
152
|
+
mode: z
|
|
153
|
+
.enum(["inherit", "drop-to-uid"])
|
|
154
|
+
.default("inherit")
|
|
155
|
+
.describe(
|
|
156
|
+
"inherit = run as the host process UID (current). drop-to-uid = run as the configured low-priv UID/GID.",
|
|
157
|
+
),
|
|
158
|
+
uid: z.number().int().nonnegative().optional(),
|
|
159
|
+
gid: z.number().int().nonnegative().optional(),
|
|
160
|
+
})
|
|
161
|
+
.strict();
|
|
162
|
+
|
|
163
|
+
export type PrivilegePolicy = z.infer<typeof privilegePolicySchema>;
|
|
164
|
+
|
|
165
|
+
export const sandboxPolicySchema = z
|
|
166
|
+
.object({
|
|
167
|
+
/**
|
|
168
|
+
* Master switch. Schema default is TRUE (on-by-default with opt-out).
|
|
169
|
+
* Set `enabled: false` to restore the pre-hardening behavior (the
|
|
170
|
+
* documented opt-out). The GLOBAL default policy that the runner actually
|
|
171
|
+
* parses against is the permissive DEFAULT PROFILE, not the bare per-field
|
|
172
|
+
* zod defaults below - the field defaults stay conservative so an explicit
|
|
173
|
+
* partial override (e.g. just `{ network: { mode: "deny" } }`) doesn't
|
|
174
|
+
* accidentally widen the other layers.
|
|
175
|
+
*/
|
|
176
|
+
enabled: z.boolean().default(true),
|
|
177
|
+
onUnavailable: onUnavailableSchema.default("degrade"),
|
|
178
|
+
// `.prefault({})` runs the empty default THROUGH the nested schema so each
|
|
179
|
+
// layer's own field defaults (e.g. `filesystem.mode = "off"`) are applied.
|
|
180
|
+
// Plain `.default({})` would store a literal `{}` and skip nested defaults.
|
|
181
|
+
resources: resourceLimitsSchema.prefault({}),
|
|
182
|
+
filesystem: filesystemPolicySchema.prefault({}),
|
|
183
|
+
network: networkPolicySchema.prefault({}),
|
|
184
|
+
privilege: privilegePolicySchema.prefault({}),
|
|
185
|
+
})
|
|
186
|
+
.strict();
|
|
187
|
+
|
|
188
|
+
export type SandboxPolicy = z.infer<typeof sandboxPolicySchema>;
|
|
189
|
+
export type SandboxPolicyInput = z.input<typeof sandboxPolicySchema>;
|