@checkstack/notification-common 0.0.2

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 ADDED
@@ -0,0 +1,126 @@
1
+ # @checkstack/notification-common
2
+
3
+ ## 0.0.2
4
+
5
+ ### Patch Changes
6
+
7
+ - d20d274: Initial release of all @checkstack packages. Rebranded from Checkmate to Checkstack with new npm organization @checkstack and domain checkstack.dev.
8
+ - Updated dependencies [d20d274]
9
+ - @checkstack/common@0.0.2
10
+ - @checkstack/signal-common@0.0.2
11
+
12
+ ## 0.1.1
13
+
14
+ ### Patch Changes
15
+
16
+ - Updated dependencies [a65e002]
17
+ - @checkstack/common@0.2.0
18
+ - @checkstack/signal-common@0.1.1
19
+
20
+ ## 0.1.0
21
+
22
+ ### Minor Changes
23
+
24
+ - b55fae6: Added realtime Signal Service for backend-to-frontend push notifications via WebSockets.
25
+
26
+ ## New Packages
27
+
28
+ - **@checkstack/signal-common**: Shared types including `Signal`, `SignalService`, `createSignal()`, and WebSocket protocol messages
29
+ - **@checkstack/signal-backend**: `SignalServiceImpl` with EventBus integration and Bun WebSocket handler using native pub/sub
30
+ - **@checkstack/signal-frontend**: React `SignalProvider` and `useSignal()` hook for consuming typed signals
31
+
32
+ ## Changes
33
+
34
+ - **@checkstack/backend-api**: Added `coreServices.signalService` reference for plugins to emit signals
35
+ - **@checkstack/backend**: Integrated WebSocket server at `/api/signals/ws` with session-based authentication
36
+
37
+ ## Usage
38
+
39
+ Backend plugins can emit signals:
40
+
41
+ ```typescript
42
+ import { coreServices } from "@checkstack/backend-api";
43
+ import { NOTIFICATION_RECEIVED } from "@checkstack/notification-common";
44
+
45
+ const signalService = context.signalService;
46
+ await signalService.sendToUser(NOTIFICATION_RECEIVED, userId, { ... });
47
+ ```
48
+
49
+ Frontend components subscribe to signals:
50
+
51
+ ```tsx
52
+ import { useSignal } from "@checkstack/signal-frontend";
53
+ import { NOTIFICATION_RECEIVED } from "@checkstack/notification-common";
54
+
55
+ useSignal(NOTIFICATION_RECEIVED, (payload) => {
56
+ // Handle realtime notification
57
+ });
58
+ ```
59
+
60
+ - b354ab3: # Strategy Instructions Support & Telegram Notification Plugin
61
+
62
+ ## Strategy Instructions Interface
63
+
64
+ Added `adminInstructions` and `userInstructions` optional fields to the `NotificationStrategy` interface. These allow strategies to export markdown-formatted setup guides that are displayed in the configuration UI:
65
+
66
+ - **`adminInstructions`**: Shown when admins configure platform-wide strategy settings (e.g., how to create API keys)
67
+ - **`userInstructions`**: Shown when users configure their personal settings (e.g., how to link their account)
68
+
69
+ ### Updated Components
70
+
71
+ - `StrategyConfigCard` now accepts an `instructions` prop and renders it before config sections
72
+ - `StrategyCard` passes `adminInstructions` to `StrategyConfigCard`
73
+ - `UserChannelCard` renders `userInstructions` when users need to connect
74
+
75
+ ## New Telegram Notification Plugin
76
+
77
+ Added `@checkstack/notification-telegram-backend` plugin for sending notifications via Telegram:
78
+
79
+ - Uses [grammY](https://grammy.dev/) framework for Telegram Bot API integration
80
+ - Sends messages with MarkdownV2 formatting and inline keyboard buttons for actions
81
+ - Includes comprehensive admin instructions for bot setup via @BotFather
82
+ - Includes user instructions for account linking
83
+
84
+ ### Configuration
85
+
86
+ Admins need to configure a Telegram Bot Token obtained from @BotFather.
87
+
88
+ ### User Linking
89
+
90
+ The strategy uses `contactResolution: { type: "custom" }` for Telegram Login Widget integration. Full frontend integration for the Login Widget is pending future work.
91
+
92
+ ### Patch Changes
93
+
94
+ - ffc28f6: ### Anonymous Role and Public Access
95
+
96
+ Introduces a configurable "anonymous" role for managing permissions available to unauthenticated users.
97
+
98
+ **Core Changes:**
99
+
100
+ - Added `userType: "public"` - endpoints accessible by both authenticated users (with their permissions) and anonymous users (with anonymous role permissions)
101
+ - Renamed `userType: "both"` to `"authenticated"` for clarity
102
+ - Renamed `isDefault` to `isAuthenticatedDefault` on Permission interface
103
+ - Added `isPublicDefault` flag for permissions that should be granted to the anonymous role by default
104
+
105
+ **Backend Infrastructure:**
106
+
107
+ - New `anonymous` system role created during auth-backend initialization
108
+ - New `disabled_public_default_permission` table tracks admin-disabled public defaults
109
+ - `autoAuthMiddleware` now checks anonymous role permissions for unauthenticated public endpoint access
110
+ - `AuthService.getAnonymousPermissions()` with 1-minute caching for performance
111
+ - Anonymous role filtered from `getRoles` endpoint (not assignable to users)
112
+ - Validation prevents assigning anonymous role to users
113
+
114
+ **Catalog Integration:**
115
+
116
+ - `catalog.read` permission now has both `isAuthenticatedDefault` and `isPublicDefault`
117
+ - Read endpoints (`getSystems`, `getGroups`, `getEntities`) now use `userType: "public"`
118
+
119
+ **UI:**
120
+
121
+ - New `PermissionGate` component for conditionally rendering content based on permissions
122
+
123
+ - Updated dependencies [ffc28f6]
124
+ - Updated dependencies [b55fae6]
125
+ - @checkstack/common@0.1.0
126
+ - @checkstack/signal-common@0.1.0
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@checkstack/notification-common",
3
+ "version": "0.0.2",
4
+ "type": "module",
5
+ "exports": {
6
+ ".": {
7
+ "types": "./src/index.ts",
8
+ "import": "./src/index.ts"
9
+ }
10
+ },
11
+ "dependencies": {
12
+ "@checkstack/common": "workspace:*",
13
+ "@checkstack/signal-common": "workspace:*",
14
+ "@orpc/contract": "^1.13.2",
15
+ "zod": "^4.0.0"
16
+ },
17
+ "devDependencies": {
18
+ "@checkstack/tsconfig": "workspace:*",
19
+ "typescript": "^5.7.2",
20
+ "@checkstack/scripts": "workspace:*"
21
+ },
22
+ "scripts": {
23
+ "typecheck": "tsc --noEmit",
24
+ "lint": "bun run lint:code",
25
+ "lint:code": "eslint . --max-warnings 0"
26
+ }
27
+ }
package/src/index.ts ADDED
@@ -0,0 +1,6 @@
1
+ export * from "./schemas";
2
+ export * from "./permissions";
3
+ export * from "./rpc-contract";
4
+ export * from "./signals";
5
+ export * from "./plugin-metadata";
6
+ export { notificationRoutes } from "./routes";
@@ -0,0 +1,12 @@
1
+ import { createPermission } from "@checkstack/common";
2
+
3
+ export const permissions = {
4
+ /** Configure retention policy and send broadcasts */
5
+ notificationAdmin: createPermission(
6
+ "notification",
7
+ "manage",
8
+ "Configure notification settings and send broadcasts"
9
+ ),
10
+ };
11
+
12
+ export const permissionList = Object.values(permissions);
@@ -0,0 +1,9 @@
1
+ import { definePluginMetadata } from "@checkstack/common";
2
+
3
+ /**
4
+ * Plugin metadata for the notification plugin.
5
+ * Exported from the common package so both backend and frontend can reference it.
6
+ */
7
+ export const pluginMetadata = definePluginMetadata({
8
+ pluginId: "notification",
9
+ });
package/src/routes.ts ADDED
@@ -0,0 +1,9 @@
1
+ import { createRoutes } from "@checkstack/common";
2
+
3
+ /**
4
+ * Route definitions for the notification plugin.
5
+ */
6
+ export const notificationRoutes = createRoutes("notification", {
7
+ home: "/",
8
+ settings: "/settings",
9
+ });
@@ -0,0 +1,402 @@
1
+ import { oc } from "@orpc/contract";
2
+ import { z } from "zod";
3
+ import { permissions } from "./permissions";
4
+ import { pluginMetadata } from "./plugin-metadata";
5
+ import {
6
+ createClientDefinition,
7
+ type ProcedureMetadata,
8
+ } from "@checkstack/common";
9
+ import {
10
+ NotificationSchema,
11
+ NotificationGroupSchema,
12
+ EnrichedSubscriptionSchema,
13
+ RetentionSettingsSchema,
14
+ PaginationInputSchema,
15
+ } from "./schemas";
16
+
17
+ // Base builder with full metadata support (userType + permissions)
18
+ const _base = oc.$meta<ProcedureMetadata>({});
19
+
20
+ // Notification RPC Contract
21
+ export const notificationContract = {
22
+ // ==========================================================================
23
+ // USER NOTIFICATION ENDPOINTS (userType: "user")
24
+ // ==========================================================================
25
+
26
+ // Get current user's notifications (paginated)
27
+ getNotifications: _base
28
+ .meta({
29
+ userType: "user",
30
+ })
31
+ .input(PaginationInputSchema)
32
+ .output(
33
+ z.object({
34
+ notifications: z.array(NotificationSchema),
35
+ total: z.number(),
36
+ })
37
+ ),
38
+
39
+ // Get unread count for badge
40
+ getUnreadCount: _base
41
+ .meta({
42
+ userType: "user",
43
+ })
44
+ .output(z.object({ count: z.number() })),
45
+
46
+ // Mark notification(s) as read
47
+ markAsRead: _base
48
+ .meta({
49
+ userType: "user",
50
+ })
51
+ .input(
52
+ z.object({
53
+ notificationId: z.string().uuid().optional(), // If not provided, mark all as read
54
+ })
55
+ )
56
+ .output(z.void()),
57
+
58
+ // Delete a notification
59
+ deleteNotification: _base
60
+ .meta({
61
+ userType: "user",
62
+ })
63
+ .input(z.object({ notificationId: z.string().uuid() }))
64
+ .output(z.void()),
65
+
66
+ // ==========================================================================
67
+ // GROUP & SUBSCRIPTION ENDPOINTS (userType: "user")
68
+ // ==========================================================================
69
+
70
+ // Get all available notification groups
71
+ getGroups: _base
72
+ .meta({
73
+ userType: "authenticated", // Services can read groups too
74
+ })
75
+ .output(z.array(NotificationGroupSchema)),
76
+
77
+ // Get current user's subscriptions with group details
78
+ getSubscriptions: _base
79
+ .meta({
80
+ userType: "user",
81
+ })
82
+ .output(z.array(EnrichedSubscriptionSchema)),
83
+
84
+ // Subscribe to a notification group
85
+ subscribe: _base
86
+ .meta({
87
+ userType: "user",
88
+ })
89
+ .input(z.object({ groupId: z.string() }))
90
+ .output(z.void()),
91
+
92
+ // Unsubscribe from a notification group
93
+ unsubscribe: _base
94
+ .meta({
95
+ userType: "user",
96
+ })
97
+ .input(z.object({ groupId: z.string() }))
98
+ .output(z.void()),
99
+
100
+ // ==========================================================================
101
+ // ADMIN SETTINGS ENDPOINTS (userType: "user" with admin permissions)
102
+ // ==========================================================================
103
+
104
+ // Get retention schema for DynamicForm
105
+ getRetentionSchema: _base
106
+ .meta({
107
+ userType: "user",
108
+ permissions: [permissions.notificationAdmin.id],
109
+ })
110
+ .output(z.record(z.string(), z.unknown())),
111
+
112
+ // Get retention settings
113
+ getRetentionSettings: _base
114
+ .meta({
115
+ userType: "user",
116
+ permissions: [permissions.notificationAdmin.id],
117
+ })
118
+ .output(RetentionSettingsSchema),
119
+
120
+ // Update retention settings
121
+ setRetentionSettings: _base
122
+ .meta({
123
+ userType: "user",
124
+ permissions: [permissions.notificationAdmin.id],
125
+ })
126
+ .input(RetentionSettingsSchema)
127
+ .output(z.void()),
128
+
129
+ // ==========================================================================
130
+ // BACKEND-TO-BACKEND GROUP MANAGEMENT (userType: "service")
131
+ // ==========================================================================
132
+
133
+ // Create a notification group (for plugins to register their groups)
134
+ createGroup: _base
135
+ .meta({ userType: "service" })
136
+ .input(
137
+ z.object({
138
+ groupId: z
139
+ .string()
140
+ .describe(
141
+ "Unique group identifier, will be namespaced with ownerPlugin"
142
+ ),
143
+ name: z.string().describe("Display name for the group"),
144
+ description: z
145
+ .string()
146
+ .describe("Description of what notifications this group provides"),
147
+ ownerPlugin: z.string().describe("Plugin ID that owns this group"),
148
+ })
149
+ )
150
+ .output(z.object({ id: z.string() })),
151
+
152
+ // Delete a notification group
153
+ deleteGroup: _base
154
+ .meta({ userType: "service" })
155
+ .input(
156
+ z.object({
157
+ groupId: z.string().describe("Full namespaced group ID to delete"),
158
+ ownerPlugin: z
159
+ .string()
160
+ .describe("Plugin ID that owns this group (for validation)"),
161
+ })
162
+ )
163
+ .output(z.object({ success: z.boolean() })),
164
+
165
+ // Get subscribers for a specific notification group
166
+ getGroupSubscribers: _base
167
+ .meta({ userType: "service" })
168
+ .input(
169
+ z.object({
170
+ groupId: z
171
+ .string()
172
+ .describe("Full namespaced group ID (e.g., 'catalog.system.123')"),
173
+ })
174
+ )
175
+ .output(z.object({ userIds: z.array(z.string()) })),
176
+
177
+ // Send notifications to a list of users (deduplicated by caller)
178
+ notifyUsers: _base
179
+ .meta({ userType: "service" })
180
+ .input(
181
+ z.object({
182
+ userIds: z.array(z.string()),
183
+ title: z.string(),
184
+ /** Notification body in markdown format */
185
+ body: z.string().describe("Notification body (supports markdown)"),
186
+ importance: z.enum(["info", "warning", "critical"]).optional(),
187
+ /** Primary action button */
188
+ action: z
189
+ .object({
190
+ label: z.string(),
191
+ url: z.string(),
192
+ })
193
+ .optional(),
194
+ })
195
+ )
196
+ .output(z.object({ notifiedCount: z.number() })),
197
+
198
+ // Notify all subscribers of multiple groups (deduplicates internally)
199
+ // Use this when an event affects multiple groups and you want to avoid
200
+ // duplicate notifications for users subscribed to multiple affected groups.
201
+ notifyGroups: _base
202
+ .meta({ userType: "service" })
203
+ .input(
204
+ z.object({
205
+ groupIds: z
206
+ .array(z.string())
207
+ .describe("Full namespaced group IDs to notify"),
208
+ title: z.string(),
209
+ /** Notification body in markdown format */
210
+ body: z.string().describe("Notification body (supports markdown)"),
211
+ importance: z.enum(["info", "warning", "critical"]).optional(),
212
+ /** Primary action button */
213
+ action: z
214
+ .object({
215
+ label: z.string(),
216
+ url: z.string(),
217
+ })
218
+ .optional(),
219
+ })
220
+ )
221
+ .output(z.object({ notifiedCount: z.number() })),
222
+
223
+ // Send transactional notification via ALL enabled strategies (no internal notification created)
224
+ // For security-critical messages like password reset, 2FA, account verification, etc.
225
+ // Unlike regular notifications, this bypasses user preferences and does not create a bell notification.
226
+ sendTransactional: _base
227
+ .meta({ userType: "service" })
228
+ .input(
229
+ z.object({
230
+ userId: z.string().describe("User to notify"),
231
+ notification: z.object({
232
+ title: z.string(),
233
+ body: z.string().describe("Notification body (supports markdown)"),
234
+ action: z
235
+ .object({
236
+ label: z.string(),
237
+ url: z.string(),
238
+ })
239
+ .optional(),
240
+ }),
241
+ })
242
+ )
243
+ .output(
244
+ z.object({
245
+ deliveredCount: z
246
+ .number()
247
+ .describe("Number of strategies that delivered successfully"),
248
+ results: z.array(
249
+ z.object({
250
+ strategyId: z.string(),
251
+ success: z.boolean(),
252
+ error: z.string().optional(),
253
+ })
254
+ ),
255
+ })
256
+ ),
257
+
258
+ // ==========================================================================
259
+ // DELIVERY STRATEGY ADMIN ENDPOINTS (userType: "user" with admin permissions)
260
+ // ==========================================================================
261
+
262
+ // Get all registered delivery strategies with current config
263
+ getDeliveryStrategies: _base
264
+ .meta({
265
+ userType: "user",
266
+ permissions: [permissions.notificationAdmin.id],
267
+ })
268
+ .output(
269
+ z.array(
270
+ z.object({
271
+ qualifiedId: z.string(),
272
+ displayName: z.string(),
273
+ description: z.string().optional(),
274
+ icon: z.string().optional(),
275
+ ownerPluginId: z.string(),
276
+ contactResolution: z.object({
277
+ type: z.enum([
278
+ "auth-email",
279
+ "auth-provider",
280
+ "user-config",
281
+ "oauth-link",
282
+ "custom",
283
+ ]),
284
+ provider: z.string().optional(),
285
+ field: z.string().optional(),
286
+ }),
287
+ requiresUserConfig: z.boolean(),
288
+ requiresOAuthLink: z.boolean(),
289
+ configSchema: z.record(z.string(), z.unknown()),
290
+ userConfigSchema: z.record(z.string(), z.unknown()).optional(),
291
+ /** Layout config schema for admin customization (logo, colors, etc.) */
292
+ layoutConfigSchema: z.record(z.string(), z.unknown()).optional(),
293
+ enabled: z.boolean(),
294
+ config: z.record(z.string(), z.unknown()).optional(),
295
+ /** Current layout config values */
296
+ layoutConfig: z.record(z.string(), z.unknown()).optional(),
297
+ /** Markdown instructions for admins (setup guides, etc.) */
298
+ adminInstructions: z.string().optional(),
299
+ })
300
+ )
301
+ ),
302
+
303
+ // Update strategy enabled state and config
304
+ updateDeliveryStrategy: _base
305
+ .meta({
306
+ userType: "user",
307
+ permissions: [permissions.notificationAdmin.id],
308
+ })
309
+ .input(
310
+ z.object({
311
+ strategyId: z.string().describe("Qualified strategy ID"),
312
+ enabled: z.boolean(),
313
+ config: z.record(z.string(), z.unknown()).optional(),
314
+ /** Layout customization (logo, colors, footer) */
315
+ layoutConfig: z.record(z.string(), z.unknown()).optional(),
316
+ })
317
+ )
318
+ .output(z.void()),
319
+
320
+ // ==========================================================================
321
+ // USER DELIVERY PREFERENCE ENDPOINTS (userType: "user")
322
+ // ==========================================================================
323
+
324
+ // Get available delivery channels for current user
325
+ getUserDeliveryChannels: _base.meta({ userType: "user" }).output(
326
+ z.array(
327
+ z.object({
328
+ strategyId: z.string(),
329
+ displayName: z.string(),
330
+ description: z.string().optional(),
331
+ icon: z.string().optional(),
332
+ contactResolution: z.object({
333
+ type: z.enum([
334
+ "auth-email",
335
+ "auth-provider",
336
+ "user-config",
337
+ "oauth-link",
338
+ ]),
339
+ }),
340
+ enabled: z.boolean(),
341
+ isConfigured: z.boolean(),
342
+ linkedAt: z.coerce.date().optional(),
343
+ /** JSON Schema for user config (for DynamicForm) */
344
+ userConfigSchema: z.record(z.string(), z.unknown()).optional(),
345
+ /** Current user config values */
346
+ userConfig: z.record(z.string(), z.unknown()).optional(),
347
+ /** Markdown instructions for users (connection guides, etc.) */
348
+ userInstructions: z.string().optional(),
349
+ })
350
+ )
351
+ ),
352
+
353
+ // Update user's preference for a delivery channel
354
+ setUserDeliveryPreference: _base
355
+ .meta({ userType: "user" })
356
+ .input(
357
+ z.object({
358
+ strategyId: z.string(),
359
+ enabled: z.boolean(),
360
+ userConfig: z.record(z.string(), z.unknown()).optional(),
361
+ })
362
+ )
363
+ .output(z.void()),
364
+
365
+ // Get OAuth link URL for a strategy (starts OAuth flow)
366
+ getDeliveryOAuthUrl: _base
367
+ .meta({ userType: "user" })
368
+ .input(
369
+ z.object({
370
+ strategyId: z.string(),
371
+ returnUrl: z.string().optional(),
372
+ })
373
+ )
374
+ .output(z.object({ authUrl: z.string() })),
375
+
376
+ // Unlink OAuth-connected delivery channel
377
+ unlinkDeliveryChannel: _base
378
+ .meta({ userType: "user" })
379
+ .input(z.object({ strategyId: z.string() }))
380
+ .output(z.void()),
381
+
382
+ // Send a test notification to the current user via a specific strategy
383
+ sendTestNotification: _base
384
+ .meta({ userType: "user" })
385
+ .input(z.object({ strategyId: z.string() }))
386
+ .output(
387
+ z.object({
388
+ success: z.boolean(),
389
+ error: z.string().optional(),
390
+ })
391
+ ),
392
+ };
393
+
394
+ // Export contract type
395
+ export type NotificationContract = typeof notificationContract;
396
+
397
+ // Export client definition for type-safe forPlugin usage
398
+ // Use: const client = rpcApi.forPlugin(NotificationApi);
399
+ export const NotificationApi = createClientDefinition(
400
+ notificationContract,
401
+ pluginMetadata
402
+ );
package/src/schemas.ts ADDED
@@ -0,0 +1,186 @@
1
+ import { z } from "zod";
2
+
3
+ // Notification importance levels
4
+ export const ImportanceSchema = z.enum(["info", "warning", "critical"]);
5
+ export type Importance = z.infer<typeof ImportanceSchema>;
6
+
7
+ // Notification action for CTA buttons
8
+ export const NotificationActionSchema = z.object({
9
+ label: z.string(),
10
+ url: z.string(),
11
+ });
12
+ export type NotificationAction = z.infer<typeof NotificationActionSchema>;
13
+
14
+ // Core notification schema
15
+ export const NotificationSchema = z.object({
16
+ id: z.string().uuid(),
17
+ userId: z.string(),
18
+ title: z.string(),
19
+ /** Notification body (supports markdown) */
20
+ body: z.string(),
21
+ /** Primary action button */
22
+ action: NotificationActionSchema.optional(),
23
+ importance: ImportanceSchema,
24
+ isRead: z.boolean(),
25
+ groupId: z.string().optional(),
26
+ createdAt: z.coerce.date(),
27
+ });
28
+ export type Notification = z.infer<typeof NotificationSchema>;
29
+
30
+ // Notification group schema (namespaced: "pluginId.groupName")
31
+ export const NotificationGroupSchema = z.object({
32
+ id: z.string(),
33
+ name: z.string(),
34
+ description: z.string(),
35
+ ownerPlugin: z.string(),
36
+ createdAt: z.coerce.date(),
37
+ });
38
+ export type NotificationGroup = z.infer<typeof NotificationGroupSchema>;
39
+
40
+ // User subscription to a notification group
41
+ export const NotificationSubscriptionSchema = z.object({
42
+ userId: z.string(),
43
+ groupId: z.string(),
44
+ subscribedAt: z.coerce.date(),
45
+ });
46
+ export type NotificationSubscription = z.infer<
47
+ typeof NotificationSubscriptionSchema
48
+ >;
49
+
50
+ // Enriched subscription with group details for display
51
+ export const EnrichedSubscriptionSchema = z.object({
52
+ groupId: z.string(),
53
+ groupName: z.string(),
54
+ groupDescription: z.string(),
55
+ ownerPlugin: z.string(),
56
+ subscribedAt: z.coerce.date(),
57
+ });
58
+ export type EnrichedSubscription = z.infer<typeof EnrichedSubscriptionSchema>;
59
+
60
+ // Retention settings
61
+ export const RetentionSettingsSchema = z.object({
62
+ retentionDays: z.number().min(1).max(365),
63
+ enabled: z.boolean(),
64
+ });
65
+ export type RetentionSettings = z.infer<typeof RetentionSettingsSchema>;
66
+
67
+ // --- Input Schemas ---
68
+
69
+ export const CreateNotificationInputSchema = z.object({
70
+ userId: z.string(),
71
+ title: z.string(),
72
+ /** Notification body (supports markdown) */
73
+ body: z.string(),
74
+ /** Primary action button */
75
+ action: NotificationActionSchema.optional(),
76
+ importance: ImportanceSchema.default("info"),
77
+ });
78
+ export type CreateNotificationInput = z.infer<
79
+ typeof CreateNotificationInputSchema
80
+ >;
81
+
82
+ export const NotificationGroupInputSchema = z.object({
83
+ groupId: z.string(),
84
+ name: z.string(),
85
+ description: z.string(),
86
+ });
87
+ export type NotificationGroupInput = z.infer<
88
+ typeof NotificationGroupInputSchema
89
+ >;
90
+
91
+ // Pagination schema for listing notifications
92
+ export const PaginationInputSchema = z.object({
93
+ limit: z.number().min(1).max(100).default(20),
94
+ offset: z.number().min(0).default(0),
95
+ unreadOnly: z.boolean().default(false),
96
+ });
97
+ export type PaginationInput = z.infer<typeof PaginationInputSchema>;
98
+
99
+ // --- Notification Strategy Schemas ---
100
+
101
+ // Contact resolution type
102
+ export const ContactResolutionSchema = z.discriminatedUnion("type", [
103
+ z.object({ type: z.literal("auth-email") }),
104
+ z.object({ type: z.literal("auth-provider"), provider: z.string() }),
105
+ z.object({ type: z.literal("user-config"), field: z.string() }),
106
+ z.object({ type: z.literal("oauth-link") }),
107
+ ]);
108
+ export type ContactResolution = z.infer<typeof ContactResolutionSchema>;
109
+
110
+ // Strategy info for API responses
111
+ export const NotificationStrategyInfoSchema = z.object({
112
+ /** Qualified ID: {pluginId}.{strategyId} */
113
+ qualifiedId: z.string(),
114
+ /** Display name */
115
+ displayName: z.string(),
116
+ /** Description */
117
+ description: z.string().optional(),
118
+ /** Lucide icon name */
119
+ icon: z.string().optional(),
120
+ /** Owner plugin ID */
121
+ ownerPluginId: z.string(),
122
+ /** How contact info is resolved */
123
+ contactResolution: ContactResolutionSchema,
124
+ /** Whether strategy requires user config */
125
+ requiresUserConfig: z.boolean(),
126
+ /** Whether strategy requires OAuth linking */
127
+ requiresOAuthLink: z.boolean(),
128
+ /** JSON Schema for admin config (for DynamicForm) */
129
+ configSchema: z.record(z.string(), z.unknown()),
130
+ /** JSON Schema for user config (if applicable) */
131
+ userConfigSchema: z.record(z.string(), z.unknown()).optional(),
132
+ });
133
+ export type NotificationStrategyInfo = z.infer<
134
+ typeof NotificationStrategyInfoSchema
135
+ >;
136
+
137
+ // User's preference for a specific strategy
138
+ export const UserNotificationPreferenceSchema = z.object({
139
+ strategyId: z.string(),
140
+ enabled: z.boolean(),
141
+ /** Whether this channel is ready (has contact info / is linked) */
142
+ isConfigured: z.boolean(),
143
+ /** When external account was linked (for OAuth strategies) */
144
+ linkedAt: z.coerce.date().optional(),
145
+ });
146
+ export type UserNotificationPreference = z.infer<
147
+ typeof UserNotificationPreferenceSchema
148
+ >;
149
+
150
+ // External notification payload
151
+ export const ExternalNotificationPayloadSchema = z.object({
152
+ title: z.string(),
153
+ /** Markdown-formatted body content */
154
+ body: z.string().optional(),
155
+ importance: ImportanceSchema.default("info"),
156
+ /** Optional call-to-action */
157
+ action: z
158
+ .object({
159
+ label: z.string(),
160
+ url: z.string(),
161
+ })
162
+ .optional(),
163
+ /** Source type for filtering (e.g., "healthcheck.alert", "password-reset") */
164
+ type: z.string(),
165
+ });
166
+ export type ExternalNotificationPayload = z.infer<
167
+ typeof ExternalNotificationPayloadSchema
168
+ >;
169
+
170
+ // External delivery result
171
+ export const ExternalDeliveryResultSchema = z.object({
172
+ sent: z.number(),
173
+ failed: z.number(),
174
+ skipped: z.number(),
175
+ });
176
+ export type ExternalDeliveryResult = z.infer<
177
+ typeof ExternalDeliveryResultSchema
178
+ >;
179
+
180
+ // Transactional message result
181
+ export const TransactionalResultSchema = z.object({
182
+ success: z.boolean(),
183
+ externalId: z.string().optional(),
184
+ error: z.string().optional(),
185
+ });
186
+ export type TransactionalResult = z.infer<typeof TransactionalResultSchema>;
package/src/signals.ts ADDED
@@ -0,0 +1,38 @@
1
+ import { createSignal } from "@checkstack/signal-common";
2
+ import { z } from "zod";
3
+ import { ImportanceSchema } from "./schemas";
4
+
5
+ /**
6
+ * Signal emitted when a new notification is received.
7
+ * Used to update the notification bell count and show realtime notifications.
8
+ */
9
+ export const NOTIFICATION_RECEIVED = createSignal(
10
+ "notification.received",
11
+ z.object({
12
+ id: z.string(),
13
+ title: z.string(),
14
+ body: z.string(),
15
+ importance: ImportanceSchema,
16
+ })
17
+ );
18
+
19
+ /**
20
+ * Signal emitted when the unread notification count changes.
21
+ * Used to update the badge count on the notification bell.
22
+ */
23
+ export const NOTIFICATION_COUNT_CHANGED = createSignal(
24
+ "notification.countChanged",
25
+ z.object({
26
+ unreadCount: z.number(),
27
+ })
28
+ );
29
+
30
+ /**
31
+ * Signal emitted when a notification is marked as read.
32
+ */
33
+ export const NOTIFICATION_READ = createSignal(
34
+ "notification.read",
35
+ z.object({
36
+ notificationId: z.string().optional(), // undefined means all marked as read
37
+ })
38
+ );
package/tsconfig.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "extends": "@checkstack/tsconfig/common.json",
3
+ "include": [
4
+ "src"
5
+ ]
6
+ }