@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.
|
|
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.
|
|
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.
|
|
23
|
-
"@checkstack/queue-api": "0.3.
|
|
24
|
-
"@checkstack/signal-backend": "0.2.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
}
|