@checkstack/backend 0.4.13 → 0.4.15

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 CHANGED
@@ -1,5 +1,44 @@
1
1
  # @checkstack/backend
2
2
 
3
+ ## 0.4.15
4
+
5
+ ### Patch Changes
6
+
7
+ - 4d59cc7: Prune devDependencies and development-only source folders (like `core/scripts` and `test-utils-*`) from the production Docker image to reduce size and improve security.
8
+ - b839ccb: Security: Hardened production Docker image by upgrading Alpine system libraries, migrating to Drizzle beta (v1.0.0-beta.21), and implementing aggressive binary pruning to eliminate vulnerable build-time tools (esbuild/drizzle-kit).
9
+ - Updated dependencies [67158e2]
10
+ - @checkstack/api-docs-common@0.1.8
11
+ - @checkstack/auth-common@0.5.7
12
+ - @checkstack/backend-api@0.8.2
13
+ - @checkstack/common@0.6.4
14
+ - @checkstack/drizzle-helper@0.0.4
15
+ - @checkstack/queue-api@0.2.7
16
+ - @checkstack/signal-backend@0.1.13
17
+ - @checkstack/signal-common@0.1.8
18
+
19
+ ## 0.4.14
20
+
21
+ ### Patch Changes
22
+
23
+ - 0ebbe56: Security Vulnerability Remediation completed:
24
+ - Refactored core authorization to Fail-Closed architecture with secure defaults.
25
+ - Implemented `assertTeamManagementAccess` to resolve BOLA in Teams Management.
26
+ - Protected internal S2S capabilities via explicit wildcard `serviceScope` definitions.
27
+ - Disarmed OS Command Injection in DiskCollector via strict regex validation and bash escaping.
28
+ - Re-architected inline script processing executing scripts in sandboxed Web Worker contexts.
29
+ - Isolated subprocess environment scopes in PingStrategy limiting variable leakage.
30
+ - Enforced strict token/API Key parsing with URLSearchParams checking.
31
+ - Explicitly fail-fast on missing DATABASE_URL configuration across independent backend clusters.
32
+ - Activated strict HTTP Security Headers (HSTS, CSP, X-Frame-Options) across the API automatically.
33
+ - Updated dependencies [0ebbe56]
34
+ - @checkstack/auth-common@0.5.6
35
+ - @checkstack/backend-api@0.8.1
36
+ - @checkstack/common@0.6.3
37
+ - @checkstack/queue-api@0.2.6
38
+ - @checkstack/signal-backend@0.1.12
39
+ - @checkstack/api-docs-common@0.1.7
40
+ - @checkstack/signal-common@0.1.7
41
+
3
42
  ## 0.4.13
4
43
 
5
44
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,9 @@
1
1
  {
2
2
  "name": "@checkstack/backend",
3
- "version": "0.4.13",
3
+ "version": "0.4.15",
4
+ "checkstack": {
5
+ "type": "backend"
6
+ },
4
7
  "type": "module",
5
8
  "scripts": {
6
9
  "dev": "bun --env-file=../../.env --watch src/index.ts",
@@ -10,33 +13,33 @@
10
13
  "lint:code": "eslint . --max-warnings 0"
11
14
  },
12
15
  "dependencies": {
13
- "@checkstack/api-docs-common": "0.1.6",
14
- "@checkstack/auth-common": "0.5.5",
15
- "@checkstack/backend-api": "0.7.0",
16
- "@checkstack/common": "0.6.2",
16
+ "@checkstack/api-docs-common": "0.1.7",
17
+ "@checkstack/auth-common": "0.5.6",
18
+ "@checkstack/backend-api": "0.8.1",
19
+ "@checkstack/common": "0.6.3",
17
20
  "@checkstack/drizzle-helper": "0.0.3",
18
- "@checkstack/queue-api": "0.2.4",
19
- "@checkstack/signal-backend": "0.1.10",
20
- "@checkstack/signal-common": "0.1.6",
21
+ "@checkstack/queue-api": "0.2.6",
22
+ "@checkstack/signal-backend": "0.1.12",
23
+ "@checkstack/signal-common": "0.1.7",
21
24
  "@hono/zod-validator": "^0.7.6",
22
- "@orpc/client": "^1.13.2",
25
+ "@orpc/client": "^1.13.14",
26
+ "@orpc/contract": "^1.13.14",
23
27
  "@orpc/openapi": "^1.13.2",
24
28
  "@orpc/server": "^1.13.2",
25
29
  "@orpc/zod": "^1.13.2",
26
30
  "better-auth": "^1.4.7",
27
- "drizzle-orm": "^0.45.1",
28
- "hono": "^4.0.0",
31
+ "drizzle-orm": "^0.45.0",
32
+ "hono": "^4.12.14",
29
33
  "jose": "^6.1.3",
30
34
  "pg": "^8.11.0",
31
35
  "winston": "^3.19.0",
32
36
  "zod": "^4.2.1"
33
37
  },
34
38
  "devDependencies": {
35
- "drizzle-kit": "^0.31.8",
36
39
  "@types/pg": "^8.11.0",
37
40
  "@types/bun": "latest",
38
41
  "@checkstack/tsconfig": "0.0.3",
39
42
  "@checkstack/scripts": "0.1.1",
40
- "@checkstack/test-utils-backend": "0.1.10"
43
+ "@checkstack/test-utils-backend": "0.1.12"
41
44
  }
42
45
  }
