@checkstack/backend 0.10.1 → 0.10.3

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,119 @@
1
1
  # @checkstack/backend
2
2
 
3
+ ## 0.10.3
4
+
5
+ ### Patch Changes
6
+
7
+ - f23f3c9: Add `correlationMiddleware` to `@checkstack/backend-api` and apply it
8
+ to every plugin/core router so each request carries a stable
9
+ `x-correlation-id` (read from the inbound header, or freshly minted
10
+ via `crypto.randomUUID()` when absent) and an auto-injected child
11
+ logger bound with `{ correlationId, pluginId, userId? }`. The ID is
12
+ echoed back on the response header so the caller can correlate their
13
+ client-side trace to the server logs.
14
+
15
+ The `Logger` interface in `@checkstack/backend-api` now formally
16
+ documents the structured-metadata convention (`logger.info("msg",
17
+ { ...meta })`) alongside the long-standing varargs shape. Winston's
18
+ splat handling already routes both shapes through the same vararg
19
+ slot, so existing call sites are unaffected. A new optional
20
+ `Logger.child(meta)` method captures the metadata-binding contract the
21
+ new middleware relies on; production loggers always implement it,
22
+ minimal test mocks may omit it (the middleware falls back gracefully).
23
+
24
+ `RpcContext` grew two optional `Headers` bags, `requestHeaders` and
25
+ `responseHeaders`, populated by the outer Hono `/api/*` and `/rest/*`
26
+ handlers in `@checkstack/backend`. They are write-through observation
27
+ points for middleware; an `RpcContext` constructed without them (S2S
28
+ clients, tests) keeps working — the echo is a silent no-op and the ID
29
+ is still bound onto the child logger for server-side correlation.
30
+
31
+ The scaffolding template in `@checkstack/scripts` was updated so any
32
+ new plugin generated via `bun run create` wires the middleware in the
33
+ expected `.use(correlationMiddleware).use(autoAuthMiddleware)` order
34
+ out of the box.
35
+
36
+ - f23f3c9: Phase 9 of the v1 polishing plan: tighten the plugin loader's boot-time
37
+ hook policy and backfill notification-router test coverage.
38
+
39
+ `@checkstack/backend` adopts an explicit per-hook policy for the two
40
+ boot-time hooks the plugin loader emits. `pluginInitialized` now
41
+ **halts the boot** if a subscriber throws — a failing subscriber here
42
+ means a downstream never wired itself against the freshly initialised
43
+ plugin, and continuing past that would leave the platform serving
44
+ traffic in a half-wired state. `accessRulesRegistered` keeps its
45
+ log-and-continue behaviour but escalates to `error` level and emits a
46
+ summary count if any subscriber failed; boot-blocking this hook would
47
+ let one misbehaving plugin DOS every other plugin on the same
48
+ instance. The policy is documented inline at each emit site and in a
49
+ new `docs/src/content/docs/backend/plugin-hook-policy.md` page.
50
+ **BREAKING CHANGE**: subscribers to `pluginInitialized` that
51
+ previously threw silently (logged and swallowed) now halt platform
52
+ boot. Audit subscribers and ensure they handle their own internal
53
+ errors before throwing.
54
+
55
+ `@checkstack/notification-backend` ships a real
56
+ `core/notification-backend/src/router.test.ts` covering the dispatch
57
+ fan-out (`notifyForSubscription`: zero subscribers, multi-recipient
58
+ insert, `excludeUserIds`, plus NOT_FOUND/FORBIDDEN guard rails), the
59
+ canonical paginated read on `getNotifications` (envelope shape,
60
+ `unreadOnly` filter propagation, null→undefined column mapping), the
61
+ service-only `createGroup` upsert behaviour (happy path + idempotent
62
+ re-create), and the multi-strategy `sendTransactional` path with a
63
+ focused fallback-style assertion: when one strategy throws, the
64
+ dispatch loop continues to the next and surfaces the failure as a
65
+ per-strategy `success: false` row instead of short-circuiting. No
66
+ runtime changes to the notification router.
67
+
68
+ - Updated dependencies [f23f3c9]
69
+ - Updated dependencies [f23f3c9]
70
+ - Updated dependencies [f23f3c9]
71
+ - @checkstack/common@0.11.0
72
+ - @checkstack/backend-api@0.17.0
73
+ - @checkstack/api-docs-common@0.1.14
74
+ - @checkstack/auth-common@0.7.1
75
+ - @checkstack/pluginmanager-common@0.2.3
76
+ - @checkstack/signal-backend@0.2.8
77
+ - @checkstack/signal-common@0.2.4
78
+ - @checkstack/cache-api@0.3.4
79
+ - @checkstack/queue-api@0.3.4
80
+
81
+ ## 0.10.2
82
+
83
+ ### Patch Changes
84
+
85
+ - a06b899: Dead-code audit cleanup and a small platform of shared notification helpers.
86
+
87
+ **Removed (dead code)**
88
+
89
+ - `core/backend/src/plugin-manager/deregistration-guard.ts` deleted. The exported `assertCanDeregister()` was never called and was a less-complete version of the dependents+isUninstallable checks already done inline by `previewUninstallOriginator` / `uninstallOriginator` in `plugin-manager-orchestrator.ts`.
90
+ - `createMockQueueFactory` deprecated alias removed from `@checkstack/test-utils-backend`. Use `createMockQueueManager` directly.
91
+
92
+ **New shared helpers**
93
+
94
+ - `@checkstack/backend-api` now exports `requestTimeoutMs()` — a Zod field builder for outbound HTTP request timeouts (1s..60s, default 10s). Replaces hand-rolled `configNumber({}).min(1000).max(60_000).default(10_000)` in `integration-webhook-backend`, `integration-script-backend`, and `healthcheck-script-backend`'s inline collector.
95
+ - `@checkstack/notification-common` now exports `SubjectStatusSchema` / `SubjectStatus`, mirroring the existing `ImportanceSchema`.
96
+ - `@checkstack/notification-backend` now exports:
97
+ - `SUBJECT_STATUS_EMOJI` / `IMPORTANCE_EMOJI` — the shared status / importance emoji maps that Discord, Slack, Teams, Webex and Telegram previously each redefined inline.
98
+ - `postJson(opts)` — a timeout-bounded `fetch` wrapper that handles non-2xx logging and error mapping for webhook-style POSTs. Returns `{ ok: true, response } | { ok: false, error }`.
99
+
100
+ **Migrated to shared helpers**
101
+
102
+ - Discord, Slack, Gotify, Pushover notification backends now use `postJson`. Outer try/catch + per-plugin error mapping deleted (~140 LOC).
103
+ - Discord, Slack, Teams, Telegram, Webex notification backends now use `IMPORTANCE_EMOJI`. Discord, Slack, Teams use `SUBJECT_STATUS_EMOJI`.
104
+ - Teams, Webex, Backstage, Telegram kept their inline fetch/Bot logic: their error strings surface server response bodies to operators, or the transport isn't raw `fetch` (Telegram uses `grammy`'s `Bot`).
105
+
106
+ **API surface tightening**
107
+
108
+ - Per-plugin test-only re-exports in 6 notification backends (Pushover, Gotify, Backstage, Slack, Discord, Teams) and the `CertificateInfo` interface in `healthcheck-tls-backend/strategy.ts` are now JSDoc-tagged `@internal`. No behaviour change; signals that downstream consumers must not depend on them.
109
+
110
+ - Updated dependencies [a06b899]
111
+ - Updated dependencies [a06b899]
112
+ - @checkstack/backend-api@0.16.0
113
+ - @checkstack/cache-api@0.3.3
114
+ - @checkstack/queue-api@0.3.3
115
+ - @checkstack/signal-backend@0.2.7
116
+
3
117
  ## 0.10.1
