@checkstack/backend 0.10.2 → 0.10.4

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,92 @@
1
1
  # @checkstack/backend
2
2
 
3
+ ## 0.10.4
4
+
5
+ ### Patch Changes
6
+
7
+ - @checkstack/backend-api@0.17.1
8
+ - @checkstack/cache-api@0.3.5
9
+ - @checkstack/queue-api@0.3.5
10
+ - @checkstack/signal-backend@0.2.9
11
+
12
+ ## 0.10.3
13
+
14
+ ### Patch Changes
15
+
16
+ - f23f3c9: Add `correlationMiddleware` to `@checkstack/backend-api` and apply it
17
+ to every plugin/core router so each request carries a stable
18
+ `x-correlation-id` (read from the inbound header, or freshly minted
19
+ via `crypto.randomUUID()` when absent) and an auto-injected child
20
+ logger bound with `{ correlationId, pluginId, userId? }`. The ID is
21
+ echoed back on the response header so the caller can correlate their
22
+ client-side trace to the server logs.
23
+
24
+ The `Logger` interface in `@checkstack/backend-api` now formally
25
+ documents the structured-metadata convention (`logger.info("msg",
26
+ { ...meta })`) alongside the long-standing varargs shape. Winston's
27
+ splat handling already routes both shapes through the same vararg
28
+ slot, so existing call sites are unaffected. A new optional
29
+ `Logger.child(meta)` method captures the metadata-binding contract the
30
+ new middleware relies on; production loggers always implement it,
31
+ minimal test mocks may omit it (the middleware falls back gracefully).
32
+
33
+ `RpcContext` grew two optional `Headers` bags, `requestHeaders` and
34
+ `responseHeaders`, populated by the outer Hono `/api/*` and `/rest/*`
35
+ handlers in `@checkstack/backend`. They are write-through observation
36
+ points for middleware; an `RpcContext` constructed without them (S2S
37
+ clients, tests) keeps working — the echo is a silent no-op and the ID
38
+ is still bound onto the child logger for server-side correlation.
39
+
40
+ The scaffolding template in `@checkstack/scripts` was updated so any
41
+ new plugin generated via `bun run create` wires the middleware in the
42
+ expected `.use(correlationMiddleware).use(autoAuthMiddleware)` order
43
+ out of the box.
44
+
45
+ - f23f3c9: Phase 9 of the v1 polishing plan: tighten the plugin loader's boot-time
46
+ hook policy and backfill notification-router test coverage.
47
+
48
+ `@checkstack/backend` adopts an explicit per-hook policy for the two
49
+ boot-time hooks the plugin loader emits. `pluginInitialized` now
50
+ **halts the boot** if a subscriber throws — a failing subscriber here
51
+ means a downstream never wired itself against the freshly initialised
52
+ plugin, and continuing past that would leave the platform serving
53
+ traffic in a half-wired state. `accessRulesRegistered` keeps its
54
+ log-and-continue behaviour but escalates to `error` level and emits a
55
+ summary count if any subscriber failed; boot-blocking this hook would
56
+ let one misbehaving plugin DOS every other plugin on the same
57
+ instance. The policy is documented inline at each emit site and in a
58
+ new `docs/src/content/docs/backend/plugin-hook-policy.md` page.
59
+ **BREAKING CHANGE**: subscribers to `pluginInitialized` that
60
+ previously threw silently (logged and swallowed) now halt platform
61
+ boot. Audit subscribers and ensure they handle their own internal
62
+ errors before throwing.
63
+
64
+ `@checkstack/notification-backend` ships a real
65
+ `core/notification-backend/src/router.test.ts` covering the dispatch
66
+ fan-out (`notifyForSubscription`: zero subscribers, multi-recipient
67
+ insert, `excludeUserIds`, plus NOT_FOUND/FORBIDDEN guard rails), the
68
+ canonical paginated read on `getNotifications` (envelope shape,
69
+ `unreadOnly` filter propagation, null→undefined column mapping), the
70
+ service-only `createGroup` upsert behaviour (happy path + idempotent
71
+ re-create), and the multi-strategy `sendTransactional` path with a
72
+ focused fallback-style assertion: when one strategy throws, the
73
+ dispatch loop continues to the next and surfaces the failure as a
74
+ per-strategy `success: false` row instead of short-circuiting. No
75
+ runtime changes to the notification router.
76
+
77
+ - Updated dependencies [f23f3c9]
78
+ - Updated dependencies [f23f3c9]
79
+ - Updated dependencies [f23f3c9]
80
+ - @checkstack/common@0.11.0
81
+ - @checkstack/backend-api@0.17.0
82
+ - @checkstack/api-docs-common@0.1.14
83
+ - @checkstack/auth-common@0.7.1
84
+ - @checkstack/pluginmanager-common@0.2.3
85
+ - @checkstack/signal-backend@0.2.8
86
+ - @checkstack/signal-common@0.2.4
87
+ - @checkstack/cache-api@0.3.4
88
+ - @checkstack/queue-api@0.3.4
89
+
3
90
  ## 0.10.2
4
91
 
5
92
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkstack/backend",
3
- "version": "0.10.2",
3
+ "version": "0.10.4",
4
4
  "license": "Elastic-2.0",
5
5
  "checkstack": {
6
6
  "type": "backend"
@@ -14,16 +14,16 @@
14
14
  "lint:code": "eslint . --max-warnings 0"
15
15
  },
16
16
  "dependencies": {
17
- "@checkstack/api-docs-common": "0.1.13",
18
- "@checkstack/auth-common": "0.7.0",
19
- "@checkstack/backend-api": "0.15.3",
20
- "@checkstack/common": "0.10.0",
17
+ "@checkstack/api-docs-common": "0.1.14",
18
+ "@checkstack/auth-common": "0.7.1",
19
+ "@checkstack/backend-api": "0.17.0",
20
+ "@checkstack/common": "0.11.0",
21
21
  "@checkstack/drizzle-helper": "0.0.5",
22
- "@checkstack/cache-api": "0.3.2",
23
- "@checkstack/queue-api": "0.3.2",
24
- "@checkstack/signal-backend": "0.2.6",
25
- "@checkstack/signal-common": "0.2.3",
26
- "@checkstack/pluginmanager-common": "0.2.2",
22
+ "@checkstack/cache-api": "0.3.4",
23
+ "@checkstack/queue-api": "0.3.4",
24
+ "@checkstack/signal-backend": "0.2.8",
25
+ "@checkstack/signal-common": "0.2.4",
26
+ "@checkstack/pluginmanager-common": "0.2.3",
27
27
  "@hono/zod-validator": "^0.7.6",
28
28
  "@orpc/client": "^1.13.14",
29
29
  "@orpc/contract": "^1.13.14",
@@ -45,8 +45,8 @@
45
45
  "@types/bun": "latest",
46
46
  "@types/semver": "^7.5.0",
47
47
  "@checkstack/tsconfig": "0.0.7",
48
- "@checkstack/scripts": "0.3.2",
49
- "@checkstack/test-utils-backend": "0.1.27",
48
+ "@checkstack/scripts": "0.3.3",
49
+ "@checkstack/test-utils-backend": "0.1.29",
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({