ntfy-bridge 0.1.0 → 0.1.1
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/dist/index.js +42 -14
- package/dist/lib.d.ts +5 -2
- package/dist/lib.js +15 -1
- package/dist/lib.test.js +36 -0
- package/package.json +1 -1
- package/src/index.ts +43 -16
- package/src/lib.test.ts +38 -0
- package/src/lib.ts +21 -2
package/dist/index.js
CHANGED
|
@@ -5,6 +5,8 @@ function parseArgs() {
|
|
|
5
5
|
const args = process.argv.slice(2);
|
|
6
6
|
const topics = [];
|
|
7
7
|
let forward = "";
|
|
8
|
+
const headers = {};
|
|
9
|
+
let openclawMode = false;
|
|
8
10
|
for (let i = 0; i < args.length; i++) {
|
|
9
11
|
const arg = args[i];
|
|
10
12
|
if (arg === "--topic" || arg === "-t") {
|
|
@@ -13,21 +15,35 @@ function parseArgs() {
|
|
|
13
15
|
else if (arg === "--forward" || arg === "-f") {
|
|
14
16
|
forward = args[++i];
|
|
15
17
|
}
|
|
18
|
+
else if (arg === "--header" || arg === "-H") {
|
|
19
|
+
const header = args[++i];
|
|
20
|
+
const colonIdx = header.indexOf(":");
|
|
21
|
+
if (colonIdx > 0) {
|
|
22
|
+
const key = header.slice(0, colonIdx).trim();
|
|
23
|
+
const value = header.slice(colonIdx + 1).trim();
|
|
24
|
+
headers[key] = value;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
else if (arg === "--openclaw") {
|
|
28
|
+
openclawMode = true;
|
|
29
|
+
}
|
|
16
30
|
else if (arg === "--help" || arg === "-h") {
|
|
17
31
|
console.log(`
|
|
18
32
|
ntfy-bridge - Local bridge from ntfy.sh to localhost
|
|
19
33
|
|
|
20
34
|
Usage:
|
|
21
|
-
ntfy-bridge --topic <topic> --forward <url>
|
|
35
|
+
ntfy-bridge --topic <topic> --forward <url> [options]
|
|
22
36
|
|
|
23
37
|
Options:
|
|
24
|
-
-t, --topic <topic>
|
|
25
|
-
-f, --forward <url>
|
|
26
|
-
-
|
|
38
|
+
-t, --topic <topic> ntfy.sh topic to subscribe to (can be repeated)
|
|
39
|
+
-f, --forward <url> Local endpoint to forward messages to
|
|
40
|
+
-H, --header <k:v> Custom header to include in forward requests (can be repeated)
|
|
41
|
+
--openclaw Format payload for OpenClaw hooks (sends {text: "..."})
|
|
42
|
+
-h, --help Show this help
|
|
27
43
|
|
|
28
44
|
Examples:
|
|
29
45
|
ntfy-bridge -t alerts -f http://localhost:8080/hooks
|
|
30
|
-
ntfy-bridge -t
|
|
46
|
+
ntfy-bridge -t alerts -f http://localhost:18789/hooks/wake --openclaw -H "Authorization: Bearer token"
|
|
31
47
|
`);
|
|
32
48
|
process.exit(0);
|
|
33
49
|
}
|
|
@@ -40,15 +56,25 @@ Examples:
|
|
|
40
56
|
console.error("Error: No forward URL specified. Use --forward <url>");
|
|
41
57
|
process.exit(1);
|
|
42
58
|
}
|
|
43
|
-
return { topics, forward };
|
|
59
|
+
return { topics, forward, headers, openclawMode };
|
|
44
60
|
}
|
|
45
|
-
async function forwardMessage(msg, forwardUrl) {
|
|
46
|
-
|
|
61
|
+
async function forwardMessage(msg, forwardUrl, customHeaders = {}, openclawMode = false) {
|
|
62
|
+
let body;
|
|
63
|
+
if (openclawMode) {
|
|
64
|
+
// OpenClaw expects {text: "message"} format
|
|
65
|
+
const text = msg.title
|
|
66
|
+
? `[${msg.topic}] ${msg.title}: ${msg.message || ""}`
|
|
67
|
+
: `[${msg.topic}] ${msg.message || msg.id}`;
|
|
68
|
+
body = JSON.stringify({ text });
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
body = JSON.stringify(createPayload(msg));
|
|
72
|
+
}
|
|
47
73
|
try {
|
|
48
74
|
const response = await fetch(forwardUrl, {
|
|
49
75
|
method: "POST",
|
|
50
|
-
headers: { "Content-Type": "application/json" },
|
|
51
|
-
body
|
|
76
|
+
headers: { "Content-Type": "application/json", ...customHeaders },
|
|
77
|
+
body,
|
|
52
78
|
});
|
|
53
79
|
if (response.ok) {
|
|
54
80
|
console.log(`[${msg.topic}] Forwarded: ${msg.title || msg.message || msg.id}`);
|
|
@@ -61,7 +87,7 @@ async function forwardMessage(msg, forwardUrl) {
|
|
|
61
87
|
console.error(`[${msg.topic}] Forward failed:`, error);
|
|
62
88
|
}
|
|
63
89
|
}
|
|
64
|
-
function subscribeToTopic(topic, forwardUrl) {
|
|
90
|
+
function subscribeToTopic(topic, forwardUrl, headers, openclawMode) {
|
|
65
91
|
const url = normalizeTopicUrl(topic);
|
|
66
92
|
console.log(`Subscribing to: ${url}`);
|
|
67
93
|
const es = new EventSource(url);
|
|
@@ -72,7 +98,7 @@ function subscribeToTopic(topic, forwardUrl) {
|
|
|
72
98
|
try {
|
|
73
99
|
const msg = JSON.parse(event.data);
|
|
74
100
|
if (msg.event === "message" || !msg.event) {
|
|
75
|
-
await forwardMessage(msg, forwardUrl);
|
|
101
|
+
await forwardMessage(msg, forwardUrl, headers, openclawMode);
|
|
76
102
|
}
|
|
77
103
|
}
|
|
78
104
|
catch (error) {
|
|
@@ -85,12 +111,14 @@ function subscribeToTopic(topic, forwardUrl) {
|
|
|
85
111
|
};
|
|
86
112
|
}
|
|
87
113
|
function main() {
|
|
88
|
-
const { topics, forward } = parseArgs();
|
|
114
|
+
const { topics, forward, headers, openclawMode } = parseArgs();
|
|
89
115
|
console.log("ntfy-bridge starting");
|
|
90
116
|
console.log(`Forwarding to: ${forward}`);
|
|
117
|
+
if (openclawMode)
|
|
118
|
+
console.log("OpenClaw mode enabled");
|
|
91
119
|
console.log(`Subscribing to ${topics.length} topic(s)`);
|
|
92
120
|
for (const topic of topics) {
|
|
93
|
-
subscribeToTopic(topic, forward);
|
|
121
|
+
subscribeToTopic(topic, forward, headers, openclawMode);
|
|
94
122
|
}
|
|
95
123
|
// Keep alive
|
|
96
124
|
process.on("SIGINT", () => {
|
package/dist/lib.d.ts
CHANGED
|
@@ -21,7 +21,10 @@ export interface BridgePayload {
|
|
|
21
21
|
}
|
|
22
22
|
export declare function normalizeTopicUrl(topic: string): string;
|
|
23
23
|
export declare function createPayload(msg: NtfyMessage): BridgePayload;
|
|
24
|
-
export
|
|
24
|
+
export interface ParsedArgs {
|
|
25
25
|
topics: string[];
|
|
26
26
|
forward: string;
|
|
27
|
-
|
|
27
|
+
headers: Record<string, string>;
|
|
28
|
+
openclawMode: boolean;
|
|
29
|
+
}
|
|
30
|
+
export declare function parseArgs(argv: string[]): ParsedArgs | null;
|
package/dist/lib.js
CHANGED
|
@@ -26,6 +26,8 @@ export function parseArgs(argv) {
|
|
|
26
26
|
const args = argv.slice(2);
|
|
27
27
|
const topics = [];
|
|
28
28
|
let forward = "";
|
|
29
|
+
const headers = {};
|
|
30
|
+
let openclawMode = false;
|
|
29
31
|
for (let i = 0; i < args.length; i++) {
|
|
30
32
|
const arg = args[i];
|
|
31
33
|
if (arg === "--topic" || arg === "-t") {
|
|
@@ -34,6 +36,18 @@ export function parseArgs(argv) {
|
|
|
34
36
|
else if (arg === "--forward" || arg === "-f") {
|
|
35
37
|
forward = args[++i];
|
|
36
38
|
}
|
|
39
|
+
else if (arg === "--header" || arg === "-H") {
|
|
40
|
+
const header = args[++i];
|
|
41
|
+
const colonIdx = header.indexOf(":");
|
|
42
|
+
if (colonIdx > 0) {
|
|
43
|
+
const key = header.slice(0, colonIdx).trim();
|
|
44
|
+
const value = header.slice(colonIdx + 1).trim();
|
|
45
|
+
headers[key] = value;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
else if (arg === "--openclaw") {
|
|
49
|
+
openclawMode = true;
|
|
50
|
+
}
|
|
37
51
|
else if (arg === "--help" || arg === "-h") {
|
|
38
52
|
return null; // Signal help requested
|
|
39
53
|
}
|
|
@@ -41,5 +55,5 @@ export function parseArgs(argv) {
|
|
|
41
55
|
if (topics.length === 0 || !forward) {
|
|
42
56
|
return null;
|
|
43
57
|
}
|
|
44
|
-
return { topics, forward };
|
|
58
|
+
return { topics, forward, headers, openclawMode };
|
|
45
59
|
}
|
package/dist/lib.test.js
CHANGED
|
@@ -59,6 +59,8 @@ describe("parseArgs", () => {
|
|
|
59
59
|
expect(result).toEqual({
|
|
60
60
|
topics: ["alerts"],
|
|
61
61
|
forward: "http://localhost:8080",
|
|
62
|
+
headers: {},
|
|
63
|
+
openclawMode: false,
|
|
62
64
|
});
|
|
63
65
|
});
|
|
64
66
|
it("parses multiple topics", () => {
|
|
@@ -71,6 +73,40 @@ describe("parseArgs", () => {
|
|
|
71
73
|
expect(result).toEqual({
|
|
72
74
|
topics: ["alerts", "news"],
|
|
73
75
|
forward: "http://localhost:8080",
|
|
76
|
+
headers: {},
|
|
77
|
+
openclawMode: false,
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
it("parses custom headers", () => {
|
|
81
|
+
const result = parseArgs([
|
|
82
|
+
"node", "script",
|
|
83
|
+
"-t", "alerts",
|
|
84
|
+
"-f", "http://localhost:8080",
|
|
85
|
+
"-H", "Authorization: Bearer token123",
|
|
86
|
+
"-H", "X-Custom: value"
|
|
87
|
+
]);
|
|
88
|
+
expect(result).toEqual({
|
|
89
|
+
topics: ["alerts"],
|
|
90
|
+
forward: "http://localhost:8080",
|
|
91
|
+
headers: {
|
|
92
|
+
"Authorization": "Bearer token123",
|
|
93
|
+
"X-Custom": "value",
|
|
94
|
+
},
|
|
95
|
+
openclawMode: false,
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
it("parses openclaw mode", () => {
|
|
99
|
+
const result = parseArgs([
|
|
100
|
+
"node", "script",
|
|
101
|
+
"-t", "alerts",
|
|
102
|
+
"-f", "http://localhost:8080",
|
|
103
|
+
"--openclaw"
|
|
104
|
+
]);
|
|
105
|
+
expect(result).toEqual({
|
|
106
|
+
topics: ["alerts"],
|
|
107
|
+
forward: "http://localhost:8080",
|
|
108
|
+
headers: {},
|
|
109
|
+
openclawMode: true,
|
|
74
110
|
});
|
|
75
111
|
});
|
|
76
112
|
it("returns null for help flag", () => {
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import EventSource from "eventsource";
|
|
4
|
-
import { normalizeTopicUrl, createPayload, NtfyMessage } from "./lib.js";
|
|
4
|
+
import { normalizeTopicUrl, createPayload, NtfyMessage, ParsedArgs } from "./lib.js";
|
|
5
5
|
|
|
6
|
-
function parseArgs():
|
|
6
|
+
function parseArgs(): ParsedArgs {
|
|
7
7
|
const args = process.argv.slice(2);
|
|
8
8
|
const topics: string[] = [];
|
|
9
9
|
let forward = "";
|
|
10
|
+
const headers: Record<string, string> = {};
|
|
11
|
+
let openclawMode = false;
|
|
10
12
|
|
|
11
13
|
for (let i = 0; i < args.length; i++) {
|
|
12
14
|
const arg = args[i];
|
|
@@ -14,21 +16,33 @@ function parseArgs(): { topics: string[]; forward: string } {
|
|
|
14
16
|
topics.push(args[++i]);
|
|
15
17
|
} else if (arg === "--forward" || arg === "-f") {
|
|
16
18
|
forward = args[++i];
|
|
19
|
+
} else if (arg === "--header" || arg === "-H") {
|
|
20
|
+
const header = args[++i];
|
|
21
|
+
const colonIdx = header.indexOf(":");
|
|
22
|
+
if (colonIdx > 0) {
|
|
23
|
+
const key = header.slice(0, colonIdx).trim();
|
|
24
|
+
const value = header.slice(colonIdx + 1).trim();
|
|
25
|
+
headers[key] = value;
|
|
26
|
+
}
|
|
27
|
+
} else if (arg === "--openclaw") {
|
|
28
|
+
openclawMode = true;
|
|
17
29
|
} else if (arg === "--help" || arg === "-h") {
|
|
18
30
|
console.log(`
|
|
19
31
|
ntfy-bridge - Local bridge from ntfy.sh to localhost
|
|
20
32
|
|
|
21
33
|
Usage:
|
|
22
|
-
ntfy-bridge --topic <topic> --forward <url>
|
|
34
|
+
ntfy-bridge --topic <topic> --forward <url> [options]
|
|
23
35
|
|
|
24
36
|
Options:
|
|
25
|
-
-t, --topic <topic>
|
|
26
|
-
-f, --forward <url>
|
|
27
|
-
-
|
|
37
|
+
-t, --topic <topic> ntfy.sh topic to subscribe to (can be repeated)
|
|
38
|
+
-f, --forward <url> Local endpoint to forward messages to
|
|
39
|
+
-H, --header <k:v> Custom header to include in forward requests (can be repeated)
|
|
40
|
+
--openclaw Format payload for OpenClaw hooks (sends {text: "..."})
|
|
41
|
+
-h, --help Show this help
|
|
28
42
|
|
|
29
43
|
Examples:
|
|
30
44
|
ntfy-bridge -t alerts -f http://localhost:8080/hooks
|
|
31
|
-
ntfy-bridge -t
|
|
45
|
+
ntfy-bridge -t alerts -f http://localhost:18789/hooks/wake --openclaw -H "Authorization: Bearer token"
|
|
32
46
|
`);
|
|
33
47
|
process.exit(0);
|
|
34
48
|
}
|
|
@@ -43,20 +57,32 @@ Examples:
|
|
|
43
57
|
process.exit(1);
|
|
44
58
|
}
|
|
45
59
|
|
|
46
|
-
return { topics, forward };
|
|
60
|
+
return { topics, forward, headers, openclawMode };
|
|
47
61
|
}
|
|
48
62
|
|
|
49
63
|
async function forwardMessage(
|
|
50
64
|
msg: NtfyMessage,
|
|
51
|
-
forwardUrl: string
|
|
65
|
+
forwardUrl: string,
|
|
66
|
+
customHeaders: Record<string, string> = {},
|
|
67
|
+
openclawMode: boolean = false
|
|
52
68
|
): Promise<void> {
|
|
53
|
-
|
|
69
|
+
let body: string;
|
|
70
|
+
|
|
71
|
+
if (openclawMode) {
|
|
72
|
+
// OpenClaw expects {text: "message"} format
|
|
73
|
+
const text = msg.title
|
|
74
|
+
? `[${msg.topic}] ${msg.title}: ${msg.message || ""}`
|
|
75
|
+
: `[${msg.topic}] ${msg.message || msg.id}`;
|
|
76
|
+
body = JSON.stringify({ text });
|
|
77
|
+
} else {
|
|
78
|
+
body = JSON.stringify(createPayload(msg));
|
|
79
|
+
}
|
|
54
80
|
|
|
55
81
|
try {
|
|
56
82
|
const response = await fetch(forwardUrl, {
|
|
57
83
|
method: "POST",
|
|
58
|
-
headers: { "Content-Type": "application/json" },
|
|
59
|
-
body
|
|
84
|
+
headers: { "Content-Type": "application/json", ...customHeaders },
|
|
85
|
+
body,
|
|
60
86
|
});
|
|
61
87
|
|
|
62
88
|
if (response.ok) {
|
|
@@ -69,7 +95,7 @@ async function forwardMessage(
|
|
|
69
95
|
}
|
|
70
96
|
}
|
|
71
97
|
|
|
72
|
-
function subscribeToTopic(topic: string, forwardUrl: string): void {
|
|
98
|
+
function subscribeToTopic(topic: string, forwardUrl: string, headers: Record<string, string>, openclawMode: boolean): void {
|
|
73
99
|
const url = normalizeTopicUrl(topic);
|
|
74
100
|
console.log(`Subscribing to: ${url}`);
|
|
75
101
|
|
|
@@ -83,7 +109,7 @@ function subscribeToTopic(topic: string, forwardUrl: string): void {
|
|
|
83
109
|
try {
|
|
84
110
|
const msg: NtfyMessage = JSON.parse(event.data);
|
|
85
111
|
if (msg.event === "message" || !msg.event) {
|
|
86
|
-
await forwardMessage(msg, forwardUrl);
|
|
112
|
+
await forwardMessage(msg, forwardUrl, headers, openclawMode);
|
|
87
113
|
}
|
|
88
114
|
} catch (error) {
|
|
89
115
|
console.warn(`Failed to parse message:`, error);
|
|
@@ -97,14 +123,15 @@ function subscribeToTopic(topic: string, forwardUrl: string): void {
|
|
|
97
123
|
}
|
|
98
124
|
|
|
99
125
|
function main(): void {
|
|
100
|
-
const { topics, forward } = parseArgs();
|
|
126
|
+
const { topics, forward, headers, openclawMode } = parseArgs();
|
|
101
127
|
|
|
102
128
|
console.log("ntfy-bridge starting");
|
|
103
129
|
console.log(`Forwarding to: ${forward}`);
|
|
130
|
+
if (openclawMode) console.log("OpenClaw mode enabled");
|
|
104
131
|
console.log(`Subscribing to ${topics.length} topic(s)`);
|
|
105
132
|
|
|
106
133
|
for (const topic of topics) {
|
|
107
|
-
subscribeToTopic(topic, forward);
|
|
134
|
+
subscribeToTopic(topic, forward, headers, openclawMode);
|
|
108
135
|
}
|
|
109
136
|
|
|
110
137
|
// Keep alive
|
package/src/lib.test.ts
CHANGED
|
@@ -79,6 +79,8 @@ describe("parseArgs", () => {
|
|
|
79
79
|
expect(result).toEqual({
|
|
80
80
|
topics: ["alerts"],
|
|
81
81
|
forward: "http://localhost:8080",
|
|
82
|
+
headers: {},
|
|
83
|
+
openclawMode: false,
|
|
82
84
|
});
|
|
83
85
|
});
|
|
84
86
|
|
|
@@ -92,6 +94,42 @@ describe("parseArgs", () => {
|
|
|
92
94
|
expect(result).toEqual({
|
|
93
95
|
topics: ["alerts", "news"],
|
|
94
96
|
forward: "http://localhost:8080",
|
|
97
|
+
headers: {},
|
|
98
|
+
openclawMode: false,
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("parses custom headers", () => {
|
|
103
|
+
const result = parseArgs([
|
|
104
|
+
"node", "script",
|
|
105
|
+
"-t", "alerts",
|
|
106
|
+
"-f", "http://localhost:8080",
|
|
107
|
+
"-H", "Authorization: Bearer token123",
|
|
108
|
+
"-H", "X-Custom: value"
|
|
109
|
+
]);
|
|
110
|
+
expect(result).toEqual({
|
|
111
|
+
topics: ["alerts"],
|
|
112
|
+
forward: "http://localhost:8080",
|
|
113
|
+
headers: {
|
|
114
|
+
"Authorization": "Bearer token123",
|
|
115
|
+
"X-Custom": "value",
|
|
116
|
+
},
|
|
117
|
+
openclawMode: false,
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("parses openclaw mode", () => {
|
|
122
|
+
const result = parseArgs([
|
|
123
|
+
"node", "script",
|
|
124
|
+
"-t", "alerts",
|
|
125
|
+
"-f", "http://localhost:8080",
|
|
126
|
+
"--openclaw"
|
|
127
|
+
]);
|
|
128
|
+
expect(result).toEqual({
|
|
129
|
+
topics: ["alerts"],
|
|
130
|
+
forward: "http://localhost:8080",
|
|
131
|
+
headers: {},
|
|
132
|
+
openclawMode: true,
|
|
95
133
|
});
|
|
96
134
|
});
|
|
97
135
|
|
package/src/lib.ts
CHANGED
|
@@ -45,10 +45,19 @@ export function createPayload(msg: NtfyMessage): BridgePayload {
|
|
|
45
45
|
};
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
export
|
|
48
|
+
export interface ParsedArgs {
|
|
49
|
+
topics: string[];
|
|
50
|
+
forward: string;
|
|
51
|
+
headers: Record<string, string>;
|
|
52
|
+
openclawMode: boolean;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function parseArgs(argv: string[]): ParsedArgs | null {
|
|
49
56
|
const args = argv.slice(2);
|
|
50
57
|
const topics: string[] = [];
|
|
51
58
|
let forward = "";
|
|
59
|
+
const headers: Record<string, string> = {};
|
|
60
|
+
let openclawMode = false;
|
|
52
61
|
|
|
53
62
|
for (let i = 0; i < args.length; i++) {
|
|
54
63
|
const arg = args[i];
|
|
@@ -56,6 +65,16 @@ export function parseArgs(argv: string[]): { topics: string[]; forward: string }
|
|
|
56
65
|
topics.push(args[++i]);
|
|
57
66
|
} else if (arg === "--forward" || arg === "-f") {
|
|
58
67
|
forward = args[++i];
|
|
68
|
+
} else if (arg === "--header" || arg === "-H") {
|
|
69
|
+
const header = args[++i];
|
|
70
|
+
const colonIdx = header.indexOf(":");
|
|
71
|
+
if (colonIdx > 0) {
|
|
72
|
+
const key = header.slice(0, colonIdx).trim();
|
|
73
|
+
const value = header.slice(colonIdx + 1).trim();
|
|
74
|
+
headers[key] = value;
|
|
75
|
+
}
|
|
76
|
+
} else if (arg === "--openclaw") {
|
|
77
|
+
openclawMode = true;
|
|
59
78
|
} else if (arg === "--help" || arg === "-h") {
|
|
60
79
|
return null; // Signal help requested
|
|
61
80
|
}
|
|
@@ -65,5 +84,5 @@ export function parseArgs(argv: string[]): { topics: string[]; forward: string }
|
|
|
65
84
|
return null;
|
|
66
85
|
}
|
|
67
86
|
|
|
68
|
-
return { topics, forward };
|
|
87
|
+
return { topics, forward, headers, openclawMode };
|
|
69
88
|
}
|