package/src/db.ts CHANGED
@@ -17,4 +17,4 @@ export const adminPool = new Pool({
17
17
  connectionString,
18
18
  });
19
19
 
20
- export const db = drizzle(adminPool, { schema });
20
+ export const db = drizzle({ client: adminPool, schema });
package/src/index.ts CHANGED
@@ -60,6 +60,15 @@ app.use(
60
60
  );
61
61
  app.use("*", logger());
62
62
 
63
+ // SECURITY: Add missing standard security headers across all API responses
64
+ app.use("/api/*", async (c, next) => {
65
+ await next();
66
+ c.res.headers.set("X-Content-Type-Options", "nosniff");
67
+ c.res.headers.set("X-Frame-Options", "DENY");
68
+ c.res.headers.set("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'");
69
+ c.res.headers.set("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
70
+ });
71
+
63
72
  // Runtime config endpoint - returns BASE_URL for frontend
64
73
  app.get("/api/config", (c) => {
65
74
  const baseUrl = process.env.BASE_URL || "http://localhost:3000";
@@ -176,9 +176,12 @@ export function registerCoreServices({
176
176
  });
177
177
  const authClient = rpcClient.forPlugin(AuthApi);
178
178
  return await authClient.checkResourceTeamAccess(params);
179
- } catch {
180
- // Fall back to global access on error
181
- return { hasAccess: params.hasGlobalAccess };
179
+ } catch (error) {
180
+ // SECURITY: Fail-Closed deny access when auth service is unavailable
181
+ rootLogger.error(
182
+ `[auth] checkResourceTeamAccess: S2S call failed for resource ${params.resourceType}:${params.resourceId}. Denying access (Fail-Closed). Error: ${error}`,
183
+ );
184
+ return { hasAccess: false };
182
185
  }
183
186
  },
184
187
 
@@ -189,9 +192,12 @@ export function registerCoreServices({
189
192
  });
190
193
  const authClient = rpcClient.forPlugin(AuthApi);
191
194
  return await authClient.getAccessibleResourceIds(params);
192
- } catch {
193
- // Fall back to global access on error
194
- return params.hasGlobalAccess ? params.resourceIds : [];
195
+ } catch (error) {
196
+ // SECURITY: Fail-Closed return empty set when auth service is unavailable
197
+ rootLogger.error(
198
+ `[auth] getAccessibleResourceIds: S2S call failed for resource type ${params.resourceType}. Denying access (Fail-Closed). Error: ${error}`,
199
+ );
200
+ return [];
195
201
  }
196
202
  },
197
203
  };
@@ -38,6 +38,7 @@ describe("extractPluginMetadata", () => {
38
38
  name: "@checkstack/test-backend",
39
39
  version: "0.0.1",
40
40
  type: "module",
41
+ checkstack: { type: "backend" },
41
42
  })
42
43
  );
43
44
 
