@checkstack/notification-backend 1.0.5 → 1.2.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.
- package/CHANGELOG.md +206 -0
- package/drizzle/0007_funny_hobgoblin.sql +14 -0
- package/drizzle/meta/0007_snapshot.json +644 -0
- package/drizzle/meta/_journal.json +7 -0
- package/package.json +8 -8
- package/src/delivery-attempts.test.ts +482 -0
- package/src/delivery-attempts.ts +146 -0
- package/src/index.ts +8 -0
- package/src/post-json.test.ts +133 -0
- package/src/post-json.ts +86 -0
- package/src/render.ts +29 -0
- package/src/router.test.ts +1021 -25
- package/src/router.ts +81 -9
- package/src/schema.ts +52 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,211 @@
|
|
|
1
1
|
# @checkstack/notification-backend
|
|
2
2
|
|
|
3
|
+
## 1.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- f23f3c9: Add per-channel notification delivery-attempt tracking
|
|
8
|
+
(Phase 8 of the v1 polishing plan). The external dispatch loop now
|
|
9
|
+
persists one row per `strategy.send(...)` call into a new
|
|
10
|
+
`notification_delivery_attempts` table - both successes and
|
|
11
|
+
failures - so silent delivery breakage (misconfigured webhooks, dead
|
|
12
|
+
channels) becomes queryable instead of buried in logs.
|
|
13
|
+
|
|
14
|
+
- `@checkstack/notification-backend` adds the
|
|
15
|
+
`notification_delivery_attempts` table, the matching Drizzle
|
|
16
|
+
migration, and a new `dispatchWithAttempt` helper that wraps every
|
|
17
|
+
external `strategy.send(...)` with duration measurement and
|
|
18
|
+
best-effort row persistence. The insert is intentionally
|
|
19
|
+
fire-and-forget: if writing the attempt row itself errors, the
|
|
20
|
+
dispatch loop logs and continues so visibility tracking can never
|
|
21
|
+
introduce a _new_ silent failure.
|
|
22
|
+
- `@checkstack/notification-common` exports a new
|
|
23
|
+
`DeliveryAttemptSchema` zod schema, the
|
|
24
|
+
`ListDeliveryAttemptsInputSchema =
|
|
25
|
+
PaginationInput.extend({ notificationId })` input, and a new
|
|
26
|
+
`getDeliveryAttempts` procedure on the contract. The procedure is
|
|
27
|
+
gated by the existing `notificationAccess.admin`
|
|
28
|
+
(`notification:manage`) access rule - no new permission was
|
|
29
|
+
introduced.
|
|
30
|
+
- `@checkstack/notification-frontend` adds a minimal admin-only
|
|
31
|
+
`DeliveryAttemptsPage` (route id `notification.deliveryAttempts`,
|
|
32
|
+
path `/notifications/delivery-attempts`) and an "Open inspector"
|
|
33
|
+
link from the Notification Settings page for users with
|
|
34
|
+
`notification:manage`. No client-side `isAdmin` gate - the FORBIDDEN
|
|
35
|
+
case is rendered via the standard error-state branch on the page,
|
|
36
|
+
enforced by the contract.
|
|
37
|
+
|
|
38
|
+
Visibility only: there is no retry mechanism in this phase. A
|
|
39
|
+
`failure` row is a final outcome an admin actions manually
|
|
40
|
+
(re-trigger the source event, fix the misconfigured channel).
|
|
41
|
+
Automated retries are deferred to v1.1.
|
|
42
|
+
|
|
43
|
+
Strategy errors thrown during `send(...)` are persisted via
|
|
44
|
+
`extractErrorMessage(error)` so secrets potentially embedded in raw
|
|
45
|
+
error objects (webhook URLs, OAuth tokens reachable from the strategy
|
|
46
|
+
send context) are not stored verbatim.
|
|
47
|
+
|
|
48
|
+
See the new
|
|
49
|
+
`docs/src/content/docs/backend/notification-delivery.md` page for the
|
|
50
|
+
full surface description.
|
|
51
|
+
|
|
52
|
+
### Patch Changes
|
|
53
|
+
|
|
54
|
+
- f23f3c9: Add `correlationMiddleware` to `@checkstack/backend-api` and apply it
|
|
55
|
+
to every plugin/core router so each request carries a stable
|
|
56
|
+
`x-correlation-id` (read from the inbound header, or freshly minted
|
|
57
|
+
via `crypto.randomUUID()` when absent) and an auto-injected child
|
|
58
|
+
logger bound with `{ correlationId, pluginId, userId? }`. The ID is
|
|
59
|
+
echoed back on the response header so the caller can correlate their
|
|
60
|
+
client-side trace to the server logs.
|
|
61
|
+
|
|
62
|
+
The `Logger` interface in `@checkstack/backend-api` now formally
|
|
63
|
+
documents the structured-metadata convention (`logger.info("msg",
|
|
64
|
+
{ ...meta })`) alongside the long-standing varargs shape. Winston's
|
|
65
|
+
splat handling already routes both shapes through the same vararg
|
|
66
|
+
slot, so existing call sites are unaffected. A new optional
|
|
67
|
+
`Logger.child(meta)` method captures the metadata-binding contract the
|
|
68
|
+
new middleware relies on; production loggers always implement it,
|
|
69
|
+
minimal test mocks may omit it (the middleware falls back gracefully).
|
|
70
|
+
|
|
71
|
+
`RpcContext` grew two optional `Headers` bags, `requestHeaders` and
|
|
72
|
+
`responseHeaders`, populated by the outer Hono `/api/*` and `/rest/*`
|
|
73
|
+
handlers in `@checkstack/backend`. They are write-through observation
|
|
74
|
+
points for middleware; an `RpcContext` constructed without them (S2S
|
|
75
|
+
clients, tests) keeps working — the echo is a silent no-op and the ID
|
|
76
|
+
is still bound onto the child logger for server-side correlation.
|
|
77
|
+
|
|
78
|
+
The scaffolding template in `@checkstack/scripts` was updated so any
|
|
79
|
+
new plugin generated via `bun run create` wires the middleware in the
|
|
80
|
+
expected `.use(correlationMiddleware).use(autoAuthMiddleware)` order
|
|
81
|
+
out of the box.
|
|
82
|
+
|
|
83
|
+
- f23f3c9: Sweep every paginated `*-common` contract onto the canonical
|
|
84
|
+
`PaginationInput` / `PaginatedResult` from `@checkstack/common` and
|
|
85
|
+
remove the now-unused legacy exports.
|
|
86
|
+
|
|
87
|
+
**BREAKING CHANGE** - `@checkstack/common` drops the deprecated
|
|
88
|
+
`PaginationInputSchema`, `paginatedOutput`, and `PaginatedResponse`
|
|
89
|
+
symbols. Callers must consume `PaginationInput` (input) and
|
|
90
|
+
`PaginatedResult(itemSchema)` (output) instead. The canonical input is
|
|
91
|
+
`{ limit (1-100, default 20), offset (>= 0, default 0) }`; the
|
|
92
|
+
canonical output envelope is
|
|
93
|
+
`{ items, total, limit, offset }`.
|
|
94
|
+
|
|
95
|
+
**BREAKING CHANGE** - `@checkstack/notification-common` migrates
|
|
96
|
+
`getNotifications` off the legacy `PaginationInputSchema`
|
|
97
|
+
(`{ limit, offset, unreadOnly }` with output `{ notifications, total }`)
|
|
98
|
+
onto `ListNotificationsInputSchema =
|
|
99
|
+
PaginationInput.extend({ unreadOnly })` and
|
|
100
|
+
`PaginatedResult(NotificationSchema)`. The output key changes from
|
|
101
|
+
`notifications` to `items`, and `limit` / `offset` are now echoed on
|
|
102
|
+
the response. The `PaginationInput` type alias previously exported
|
|
103
|
+
from `notification-common` is removed - use `ListNotificationsInput`
|
|
104
|
+
or the canonical `PaginationInput` from `@checkstack/common`.
|
|
105
|
+
|
|
106
|
+
**BREAKING CHANGE** - `@checkstack/integration-common` migrates
|
|
107
|
+
`listSubscriptions` (inline `{ page, pageSize, ... }` -> output
|
|
108
|
+
`{ subscriptions, total }`) and `getDeliveryLogs` (via
|
|
109
|
+
`DeliveryLogQueryInputSchema` `{ subscriptionId?, eventType?, status?,
|
|
110
|
+
page, pageSize }` -> output `{ logs, total }`) onto the canonical
|
|
111
|
+
`PaginationInput.extend({...})` input and
|
|
112
|
+
`PaginatedResult(itemSchema)` output. External callers must switch
|
|
113
|
+
from `{ page, pageSize }` to `{ limit, offset }` and read response
|
|
114
|
+
items from `data.items` (no more `data.subscriptions` / `data.logs`).
|
|
115
|
+
|
|
116
|
+
The matching `*-backend` handlers were updated to consume the new
|
|
117
|
+
input shape (`offset` arithmetic in lieu of `(page - 1) * pageSize`)
|
|
118
|
+
and to echo `limit` / `offset` on the response. The `*-frontend` call
|
|
119
|
+
sites in `NotificationsPage`, `NotificationBell`, `IntegrationsPage`,
|
|
120
|
+
and `DeliveryLogsPage` were updated to send the new input shape and
|
|
121
|
+
read `data.items`.
|
|
122
|
+
|
|
123
|
+
- f23f3c9: Phase 9 of the v1 polishing plan: tighten the plugin loader's boot-time
|
|
124
|
+
hook policy and backfill notification-router test coverage.
|
|
125
|
+
|
|
126
|
+
`@checkstack/backend` adopts an explicit per-hook policy for the two
|
|
127
|
+
boot-time hooks the plugin loader emits. `pluginInitialized` now
|
|
128
|
+
**halts the boot** if a subscriber throws — a failing subscriber here
|
|
129
|
+
means a downstream never wired itself against the freshly initialised
|
|
130
|
+
plugin, and continuing past that would leave the platform serving
|
|
131
|
+
traffic in a half-wired state. `accessRulesRegistered` keeps its
|
|
132
|
+
log-and-continue behaviour but escalates to `error` level and emits a
|
|
133
|
+
summary count if any subscriber failed; boot-blocking this hook would
|
|
134
|
+
let one misbehaving plugin DOS every other plugin on the same
|
|
135
|
+
instance. The policy is documented inline at each emit site and in a
|
|
136
|
+
new `docs/src/content/docs/backend/plugin-hook-policy.md` page.
|
|
137
|
+
**BREAKING CHANGE**: subscribers to `pluginInitialized` that
|
|
138
|
+
previously threw silently (logged and swallowed) now halt platform
|
|
139
|
+
boot. Audit subscribers and ensure they handle their own internal
|
|
140
|
+
errors before throwing.
|
|
141
|
+
|
|
142
|
+
`@checkstack/notification-backend` ships a real
|
|
143
|
+
`core/notification-backend/src/router.test.ts` covering the dispatch
|
|
144
|
+
fan-out (`notifyForSubscription`: zero subscribers, multi-recipient
|
|
145
|
+
insert, `excludeUserIds`, plus NOT_FOUND/FORBIDDEN guard rails), the
|
|
146
|
+
canonical paginated read on `getNotifications` (envelope shape,
|
|
147
|
+
`unreadOnly` filter propagation, null→undefined column mapping), the
|
|
148
|
+
service-only `createGroup` upsert behaviour (happy path + idempotent
|
|
149
|
+
re-create), and the multi-strategy `sendTransactional` path with a
|
|
150
|
+
focused fallback-style assertion: when one strategy throws, the
|
|
151
|
+
dispatch loop continues to the next and surfaces the failure as a
|
|
152
|
+
per-strategy `success: false` row instead of short-circuiting. No
|
|
153
|
+
runtime changes to the notification router.
|
|
154
|
+
|
|
155
|
+
- Updated dependencies [f23f3c9]
|
|
156
|
+
- Updated dependencies [f23f3c9]
|
|
157
|
+
- Updated dependencies [f23f3c9]
|
|
158
|
+
- Updated dependencies [f23f3c9]
|
|
159
|
+
- @checkstack/common@0.11.0
|
|
160
|
+
- @checkstack/backend-api@0.17.0
|
|
161
|
+
- @checkstack/auth-backend@0.4.29
|
|
162
|
+
- @checkstack/notification-common@1.2.0
|
|
163
|
+
- @checkstack/auth-common@0.7.1
|
|
164
|
+
- @checkstack/signal-common@0.2.4
|
|
165
|
+
- @checkstack/cache-api@0.3.4
|
|
166
|
+
- @checkstack/queue-api@0.3.4
|
|
167
|
+
- @checkstack/cache-utils@0.2.9
|
|
168
|
+
|
|
169
|
+
## 1.1.0
|
|
170
|
+
|
|
171
|
+
### Minor Changes
|
|
172
|
+
|
|
173
|
+
- a06b899: Dead-code audit cleanup and a small platform of shared notification helpers.
|
|
174
|
+
|
|
175
|
+
**Removed (dead code)**
|
|
176
|
+
|
|
177
|
+
- `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`.
|
|
178
|
+
- `createMockQueueFactory` deprecated alias removed from `@checkstack/test-utils-backend`. Use `createMockQueueManager` directly.
|
|
179
|
+
|
|
180
|
+
**New shared helpers**
|
|
181
|
+
|
|
182
|
+
- `@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.
|
|
183
|
+
- `@checkstack/notification-common` now exports `SubjectStatusSchema` / `SubjectStatus`, mirroring the existing `ImportanceSchema`.
|
|
184
|
+
- `@checkstack/notification-backend` now exports:
|
|
185
|
+
- `SUBJECT_STATUS_EMOJI` / `IMPORTANCE_EMOJI` — the shared status / importance emoji maps that Discord, Slack, Teams, Webex and Telegram previously each redefined inline.
|
|
186
|
+
- `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 }`.
|
|
187
|
+
|
|
188
|
+
**Migrated to shared helpers**
|
|
189
|
+
|
|
190
|
+
- Discord, Slack, Gotify, Pushover notification backends now use `postJson`. Outer try/catch + per-plugin error mapping deleted (~140 LOC).
|
|
191
|
+
- Discord, Slack, Teams, Telegram, Webex notification backends now use `IMPORTANCE_EMOJI`. Discord, Slack, Teams use `SUBJECT_STATUS_EMOJI`.
|
|
192
|
+
- 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`).
|
|
193
|
+
|
|
194
|
+
**API surface tightening**
|
|
195
|
+
|
|
196
|
+
- 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.
|
|
197
|
+
|
|
198
|
+
### Patch Changes
|
|
199
|
+
|
|
200
|
+
- Updated dependencies [a06b899]
|
|
201
|
+
- Updated dependencies [a06b899]
|
|
202
|
+
- @checkstack/backend-api@0.16.0
|
|
203
|
+
- @checkstack/notification-common@1.1.1
|
|
204
|
+
- @checkstack/auth-backend@0.4.28
|
|
205
|
+
- @checkstack/cache-api@0.3.3
|
|
206
|
+
- @checkstack/queue-api@0.3.3
|
|
207
|
+
- @checkstack/cache-utils@0.2.8
|
|
208
|
+
|
|
3
209
|
## 1.0.5
|
|
4
210
|
|
|
5
211
|
### Patch Changes
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
CREATE TYPE "notification_delivery_status" AS ENUM('success', 'failure');--> statement-breakpoint
|
|
2
|
+
CREATE TABLE "notification_delivery_attempts" (
|
|
3
|
+
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
4
|
+
"notification_id" uuid NOT NULL,
|
|
5
|
+
"strategy_qualified_id" text NOT NULL,
|
|
6
|
+
"attempted_at" timestamp DEFAULT now() NOT NULL,
|
|
7
|
+
"status" "notification_delivery_status" NOT NULL,
|
|
8
|
+
"error_message" text,
|
|
9
|
+
"duration_ms" integer NOT NULL
|
|
10
|
+
);
|
|
11
|
+
--> statement-breakpoint
|
|
12
|
+
ALTER TABLE "notification_delivery_attempts" ADD CONSTRAINT "notification_delivery_attempts_notification_id_notifications_id_fk" FOREIGN KEY ("notification_id") REFERENCES "notifications"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
13
|
+
CREATE INDEX "notification_delivery_attempts_notification_idx" ON "notification_delivery_attempts" USING btree ("notification_id");--> statement-breakpoint
|
|
14
|
+
CREATE INDEX "notification_delivery_attempts_attempted_at_idx" ON "notification_delivery_attempts" USING btree ("attempted_at");
|