@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 +126 -0
- package/package.json +27 -0
- package/src/index.ts +6 -0
- package/src/permissions.ts +12 -0
- package/src/plugin-metadata.ts +9 -0
- package/src/routes.ts +9 -0
- package/src/rpc-contract.ts +402 -0
- package/src/schemas.ts +186 -0
- package/src/signals.ts +38 -0
- package/tsconfig.json +6 -0
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,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,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
|
+
);
|