@checkstack/backend-api 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 +228 -0
- package/package.json +33 -0
- package/src/assertions.test.ts +345 -0
- package/src/assertions.ts +371 -0
- package/src/auth-strategy.ts +58 -0
- package/src/chart-metadata.ts +77 -0
- package/src/config-service.ts +71 -0
- package/src/config-versioning.ts +310 -0
- package/src/contract.ts +8 -0
- package/src/core-services.ts +45 -0
- package/src/email-layout.ts +246 -0
- package/src/encryption.ts +95 -0
- package/src/event-bus-types.ts +28 -0
- package/src/extension-point.ts +11 -0
- package/src/health-check.ts +68 -0
- package/src/hooks.ts +182 -0
- package/src/index.ts +23 -0
- package/src/markdown.test.ts +106 -0
- package/src/markdown.ts +104 -0
- package/src/notification-strategy.ts +436 -0
- package/src/oauth-handler.ts +442 -0
- package/src/plugin-admin-contract.ts +64 -0
- package/src/plugin-system.ts +103 -0
- package/src/rpc.ts +284 -0
- package/src/schema-utils.ts +79 -0
- package/src/service-ref.ts +15 -0
- package/src/test-utils.ts +65 -0
- package/src/types.ts +111 -0
- package/src/zod-config.ts +149 -0
- package/tsconfig.json +6 -0
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
import type { Versioned, VersionedRecord } from "./config-versioning";
|
|
2
|
+
import type { Logger } from "./types";
|
|
3
|
+
import type { PluginMetadata, LucideIconName } from "@checkstack/common";
|
|
4
|
+
|
|
5
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
6
|
+
// Contact Resolution Types
|
|
7
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Defines how a notification strategy resolves user contact information.
|
|
11
|
+
*/
|
|
12
|
+
export type NotificationContactResolution =
|
|
13
|
+
| { type: "auth-email" } // Uses user.email from auth system
|
|
14
|
+
| { type: "auth-provider"; provider: string } // Uses email from specific OAuth provider
|
|
15
|
+
| { type: "user-config"; field: string } // User provides via settings form (e.g., phone number)
|
|
16
|
+
| { type: "oauth-link" }; // Requires OAuth flow (Slack, Discord)
|
|
17
|
+
|
|
18
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
19
|
+
// Payload and Result Types
|
|
20
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The notification content to send via external channel.
|
|
24
|
+
*/
|
|
25
|
+
export interface NotificationPayload {
|
|
26
|
+
/** Notification title/subject */
|
|
27
|
+
title: string;
|
|
28
|
+
/**
|
|
29
|
+
* Markdown-formatted body content.
|
|
30
|
+
* Strategies that support rich rendering will parse this.
|
|
31
|
+
* Strategies that don't (e.g., SMS) will convert to plain text.
|
|
32
|
+
*/
|
|
33
|
+
body?: string;
|
|
34
|
+
/** Importance level for visual differentiation */
|
|
35
|
+
importance: "info" | "warning" | "critical";
|
|
36
|
+
/**
|
|
37
|
+
* Optional call-to-action with custom label.
|
|
38
|
+
* Strategies will render this appropriately (button for email, link for text).
|
|
39
|
+
*/
|
|
40
|
+
action?: {
|
|
41
|
+
label: string;
|
|
42
|
+
url: string;
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Source type identifier for filtering and templates.
|
|
46
|
+
* Examples: "password-reset", "healthcheck.alert", "maintenance.reminder"
|
|
47
|
+
*/
|
|
48
|
+
type: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Result of sending a notification.
|
|
53
|
+
*/
|
|
54
|
+
export interface NotificationDeliveryResult {
|
|
55
|
+
/** Whether the notification was sent successfully */
|
|
56
|
+
success: boolean;
|
|
57
|
+
/** Strategy-specific external message ID for tracking */
|
|
58
|
+
externalId?: string;
|
|
59
|
+
/** Error message if send failed */
|
|
60
|
+
error?: string;
|
|
61
|
+
/** For rate limiting or retry logic (milliseconds) */
|
|
62
|
+
retryAfterMs?: number;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
66
|
+
// Send Context
|
|
67
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Context passed to the strategy's send() method.
|
|
71
|
+
*/
|
|
72
|
+
export interface NotificationSendContext<
|
|
73
|
+
TConfig,
|
|
74
|
+
TUserConfig = undefined,
|
|
75
|
+
TLayoutConfig = undefined
|
|
76
|
+
> {
|
|
77
|
+
/** Full user identity from auth system */
|
|
78
|
+
user: {
|
|
79
|
+
userId: string;
|
|
80
|
+
email?: string;
|
|
81
|
+
displayName?: string;
|
|
82
|
+
};
|
|
83
|
+
/** Resolved contact for this channel (email, phone, slack user ID, etc.) */
|
|
84
|
+
contact: string;
|
|
85
|
+
/** The notification content to send */
|
|
86
|
+
notification: NotificationPayload;
|
|
87
|
+
/** Admin-configured strategy settings (global) */
|
|
88
|
+
strategyConfig: TConfig;
|
|
89
|
+
/** User-specific settings (if userConfig schema is defined) */
|
|
90
|
+
userConfig: TUserConfig | undefined;
|
|
91
|
+
/** Admin-configured layout settings (if strategy defines layoutConfig) */
|
|
92
|
+
layoutConfig: TLayoutConfig | undefined;
|
|
93
|
+
/** Logger for strategy to log errors and diagnostics */
|
|
94
|
+
logger: Logger;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
98
|
+
// OAuth Configuration for Strategies
|
|
99
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* OAuth 2.0 configuration for notification strategies.
|
|
103
|
+
*
|
|
104
|
+
* When a strategy provides this configuration, the notification-backend
|
|
105
|
+
* registry automatically registers HTTP endpoints for the OAuth flow:
|
|
106
|
+
* - GET /api/notification/oauth/{qualifiedId}/auth
|
|
107
|
+
* - GET /api/notification/oauth/{qualifiedId}/callback
|
|
108
|
+
* - POST /api/notification/oauth/{qualifiedId}/refresh
|
|
109
|
+
* - DELETE /api/notification/oauth/{qualifiedId}/unlink
|
|
110
|
+
*/
|
|
111
|
+
export interface StrategyOAuthConfig<TConfig = unknown> {
|
|
112
|
+
/**
|
|
113
|
+
* OAuth 2.0 client ID.
|
|
114
|
+
* Receives the strategy config so credentials can be extracted directly.
|
|
115
|
+
*/
|
|
116
|
+
clientId: (config: TConfig) => string;
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* OAuth 2.0 client secret.
|
|
120
|
+
* Receives the strategy config so credentials can be extracted directly.
|
|
121
|
+
*/
|
|
122
|
+
clientSecret: (config: TConfig) => string;
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Scopes to request from the OAuth provider.
|
|
126
|
+
*/
|
|
127
|
+
scopes: string[];
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Provider's authorization URL (where users are redirected to consent).
|
|
131
|
+
* Receives the strategy config for tenant-specific URLs.
|
|
132
|
+
* @example (config) => `https://login.microsoftonline.com/${config.tenantId}/oauth2/v2.0/authorize`
|
|
133
|
+
*/
|
|
134
|
+
authorizationUrl: (config: TConfig) => string;
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Provider's token exchange URL.
|
|
138
|
+
* Receives the strategy config for tenant-specific URLs.
|
|
139
|
+
* @example (config) => `https://login.microsoftonline.com/${config.tenantId}/oauth2/v2.0/token`
|
|
140
|
+
*/
|
|
141
|
+
tokenUrl: (config: TConfig) => string;
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Extract the user's external ID from the token response.
|
|
145
|
+
* This ID is used to identify the user on the external platform.
|
|
146
|
+
*
|
|
147
|
+
* @example (response) => (response.authed_user as { id: string }).id // Slack
|
|
148
|
+
*/
|
|
149
|
+
extractExternalId: (tokenResponse: Record<string, unknown>) => string;
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Optional: Extract access token from response.
|
|
153
|
+
* Default: response.access_token
|
|
154
|
+
*/
|
|
155
|
+
extractAccessToken?: (response: Record<string, unknown>) => string;
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Optional: Extract refresh token from response.
|
|
159
|
+
* Default: response.refresh_token
|
|
160
|
+
*/
|
|
161
|
+
extractRefreshToken?: (
|
|
162
|
+
response: Record<string, unknown>
|
|
163
|
+
) => string | undefined;
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Optional: Extract token expiration (seconds from now).
|
|
167
|
+
* Default: response.expires_in
|
|
168
|
+
*/
|
|
169
|
+
extractExpiresIn?: (response: Record<string, unknown>) => number | undefined;
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Optional: Custom state encoder for CSRF protection.
|
|
173
|
+
* Default implementation encodes userId and returnUrl as base64 JSON.
|
|
174
|
+
*/
|
|
175
|
+
encodeState?: (userId: string, returnUrl: string) => string;
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Optional: Custom state decoder.
|
|
179
|
+
* Must match the encoder implementation.
|
|
180
|
+
*/
|
|
181
|
+
decodeState?: (state: string) => { userId: string; returnUrl: string };
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Optional: Custom authorization URL builder.
|
|
185
|
+
* Use when provider has non-standard OAuth parameters.
|
|
186
|
+
*/
|
|
187
|
+
buildAuthUrl?: (params: {
|
|
188
|
+
clientId: string;
|
|
189
|
+
redirectUri: string;
|
|
190
|
+
scopes: string[];
|
|
191
|
+
state: string;
|
|
192
|
+
}) => string;
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Optional: Custom token refresh logic.
|
|
196
|
+
* Only needed if the provider uses refresh tokens.
|
|
197
|
+
*/
|
|
198
|
+
refreshToken?: (refreshToken: string) => Promise<{
|
|
199
|
+
accessToken: string;
|
|
200
|
+
refreshToken?: string;
|
|
201
|
+
expiresIn?: number;
|
|
202
|
+
}>;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
206
|
+
// Notification Strategy Interface
|
|
207
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Represents a notification delivery strategy (e.g., SMTP, Slack, Discord).
|
|
211
|
+
*
|
|
212
|
+
* Strategies are registered via the `notificationStrategyExtensionPoint` and
|
|
213
|
+
* are namespaced by their owning plugin's ID to prevent conflicts.
|
|
214
|
+
*
|
|
215
|
+
* @example
|
|
216
|
+
* ```typescript
|
|
217
|
+
* const smtpStrategy: NotificationStrategy<SmtpConfig> = {
|
|
218
|
+
* id: 'smtp',
|
|
219
|
+
* displayName: 'Email (SMTP)',
|
|
220
|
+
* icon: 'mail',
|
|
221
|
+
* config: new Versioned({ version: 1, schema: smtpConfigSchema }),
|
|
222
|
+
* contactResolution: { type: 'auth-email' },
|
|
223
|
+
* async send({ contact, notification, strategyConfig }) {
|
|
224
|
+
* await sendEmail({ to: contact, subject: notification.title, ... });
|
|
225
|
+
* return { success: true };
|
|
226
|
+
* }
|
|
227
|
+
* };
|
|
228
|
+
* ```
|
|
229
|
+
*/
|
|
230
|
+
export interface NotificationStrategy<
|
|
231
|
+
TConfig = unknown,
|
|
232
|
+
TUserConfig = undefined,
|
|
233
|
+
TLayoutConfig = undefined
|
|
234
|
+
> {
|
|
235
|
+
/**
|
|
236
|
+
* Unique identifier within the owning plugin's namespace.
|
|
237
|
+
* Will be qualified as `{pluginId}.{id}` at runtime.
|
|
238
|
+
* Example: 'smtp' becomes 'notification-smtp.smtp'
|
|
239
|
+
*/
|
|
240
|
+
id: string;
|
|
241
|
+
|
|
242
|
+
/** Display name shown in UI */
|
|
243
|
+
displayName: string;
|
|
244
|
+
|
|
245
|
+
/** Optional description of the channel */
|
|
246
|
+
description?: string;
|
|
247
|
+
|
|
248
|
+
/** Lucide icon name in PascalCase (e.g., 'Mail', 'MessageCircle') */
|
|
249
|
+
icon?: LucideIconName;
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Global strategy configuration (admin-managed).
|
|
253
|
+
* Uses Versioned<T> for schema evolution and migration support.
|
|
254
|
+
*/
|
|
255
|
+
config: Versioned<TConfig>;
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Per-user configuration schema (if users need to provide info).
|
|
259
|
+
*
|
|
260
|
+
* Examples:
|
|
261
|
+
* - SMTP: undefined (uses auth email, no user config needed)
|
|
262
|
+
* - SMS: new Versioned({ schema: z.object({ phoneNumber: z.string() }) })
|
|
263
|
+
* - Slack: undefined (uses OAuth linking)
|
|
264
|
+
*/
|
|
265
|
+
userConfig?: Versioned<TUserConfig>;
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Layout configuration for admin customization (optional).
|
|
269
|
+
*
|
|
270
|
+
* Only applicable for strategies that support rich layouts (e.g., email).
|
|
271
|
+
* If defined, admins can customize branding (logo, colors, footer) via
|
|
272
|
+
* the settings UI and the layout is passed to send() as `layoutConfig`.
|
|
273
|
+
*
|
|
274
|
+
* @example
|
|
275
|
+
* ```typescript
|
|
276
|
+
* layoutConfig: new Versioned({
|
|
277
|
+
* version: 1,
|
|
278
|
+
* schema: z.object({
|
|
279
|
+
* logoUrl: z.string().url().optional(),
|
|
280
|
+
* primaryColor: z.string().default("#3b82f6"),
|
|
281
|
+
* footerText: z.string().default("Sent by Checkstack"),
|
|
282
|
+
* }),
|
|
283
|
+
* })
|
|
284
|
+
* ```
|
|
285
|
+
*/
|
|
286
|
+
layoutConfig?: Versioned<TLayoutConfig>;
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* How this strategy resolves user contact information.
|
|
290
|
+
*/
|
|
291
|
+
contactResolution: NotificationContactResolution;
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Send a notification via this channel.
|
|
295
|
+
*
|
|
296
|
+
* @param context - Send context with user, contact, notification, and config
|
|
297
|
+
* @returns Result indicating success/failure
|
|
298
|
+
*/
|
|
299
|
+
send(
|
|
300
|
+
context: NotificationSendContext<TConfig, TUserConfig, TLayoutConfig>
|
|
301
|
+
): Promise<NotificationDeliveryResult>;
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* OAuth configuration for strategies that use OAuth linking.
|
|
305
|
+
*
|
|
306
|
+
* When provided, the notification-backend registry automatically registers
|
|
307
|
+
* HTTP handlers for the OAuth flow. No manual endpoint registration needed.
|
|
308
|
+
*
|
|
309
|
+
* Required when contactResolution is { type: 'oauth-link' }.
|
|
310
|
+
*
|
|
311
|
+
* @example
|
|
312
|
+
* ```typescript
|
|
313
|
+
* oauth: {
|
|
314
|
+
* clientId: () => configService.get('slack.clientId'),
|
|
315
|
+
* clientSecret: () => configService.get('slack.clientSecret'),
|
|
316
|
+
* scopes: ['users:read', 'chat:write'],
|
|
317
|
+
* authorizationUrl: 'https://slack.com/oauth/v2/authorize',
|
|
318
|
+
* tokenUrl: 'https://slack.com/api/oauth.v2.access',
|
|
319
|
+
* extractExternalId: (res) => (res.authed_user as { id: string }).id,
|
|
320
|
+
* }
|
|
321
|
+
* ```
|
|
322
|
+
*/
|
|
323
|
+
oauth?: StrategyOAuthConfig<TConfig>;
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Markdown instructions shown when admins configure platform-wide strategy settings.
|
|
327
|
+
* Displayed in the StrategyConfigCard before the configuration form.
|
|
328
|
+
*
|
|
329
|
+
* Use this to provide setup guidance (e.g., how to create API keys, register apps).
|
|
330
|
+
*
|
|
331
|
+
* @example
|
|
332
|
+
* ```typescript
|
|
333
|
+
* adminInstructions: `
|
|
334
|
+
* ## Setup a Telegram Bot
|
|
335
|
+
* 1. Open [@BotFather](https://t.me/BotFather) in Telegram
|
|
336
|
+
* 2. Send \`/newbot\` and follow the prompts
|
|
337
|
+
* 3. Copy the bot token
|
|
338
|
+
* `
|
|
339
|
+
* ```
|
|
340
|
+
*/
|
|
341
|
+
adminInstructions?: string;
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Markdown instructions shown when users configure their personal settings.
|
|
345
|
+
* Displayed in the UserChannelCard when connecting/configuring.
|
|
346
|
+
*
|
|
347
|
+
* Use this to guide users through linking their account or setting up the channel.
|
|
348
|
+
*/
|
|
349
|
+
userInstructions?: string;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
353
|
+
// Registry Types
|
|
354
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Registered strategy with full namespace information.
|
|
358
|
+
*/
|
|
359
|
+
export interface RegisteredNotificationStrategy<
|
|
360
|
+
TConfig = unknown,
|
|
361
|
+
TUserConfig = undefined,
|
|
362
|
+
TLayoutConfig = undefined
|
|
363
|
+
> extends NotificationStrategy<TConfig, TUserConfig, TLayoutConfig> {
|
|
364
|
+
/** Fully qualified ID: `{pluginId}.{id}` */
|
|
365
|
+
qualifiedId: string;
|
|
366
|
+
/** Plugin that registered this strategy */
|
|
367
|
+
ownerPluginId: string;
|
|
368
|
+
/**
|
|
369
|
+
* Dynamically generated permission ID for this strategy.
|
|
370
|
+
* Format: `{ownerPluginId}.strategy.{id}.use`
|
|
371
|
+
*/
|
|
372
|
+
permissionId: string;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Registry for notification strategies.
|
|
377
|
+
* Maintained by notification-backend.
|
|
378
|
+
*/
|
|
379
|
+
export interface NotificationStrategyRegistry {
|
|
380
|
+
/**
|
|
381
|
+
* Register a notification strategy.
|
|
382
|
+
* Must be called during plugin initialization.
|
|
383
|
+
*
|
|
384
|
+
* @param strategy - The strategy to register
|
|
385
|
+
* @param pluginMetadata - Plugin metadata for namespacing
|
|
386
|
+
*/
|
|
387
|
+
register<TConfig, TUserConfig, TLayoutConfig>(
|
|
388
|
+
strategy: NotificationStrategy<TConfig, TUserConfig, TLayoutConfig>,
|
|
389
|
+
pluginMetadata: PluginMetadata
|
|
390
|
+
): void;
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Get a strategy by its qualified ID.
|
|
394
|
+
*
|
|
395
|
+
* @param qualifiedId - Full ID in format `{pluginId}.{strategyId}`
|
|
396
|
+
*/
|
|
397
|
+
getStrategy(
|
|
398
|
+
qualifiedId: string
|
|
399
|
+
): RegisteredNotificationStrategy<unknown, unknown, unknown> | undefined;
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Get all registered strategies.
|
|
403
|
+
*/
|
|
404
|
+
getStrategies(): RegisteredNotificationStrategy<unknown, unknown, unknown>[];
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Get all strategies that a user has permission to use.
|
|
408
|
+
*
|
|
409
|
+
* @param userPermissions - Set of permission IDs the user has
|
|
410
|
+
*/
|
|
411
|
+
getStrategiesForUser(
|
|
412
|
+
userPermissions: Set<string>
|
|
413
|
+
): RegisteredNotificationStrategy<unknown, unknown, unknown>[];
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
417
|
+
// User Preference Types (for typings, actual storage in notification-backend)
|
|
418
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* User's notification preference for a specific strategy.
|
|
422
|
+
*/
|
|
423
|
+
export interface UserNotificationPreference {
|
|
424
|
+
/** User ID */
|
|
425
|
+
userId: string;
|
|
426
|
+
/** Qualified strategy ID */
|
|
427
|
+
strategyId: string;
|
|
428
|
+
/** User's strategy-specific config (validated via strategy.userConfig) */
|
|
429
|
+
config: VersionedRecord<unknown> | null;
|
|
430
|
+
/** Whether user has enabled this channel */
|
|
431
|
+
enabled: boolean;
|
|
432
|
+
/** External user ID from OAuth linking (e.g., Slack user ID) */
|
|
433
|
+
externalId: string | null;
|
|
434
|
+
/** When the external account was linked */
|
|
435
|
+
linkedAt: Date | null;
|
|
436
|
+
}
|