ntfy-mcp-server 1.0.0

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.
Files changed (59) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +423 -0
  3. package/dist/config/index.d.ts +23 -0
  4. package/dist/config/index.js +111 -0
  5. package/dist/index.d.ts +2 -0
  6. package/dist/index.js +108 -0
  7. package/dist/mcp-server/resources/ntfyResource/getNtfyTopic.d.ts +2 -0
  8. package/dist/mcp-server/resources/ntfyResource/getNtfyTopic.js +111 -0
  9. package/dist/mcp-server/resources/ntfyResource/index.d.ts +12 -0
  10. package/dist/mcp-server/resources/ntfyResource/index.js +72 -0
  11. package/dist/mcp-server/resources/ntfyResource/types.d.ts +27 -0
  12. package/dist/mcp-server/resources/ntfyResource/types.js +8 -0
  13. package/dist/mcp-server/server.d.ts +40 -0
  14. package/dist/mcp-server/server.js +245 -0
  15. package/dist/mcp-server/tools/ntfyTool/index.d.ts +11 -0
  16. package/dist/mcp-server/tools/ntfyTool/index.js +110 -0
  17. package/dist/mcp-server/tools/ntfyTool/ntfyMessage.d.ts +9 -0
  18. package/dist/mcp-server/tools/ntfyTool/ntfyMessage.js +289 -0
  19. package/dist/mcp-server/tools/ntfyTool/types.d.ts +252 -0
  20. package/dist/mcp-server/tools/ntfyTool/types.js +144 -0
  21. package/dist/mcp-server/utils/registrationHelper.d.ts +48 -0
  22. package/dist/mcp-server/utils/registrationHelper.js +63 -0
  23. package/dist/services/ntfy/constants.d.ts +37 -0
  24. package/dist/services/ntfy/constants.js +37 -0
  25. package/dist/services/ntfy/errors.d.ts +79 -0
  26. package/dist/services/ntfy/errors.js +134 -0
  27. package/dist/services/ntfy/index.d.ts +33 -0
  28. package/dist/services/ntfy/index.js +56 -0
  29. package/dist/services/ntfy/publisher.d.ts +66 -0
  30. package/dist/services/ntfy/publisher.js +229 -0
  31. package/dist/services/ntfy/subscriber.d.ts +81 -0
  32. package/dist/services/ntfy/subscriber.js +502 -0
  33. package/dist/services/ntfy/types.d.ts +161 -0
  34. package/dist/services/ntfy/types.js +4 -0
  35. package/dist/services/ntfy/utils.d.ts +85 -0
  36. package/dist/services/ntfy/utils.js +410 -0
  37. package/dist/types-global/errors.d.ts +35 -0
  38. package/dist/types-global/errors.js +39 -0
  39. package/dist/types-global/mcp.d.ts +30 -0
  40. package/dist/types-global/mcp.js +25 -0
  41. package/dist/types-global/tool.d.ts +61 -0
  42. package/dist/types-global/tool.js +99 -0
  43. package/dist/utils/errorHandler.d.ts +98 -0
  44. package/dist/utils/errorHandler.js +271 -0
  45. package/dist/utils/idGenerator.d.ts +94 -0
  46. package/dist/utils/idGenerator.js +149 -0
  47. package/dist/utils/index.d.ts +13 -0
  48. package/dist/utils/index.js +16 -0
  49. package/dist/utils/logger.d.ts +36 -0
  50. package/dist/utils/logger.js +92 -0
  51. package/dist/utils/rateLimiter.d.ts +115 -0
  52. package/dist/utils/rateLimiter.js +180 -0
  53. package/dist/utils/requestContext.d.ts +68 -0
  54. package/dist/utils/requestContext.js +91 -0
  55. package/dist/utils/sanitization.d.ts +224 -0
  56. package/dist/utils/sanitization.js +367 -0
  57. package/dist/utils/security.d.ts +26 -0
  58. package/dist/utils/security.js +27 -0
  59. package/package.json +47 -0