4
118
 
5
119
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkstack/backend",
3
- "version": "0.10.1",
3
+ "version": "0.10.3",
4
4
  "license": "Elastic-2.0",
5
5
  "checkstack": {
6
6
  "type": "backend"
@@ -16,12 +16,12 @@
16
16
  "dependencies": {
17
17
  "@checkstack/api-docs-common": "0.1.13",
18
18
  "@checkstack/auth-common": "0.7.0",
19
- "@checkstack/backend-api": "0.15.2",
19
+ "@checkstack/backend-api": "0.16.0",
20
20
  "@checkstack/common": "0.10.0",
21
21
  "@checkstack/drizzle-helper": "0.0.5",
22
- "@checkstack/cache-api": "0.3.1",
23
- "@checkstack/queue-api": "0.3.1",
24
- "@checkstack/signal-backend": "0.2.5",
22
+ "@checkstack/cache-api": "0.3.3",
23
+ "@checkstack/queue-api": "0.3.3",
24
+ "@checkstack/signal-backend": "0.2.7",
25
25
  "@checkstack/signal-common": "0.2.3",
26
26
  "@checkstack/pluginmanager-common": "0.2.2",
27
27
  "@hono/zod-validator": "^0.7.6",
@@ -46,7 +46,7 @@
46
46
  "@types/semver": "^7.5.0",
47
47
  "@checkstack/tsconfig": "0.0.7",
48
48
  "@checkstack/scripts": "0.3.2",
49
- "@checkstack/test-utils-backend": "0.1.26",
49
+ "@checkstack/test-utils-backend": "0.1.28",
50
50
  "drizzle-kit": "^0.31.10"
51
51
  }
52
52
  }
