@checkstack/notification-common 1.1.1 → 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 CHANGED
@@ -1,5 +1,101 @@
1
1
  # @checkstack/notification-common
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
+ - f23f3c9: Sweep every paginated `*-common` contract onto the canonical
53
+ `PaginationInput` / `PaginatedResult` from `@checkstack/common` and
54
+ remove the now-unused legacy exports.
55
+
56
+ **BREAKING CHANGE** - `@checkstack/common` drops the deprecated
57
+ `PaginationInputSchema`, `paginatedOutput`, and `PaginatedResponse`
58
+ symbols. Callers must consume `PaginationInput` (input) and
59
+ `PaginatedResult(itemSchema)` (output) instead. The canonical input is
60
+ `{ limit (1-100, default 20), offset (>= 0, default 0) }`; the
61
+ canonical output envelope is
62
+ `{ items, total, limit, offset }`.
63
+
64
+ **BREAKING CHANGE** - `@checkstack/notification-common` migrates
65
+ `getNotifications` off the legacy `PaginationInputSchema`
66
+ (`{ limit, offset, unreadOnly }` with output `{ notifications, total }`)
67
+ onto `ListNotificationsInputSchema =
68
+ PaginationInput.extend({ unreadOnly })` and
69
+ `PaginatedResult(NotificationSchema)`. The output key changes from
70
+ `notifications` to `items`, and `limit` / `offset` are now echoed on
71
+ the response. The `PaginationInput` type alias previously exported
72
+ from `notification-common` is removed - use `ListNotificationsInput`
73
+ or the canonical `PaginationInput` from `@checkstack/common`.
74
+
75
+ **BREAKING CHANGE** - `@checkstack/integration-common` migrates
76
+ `listSubscriptions` (inline `{ page, pageSize, ... }` -> output
77
+ `{ subscriptions, total }`) and `getDeliveryLogs` (via
78
+ `DeliveryLogQueryInputSchema` `{ subscriptionId?, eventType?, status?,
79
+ page, pageSize }` -> output `{ logs, total }`) onto the canonical
80
+ `PaginationInput.extend({...})` input and
81
+ `PaginatedResult(itemSchema)` output. External callers must switch
82
+ from `{ page, pageSize }` to `{ limit, offset }` and read response
83
+ items from `data.items` (no more `data.subscriptions` / `data.logs`).
84
+
85
+ The matching `*-backend` handlers were updated to consume the new
86
+ input shape (`offset` arithmetic in lieu of `(page - 1) * pageSize`)
87
+ and to echo `limit` / `offset` on the response. The `*-frontend` call
88
+ sites in `NotificationsPage`, `NotificationBell`, `IntegrationsPage`,
89
+ and `DeliveryLogsPage` were updated to send the new input shape and
90
+ read `data.items`.
91
+
92
+ ### Patch Changes
93
+
94
+ - Updated dependencies [f23f3c9]
95
+ - Updated dependencies [f23f3c9]
96
+ - @checkstack/common@0.11.0
97
+ - @checkstack/signal-common@0.2.4
98
+
3
99
  ## 1.1.1
4
100
 
5
101
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkstack/notification-common",
3
- "version": "1.1.1",
3
+ "version": "1.2.0",
4
4
  "license": "Elastic-2.0",
5
5
  "type": "module",
6
6
  "exports": {
package/src/routes.ts CHANGED
@@ -6,4 +6,10 @@ import { createRoutes } from "@checkstack/common";
6
6
  export const notificationRoutes = createRoutes("notification", {
7
7
  home: "/",
8
8
  settings: "/settings",
9
+ /**
10
+ * Admin-only visibility surface for per-channel delivery attempts.
11
+ * Visibility, not a dashboard — see Phase 8 of the v1 polishing
12
+ * plan and `docs/.../backend/notification-delivery.md`.
13
+ */
14
+ deliveryAttempts: "/delivery-attempts",
9
15
  });
@@ -1,14 +1,20 @@
1
1
  import { z } from "zod";
2
2
  import { notificationAccess } from "./access";
3
3
  import { pluginMetadata } from "./plugin-metadata";
4
- import { createClientDefinition, proc } from "@checkstack/common";
4
+ import {
5
+ createClientDefinition,
6
+ PaginatedResult,
7
+ proc,
8
+ } from "@checkstack/common";
5
9
  import {
6
10
  NotificationSchema,
7
11
  NotificationGroupSchema,
8
12
  EnrichedSubscriptionSchema,
9
13
  RetentionSettingsSchema,
10
- PaginationInputSchema,
14
+ ListNotificationsInputSchema,
11
15
  NotificationSubjectSchema,
16
+ DeliveryAttemptSchema,
17
+ ListDeliveryAttemptsInputSchema,
12
18
  } from "./schemas";
13
19
 
14
20
  // Shared input fragments for the notify* procedures.
@@ -78,13 +84,8 @@ export const notificationContract = {
78
84
  userType: "user",
79
85
  access: [],
80
86
  })
81
- .input(PaginationInputSchema)
82
- .output(
83
- z.object({
84
- notifications: z.array(NotificationSchema),
85
- total: z.number(),
86
- })
87
- ),
87
+ .input(ListNotificationsInputSchema)
88
+ .output(PaginatedResult(NotificationSchema)),
88
89
 
