@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.
@@ -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
+ }