@@ -0,0 +1,502 @@
1
+ /**
2
+ * Ntfy subscriber implementation
3
+ */
4
+ import { BaseErrorCode } from '../../types-global/errors.js';
5
+ import { ErrorHandler } from '../../utils/errorHandler.js';
6
+ import { logger } from '../../utils/logger.js';
7
+ import { sanitizeInput, sanitizeInputForLogging } from '../../utils/sanitization.js';
8
+ import { createRequestContext } from '../../utils/requestContext.js';
9
+ import { idGenerator } from '../../utils/idGenerator.js';
10
+ import { DEFAULT_REQUEST_TIMEOUT, DEFAULT_SUBSCRIPTION_OPTIONS, ERROR_MESSAGES, KEEPALIVE_TIMEOUT, MAX_RECONNECT_ATTEMPTS, RECONNECT_DELAY, SUBSCRIPTION_ENDPOINTS } from './constants.js';
11
+ import { NtfyConnectionError, NtfyError, NtfyInvalidTopicError, NtfyParseError, NtfySubscriptionClosedError, NtfyTimeoutError, ntfyErrorMapper } from './errors.js';
12
+ import { buildSubscriptionUrlSync, createAbortControllerWithTimeout, createRequestHeadersSync, validateTopicSync, parseJsonMessageSync } from './utils.js';
13
+ /**
14
+ * NtfySubscriber class for subscribing to ntfy topics
15
+ */
16
+ export class NtfySubscriber {
17
+ /**
18
+ * Creates a new NtfySubscriber instance
19
+ * @param handlers Event handlers for the subscription
20
+ */
21
+ constructor(handlers = {}) {
22
+ this.handlers = handlers;
23
+ this.connectionActive = false;
24
+ this.lastKeepaliveTime = 0;
25
+ this.reconnectAttempts = 0;
26
+ this.subscriberId = idGenerator.generateRandomString(8);
27
+ // Create logger with subscriber context
28
+ this.logger = logger.createChildLogger({
29
+ module: 'NtfySubscriber',
30
+ subscriberId: this.subscriberId,
31
+ subscriptionTime: new Date().toISOString()
32
+ });
33
+ this.logger.debug('NtfySubscriber instance created');
34
+ }
35
+ /**
36
+ * Subscribe to a ntfy topic
37
+ * @param topic Topic to subscribe to (can be comma-separated for multiple topics)
38
+ * @param options Subscription options
39
+ * @returns Promise that resolves when the subscription is established
40
+ * @throws NtfyInvalidTopicError if the topic name is invalid
41
+ * @throws NtfyConnectionError if the connection fails
42
+ */
43
+ async subscribe(topic, options = {}) {
44
+ return ErrorHandler.tryCatch(async () => {
45
+ // Create a request context for tracking this operation
46
+ const requestCtx = createRequestContext({
47
+ operation: 'subscribe',
48
+ topic,
49
+ subscriberId: this.subscriberId,
50
+ options: sanitizeInputForLogging(options)
51
+ });
52
+ // Validate topic
53
+ if (!validateTopicSync(topic)) {
54
+ this.logger.error('Invalid topic name', {
55
+ topic,
56
+ requestId: requestCtx.requestId
57
+ });
58
+ throw new NtfyInvalidTopicError(ERROR_MESSAGES.INVALID_TOPIC, topic);
59
+ }
60
+ // Store current topic for reconnect logic
61
+ this.currentTopic = topic;
62
+ // Log the subscription attempt
63
+ this.logger.info('Subscribing to topic', {
64
+ topic,
65
+ options: sanitizeInputForLogging(options),
66
+ requestId: requestCtx.requestId
67
+ });
68
+ // Merge options with defaults
69
+ const mergedOptions = { ...DEFAULT_SUBSCRIPTION_OPTIONS, ...options };
70
+ // Close any existing subscription
71
+ this.unsubscribe();
72
+ // Reset reconnect attempts
73
+ this.reconnectAttempts = 0;
74
+ // Start subscription
75
+ await this.startSubscription(topic, 'json', mergedOptions);
76
+ // Start keepalive check if this is a persistent connection
77
+ if (!mergedOptions.poll) {
78
+ this.startKeepaliveCheck();
79
+ }
80
+ this.logger.info('Successfully subscribed to topic', {
81
+ topic,
82
+ requestId: requestCtx.requestId
83
+ });
84
+ }, {
85
+ operation: 'subscribe',
86
+ context: {
87
+ topic,
88
+ subscriberId: this.subscriberId
89
+ },
90
+ input: sanitizeInputForLogging(options),
91
+ errorCode: BaseErrorCode.SERVICE_UNAVAILABLE,
92
+ errorMapper: ntfyErrorMapper,
93
+ rethrow: true
94
+ });
95
+ }
96
+ /**
97
+ * Unsubscribe from the current topic
98
+ */
99
+ unsubscribe() {
100
+ const requestCtx = createRequestContext({
101
+ operation: 'unsubscribe',
102
+ subscriberId: this.subscriberId,
103
+ topic: this.currentTopic
104
+ });
105
+ this.logger.debug('Unsubscribing from topic', {
106
+ requestId: requestCtx.requestId,
107
+ topic: this.currentTopic
108
+ });
109
+ this.stopKeepaliveCheck();
110
+ if (this.abortController) {
111
+ this.abortController.abort();
112
+ this.abortController = undefined;
113
+ }
114
+ if (this.cleanupFn) {
115
+ this.cleanupFn();
116
+ this.cleanupFn = undefined;
117
+ }
118
+ this.connectionActive = false;
119
+ this.logger.info('Unsubscribed from topic', {
120
+ requestId: requestCtx.requestId,
121
+ topic: this.currentTopic
122
+ });
123
+ // Clear current topic
124
+ this.currentTopic = undefined;
125
+ }
126
+ /**
127
+ * Start a subscription to a topic
128
+ * @param topic Topic to subscribe to
129
+ * @param format Format to subscribe in (json, sse, raw, ws)
130
+ * @param options Subscription options
131
+ */
132
+ async startSubscription(topic, format, options) {
133
+ const requestCtx = createRequestContext({
134
+ operation: 'startSubscription',
135
+ subscriberId: this.subscriberId,
136
+ topic,
137
+ format
138
+ });
139
+ const sanitizedTopic = sanitizeInput.string(topic);
140
+ this.logger.debug('Starting subscription', {
141
+ topic: sanitizedTopic,
142
+ format,
143
+ requestId: requestCtx.requestId
144
+ });
145
+ const url = buildSubscriptionUrlSync(topic, SUBSCRIPTION_ENDPOINTS[format], options);
146
+ const headers = createRequestHeadersSync(options);
147
+ // Create abort controller with timeout
148
+ const { controller, cleanup } = createAbortControllerWithTimeout(DEFAULT_REQUEST_TIMEOUT);
149
+ this.abortController = controller;
150
+ this.cleanupFn = cleanup;
151
+ try {
152
+ // Make the request
153
+ this.logger.debug('Sending subscription request', {
154
+ url,
155
+ requestId: requestCtx.requestId
156
+ });
157
+ const response = await fetch(url, {
158
+ method: 'GET',
159
+ headers,
160
+ signal: controller.signal,
161
+ });
162
+ // Check response status
163
+ if (!response.ok) {
164
+ this.logger.error('HTTP error from ntfy server', {
165
+ status: response.status,
166
+ statusText: response.statusText,
167
+ url,
168
+ requestId: requestCtx.requestId
169
+ });
170
+ throw new NtfyConnectionError(`HTTP Error: ${response.status} ${response.statusText}`, url);
171
+ }
172
+ // Set connection as active
173
+ this.connectionActive = true;
174
+ this.logger.debug('Connection established', {
175
+ requestId: requestCtx.requestId
176
+ });
177
+ // Get response body as reader
178
+ const reader = response.body?.getReader();
179
+ if (!reader) {
180
+ this.logger.error('No response body available', {
181
+ url,
182
+ requestId: requestCtx.requestId
183
+ });
184
+ throw new NtfyConnectionError('No response body available', url);
185
+ }
186
+ // Process the stream
187
+ await this.processJsonStream(reader, requestCtx.requestId);
188
+ }
189
+ catch (error) {
190
+ // Clean up and rethrow
191
+ this.logger.error('Error starting subscription', {
192
+ error: error instanceof Error ? error.message : String(error),
193
+ topic: sanitizedTopic,
194
+ url,
195
+ requestId: requestCtx.requestId
196
+ });
197
+ this.cleanupFn();
198
+ this.cleanupFn = undefined;
199
+ this.abortController = undefined;
200
+ this.connectionActive = false;
201
+ // Attempt reconnect if appropriate
202
+ if (!options.poll &&
203
+ !(error instanceof NtfySubscriptionClosedError) &&
204
+ this.reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
205
+ this.scheduleReconnect(topic, format, options);
206
+ }
207
+ else {
208
+ throw error;
209
+ }
210
+ }
211
+ }
212
+ /**
213
+ * Process a JSON stream from ntfy
214
+ * @param reader ReadableStreamDefaultReader to read from
215
+ * @param requestId Request ID for logging
216
+ */
217
+ async processJsonStream(reader, requestId) {
218
+ const decoder = new TextDecoder();
219
+ let buffer = '';
220
+ this.logger.debug('Starting to process JSON stream', { requestId });
221
+ while (this.connectionActive) {
222
+ try {
223
+ const { done, value } = await reader.read();
224
+ if (done) {
225
+ // Stream has ended
226
+ this.logger.info('Stream ended normally', { requestId });
227
+ this.connectionActive = false;
228
+ if (this.handlers.onClose) {
229
+ try {
230
+ this.handlers.onClose();
231
+ }
232
+ catch (error) {
233
+ this.logger.error('Error in onClose handler', {
234
+ error: error instanceof Error ? error.message : String(error),
235
+ requestId
236
+ });
237
+ }
238
+ }
239
+ break;
240
+ }
241
+ // Decode the chunk and add to buffer
242
+ buffer += decoder.decode(value, { stream: true });
243
+ // Process any complete lines in the buffer
244
+ const lines = buffer.split('\n');
245
+ buffer = lines.pop() || ''; // Keep the last incomplete line in the buffer
246
+ // Process each complete line
247
+ for (const line of lines) {
248
+ if (line.trim()) {
249
+ try {
250
+ const message = parseJsonMessageSync(line);
251
+ this.handleMessage(message, requestId);
252
+ }
253
+ catch (error) {
254
+ this.handleParseError(error, line, requestId);
255
+ }
256
+ }
257
+ }
258
+ }
259
+ catch (error) {
260
+ // Handle read errors
261
+ this.connectionActive = false;
262
+ this.logger.error('Error reading from stream', {
263
+ error: error instanceof Error ? error.message : String(error),
264
+ errorType: error instanceof Error ? error.name : typeof error,
265
+ requestId
266
+ });
267
+ // Handle various error types more specifically
268
+ if (error instanceof Error) {
269
+ // AbortError - intentional close
270
+ if (error.name === 'AbortError') {
271
+ throw new NtfySubscriptionClosedError('Subscription aborted');
272
+ }
273
+ // Network errors
274
+ if (error.name === 'NetworkError' ||
275
+ error.name === 'TypeError' ||
276
+ error.message.includes('network') ||
277
+ error.message.includes('connection')) {
278
+ const connectionError = new NtfyConnectionError(`Network error during stream processing: ${error.message}`);
279
+ // Add additional context to the error details
280
+ connectionError.details = {
281
+ originalError: error.name,
282
+ originalMessage: error.message
283
+ };
284
+ throw connectionError;
285
+ }
286
+ // Timeout errors
287
+ if (error.name === 'TimeoutError' ||
288
+ error.message.includes('timeout') ||
289
+ error.message.includes('timed out')) {
290
+ throw new NtfyTimeoutError(`Stream reading timed out: ${error.message}`, DEFAULT_REQUEST_TIMEOUT);
291
+ }
292
+ }
293
+ // Default case - generic connection error
294
+ throw new NtfyConnectionError(`Error reading from stream: ${error instanceof Error ? error.message : String(error)}`);
295
+ }
296
+ }
297
+ }
298
+ /**
299
+ * Handle a message from ntfy
300
+ * @param message Message from ntfy
301
+ * @param requestId Request ID for logging
302
+ */
303
+ handleMessage(message, requestId) {
304
+ // Update last keepalive time for any message
305
+ this.lastKeepaliveTime = Date.now();
306
+ // Log message receipt at debug level
307
+ this.logger.debug('Received message', {
308
+ messageId: message.id,
309
+ eventType: message.event,
310
+ topic: message.topic,
311
+ requestId
312
+ });
313
+ // Call the appropriate handler based on message type
314
+ try {
315
+ switch (message.event) {
316
+ case 'message':
317
+ if (this.handlers.onMessage) {
318
+ const notificationMessage = message;
319
+ this.logger.debug('Processing notification message', {
320
+ messageId: notificationMessage.id,
321
+ hasTitle: !!notificationMessage.title,
322
+ requestId
323
+ });
324
+ this.handlers.onMessage(notificationMessage);
325
+ }
326
+ break;
327
+ case 'open':
328
+ if (this.handlers.onOpen) {
329
+ this.logger.debug('Processing open message', { requestId });
330
+ this.handlers.onOpen(message);
331
+ }
332
+ break;
333
+ case 'keepalive':
334
+ if (this.handlers.onKeepalive) {
335
+ this.logger.debug('Processing keepalive message', { requestId });
336
+ this.handlers.onKeepalive(message);
337
+ }
338
+ break;
339
+ }
340
+ // Always call onAnyMessage if it exists
341
+ if (this.handlers.onAnyMessage) {
342
+ this.handlers.onAnyMessage(message);
343
+ }
344
+ }
345
+ catch (error) {
346
+ this.logger.error('Error in message handler', {
347
+ error: error instanceof Error ? error.message : String(error),
348
+ messageType: message.event,
349
+ messageId: message.id,
350
+ requestId
351
+ });
352
+ // Don't rethrow to avoid breaking the stream processing
353
+ }
354
+ }
355
+ /**
356
+ * Handle a parse error
357
+ * @param error Error that occurred
358
+ * @param rawData Raw data that caused the error
359
+ * @param requestId Request ID for logging
360
+ */
361
+ handleParseError(error, rawData, requestId) {
362
+ this.logger.error('Failed to parse message', {
363
+ error: error instanceof Error ? error.message : String(error),
364
+ rawData: rawData.length > 100 ? `${rawData.substring(0, 100)}...` : rawData,
365
+ requestId
366
+ });
367
+ if (this.handlers.onError) {
368
+ try {
369
+ if (error instanceof NtfyParseError) {
370
+ this.handlers.onError(error);
371
+ }
372
+ else {
373
+ const parsedError = new NtfyParseError(`Failed to parse message: ${error instanceof Error ? error.message : String(error)}`, rawData);
374
+ this.handlers.onError(parsedError);
375
+ }
376
+ }
377
+ catch (handlerError) {
378
+ this.logger.error('Error in error handler', {
379
+ error: handlerError instanceof Error ? handlerError.message : String(handlerError),
380
+ requestId
381
+ });
382
+ }
383
+ }
384
+ }
385
+ /**
386
+ * Handle a subscription error
387
+ * @param error Error that occurred
388
+ * @param requestId Request ID for logging
389
+ */
390
+ handleSubscriptionError(error, requestId) {
391
+ this.logger.error('Subscription error', {
392
+ error: error instanceof Error ? error.message : String(error),
393
+ errorType: error instanceof Error ? error.name : 'Unknown',
394
+ requestId
395
+ });
396
+ if (this.handlers.onError) {
397
+ try {
398
+ if (error instanceof NtfyError) {
399
+ this.handlers.onError(error);
400
+ }
401
+ else {
402
+ this.handlers.onError(new NtfyConnectionError(`Subscription error: ${error instanceof Error ? error.message : String(error)}`));
403
+ }
404
+ }
405
+ catch (handlerError) {
406
+ this.logger.error('Error in error handler', {
407
+ error: handlerError instanceof Error ? handlerError.message : String(handlerError),
408
+ requestId
409
+ });
410
+ }
411
+ }
412
+ }
413
+ /**
414
+ * Start the keepalive check interval
415
+ */
416
+ startKeepaliveCheck() {
417
+ const requestCtx = createRequestContext({
418
+ operation: 'startKeepaliveCheck',
419
+ subscriberId: this.subscriberId,
420
+ topic: this.currentTopic
421
+ });
422
+ this.logger.debug('Starting keepalive check', {
423
+ timeout: KEEPALIVE_TIMEOUT,
424
+ checkInterval: KEEPALIVE_TIMEOUT / 2,
425
+ requestId: requestCtx.requestId
426
+ });
427
+ this.stopKeepaliveCheck();
428
+ this.lastKeepaliveTime = Date.now();
429
+ this.keepaliveCheckInterval = setInterval(() => {
430
+ const now = Date.now();
431
+ const elapsed = now - this.lastKeepaliveTime;
432
+ this.logger.debug('Keepalive check', {
433
+ elapsed,
434
+ threshold: KEEPALIVE_TIMEOUT,
435
+ requestId: requestCtx.requestId
436
+ });
437
+ if (elapsed > KEEPALIVE_TIMEOUT && this.connectionActive) {
438
+ // Connection has timed out
439
+ this.logger.warn('Keepalive timeout detected', {
440
+ elapsed,
441
+ threshold: KEEPALIVE_TIMEOUT,
442
+ requestId: requestCtx.requestId
443
+ });
444
+ this.handleSubscriptionError(new NtfyTimeoutError('Keepalive timeout', KEEPALIVE_TIMEOUT), requestCtx.requestId);
445
+ this.unsubscribe();
446
+ }
447
+ }, KEEPALIVE_TIMEOUT / 2);
448
+ }
449
+ /**
450
+ * Stop the keepalive check interval
451
+ */
452
+ stopKeepaliveCheck() {
453
+ if (this.keepaliveCheckInterval) {
454
+ this.logger.debug('Stopping keepalive check');
455
+ clearInterval(this.keepaliveCheckInterval);
456
+ this.keepaliveCheckInterval = undefined;
457
+ }
458
+ }
459
+ /**
460
+ * Schedule a reconnection attempt
461
+ * @param topic Topic to reconnect to
462
+ * @param format Format to reconnect with
463
+ * @param options Subscription options
464
+ */
465
+ scheduleReconnect(topic, format, options) {
466
+ const requestCtx = createRequestContext({
467
+ operation: 'scheduleReconnect',
468
+ subscriberId: this.subscriberId,
469
+ topic,
470
+ format
471
+ });
472
+ this.reconnectAttempts++;
473
+ // Add jitter to prevent thundering herd problem
474
+ // and cap at a maximum delay of 30 seconds
475
+ const MAX_BACKOFF_DELAY = 30000; // 30 seconds
476
+ const baseDelay = RECONNECT_DELAY * this.reconnectAttempts;
477
+ const jitter = Math.floor(Math.random() * 1000); // Add up to 1 second of jitter
478
+ const delay = Math.min(baseDelay + jitter, MAX_BACKOFF_DELAY);
479
+ this.logger.info('Scheduling reconnection attempt', {
480
+ topic,
481
+ attemptNumber: this.reconnectAttempts,
482
+ maxAttempts: MAX_RECONNECT_ATTEMPTS,
483
+ baseDelay: baseDelay,
484
+ jitter: jitter,
485
+ actualDelay: delay,
486
+ maxBackoff: MAX_BACKOFF_DELAY,
487
+ requestId: requestCtx.requestId
488
+ });
489
+ setTimeout(() => {
490
+ if (!this.connectionActive) {
491
+ this.logger.info('Attempting reconnection', {
492
+ topic,
493
+ attemptNumber: this.reconnectAttempts,
494
+ requestId: requestCtx.requestId
495
+ });
496
+ this.startSubscription(topic, format, options).catch((error) => {
497
+ this.handleSubscriptionError(error, requestCtx.requestId);
498
+ });
499
+ }
500
+ }, delay);
501
+ }
502
+ }
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Types for the ntfy service
3
+ */
4
+ /**
5
+ * Message event types from ntfy
6
+ */
7
+ export type NtfyEventType = 'open' | 'message' | 'keepalive' | 'poll_request';
8
+ /**
9
+ * Message priority levels
10
+ * - 1: min priority
11
+ * - 2: low priority
12
+ * - 3: default priority
13
+ * - 4: high priority
14
+ * - 5: max priority
15
+ */
16
+ export type NtfyPriority = 1 | 2 | 3 | 4 | 5;
17
+ /**
18
+ * Subscription format types
19
+ */
20
+ export type NtfySubscriptionFormat = 'json' | 'sse' | 'raw' | 'ws';
21
+ /**
22
+ * Attachment information
23
+ */
24
+ export interface NtfyAttachment {
25
+ /** Name of the attachment */
26
+ name: string;
27
+ /** URL of the attachment */
28
+ url: string;
29
+ /** Mime type of the attachment (only defined if attachment was uploaded to ntfy server) */
30
+ type?: string;
31
+ /** Size of the attachment in bytes (only defined if attachment was uploaded to ntfy server) */
32
+ size?: number;
33
+ /** Attachment expiry date as Unix time stamp (only defined if attachment was uploaded to ntfy server) */
34
+ expires?: number;
35
+ }
36
+ /**
37
+ * Action button definition
38
+ */
39
+ export interface NtfyAction {
40
+ /** Action identifier */
41
+ id: string;
42
+ /** Label for the action button */
43
+ label: string;
44
+ /** Action type (e.g., view, broadcast, http) */
45
+ action: string;
46
+ /** URL or data for the action */
47
+ url?: string;
48
+ /** HTTP method for http actions */
49
+ method?: string;
50
+ /** Additional headers for http actions */
51
+ headers?: Record<string, string>;
52
+ /** Body for http actions */
53
+ body?: string;
54
+ /** Clear notification after action */
55
+ clear?: boolean;
56
+ }
57
+ /**
58
+ * Base message interface with common fields for all message types
59
+ */
60
+ export interface NtfyBaseMessage {
61
+ /** Randomly chosen message identifier */
62
+ id: string;
63
+ /** Message date time, as Unix time stamp */
64
+ time: number;
65
+ /** Message type */
66
+ event: NtfyEventType;
67
+ /** Comma-separated list of topics the message is associated with */
68
+ topic: string;
69
+ /** Unix time stamp indicating when the message will be deleted */
70
+ expires?: number;
71
+ }
72
+ /**
73
+ * Regular notification message
74
+ */
75
+ export interface NtfyNotificationMessage extends NtfyBaseMessage {
76
+ event: 'message';
77
+ /** Message body */
78
+ message: string;
79
+ /** Message title */
80
+ title?: string;
81
+ /** List of tags that may or not map to emojis */
82
+ tags?: string[];
83
+ /** Message priority with 1=min, 3=default and 5=max */
84
+ priority?: NtfyPriority;
85
+ /** Website opened when notification is clicked */
86
+ click?: string;
87
+ /** Action buttons that can be displayed in the notification */
88
+ actions?: NtfyAction[];
89
+ /** Details about an attachment */
90
+ attachment?: NtfyAttachment;
91
+ }
92
+ /**
93
+ * Connection open message
94
+ */
95
+ export interface NtfyOpenMessage extends NtfyBaseMessage {
96
+ event: 'open';
97
+ }
98
+ /**
99
+ * Keepalive message to maintain connection
100
+ */
101
+ export interface NtfyKeepaliveMessage extends NtfyBaseMessage {
102
+ event: 'keepalive';
103
+ }
104
+ /**
105
+ * Poll request message
106
+ */
107
+ export interface NtfyPollRequestMessage extends NtfyBaseMessage {
108
+ event: 'poll_request';
109
+ }
110
+ /**
111
+ * Union of all message types
112
+ */
113
+ export type NtfyMessage = NtfyNotificationMessage | NtfyOpenMessage | NtfyKeepaliveMessage | NtfyPollRequestMessage;
114
+ /**
115
+ * Options for subscribing to ntfy topics
116
+ */
117
+ export interface NtfySubscriptionOptions {
118
+ /** Whether to poll for messages instead of maintaining a connection */
119
+ poll?: boolean;
120
+ /** Return cached messages since timestamp, duration or message ID */
121
+ since?: string | number;
122
+ /** Include scheduled/delayed messages */
123
+ scheduled?: boolean;
124
+ /** Filter by message ID */
125
+ id?: string;
126
+ /** Filter by message content */
127
+ message?: string;
128
+ /** Filter by title */
129
+ title?: string;
130
+ /** Filter by priority (comma-separated list) */
131
+ priority?: string;
132
+ /** Filter by tags (comma-separated list) */
133
+ tags?: string;
134
+ /** Base URL for the ntfy server */
135
+ baseUrl?: string;
136
+ /** Authentication token or credentials */
137
+ auth?: string;
138
+ /** Basic auth username */
139
+ username?: string;
140
+ /** Basic auth password */
141
+ password?: string;
142
+ /** Additional headers to include in requests */
143
+ headers?: Record<string, string>;
144
+ }
145
+ /**
146
+ * Subscription event handlers
147
+ */
148
+ export interface NtfySubscriptionHandlers {
149
+ /** Called when a message is received */
150
+ onMessage?: (message: NtfyNotificationMessage) => void;
151
+ /** Called when the connection is opened */
152
+ onOpen?: (message: NtfyOpenMessage) => void;
153
+ /** Called when a keepalive message is received */
154
+ onKeepalive?: (message: NtfyKeepaliveMessage) => void;
155
+ /** Called when an error occurs */
156
+ onError?: (error: Error) => void;
157
+ /** Called when the connection is closed */
158
+ onClose?: () => void;
159
+ /** Called when any message is received (regardless of type) */
160
+ onAnyMessage?: (message: NtfyMessage) => void;
161
+ }
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Types for the ntfy service
3
+ */
4
+ export {};