@@ -57,6 +58,7 @@ describe("extractPluginMetadata", () => {
57
58
  mockReadFileSync.mockReturnValue(
58
59
  JSON.stringify({
59
60
  name: "@checkstack/test-frontend",
61
+ checkstack: { type: "frontend" },
60
62
  })
61
63
  );
62
64
 
@@ -71,6 +73,7 @@ describe("extractPluginMetadata", () => {
71
73
  mockReadFileSync.mockReturnValue(
72
74
  JSON.stringify({
73
75
  name: "@checkstack/test-common",
76
+ checkstack: { type: "common" },
74
77
  })
75
78
  );
76
79
 
@@ -159,13 +162,13 @@ describe("discoverLocalPlugins", () => {
159
162
  // Mock package.json reads
160
163
  mockReadFileSync.mockImplementation(((filePath: string) => {
161
164
  if (filePath.includes("auth-backend")) {
162
- return JSON.stringify({ name: "@checkstack/auth-backend" });
165
+ return JSON.stringify({ name: "@checkstack/auth-backend", checkstack: { type: "backend" } });
163
166
  }
164
167
  if (filePath.includes("catalog-backend")) {
165
- return JSON.stringify({ name: "@checkstack/catalog-backend" });
168
+ return JSON.stringify({ name: "@checkstack/catalog-backend", checkstack: { type: "backend" } });
166
169
  }
167
170
  if (filePath.includes("invalid-plugin")) {
168
- return JSON.stringify({ name: "@checkstack/invalid-plugin" });
171
+ return JSON.stringify({ name: "@checkstack/invalid-plugin" }); // Missing block
169
172
  }
170
173
  return "{}";
171
174
  }) as typeof mockReadFileSync);
@@ -198,13 +201,13 @@ describe("discoverLocalPlugins", () => {
198
201
 
199
202
  mockReadFileSync.mockImplementation(((filePath: string) => {
200
203
  if (filePath.includes("auth-backend")) {
201
- return JSON.stringify({ name: "@checkstack/auth-backend" });
204
+ return JSON.stringify({ name: "@checkstack/auth-backend", checkstack: { type: "backend" } });
202
205
  }
203
206
  if (filePath.includes("auth-frontend")) {
204
- return JSON.stringify({ name: "@checkstack/auth-frontend" });
207
+ return JSON.stringify({ name: "@checkstack/auth-frontend", checkstack: { type: "frontend" } });
205
208
  }
206
209
  if (filePath.includes("auth-common")) {
207
- return JSON.stringify({ name: "@checkstack/auth-common" });
210
+ return JSON.stringify({ name: "@checkstack/auth-common", checkstack: { type: "common" } });
208
211
  }
209
212
  return "{}";
210
213
  }) as typeof mockReadFileSync);
@@ -1,8 +1,9 @@
1
1
  import path from "node:path";
2
2
  import fs from "node:fs";
3
- import { eq, and } from "drizzle-orm";
3
+ import { eq, and, notInArray } from "drizzle-orm";
4
4
  import type { SafeDatabase } from "@checkstack/backend-api";
5
5
  import { plugins } from "../schema";
6
+ import { rootLogger } from "../logger";
6
7
 
7
8
  export interface PluginMetadata {
8
9
  packageName: string; // From package.json "name"
@@ -34,16 +35,28 @@ export function extractPluginMetadata({
34
35
  return undefined;
35
36
  }
36
37
 
37
- // Determine plugin type from package name suffix
38
- let type: "backend" | "frontend" | "common";
39
- if (pkgJson.name.endsWith("-backend")) {
40
- type = "backend";
41
- } else if (pkgJson.name.endsWith("-frontend")) {
42
- type = "frontend";
43
- } else if (pkgJson.name.endsWith("-common")) {
44
- type = "common";
45
- } else {
46
- return undefined; // Not a valid plugin package
38
+ // Transition: Strictly require checkstack metadata block
39
+ if (!pkgJson.checkstack || typeof pkgJson.checkstack !== "object") {
40
+ if (pkgJson.name.endsWith("-backend") || pkgJson.name.endsWith("-frontend") || pkgJson.name.endsWith("-common")) {
41
+ rootLogger.debug(`⏭️ Skipping package '${pkgJson.name}': Missing 'checkstack' metadata block in package.json`);
42
+ }
43
+ return undefined;
44
+ }
45
+
46
+ // Exclusion: Never discover the host platform itself as a plugin
47
+ if (pkgJson.name === "@checkstack/backend") {
48
+ return undefined;
49
+ }
50
+
51
+ const type = pkgJson.checkstack.type;
52
+ if (type === "tooling") {
53
+ rootLogger.debug(`⏭️ Skipping package '${pkgJson.name}': Identified as 'tooling'`);
54
+ return undefined;
55
+ }
56
+
57
+ if (type !== "backend" && type !== "frontend" && type !== "common") {
58
+ rootLogger.debug(`⏭️ Skipping package '${pkgJson.name}': Invalid checkstack type '${type}'`);
59
+ return undefined;
47
60
  }
48
61
 
49
62
  return {
@@ -52,7 +65,8 @@ export function extractPluginMetadata({
52
65
  type,
53
66
  enabled: true, // Local plugins are always enabled
54
67
  };
55
- } catch {
68
+ } catch (error) {
69
+ rootLogger.debug(`⚠️ Failed to read package.json for ${pluginDir}:`, error);
56
70
  return undefined;
57
71
  }
58
72
  }
@@ -154,4 +168,26 @@ export async function syncPluginsToDatabase({
154
168
  }
155
169
  }
156
170
  }
171
+
172
+ // 3. Prune local plugins from DB that are no longer present on disk
173
+ // This is critical for Docker production builds where folders are pruned
174
+ const localPackageNames = localPlugins.map((p) => p.packageName);
175
+
176
+ try {
177
+ const whereCondition = and(
178
+ eq(plugins.isUninstallable, false),
179
+ localPackageNames.length > 0
180
+ ? notInArray(plugins.name, localPackageNames)
181
+ : undefined,
182
+ );
183
+
184
+ // satisfy unicorn/prefer-ternary
185
+ await (localPackageNames.length > 0
186
+ ? db.delete(plugins).where(whereCondition)
187
+ : db.delete(plugins).where(eq(plugins.isUninstallable, false)));
188
+
189
+ rootLogger.debug(" -> Local plugin synchronization complete");
190
+ } catch (error) {
191
+ rootLogger.error("❌ Failed to prune stale plugins from database:", error);
192
+ }
157
193
  }