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.
- package/LICENSE +201 -0
- package/README.md +423 -0
- package/dist/config/index.d.ts +23 -0
- package/dist/config/index.js +111 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +108 -0
- package/dist/mcp-server/resources/ntfyResource/getNtfyTopic.d.ts +2 -0
- package/dist/mcp-server/resources/ntfyResource/getNtfyTopic.js +111 -0
- package/dist/mcp-server/resources/ntfyResource/index.d.ts +12 -0
- package/dist/mcp-server/resources/ntfyResource/index.js +72 -0
- package/dist/mcp-server/resources/ntfyResource/types.d.ts +27 -0
- package/dist/mcp-server/resources/ntfyResource/types.js +8 -0
- package/dist/mcp-server/server.d.ts +40 -0
- package/dist/mcp-server/server.js +245 -0
- package/dist/mcp-server/tools/ntfyTool/index.d.ts +11 -0
- package/dist/mcp-server/tools/ntfyTool/index.js +110 -0
- package/dist/mcp-server/tools/ntfyTool/ntfyMessage.d.ts +9 -0
- package/dist/mcp-server/tools/ntfyTool/ntfyMessage.js +289 -0
- package/dist/mcp-server/tools/ntfyTool/types.d.ts +252 -0
- package/dist/mcp-server/tools/ntfyTool/types.js +144 -0
- package/dist/mcp-server/utils/registrationHelper.d.ts +48 -0
- package/dist/mcp-server/utils/registrationHelper.js +63 -0
- package/dist/services/ntfy/constants.d.ts +37 -0
- package/dist/services/ntfy/constants.js +37 -0
- package/dist/services/ntfy/errors.d.ts +79 -0
- package/dist/services/ntfy/errors.js +134 -0
- package/dist/services/ntfy/index.d.ts +33 -0
- package/dist/services/ntfy/index.js +56 -0
- package/dist/services/ntfy/publisher.d.ts +66 -0
- package/dist/services/ntfy/publisher.js +229 -0
- package/dist/services/ntfy/subscriber.d.ts +81 -0
- package/dist/services/ntfy/subscriber.js +502 -0
- package/dist/services/ntfy/types.d.ts +161 -0
- package/dist/services/ntfy/types.js +4 -0
- package/dist/services/ntfy/utils.d.ts +85 -0
- package/dist/services/ntfy/utils.js +410 -0
- package/dist/types-global/errors.d.ts +35 -0
- package/dist/types-global/errors.js +39 -0
- package/dist/types-global/mcp.d.ts +30 -0
- package/dist/types-global/mcp.js +25 -0
- package/dist/types-global/tool.d.ts +61 -0
- package/dist/types-global/tool.js +99 -0
- package/dist/utils/errorHandler.d.ts +98 -0
- package/dist/utils/errorHandler.js +271 -0
- package/dist/utils/idGenerator.d.ts +94 -0
- package/dist/utils/idGenerator.js +149 -0
- package/dist/utils/index.d.ts +13 -0
- package/dist/utils/index.js +16 -0
- package/dist/utils/logger.d.ts +36 -0
- package/dist/utils/logger.js +92 -0
- package/dist/utils/rateLimiter.d.ts +115 -0
- package/dist/utils/rateLimiter.js +180 -0
- package/dist/utils/requestContext.d.ts +68 -0
- package/dist/utils/requestContext.js +91 -0
- package/dist/utils/sanitization.d.ts +224 -0
- package/dist/utils/sanitization.js +367 -0
- package/dist/utils/security.d.ts +26 -0
- package/dist/utils/security.js +27 -0
- package/package.json +47 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom error classes for the ntfy service
|
|
3
|
+
*/
|
|
4
|
+
import { BaseErrorCode, McpError } from '../../types-global/errors.js';
|
|
5
|
+
import { ErrorHandler } from '../../utils/errorHandler.js';
|
|
6
|
+
/**
|
|
7
|
+
* Get message from an error object
|
|
8
|
+
* @param error The error to extract message from
|
|
9
|
+
* @returns Error message as string
|
|
10
|
+
*/
|
|
11
|
+
function getErrorMessage(error) {
|
|
12
|
+
if (error instanceof Error) {
|
|
13
|
+
return error.message;
|
|
14
|
+
}
|
|
15
|
+
if (error === null) {
|
|
16
|
+
return 'Null error occurred';
|
|
17
|
+
}
|
|
18
|
+
if (error === undefined) {
|
|
19
|
+
return 'Undefined error occurred';
|
|
20
|
+
}
|
|
21
|
+
return typeof error === 'string'
|
|
22
|
+
? error
|
|
23
|
+
: String(error);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Base error class for ntfy service errors
|
|
27
|
+
*/
|
|
28
|
+
export class NtfyError extends McpError {
|
|
29
|
+
constructor(message, details) {
|
|
30
|
+
const errorCode = details?.errorCode || BaseErrorCode.SERVICE_UNAVAILABLE;
|
|
31
|
+
super(errorCode, message, details);
|
|
32
|
+
this.name = 'NtfyError';
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Error thrown when connection to ntfy server fails
|
|
37
|
+
*/
|
|
38
|
+
export class NtfyConnectionError extends NtfyError {
|
|
39
|
+
constructor(message, url) {
|
|
40
|
+
super(message, { url });
|
|
41
|
+
this.url = url;
|
|
42
|
+
this.name = 'NtfyConnectionError';
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Error thrown when authentication fails
|
|
47
|
+
*/
|
|
48
|
+
export class NtfyAuthenticationError extends NtfyError {
|
|
49
|
+
constructor(message) {
|
|
50
|
+
super(message, { errorCode: BaseErrorCode.UNAUTHORIZED });
|
|
51
|
+
this.name = 'NtfyAuthenticationError';
|
|
52
|
+
this.code = BaseErrorCode.UNAUTHORIZED; // Ensure code is set correctly
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Error thrown when a message cannot be parsed
|
|
57
|
+
*/
|
|
58
|
+
export class NtfyParseError extends NtfyError {
|
|
59
|
+
constructor(message, rawData) {
|
|
60
|
+
super(message, {
|
|
61
|
+
rawData: rawData?.substring(0, 100), // Truncate large data for logging
|
|
62
|
+
errorCode: BaseErrorCode.VALIDATION_ERROR
|
|
63
|
+
});
|
|
64
|
+
this.rawData = rawData;
|
|
65
|
+
this.name = 'NtfyParseError';
|
|
66
|
+
this.code = BaseErrorCode.VALIDATION_ERROR; // Ensure code is set correctly
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Error thrown when a subscription is closed unexpectedly
|
|
71
|
+
*/
|
|
72
|
+
export class NtfySubscriptionClosedError extends NtfyError {
|
|
73
|
+
constructor(message, reason) {
|
|
74
|
+
super(message, { reason });
|
|
75
|
+
this.reason = reason;
|
|
76
|
+
this.name = 'NtfySubscriptionClosedError';
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Error thrown when an invalid topic name is provided
|
|
81
|
+
*/
|
|
82
|
+
export class NtfyInvalidTopicError extends NtfyError {
|
|
83
|
+
constructor(message, topic) {
|
|
84
|
+
super(message, { topic, errorCode: BaseErrorCode.VALIDATION_ERROR });
|
|
85
|
+
this.topic = topic;
|
|
86
|
+
this.name = 'NtfyInvalidTopicError';
|
|
87
|
+
this.code = BaseErrorCode.VALIDATION_ERROR; // Ensure code is set correctly
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Error thrown when a timeout occurs
|
|
92
|
+
*/
|
|
93
|
+
export class NtfyTimeoutError extends NtfyError {
|
|
94
|
+
constructor(message, timeoutMs) {
|
|
95
|
+
super(message, { timeoutMs, errorCode: BaseErrorCode.TIMEOUT });
|
|
96
|
+
this.timeoutMs = timeoutMs;
|
|
97
|
+
this.name = 'NtfyTimeoutError';
|
|
98
|
+
this.code = BaseErrorCode.TIMEOUT; // Ensure code is set correctly
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Error mapping for ntfy errors
|
|
103
|
+
*/
|
|
104
|
+
export const NTFY_ERROR_MAPPINGS = [
|
|
105
|
+
{
|
|
106
|
+
pattern: /authentication|unauthorized|auth.*failed/i,
|
|
107
|
+
errorCode: BaseErrorCode.UNAUTHORIZED,
|
|
108
|
+
factory: (error) => new NtfyAuthenticationError(getErrorMessage(error))
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
pattern: /parse|invalid.*json|invalid.*format/i,
|
|
112
|
+
errorCode: BaseErrorCode.VALIDATION_ERROR,
|
|
113
|
+
factory: (error, context) => new NtfyParseError(getErrorMessage(error), context?.rawData)
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
pattern: /invalid.*topic/i,
|
|
117
|
+
errorCode: BaseErrorCode.VALIDATION_ERROR,
|
|
118
|
+
factory: (error, context) => new NtfyInvalidTopicError(getErrorMessage(error), context?.topic)
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
pattern: /timed out|timeout|deadline exceeded/i,
|
|
122
|
+
errorCode: BaseErrorCode.TIMEOUT,
|
|
123
|
+
factory: (error, context) => new NtfyTimeoutError(getErrorMessage(error), context?.timeoutMs)
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
pattern: /connection|network|failed to connect|refused/i,
|
|
127
|
+
errorCode: BaseErrorCode.SERVICE_UNAVAILABLE,
|
|
128
|
+
factory: (error, context) => new NtfyConnectionError(getErrorMessage(error), context?.url)
|
|
129
|
+
}
|
|
130
|
+
];
|
|
131
|
+
/**
|
|
132
|
+
* Create an error mapper function for ntfy errors
|
|
133
|
+
*/
|
|
134
|
+
export const ntfyErrorMapper = ErrorHandler.createErrorMapper(NTFY_ERROR_MAPPINGS, BaseErrorCode.SERVICE_UNAVAILABLE);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ntfy service for subscribing to and publishing messages to ntfy topics
|
|
3
|
+
*
|
|
4
|
+
* This module provides functionality to:
|
|
5
|
+
* 1. Subscribe to ntfy topics to receive notifications
|
|
6
|
+
* 2. Publish messages to ntfy topics
|
|
7
|
+
* 3. Utility functions for working with ntfy
|
|
8
|
+
*/
|
|
9
|
+
import { NtfySubscriber } from './subscriber.js';
|
|
10
|
+
import { NtfySubscriptionHandlers, NtfySubscriptionOptions } from './types.js';
|
|
11
|
+
import { publish, NtfyPublishOptions, NtfyPublishResponse } from './publisher.js';
|
|
12
|
+
import { buildSubscriptionUrl, buildSubscriptionUrlSync, createBasicAuthHeader, createBasicAuthHeaderSync, isValidTopic, validateTopicSync } from './utils.js';
|
|
13
|
+
import { DEFAULT_NTFY_BASE_URL, DEFAULT_SUBSCRIPTION_OPTIONS, DEFAULT_REQUEST_TIMEOUT } from './constants.js';
|
|
14
|
+
export * from './types.js';
|
|
15
|
+
export * from './errors.js';
|
|
16
|
+
export { NtfySubscriber };
|
|
17
|
+
export { publish, NtfyPublishOptions, NtfyPublishResponse };
|
|
18
|
+
export { buildSubscriptionUrl, buildSubscriptionUrlSync, createBasicAuthHeader, createBasicAuthHeaderSync, isValidTopic, validateTopicSync };
|
|
19
|
+
export { DEFAULT_NTFY_BASE_URL, DEFAULT_SUBSCRIPTION_OPTIONS, DEFAULT_REQUEST_TIMEOUT };
|
|
20
|
+
/**
|
|
21
|
+
* Create a new ntfy subscriber with the given handlers
|
|
22
|
+
* @param handlers Event handlers for the subscription
|
|
23
|
+
* @returns A new NtfySubscriber instance
|
|
24
|
+
*/
|
|
25
|
+
export declare function createSubscriber(handlers?: NtfySubscriptionHandlers): NtfySubscriber;
|
|
26
|
+
/**
|
|
27
|
+
* Subscribe to a ntfy topic
|
|
28
|
+
* @param topic Topic to subscribe to
|
|
29
|
+
* @param handlers Event handlers for the subscription
|
|
30
|
+
* @param options Subscription options
|
|
31
|
+
* @returns A function to unsubscribe
|
|
32
|
+
*/
|
|
33
|
+
export declare function subscribe(topic: string, handlers: NtfySubscriptionHandlers, options?: NtfySubscriptionOptions): Promise<() => void>;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ntfy service for subscribing to and publishing messages to ntfy topics
|
|
3
|
+
*
|
|
4
|
+
* This module provides functionality to:
|
|
5
|
+
* 1. Subscribe to ntfy topics to receive notifications
|
|
6
|
+
* 2. Publish messages to ntfy topics
|
|
7
|
+
* 3. Utility functions for working with ntfy
|
|
8
|
+
*/
|
|
9
|
+
import { NtfySubscriber } from './subscriber.js';
|
|
10
|
+
import { publish } from './publisher.js';
|
|
11
|
+
import { buildSubscriptionUrl, buildSubscriptionUrlSync, createBasicAuthHeader, createBasicAuthHeaderSync, isValidTopic, validateTopicSync } from './utils.js';
|
|
12
|
+
import { createRequestContext } from '../../utils/requestContext.js';
|
|
13
|
+
import { idGenerator } from '../../utils/idGenerator.js';
|
|
14
|
+
import { DEFAULT_NTFY_BASE_URL, DEFAULT_SUBSCRIPTION_OPTIONS, DEFAULT_REQUEST_TIMEOUT } from './constants.js';
|
|
15
|
+
// Export types
|
|
16
|
+
export * from './types.js';
|
|
17
|
+
export * from './errors.js';
|
|
18
|
+
// Export main classes
|
|
19
|
+
export { NtfySubscriber };
|
|
20
|
+
// Export publisher functions
|
|
21
|
+
export { publish };
|
|
22
|
+
// Export utility functions
|
|
23
|
+
export {
|
|
24
|
+
// Export both sync and async versions of utilities
|
|
25
|
+
buildSubscriptionUrl, buildSubscriptionUrlSync, createBasicAuthHeader, createBasicAuthHeaderSync, isValidTopic, validateTopicSync };
|
|
26
|
+
// Export constants
|
|
27
|
+
export { DEFAULT_NTFY_BASE_URL, DEFAULT_SUBSCRIPTION_OPTIONS, DEFAULT_REQUEST_TIMEOUT };
|
|
28
|
+
/**
|
|
29
|
+
* Create a new ntfy subscriber with the given handlers
|
|
30
|
+
* @param handlers Event handlers for the subscription
|
|
31
|
+
* @returns A new NtfySubscriber instance
|
|
32
|
+
*/
|
|
33
|
+
export function createSubscriber(handlers = {}) {
|
|
34
|
+
const requestCtx = createRequestContext({
|
|
35
|
+
operation: 'createSubscriber',
|
|
36
|
+
subscriberId: idGenerator.generateRandomString(8)
|
|
37
|
+
});
|
|
38
|
+
return new NtfySubscriber(handlers);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Subscribe to a ntfy topic
|
|
42
|
+
* @param topic Topic to subscribe to
|
|
43
|
+
* @param handlers Event handlers for the subscription
|
|
44
|
+
* @param options Subscription options
|
|
45
|
+
* @returns A function to unsubscribe
|
|
46
|
+
*/
|
|
47
|
+
export async function subscribe(topic, handlers, options = {}) {
|
|
48
|
+
const requestCtx = createRequestContext({
|
|
49
|
+
operation: 'subscribe',
|
|
50
|
+
topic,
|
|
51
|
+
subscriberId: idGenerator.generateRandomString(8)
|
|
52
|
+
});
|
|
53
|
+
const subscriber = new NtfySubscriber(handlers);
|
|
54
|
+
await subscriber.subscribe(topic, options);
|
|
55
|
+
return () => subscriber.unsubscribe();
|
|
56
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { NtfyAction, NtfyAttachment, NtfyPriority } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Options for publishing to ntfy topics
|
|
4
|
+
*/
|
|
5
|
+
export interface NtfyPublishOptions {
|
|
6
|
+
/** Base URL for the ntfy server */
|
|
7
|
+
baseUrl?: string;
|
|
8
|
+
/** Authentication token */
|
|
9
|
+
auth?: string;
|
|
10
|
+
/** Basic auth username */
|
|
11
|
+
username?: string;
|
|
12
|
+
/** Basic auth password */
|
|
13
|
+
password?: string;
|
|
14
|
+
/** Additional headers to include in requests */
|
|
15
|
+
headers?: Record<string, string>;
|
|
16
|
+
/** Message title */
|
|
17
|
+
title?: string;
|
|
18
|
+
/** Message tags (emojis) */
|
|
19
|
+
tags?: string[];
|
|
20
|
+
/** Message priority (1-5) */
|
|
21
|
+
priority?: NtfyPriority;
|
|
22
|
+
/** URL to open when notification is clicked */
|
|
23
|
+
click?: string;
|
|
24
|
+
/** Message actions (buttons, etc.) */
|
|
25
|
+
actions?: NtfyAction[];
|
|
26
|
+
/** Message attachment */
|
|
27
|
+
attachment?: NtfyAttachment;
|
|
28
|
+
/** Email addresses to send the notification to */
|
|
29
|
+
email?: string;
|
|
30
|
+
/** Delay the message for a specific time (e.g., 30m, 1h, tomorrow) */
|
|
31
|
+
delay?: string;
|
|
32
|
+
/** Cache the message for a specific duration (e.g., 10m, 1h, 1d) */
|
|
33
|
+
cache?: string;
|
|
34
|
+
/** Firebase Cloud Messaging (FCM) topic to forward to */
|
|
35
|
+
firebase?: string;
|
|
36
|
+
/** Unique ID for the message */
|
|
37
|
+
id?: string;
|
|
38
|
+
/** Message expiration (e.g., 10m, 1h, 1d) */
|
|
39
|
+
expires?: string;
|
|
40
|
+
/** Whether the message should be X-Forwarded */
|
|
41
|
+
markdown?: boolean;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Response from publishing to ntfy
|
|
45
|
+
*/
|
|
46
|
+
export interface NtfyPublishResponse {
|
|
47
|
+
/** Server-assigned message ID */
|
|
48
|
+
id: string;
|
|
49
|
+
/** Time the message was received */
|
|
50
|
+
time: number;
|
|
51
|
+
/** Message expiration timestamp (if set) */
|
|
52
|
+
expires?: number;
|
|
53
|
+
/** Topic the message was published to */
|
|
54
|
+
topic: string;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Publish a message to a ntfy topic
|
|
58
|
+
*
|
|
59
|
+
* @param topic - Topic to publish to
|
|
60
|
+
* @param message - Message to publish
|
|
61
|
+
* @param options - Publishing options
|
|
62
|
+
* @returns Promise resolving to the publish response
|
|
63
|
+
* @throws NtfyInvalidTopicError if the topic name is invalid
|
|
64
|
+
* @throws NtfyConnectionError if the connection fails
|
|
65
|
+
*/
|
|
66
|
+
export declare function publish(topic: string, message: string, options?: NtfyPublishOptions): Promise<NtfyPublishResponse>;
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ntfy publisher implementation for sending notifications
|
|
3
|
+
*/
|
|
4
|
+
import { DEFAULT_NTFY_BASE_URL, DEFAULT_REQUEST_TIMEOUT, ERROR_MESSAGES } from './constants.js';
|
|
5
|
+
import { NtfyAuthenticationError, NtfyConnectionError, NtfyInvalidTopicError, ntfyErrorMapper } from './errors.js';
|
|
6
|
+
import { createTimeout, validateTopicSync, createRequestHeadersSync } from './utils.js';
|
|
7
|
+
import { BaseErrorCode, McpError } from '../../types-global/errors.js';
|
|
8
|
+
import { ErrorHandler } from '../../utils/errorHandler.js';
|
|
9
|
+
import { logger } from '../../utils/logger.js';
|
|
10
|
+
import { sanitizeInput, sanitizeInputForLogging } from '../../utils/sanitization.js';
|
|
11
|
+
import { createRequestContext } from '../../utils/requestContext.js';
|
|
12
|
+
import { idGenerator } from '../../utils/idGenerator.js';
|
|
13
|
+
// Create a module-specific logger
|
|
14
|
+
const publisherLogger = logger.createChildLogger({
|
|
15
|
+
module: 'NtfyPublisher',
|
|
16
|
+
serviceId: idGenerator.generateRandomString(8)
|
|
17
|
+
});
|
|
18
|
+
/**
|
|
19
|
+
* Publish a message to a ntfy topic
|
|
20
|
+
*
|
|
21
|
+
* @param topic - Topic to publish to
|
|
22
|
+
* @param message - Message to publish
|
|
23
|
+
* @param options - Publishing options
|
|
24
|
+
* @returns Promise resolving to the publish response
|
|
25
|
+
* @throws NtfyInvalidTopicError if the topic name is invalid
|
|
26
|
+
* @throws NtfyConnectionError if the connection fails
|
|
27
|
+
*/
|
|
28
|
+
export async function publish(topic, message, options = {}) {
|
|
29
|
+
return ErrorHandler.tryCatch(async () => {
|
|
30
|
+
// Create request context for tracking
|
|
31
|
+
const requestCtx = createRequestContext({
|
|
32
|
+
operation: 'publishNtfyMessage',
|
|
33
|
+
topic,
|
|
34
|
+
messageLength: message?.length,
|
|
35
|
+
hasTitle: !!options.title,
|
|
36
|
+
hasTags: Array.isArray(options.tags) && options.tags.length > 0,
|
|
37
|
+
priority: options.priority,
|
|
38
|
+
publishId: idGenerator.generateRandomString(8)
|
|
39
|
+
});
|
|
40
|
+
publisherLogger.info('Publishing message', {
|
|
41
|
+
topic,
|
|
42
|
+
messageLength: message?.length,
|
|
43
|
+
hasTitle: !!options.title,
|
|
44
|
+
hasTags: Array.isArray(options.tags) && options.tags.length > 0,
|
|
45
|
+
priority: options.priority,
|
|
46
|
+
requestId: requestCtx.requestId
|
|
47
|
+
});
|
|
48
|
+
// Validate topic synchronously for better performance
|
|
49
|
+
if (!validateTopicSync(topic)) {
|
|
50
|
+
publisherLogger.error('Invalid topic name', {
|
|
51
|
+
topic,
|
|
52
|
+
requestId: requestCtx.requestId
|
|
53
|
+
});
|
|
54
|
+
throw new NtfyInvalidTopicError(ERROR_MESSAGES.INVALID_TOPIC, topic);
|
|
55
|
+
}
|
|
56
|
+
// Build URL
|
|
57
|
+
const baseUrl = sanitizeInput.url(options.baseUrl || DEFAULT_NTFY_BASE_URL);
|
|
58
|
+
const url = `${baseUrl}/${sanitizeInput.string(topic)}`;
|
|
59
|
+
publisherLogger.debug('Publishing to URL', {
|
|
60
|
+
url,
|
|
61
|
+
requestId: requestCtx.requestId
|
|
62
|
+
});
|
|
63
|
+
// Prepare headers - using sync version for performance
|
|
64
|
+
const initialHeaders = createRequestHeadersSync({
|
|
65
|
+
auth: options.auth,
|
|
66
|
+
username: options.username,
|
|
67
|
+
password: options.password,
|
|
68
|
+
headers: options.headers,
|
|
69
|
+
});
|
|
70
|
+
// Convert HeadersInit to a Record for easier manipulation
|
|
71
|
+
const headers = {};
|
|
72
|
+
// Copy initial headers to our record object
|
|
73
|
+
if (initialHeaders instanceof Headers) {
|
|
74
|
+
initialHeaders.forEach((value, key) => {
|
|
75
|
+
headers[key] = value;
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
else if (Array.isArray(initialHeaders)) {
|
|
79
|
+
for (const [key, value] of initialHeaders) {
|
|
80
|
+
headers[key] = value;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
else if (initialHeaders) {
|
|
84
|
+
Object.assign(headers, initialHeaders);
|
|
85
|
+
}
|
|
86
|
+
// Set content type
|
|
87
|
+
headers['Content-Type'] = 'text/plain';
|
|
88
|
+
// Add special headers for ntfy features
|
|
89
|
+
if (options.title) {
|
|
90
|
+
headers['X-Title'] = sanitizeInput.string(options.title);
|
|
91
|
+
}
|
|
92
|
+
if (options.tags && options.tags.length > 0) {
|
|
93
|
+
// Sanitize each tag
|
|
94
|
+
const sanitizedTags = options.tags.map(tag => sanitizeInput.string(tag));
|
|
95
|
+
headers['X-Tags'] = sanitizedTags.join(',');
|
|
96
|
+
}
|
|
97
|
+
if (options.priority) {
|
|
98
|
+
headers['X-Priority'] = options.priority.toString();
|
|
99
|
+
}
|
|
100
|
+
if (options.click) {
|
|
101
|
+
headers['X-Click'] = sanitizeInput.url(options.click);
|
|
102
|
+
}
|
|
103
|
+
if (options.actions && options.actions.length > 0) {
|
|
104
|
+
// We need to sanitize the actions
|
|
105
|
+
const sanitizedActions = options.actions.map(action => ({
|
|
106
|
+
id: sanitizeInput.string(action.id),
|
|
107
|
+
label: sanitizeInput.string(action.label),
|
|
108
|
+
action: sanitizeInput.string(action.action),
|
|
109
|
+
url: action.url ? sanitizeInput.url(action.url) : undefined,
|
|
110
|
+
method: action.method ? sanitizeInput.string(action.method) : undefined,
|
|
111
|
+
headers: action.headers,
|
|
112
|
+
body: action.body ? sanitizeInput.string(action.body) : undefined,
|
|
113
|
+
clear: action.clear
|
|
114
|
+
}));
|
|
115
|
+
headers['X-Actions'] = JSON.stringify(sanitizedActions);
|
|
116
|
+
}
|
|
117
|
+
if (options.attachment) {
|
|
118
|
+
headers['X-Attach'] = sanitizeInput.url(options.attachment.url);
|
|
119
|
+
if (options.attachment.name) {
|
|
120
|
+
headers['X-Filename'] = sanitizeInput.string(options.attachment.name);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (options.email) {
|
|
124
|
+
headers['X-Email'] = sanitizeInput.string(options.email);
|
|
125
|
+
}
|
|
126
|
+
if (options.delay) {
|
|
127
|
+
headers['X-Delay'] = sanitizeInput.string(options.delay);
|
|
128
|
+
}
|
|
129
|
+
if (options.cache) {
|
|
130
|
+
headers['X-Cache'] = sanitizeInput.string(options.cache);
|
|
131
|
+
}
|
|
132
|
+
if (options.firebase) {
|
|
133
|
+
headers['X-Firebase'] = sanitizeInput.string(options.firebase);
|
|
134
|
+
}
|
|
135
|
+
if (options.id) {
|
|
136
|
+
headers['X-ID'] = sanitizeInput.string(options.id);
|
|
137
|
+
}
|
|
138
|
+
if (options.expires) {
|
|
139
|
+
headers['X-Expires'] = sanitizeInput.string(options.expires);
|
|
140
|
+
}
|
|
141
|
+
if (options.markdown) {
|
|
142
|
+
headers['X-Markdown'] = 'true';
|
|
143
|
+
}
|
|
144
|
+
// Send request with timeout
|
|
145
|
+
const controller = new AbortController();
|
|
146
|
+
const timeoutId = setTimeout(() => controller.abort(), DEFAULT_REQUEST_TIMEOUT);
|
|
147
|
+
try {
|
|
148
|
+
publisherLogger.debug('Sending HTTP request', {
|
|
149
|
+
url,
|
|
150
|
+
method: 'POST',
|
|
151
|
+
requestId: requestCtx.requestId
|
|
152
|
+
});
|
|
153
|
+
const response = await Promise.race([
|
|
154
|
+
fetch(url, {
|
|
155
|
+
method: 'POST',
|
|
156
|
+
headers,
|
|
157
|
+
body: message,
|
|
158
|
+
signal: controller.signal,
|
|
159
|
+
}),
|
|
160
|
+
createTimeout(DEFAULT_REQUEST_TIMEOUT),
|
|
161
|
+
]);
|
|
162
|
+
clearTimeout(timeoutId);
|
|
163
|
+
// Check response status
|
|
164
|
+
if (!response.ok) {
|
|
165
|
+
publisherLogger.error('HTTP error from ntfy server', {
|
|
166
|
+
status: response.status,
|
|
167
|
+
statusText: response.statusText,
|
|
168
|
+
url,
|
|
169
|
+
requestId: requestCtx.requestId
|
|
170
|
+
});
|
|
171
|
+
// Provide more specific error messages based on status code
|
|
172
|
+
let errorMessage = `HTTP Error: ${response.status} ${response.statusText}`;
|
|
173
|
+
switch (response.status) {
|
|
174
|
+
case 401:
|
|
175
|
+
errorMessage = 'Authentication failed: invalid credentials';
|
|
176
|
+
throw new NtfyAuthenticationError(errorMessage);
|
|
177
|
+
case 403:
|
|
178
|
+
errorMessage = 'Access forbidden: insufficient permissions';
|
|
179
|
+
throw new McpError(BaseErrorCode.FORBIDDEN, errorMessage, { url, statusCode: response.status });
|
|
180
|
+
case 404:
|
|
181
|
+
errorMessage = 'Topic or resource not found';
|
|
182
|
+
throw new McpError(BaseErrorCode.NOT_FOUND, errorMessage, { url, statusCode: response.status, topic });
|
|
183
|
+
case 429:
|
|
184
|
+
errorMessage = 'Too many requests: rate limit exceeded';
|
|
185
|
+
throw new McpError(BaseErrorCode.RATE_LIMITED, errorMessage, { url, statusCode: response.status });
|
|
186
|
+
case 500:
|
|
187
|
+
case 502:
|
|
188
|
+
case 503:
|
|
189
|
+
case 504:
|
|
190
|
+
errorMessage = `Server error: ${response.statusText}`;
|
|
191
|
+
// Fall through to default error handling
|
|
192
|
+
default:
|
|
193
|
+
throw new NtfyConnectionError(errorMessage, url);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
// Parse response
|
|
197
|
+
const result = await response.json();
|
|
198
|
+
publisherLogger.info('Message published successfully', {
|
|
199
|
+
messageId: result.id,
|
|
200
|
+
topic: result.topic,
|
|
201
|
+
requestId: requestCtx.requestId
|
|
202
|
+
});
|
|
203
|
+
return result;
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
clearTimeout(timeoutId);
|
|
207
|
+
if (error instanceof NtfyInvalidTopicError) {
|
|
208
|
+
throw error;
|
|
209
|
+
}
|
|
210
|
+
publisherLogger.error('Failed to publish message', {
|
|
211
|
+
error: error instanceof Error ? error.message : String(error),
|
|
212
|
+
topic,
|
|
213
|
+
url,
|
|
214
|
+
requestId: requestCtx.requestId
|
|
215
|
+
});
|
|
216
|
+
throw new NtfyConnectionError(`Error publishing to topic: ${error instanceof Error ? error.message : String(error)}`, url);
|
|
217
|
+
}
|
|
218
|
+
}, {
|
|
219
|
+
operation: 'publishNtfyMessage',
|
|
220
|
+
context: { topic },
|
|
221
|
+
input: {
|
|
222
|
+
message: message?.length > 100 ? `${message.substring(0, 100)}...` : message,
|
|
223
|
+
options: sanitizeInputForLogging(options)
|
|
224
|
+
},
|
|
225
|
+
errorCode: BaseErrorCode.SERVICE_UNAVAILABLE,
|
|
226
|
+
errorMapper: ntfyErrorMapper,
|
|
227
|
+
rethrow: true
|
|
228
|
+
});
|
|
229
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { NtfySubscriptionHandlers, NtfySubscriptionOptions } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* NtfySubscriber class for subscribing to ntfy topics
|
|
4
|
+
*/
|
|
5
|
+
export declare class NtfySubscriber {
|
|
6
|
+
private handlers;
|
|
7
|
+
private abortController?;
|
|
8
|
+
private cleanupFn?;
|
|
9
|
+
private connectionActive;
|
|
10
|
+
private lastKeepaliveTime;
|
|
11
|
+
private reconnectAttempts;
|
|
12
|
+
private keepaliveCheckInterval?;
|
|
13
|
+
private logger;
|
|
14
|
+
private subscriberId;
|
|
15
|
+
private currentTopic?;
|
|
16
|
+
/**
|
|
17
|
+
* Creates a new NtfySubscriber instance
|
|
18
|
+
* @param handlers Event handlers for the subscription
|
|
19
|
+
*/
|
|
20
|
+
constructor(handlers?: NtfySubscriptionHandlers);
|
|
21
|
+
/**
|
|
22
|
+
* Subscribe to a ntfy topic
|
|
23
|
+
* @param topic Topic to subscribe to (can be comma-separated for multiple topics)
|
|
24
|
+
* @param options Subscription options
|
|
25
|
+
* @returns Promise that resolves when the subscription is established
|
|
26
|
+
* @throws NtfyInvalidTopicError if the topic name is invalid
|
|
27
|
+
* @throws NtfyConnectionError if the connection fails
|
|
28
|
+
*/
|
|
29
|
+
subscribe(topic: string, options?: NtfySubscriptionOptions): Promise<void>;
|
|
30
|
+
/**
|
|
31
|
+
* Unsubscribe from the current topic
|
|
32
|
+
*/
|
|
33
|
+
unsubscribe(): void;
|
|
34
|
+
/**
|
|
35
|
+
* Start a subscription to a topic
|
|
36
|
+
* @param topic Topic to subscribe to
|
|
37
|
+
* @param format Format to subscribe in (json, sse, raw, ws)
|
|
38
|
+
* @param options Subscription options
|
|
39
|
+
*/
|
|
40
|
+
private startSubscription;
|
|
41
|
+
/**
|
|
42
|
+
* Process a JSON stream from ntfy
|
|
43
|
+
* @param reader ReadableStreamDefaultReader to read from
|
|
44
|
+
* @param requestId Request ID for logging
|
|
45
|
+
*/
|
|
46
|
+
private processJsonStream;
|
|
47
|
+
/**
|
|
48
|
+
* Handle a message from ntfy
|
|
49
|
+
* @param message Message from ntfy
|
|
50
|
+
* @param requestId Request ID for logging
|
|
51
|
+
*/
|
|
52
|
+
private handleMessage;
|
|
53
|
+
/**
|
|
54
|
+
* Handle a parse error
|
|
55
|
+
* @param error Error that occurred
|
|
56
|
+
* @param rawData Raw data that caused the error
|
|
57
|
+
* @param requestId Request ID for logging
|
|
58
|
+
*/
|
|
59
|
+
private handleParseError;
|
|
60
|
+
/**
|
|
61
|
+
* Handle a subscription error
|
|
62
|
+
* @param error Error that occurred
|
|
63
|
+
* @param requestId Request ID for logging
|
|
64
|
+
*/
|
|
65
|
+
private handleSubscriptionError;
|
|
66
|
+
/**
|
|
67
|
+
* Start the keepalive check interval
|
|
68
|
+
*/
|
|
69
|
+
private startKeepaliveCheck;
|
|
70
|
+
/**
|
|
71
|
+
* Stop the keepalive check interval
|
|
72
|
+
*/
|
|
73
|
+
private stopKeepaliveCheck;
|
|
74
|
+
/**
|
|
75
|
+
* Schedule a reconnection attempt
|
|
76
|
+
* @param topic Topic to reconnect to
|
|
77
|
+
* @param format Format to reconnect with
|
|
78
|
+
* @param options Subscription options
|
|
79
|
+
*/
|
|
80
|
+
private scheduleReconnect;
|
|
81
|
+
}
|