@@ -492,6 +492,23 @@ export async function loadPlugins({
492
492
  const eventBus = await deps.registry.get(coreServices.eventBus, {
493
493
  pluginId: "core",
494
494
  });
495
+ /**
496
+ * Boot-time hook policy for `pluginInitialized`: HALT on subscriber
497
+ * failure.
498
+ *
499
+ * Rationale: `pluginInitialized` fires immediately after a plugin's
500
+ * Phase 2 `init()` resolves. A throwing subscriber here means a
501
+ * structural problem with that plugin's startup (a downstream
502
+ * consumer never got the chance to wire itself against the freshly
503
+ * initialized plugin). Continuing past such a failure would leave
504
+ * the platform running in a half-wired state — surfacing it during
505
+ * boot is safer than discovering the missing wiring at request time.
506
+ *
507
+ * Matches the `afterPluginsReady` failure handling below (log +
508
+ * rethrow with a clearer message naming the failing plugin).
509
+ *
510
+ * See `docs/src/content/docs/backend/plugin-hook-policy.md`.
511
+ */
495
512
  for (const p of pendingInits) {
496
513
  try {
497
514
  await eventBus.emit(coreHooks.pluginInitialized, {
@@ -499,9 +516,13 @@ export async function loadPlugins({
499
516
  });
500
517
  } catch (error) {
501
518
  rootLogger.error(
502
- `Failed to emit pluginInitialized hook for ${p.metadata.pluginId}:`,
519
+ `❌ pluginInitialized hook subscriber threw for ${p.metadata.pluginId}; halting boot to avoid an inconsistent platform state:`,
503
520
  error,
504
521
  );
522
+ throw new Error(
523
+ `Failed pluginInitialized hook for plugin ${p.metadata.pluginId}`,
524
+ { cause: error },
525
+ );
505
526
  }
506
527
  }
507
528
 
@@ -546,6 +567,27 @@ export async function loadPlugins({
546
567
  }
547
568
  accessRulesByPlugin.get(pluginId)!.push(rule);
548
569
  }
570
+ /**
571
+ * Boot-time hook policy for `accessRulesRegistered`: CONTINUE on
572
+ * subscriber failure, escalate the log to `error`.
573
+ *
574
+ * Rationale: this hook drives downstream consumers that mirror
575
+ * access-rule registrations into their own stores (e.g. an
576
+ * access-rule sync worker that hydrates a DB cache). A single
577
+ * misbehaving subscriber here MUST NOT be allowed to block the
578
+ * entire platform from booting — that would let one buggy plugin
579
+ * DOS every other plugin on the same instance.
580
+ *
581
+ * We deliberately diverge from the `pluginInitialized` policy
582
+ * because the failure mode is operational (a downstream's cache is
583
+ * stale until it recovers), not structural (the platform itself is
584
+ * fully wired even if a subscriber threw). Boot continues, the
585
+ * loud `error` log surfaces the breakage to operators, and the
586
+ * subscriber's owning plugin can implement its own retry / repair.
587
+ *
588
+ * See `docs/src/content/docs/backend/plugin-hook-policy.md`.
589
+ */
590
+ let accessRulesHookFailures = 0;
549
591
  for (const [pluginId, accessRules] of accessRulesByPlugin) {
550
592
  try {
551
593
  await eventBus.emit(coreHooks.accessRulesRegistered, {
@@ -553,12 +595,18 @@ export async function loadPlugins({
553
595
  accessRules,
554
596
  });
555
597
  } catch (error) {
598
+ accessRulesHookFailures += 1;
556
599
  rootLogger.error(
557
- `Failed to emit accessRulesRegistered hook for ${pluginId}:`,
600
+ `❌ accessRulesRegistered hook subscriber threw for ${pluginId}; continuing boot (a single misbehaving plugin must not block the platform). Downstream consumers may have a stale view of this plugin's access rules until they recover:`,
558
601
  error,
559
602
  );
560
603
  }
561
604
  }
605
+ if (accessRulesHookFailures > 0) {
606
+ rootLogger.error(
607
+ `❌ ${accessRulesHookFailures} accessRulesRegistered hook subscriber(s) failed during boot. Platform continued but downstream access-rule consumers may be stale.`,
608
+ );
609
+ }
562
610
  // Run afterPluginsReady in topologically-sorted order, matching Phase
563
611
  // 2 init order. Iterating `pendingInits` directly would use registration
564
612
  // order, which races dependency chains: e.g. catalog's
@@ -2,6 +2,7 @@ import { implement, ORPCError } from "@orpc/server";
2
2
  import { desc } from "drizzle-orm";
3
3
  import {
4
4
  autoAuthMiddleware,
5
+ correlationMiddleware,
5
6
  coreServices,
6
7
  type RpcContext,
7
8
  type SafeDatabase,
@@ -109,6 +110,7 @@ export function createPluginManagerRouter({
109
110
 
110
111
  const impl = implement(pluginManagerContract)
111
112
  .$context<RpcContext>()
113
+ .use(correlationMiddleware)
112
114
  .use(autoAuthMiddleware);
113
115
 
114
116
  return impl.router({
@@ -1,41 +0,0 @@
1
- import { eq } from "drizzle-orm";
2
- import { SafeDatabase } from "@checkstack/backend-api";
3
- import { ORPCError } from "@orpc/server";
4
- import { plugins } from "../schema";
5
-
6
- /**
7
- * Validates that a plugin can be deregistered.
8
- * throws ORPCError if the plugin is not uninstallable or has dependents.
9
- */
10
- export async function assertCanDeregister({
11
- pluginId,
12
- db,
13
- }: {
14
- pluginId: string;
15
- db: SafeDatabase<Record<string, unknown>>;
16
- }): Promise<void> {
17
- // 1. Check if plugin exists
18
- const pluginRows = await db
19
- .select()
20
- .from(plugins)
21
- .where(eq(plugins.name, pluginId));
22
-
23
- if (pluginRows.length === 0) {
24
- throw new ORPCError("NOT_FOUND", {
25
- message: `Plugin "${pluginId}" not found`,
26
- });
27
- }
28
-
29
- const plugin = pluginRows[0];
30
-
31
- // 2. Check isUninstallable flag
32
- if (!plugin.isUninstallable) {
33
- throw new ORPCError("FORBIDDEN", {
34
- message: `Plugin "${pluginId}" is a core platform component and cannot be uninstalled`,
35
- });
36
- }
37
-
38
- // 3. TODO: Check for dependent plugins (consumers of this plugin's services)
39
- // This would require tracking service dependencies at runtime
40
- // For now, we skip this check and let the deregistration proceed
41
- }