89
90
  // Get unread count for badge
90
91
  getUnreadCount: proc({
@@ -196,6 +197,30 @@ export const notificationContract = {
196
197
  .input(RetentionSettingsSchema)
197
198
  .output(z.void()),
198
199
 
200
+ /**
201
+ * List per-channel delivery attempts (paginated, newest first).
202
+ *
203
+ * Admin-only visibility surface for external-delivery outcomes. Each
204
+ * row corresponds to one `strategy.send(...)` call against an
205
+ * external notification channel; failures expose the silent-failure
206
+ * mode the dispatch loop swallowed pre-v1.
207
+ *
208
+ * When `notificationId` is supplied, results are scoped to that
209
+ * notification; otherwise the caller sees every recent attempt
210
+ * across the platform.
211
+ *
212
+ * Visibility-only — there is no retry mechanism in v1; a `failure`
213
+ * here is a final outcome that an admin actions manually (re-trigger
214
+ * the source event, fix the misconfigured channel, etc.).
215
+ */
216
+ getDeliveryAttempts: proc({
217
+ operationType: "query",
218
+ userType: "user",
219
+ access: [notificationAccess.admin],
220
+ })
221
+ .input(ListDeliveryAttemptsInputSchema)
222
+ .output(PaginatedResult(DeliveryAttemptSchema)),
223
+
199
224
  // ==========================================================================
200
225
  // BACKEND-TO-BACKEND GROUP MANAGEMENT (userType: "service")
201
226
  // ==========================================================================
package/src/schemas.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ import { PaginationInput as CanonicalPaginationInput } from "@checkstack/common";
2
3
 
3
4
  // Notification importance levels
4
5
  export const ImportanceSchema = z.enum(["info", "warning", "critical"]);
@@ -145,13 +146,14 @@ export type NotificationGroupInput = z.infer<
145
146
  typeof NotificationGroupInputSchema
146
147
  >;
147
148
 
148
- // Pagination schema for listing notifications
149
- export const PaginationInputSchema = z.object({
150
- limit: z.number().min(1).max(100).default(20),
151
- offset: z.number().min(0).default(0),
149
+ // Notification list input extends the canonical `PaginationInput` from
150
+ // `@checkstack/common` with the notification-specific `unreadOnly` filter.
151
+ // Compose with `.extend({...})` to add further domain filters; do NOT
152
+ // redefine the base `limit` / `offset` fields.
153
+ export const ListNotificationsInputSchema = CanonicalPaginationInput.extend({
152
154
  unreadOnly: z.boolean().default(false),
153
155
  });
154
- export type PaginationInput = z.infer<typeof PaginationInputSchema>;
156
+ export type ListNotificationsInput = z.infer<typeof ListNotificationsInputSchema>;
155
157
 
156
158
  // --- Notification Strategy Schemas ---
157
159
 
@@ -241,3 +243,45 @@ export const TransactionalResultSchema = z.object({
241
243
  error: z.string().optional(),
242
244
  });
243
245
  export type TransactionalResult = z.infer<typeof TransactionalResultSchema>;
246
+
247
+ // --- Delivery attempt schemas ---
248
+
249
+ /**
250
+ * Outcome of one external `strategy.send(...)` call. Visibility-only —
251
+ * `failure` here is a final, surfaced outcome (no retries in v1).
252
+ */
253
+ export const DeliveryAttemptStatusSchema = z.enum(["success", "failure"]);
254
+ export type DeliveryAttemptStatus = z.infer<typeof DeliveryAttemptStatusSchema>;
255
+
256
+ /**
257
+ * One row from `notification_delivery_attempts`. Mirrors the Drizzle
258
+ * table 1:1 — keep these in sync; the schema-drift CI check (Phase 10)
259
+ * will catch column-name skew once it lands.
260
+ */
261
+ export const DeliveryAttemptSchema = z.object({
262
+ id: z.string().uuid(),
263
+ notificationId: z.string().uuid(),
264
+ /** Qualified strategy id, e.g. `notification-discord.send`. */
265
+ strategyQualifiedId: z.string(),
266
+ attemptedAt: z.coerce.date(),
267
+ status: DeliveryAttemptStatusSchema,
268
+ /** Sanitised via `extractErrorMessage` on the backend before persistence. */
269
+ errorMessage: z.string().nullable(),
270
+ /** Wall-clock duration of the `send()` call in milliseconds. */
271
+ durationMs: z.number().int().min(0),
272
+ });
273
+ export type DeliveryAttempt = z.infer<typeof DeliveryAttemptSchema>;
274
+
275
+ /**
276
+ * Input shape for `getDeliveryAttempts`. Extends the canonical
277
+ * `PaginationInput` with an optional `notificationId` filter — when
278
+ * supplied, results are scoped to that notification; otherwise the
279
+ * caller gets every attempt across the system (newest first), useful
280
+ * for admin "recent failures" dashboards.
281
+ */
282
+ export const ListDeliveryAttemptsInputSchema = CanonicalPaginationInput.extend({
283
+ notificationId: z.string().uuid().optional(),
284
+ });
285
+ export type ListDeliveryAttemptsInput = z.infer<
286
+ typeof ListDeliveryAttemptsInputSchema
287
+ >;