@checkstack/backend 0.14.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.
@@ -8,6 +8,7 @@ import {
8
8
  Versioned,
9
9
  VersionedAggregated,
10
10
  aggregatedCounter,
11
+ configString,
11
12
  } from "@checkstack/backend-api";
12
13
  import { createMockLogger } from "@checkstack/test-utils-backend";
13
14
  import { z } from "zod";
@@ -80,6 +81,39 @@ describe("CoreHealthCheckRegistry", () => {
80
81
  expect(registry.getStrategy(qualifiedId)).toBe(mockStrategy1);
81
82
  });
82
83
 
84
+ it("throws at register time when a field is both secret and templatable", () => {
85
+ const conflictingStrategy: HealthCheckStrategy = {
86
+ id: "conflicting-strategy",
87
+ displayName: "Conflicting",
88
+ description: "Has a field that is both secret and templatable",
89
+ config: new Versioned({
90
+ version: 1,
91
+ schema: z.object({
92
+ timeout: z.number().default(30_000),
93
+ token: configString({ "x-secret": true, "x-templatable": true }),
94
+ }),
95
+ }),
96
+ result: new Versioned({
97
+ version: 1,
98
+ schema: z.record(z.string(), z.unknown()),
99
+ }),
100
+ aggregatedResult: new VersionedAggregated({
101
+ version: 1,
102
+ fields: { count: aggregatedCounter({}) },
103
+ }),
104
+ createClient: mock(() =>
105
+ Promise.resolve({
106
+ client: { exec: async () => ({}) },
107
+ close: () => {},
108
+ }),
109
+ ),
110
+ mergeResult: mock(() => ({})),
111
+ };
112
+ expect(() =>
113
+ registry.registerWithOwner(conflictingStrategy, mockOwner),
114
+ ).toThrow(/both/);
115
+ });
116
+
83
117
  it("should overwrite an existing strategy with the same qualified ID", () => {
84
118
  const overwritingStrategy: HealthCheckStrategy = {
85
119
  ...mockStrategy1,
@@ -2,6 +2,7 @@ import type { PluginMetadata } from "@checkstack/common";
2
2
  import {
3
3
  HealthCheckRegistry,
4
4
  HealthCheckStrategy,
5
+ assertNoSecretTemplatableConflict,
5
6
  } from "@checkstack/backend-api";
6
7
  import { rootLogger } from "../logger";
7
8
 
@@ -30,6 +31,12 @@ export class CoreHealthCheckRegistry {
30
31
  ownerPlugin: PluginMetadata,
31
32
  ): void {
32
33
  const qualifiedId = `${ownerPlugin.pluginId}.${strategy.id}`;
34
+ // Load-time guard: secret + templatable on one field is a config error
35
+ // (the two are resolved in separate ordered passes).
36
+ assertNoSecretTemplatableConflict({
37
+ schema: strategy.config.schema,
38
+ schemaName: `strategy:${qualifiedId}`,
39
+ });
33
40
  if (this.strategies.has(qualifiedId)) {
34
41
  rootLogger.warn(
35
42
  `HealthCheckStrategy '${qualifiedId}' is already registered. Overwriting.`,
@@ -0,0 +1,60 @@
1
+ import { describe, it, expect } from "bun:test";
2
+ import { createServiceRef } from "@checkstack/backend-api";
3
+ import type { PluginMetadata } from "@checkstack/common";
4
+ import { ServiceRegistry } from "./service-registry";
5
+
6
+ const metadata: PluginMetadata = { pluginId: "core" };
7
+
8
+ describe("ServiceRegistry resolution precedence", () => {
9
+ it("resolves a factory before a plain instance for the same ref", async () => {
10
+ const ref = createServiceRef<string>("test.svc");
11
+ const registry = new ServiceRegistry();
12
+ registry.register(ref, "instance");
13
+ registry.registerFactory(ref, () => "factory");
14
+ // get() tries factories first — this is the long-standing behaviour the
15
+ // dev-auth fix relies on (see below).
16
+ expect(await registry.get(ref, metadata)).toBe("factory");
17
+ });
18
+
19
+ it("returns the registered instance when no factory exists", async () => {
20
+ const ref = createServiceRef<string>("test.svc.instance-only");
21
+ const registry = new ServiceRegistry();
22
+ registry.register(ref, "instance");
23
+ expect(await registry.get(ref, metadata)).toBe("instance");
24
+ });
25
+
26
+ // Regression guard (#251): the CHECKSTACK_DEV_AUTH bypass was dead code on
27
+ // the API path because the production auth FACTORY (registered by
28
+ // `registerCoreServices`) shadowed a dev auth INSTANCE registered via
29
+ // `registerService(coreServices.auth, …)` — `get()` resolves factories
30
+ // before instances, so the dev auth was never returned and every plugin
31
+ // API request 401ed. The fix registers the dev auth as a FACTORY so it
32
+ // wins. This test models that exact wiring on the auth ref.
33
+ it("dev auth registered as a factory overrides a pre-existing production auth factory", async () => {
34
+ const authRef = createServiceRef<{ kind: string }>("checkstack.auth");
35
+ const registry = new ServiceRegistry();
36
+
37
+ // Production setup: real auth registered as a factory.
38
+ const prodAuth = { kind: "production" };
39
+ registry.registerFactory(authRef, () => prodAuth);
40
+
41
+ // Sanity: before the dev override, get() returns the production factory.
42
+ expect((await registry.get(authRef, metadata)).kind).toBe("production");
43
+
44
+ // Dev path: register dev auth as a factory (NOT a plain instance).
45
+ const devAuth = { kind: "dev" };
46
+ registry.registerFactory(authRef, () => devAuth);
47
+
48
+ // The dev auth must now be what get() returns.
49
+ expect((await registry.get(authRef, metadata)).kind).toBe("dev");
50
+ });
51
+
52
+ it("a dev auth registered as a plain instance is (still) shadowed by a factory — documents why the fix uses a factory", async () => {
53
+ const authRef = createServiceRef<{ kind: string }>("checkstack.auth.legacy");
54
+ const registry = new ServiceRegistry();
55
+ registry.registerFactory(authRef, () => ({ kind: "production" }));
56
+ // This is the BUGGY shape the fix replaced: an instance is shadowed.
57
+ registry.register(authRef, { kind: "dev" });
58
+ expect((await registry.get(authRef, metadata)).kind).toBe("production");
59
+ });
60
+ });
@@ -1,28 +1,53 @@
1
- import { describe, it, expect, beforeEach, mock } from "bun:test";
1
+ import { describe, it, expect, beforeEach, afterAll, mock } from "bun:test";
2
2
  import {
3
3
  extractPluginMetadata,
4
4
  discoverLocalPlugins,
5
5
  syncPluginsToDatabase,
6
6
  type PluginMetadata,
7
7
  } from "./plugin-discovery";
8
+ import * as realFs from "node:fs";
8
9
  import fs from "node:fs";
9
10
  import path from "node:path";
10
11
 
11
- // Mock filesystem for testing
12
+ // Mock filesystem for testing.
13
+ //
14
+ // `mock.module("node:fs", …)` is PROCESS-GLOBAL in bun and is NOT undone by
15
+ // `mock.restore()`, so a partial stub would leak the broken module to every
16
+ // fs-using test that sorts after this file (e.g. the scaffold tests that call
17
+ // `mkdtempSync`/`statSync`/`writeFileSync`). Two safeguards:
18
+ // 1. The stub spreads the REAL module and overrides only the three functions
19
+ // these tests assert on — so even while active, every other fs API works.
20
+ // 2. `afterAll` re-mocks `node:fs` back to the pristine real module, undoing
21
+ // the override for subsequently-loaded test files.
22
+ // Named exports (namespace) and the default export, captured before mocking.
23
+ const realFsModule: typeof realFs = { ...realFs };
24
+ const realFsDefault = fs;
25
+
12
26
  const mockExistsSync = mock(() => true);
13
27
  const mockReadFileSync = mock(() => "{}");
14
28
  const mockReaddirSync = mock(() => []);
15
29
 
16
- mock.module("node:fs", () => {
30
+ function mockFsModule() {
17
31
  const exports = {
32
+ ...realFsModule,
18
33
  existsSync: mockExistsSync,
19
34
  readFileSync: mockReadFileSync,
20
35
  readdirSync: mockReaddirSync,
21
36
  };
22
37
  return {
23
38
  ...exports,
24
- default: exports,
39
+ default: { ...realFsDefault, ...exports },
25
40
  };
41
+ }
42
+
43
+ mock.module("node:fs", mockFsModule);
44
+
45
+ afterAll(() => {
46
+ // Restore the pristine fs module so later test files don't inherit the stub.
47
+ mock.module("node:fs", () => ({
48
+ ...realFsModule,
49
+ default: realFsDefault,
50
+ }));
26
51
  });
27
52
 
28
53
  describe("extractPluginMetadata", () => {