@checkstack/notification-common 1.1.1 → 1.2.1

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,109 @@
1
1
  # @checkstack/notification-common
2
2
 
3
+ ## 1.2.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [6d52276]
8
+ - @checkstack/common@0.12.0
9
+ - @checkstack/signal-common@0.2.5
10
+
11
+ ## 1.2.0
12
+
13
+ ### Minor Changes
14
+
15
+ - f23f3c9: Add per-channel notification delivery-attempt tracking
16
+ (Phase 8 of the v1 polishing plan). The external dispatch loop now
17
+ persists one row per `strategy.send(...)` call into a new
18
+ `notification_delivery_attempts` table - both successes and
19
+ failures - so silent delivery breakage (misconfigured webhooks, dead
20
+ channels) becomes queryable instead of buried in logs.
21
+
22
+ - `@checkstack/notification-backend` adds the
23
+ `notification_delivery_attempts` table, the matching Drizzle
24
+ migration, and a new `dispatchWithAttempt` helper that wraps every
25
+ external `strategy.send(...)` with duration measurement and
26
+ best-effort row persistence. The insert is intentionally
27
+ fire-and-forget: if writing the attempt row itself errors, the
28
+ dispatch loop logs and continues so visibility tracking can never
29
+ introduce a _new_ silent failure.
30
+ - `@checkstack/notification-common` exports a new
31
+ `DeliveryAttemptSchema` zod schema, the
32
+ `ListDeliveryAttemptsInputSchema =
33
+ PaginationInput.extend({ notificationId })` input, and a new
34
+ `getDeliveryAttempts` procedure on the contract. The procedure is
35
+ gated by the existing `notificationAccess.admin`
36
+ (`notification:manage`) access rule - no new permission was
37
+ introduced.
38
+ - `@checkstack/notification-frontend` adds a minimal admin-only
39
+ `DeliveryAttemptsPage` (route id `notification.deliveryAttempts`,
40
+ path `/notifications/delivery-attempts`) and an "Open inspector"
41
+ link from the Notification Settings page for users with
42
+ `notification:manage`. No client-side `isAdmin` gate - the FORBIDDEN
43
+ case is rendered via the standard error-state branch on the page,
44
+ enforced by the contract.
45
+
46
+ Visibility only: there is no retry mechanism in this phase. A
47
+ `failure` row is a final outcome an admin actions manually
48
+ (re-trigger the source event, fix the misconfigured channel).
49
+ Automated retries are deferred to v1.1.
50
+
51
+ Strategy errors thrown during `send(...)` are persisted via
52
+ `extractErrorMessage(error)` so secrets potentially embedded in raw
53
+ error objects (webhook URLs, OAuth tokens reachable from the strategy
54
+ send context) are not stored verbatim.
55
+
56
+ See the new
57
+ `docs/src/content/docs/backend/notification-delivery.md` page for the
58
+ full surface description.
59
+
60
+ - f23f3c9: Sweep every paginated `*-common` contract onto the canonical
61
+ `PaginationInput` / `PaginatedResult` from `@checkstack/common` and
62
+ remove the now-unused legacy exports.
63
+
64
+ **BREAKING CHANGE** - `@checkstack/common` drops the deprecated
65
+ `PaginationInputSchema`, `paginatedOutput`, and `PaginatedResponse`
66
+ symbols. Callers must consume `PaginationInput` (input) and
67
+ `PaginatedResult(itemSchema)` (output) instead. The canonical input is
68
+ `{ limit (1-100, default 20), offset (>= 0, default 0) }`; the
69
+ canonical output envelope is
70
+ `{ items, total, limit, offset }`.
71
+
72
+ **BREAKING CHANGE** - `@checkstack/notification-common` migrates
73
+ `getNotifications` off the legacy `PaginationInputSchema`
74
+ (`{ limit, offset, unreadOnly }` with output `{ notifications, total }`)
75
+ onto `ListNotificationsInputSchema =
76
+ PaginationInput.extend({ unreadOnly })` and
77
+ `PaginatedResult(NotificationSchema)`. The output key changes from
78
+ `notifications` to `items`, and `limit` / `offset` are now echoed on
79
+ the response. The `PaginationInput` type alias previously exported
80
+ from `notification-common` is removed - use `ListNotificationsInput`
81
+ or the canonical `PaginationInput` from `@checkstack/common`.
82
+
83
+ **BREAKING CHANGE** - `@checkstack/integration-common` migrates
84
+ `listSubscriptions` (inline `{ page, pageSize, ... }` -> output
85
+ `{ subscriptions, total }`) and `getDeliveryLogs` (via
86
+ `DeliveryLogQueryInputSchema` `{ subscriptionId?, eventType?, status?,
87
+ page, pageSize }` -> output `{ logs, total }`) onto the canonical
88
+ `PaginationInput.extend({...})` input and
89
+ `PaginatedResult(itemSchema)` output. External callers must switch
90
+ from `{ page, pageSize }` to `{ limit, offset }` and read response
91
+ items from `data.items` (no more `data.subscriptions` / `data.logs`).
92
+
93
+ The matching `*-backend` handlers were updated to consume the new
94
+ input shape (`offset` arithmetic in lieu of `(page - 1) * pageSize`)
95
+ and to echo `limit` / `offset` on the response. The `*-frontend` call
96
+ sites in `NotificationsPage`, `NotificationBell`, `IntegrationsPage`,
97
+ and `DeliveryLogsPage` were updated to send the new input shape and
98
+ read `data.items`.
99
+
100
+ ### Patch Changes
101
+
102
+ - Updated dependencies [f23f3c9]
103
+ - Updated dependencies [f23f3c9]
104
+ - @checkstack/common@0.11.0
105
+ - @checkstack/signal-common@0.2.4
106
+
3
107
  ## 1.1.1
4
108
 
5
109
  ### 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.1",
4
4
  "license": "Elastic-2.0",
5
5
  "type": "module",
6
6
  "exports": {
@@ -10,15 +10,15 @@
10
10
  }
11
11
  },
12
12
  "dependencies": {
13
- "@checkstack/common": "0.10.0",
14
- "@checkstack/signal-common": "0.2.3",
13
+ "@checkstack/common": "0.11.0",
14
+ "@checkstack/signal-common": "0.2.4",
15
15
  "@orpc/contract": "^1.13.14",
16
16
  "zod": "^4.0.0"
17
17
  },
18
18
  "devDependencies": {
19
19
  "@checkstack/tsconfig": "0.0.7",
20
20
  "typescript": "^5.7.2",
21
- "@checkstack/scripts": "0.3.2"
21
+ "@checkstack/scripts": "0.3.3"
22
22
  },
23
23
  "scripts": {
24
24
  "typecheck": "tsgo -b",
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
+ >;