@checkstack/notification-pushover-backend 0.1.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/CHANGELOG.md +7 -0
- package/package.json +25 -0
- package/src/index.test.ts +233 -0
- package/src/index.ts +256 -0
- package/src/plugin-metadata.ts +5 -0
- package/tsconfig.json +3 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# @checkstack/notification-pushover-backend
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- cf5f245: Added Pushover notification provider for mobile push notifications. Features include priority mapping (info→0, warning→1, critical→2 emergency), retry/expire for emergency alerts, and supplementary URL support.
|
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@checkstack/notification-pushover-backend",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "src/index.ts",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./src/index.ts",
|
|
8
|
+
"./plugin-metadata": "./src/plugin-metadata.ts"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"typecheck": "tsc --noEmit",
|
|
12
|
+
"lint": "eslint src --ext .ts",
|
|
13
|
+
"test": "bun test"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@checkstack/backend-api": "workspace:*",
|
|
17
|
+
"@checkstack/common": "workspace:*",
|
|
18
|
+
"@checkstack/notification-backend": "workspace:*",
|
|
19
|
+
"zod": "^4.2.1"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@checkstack/tsconfig": "workspace:*",
|
|
23
|
+
"typescript": "^5.7.2"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { describe, it, expect, spyOn } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
pushoverConfigSchemaV1,
|
|
4
|
+
pushoverUserConfigSchema,
|
|
5
|
+
mapImportanceToPriority,
|
|
6
|
+
PUSHOVER_API_URL,
|
|
7
|
+
EMERGENCY_RETRY_SECONDS,
|
|
8
|
+
EMERGENCY_EXPIRE_SECONDS,
|
|
9
|
+
} from "./index";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Unit tests for the Pushover Notification Strategy.
|
|
13
|
+
*
|
|
14
|
+
* Tests cover:
|
|
15
|
+
* - Config schema validation
|
|
16
|
+
* - Priority mapping
|
|
17
|
+
* - REST API interaction
|
|
18
|
+
* - Emergency notification parameters
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
describe("Pushover Notification Strategy", () => {
|
|
22
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
23
|
+
// Config Schema Validation
|
|
24
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
describe("config schema", () => {
|
|
27
|
+
it("validates admin config - requires apiToken", () => {
|
|
28
|
+
expect(() => {
|
|
29
|
+
pushoverConfigSchemaV1.parse({});
|
|
30
|
+
}).toThrow();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("accepts valid admin config", () => {
|
|
34
|
+
const result = pushoverConfigSchemaV1.parse({
|
|
35
|
+
apiToken: "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5",
|
|
36
|
+
});
|
|
37
|
+
expect(result.apiToken).toBe("a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("validates user config - requires userKey", () => {
|
|
41
|
+
expect(() => {
|
|
42
|
+
pushoverUserConfigSchema.parse({});
|
|
43
|
+
}).toThrow();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("accepts valid user config", () => {
|
|
47
|
+
const result = pushoverUserConfigSchema.parse({
|
|
48
|
+
userKey: "u1s2e3r4k5e6y7-abcdefg",
|
|
49
|
+
});
|
|
50
|
+
expect(result.userKey).toBe("u1s2e3r4k5e6y7-abcdefg");
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
55
|
+
// Priority Mapping
|
|
56
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
describe("priority mapping", () => {
|
|
59
|
+
it("maps info to normal priority (0)", () => {
|
|
60
|
+
expect(mapImportanceToPriority("info")).toBe(0);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("maps warning to high priority (1)", () => {
|
|
64
|
+
expect(mapImportanceToPriority("warning")).toBe(1);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("maps critical to emergency priority (2)", () => {
|
|
68
|
+
expect(mapImportanceToPriority("critical")).toBe(2);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
73
|
+
// Emergency Parameters
|
|
74
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
describe("emergency parameters", () => {
|
|
77
|
+
it("has correct retry interval", () => {
|
|
78
|
+
expect(EMERGENCY_RETRY_SECONDS).toBe(60);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("has correct expire duration", () => {
|
|
82
|
+
expect(EMERGENCY_EXPIRE_SECONDS).toBe(3600);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
87
|
+
// REST API Interaction
|
|
88
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
89
|
+
|
|
90
|
+
describe("REST API interaction", () => {
|
|
91
|
+
it("sends message to Pushover API", async () => {
|
|
92
|
+
let capturedBody: string | undefined;
|
|
93
|
+
let capturedUrl: string | undefined;
|
|
94
|
+
|
|
95
|
+
const mockFetch = spyOn(globalThis, "fetch").mockImplementation((async (
|
|
96
|
+
url: RequestInfo | URL,
|
|
97
|
+
options?: RequestInit,
|
|
98
|
+
) => {
|
|
99
|
+
capturedUrl = url.toString();
|
|
100
|
+
capturedBody = options?.body as string;
|
|
101
|
+
return new Response(JSON.stringify({ status: 1, request: "abc123" }), {
|
|
102
|
+
status: 200,
|
|
103
|
+
});
|
|
104
|
+
}) as unknown as typeof fetch);
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
await fetch(PUSHOVER_API_URL, {
|
|
108
|
+
method: "POST",
|
|
109
|
+
headers: { "Content-Type": "application/json" },
|
|
110
|
+
body: JSON.stringify({
|
|
111
|
+
token: "api-token",
|
|
112
|
+
user: "user-key",
|
|
113
|
+
title: "Test Alert",
|
|
114
|
+
message: "Test message body",
|
|
115
|
+
priority: 0,
|
|
116
|
+
html: 1,
|
|
117
|
+
}),
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
expect(capturedUrl).toBe(PUSHOVER_API_URL);
|
|
121
|
+
|
|
122
|
+
const parsedBody = JSON.parse(capturedBody!);
|
|
123
|
+
expect(parsedBody.token).toBe("api-token");
|
|
124
|
+
expect(parsedBody.user).toBe("user-key");
|
|
125
|
+
expect(parsedBody.title).toBe("Test Alert");
|
|
126
|
+
expect(parsedBody.priority).toBe(0);
|
|
127
|
+
expect(parsedBody.html).toBe(1);
|
|
128
|
+
} finally {
|
|
129
|
+
mockFetch.mockRestore();
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("includes URL parameters for action", async () => {
|
|
134
|
+
let capturedBody: string | undefined;
|
|
135
|
+
|
|
136
|
+
const mockFetch = spyOn(globalThis, "fetch").mockImplementation((async (
|
|
137
|
+
_url: RequestInfo | URL,
|
|
138
|
+
options?: RequestInit,
|
|
139
|
+
) => {
|
|
140
|
+
capturedBody = options?.body as string;
|
|
141
|
+
return new Response(JSON.stringify({ status: 1, request: "def456" }), {
|
|
142
|
+
status: 200,
|
|
143
|
+
});
|
|
144
|
+
}) as unknown as typeof fetch);
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
await fetch(PUSHOVER_API_URL, {
|
|
148
|
+
method: "POST",
|
|
149
|
+
headers: { "Content-Type": "application/json" },
|
|
150
|
+
body: JSON.stringify({
|
|
151
|
+
token: "api-token",
|
|
152
|
+
user: "user-key",
|
|
153
|
+
title: "Incident",
|
|
154
|
+
message: "View incident",
|
|
155
|
+
priority: 1,
|
|
156
|
+
html: 1,
|
|
157
|
+
url: "https://example.com/incident/123",
|
|
158
|
+
url_title: "View Incident",
|
|
159
|
+
}),
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const parsedBody = JSON.parse(capturedBody!);
|
|
163
|
+
expect(parsedBody.url).toBe("https://example.com/incident/123");
|
|
164
|
+
expect(parsedBody.url_title).toBe("View Incident");
|
|
165
|
+
} finally {
|
|
166
|
+
mockFetch.mockRestore();
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("includes retry/expire for emergency priority", async () => {
|
|
171
|
+
let capturedBody: string | undefined;
|
|
172
|
+
|
|
173
|
+
const mockFetch = spyOn(globalThis, "fetch").mockImplementation((async (
|
|
174
|
+
_url: RequestInfo | URL,
|
|
175
|
+
options?: RequestInit,
|
|
176
|
+
) => {
|
|
177
|
+
capturedBody = options?.body as string;
|
|
178
|
+
return new Response(
|
|
179
|
+
JSON.stringify({ status: 1, request: "ghi789", receipt: "rcpt123" }),
|
|
180
|
+
{ status: 200 },
|
|
181
|
+
);
|
|
182
|
+
}) as unknown as typeof fetch);
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
await fetch(PUSHOVER_API_URL, {
|
|
186
|
+
method: "POST",
|
|
187
|
+
headers: { "Content-Type": "application/json" },
|
|
188
|
+
body: JSON.stringify({
|
|
189
|
+
token: "api-token",
|
|
190
|
+
user: "user-key",
|
|
191
|
+
title: "Critical Alert",
|
|
192
|
+
message: "Immediate attention required",
|
|
193
|
+
priority: 2,
|
|
194
|
+
html: 1,
|
|
195
|
+
retry: EMERGENCY_RETRY_SECONDS,
|
|
196
|
+
expire: EMERGENCY_EXPIRE_SECONDS,
|
|
197
|
+
}),
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
const parsedBody = JSON.parse(capturedBody!);
|
|
201
|
+
expect(parsedBody.priority).toBe(2);
|
|
202
|
+
expect(parsedBody.retry).toBe(60);
|
|
203
|
+
expect(parsedBody.expire).toBe(3600);
|
|
204
|
+
} finally {
|
|
205
|
+
mockFetch.mockRestore();
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it("handles API errors gracefully", async () => {
|
|
210
|
+
const mockFetch = spyOn(globalThis, "fetch").mockImplementation(
|
|
211
|
+
(async () => {
|
|
212
|
+
return new Response(
|
|
213
|
+
JSON.stringify({ status: 0, errors: ["invalid token"] }),
|
|
214
|
+
{ status: 400 },
|
|
215
|
+
);
|
|
216
|
+
}) as unknown as typeof fetch,
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
try {
|
|
220
|
+
const response = await fetch(PUSHOVER_API_URL, {
|
|
221
|
+
method: "POST",
|
|
222
|
+
headers: { "Content-Type": "application/json" },
|
|
223
|
+
body: JSON.stringify({ token: "invalid", user: "key" }),
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
expect(response.ok).toBe(false);
|
|
227
|
+
expect(response.status).toBe(400);
|
|
228
|
+
} finally {
|
|
229
|
+
mockFetch.mockRestore();
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import {
|
|
3
|
+
createBackendPlugin,
|
|
4
|
+
configString,
|
|
5
|
+
Versioned,
|
|
6
|
+
type NotificationStrategy,
|
|
7
|
+
type NotificationSendContext,
|
|
8
|
+
type NotificationDeliveryResult,
|
|
9
|
+
markdownToPlainText,
|
|
10
|
+
} from "@checkstack/backend-api";
|
|
11
|
+
import { notificationStrategyExtensionPoint } from "@checkstack/notification-backend";
|
|
12
|
+
import { pluginMetadata } from "./plugin-metadata";
|
|
13
|
+
|
|
14
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
15
|
+
// Configuration Schemas
|
|
16
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Admin configuration for Pushover strategy.
|
|
20
|
+
* Admins register an app at pushover.net and provide the API token.
|
|
21
|
+
*/
|
|
22
|
+
const pushoverConfigSchemaV1 = z.object({
|
|
23
|
+
apiToken: configString({ "x-secret": true }).describe(
|
|
24
|
+
"Pushover Application API Token",
|
|
25
|
+
),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
type PushoverConfig = z.infer<typeof pushoverConfigSchemaV1>;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* User configuration for Pushover - users provide their user key.
|
|
32
|
+
*/
|
|
33
|
+
const pushoverUserConfigSchema = z.object({
|
|
34
|
+
userKey: configString({ "x-secret": true }).describe("Pushover User Key"),
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
type PushoverUserConfig = z.infer<typeof pushoverUserConfigSchema>;
|
|
38
|
+
|
|
39
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
40
|
+
// Instructions
|
|
41
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
42
|
+
|
|
43
|
+
const adminInstructions = `
|
|
44
|
+
## Register a Pushover Application
|
|
45
|
+
|
|
46
|
+
1. Go to [Pushover](https://pushover.net/) and sign in
|
|
47
|
+
2. Scroll to the bottom and click **Create an Application/API Token**
|
|
48
|
+
3. Fill in the application details:
|
|
49
|
+
- **Name**: e.g., "Checkstack Notifications"
|
|
50
|
+
- **Type**: Application
|
|
51
|
+
- **Description**: (optional)
|
|
52
|
+
4. Click **Create Application**
|
|
53
|
+
5. Copy the **API Token/Key** and paste it in the field above
|
|
54
|
+
|
|
55
|
+
> **Note**: The API token is shared across all users. Each user provides their own User Key.
|
|
56
|
+
`.trim();
|
|
57
|
+
|
|
58
|
+
const userInstructions = `
|
|
59
|
+
## Get Your Pushover User Key
|
|
60
|
+
|
|
61
|
+
1. Go to [Pushover](https://pushover.net/) and sign in (or create an account)
|
|
62
|
+
2. Your **User Key** is displayed on the main page after login
|
|
63
|
+
3. Copy the key and paste it in the field above
|
|
64
|
+
|
|
65
|
+
> **Tip**: Make sure you have the Pushover app installed on your device to receive notifications.
|
|
66
|
+
`.trim();
|
|
67
|
+
|
|
68
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
69
|
+
// Priority Mapping
|
|
70
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Maps notification importance to Pushover priority.
|
|
74
|
+
* Pushover priorities: -2=lowest, -1=low, 0=normal, 1=high, 2=emergency
|
|
75
|
+
*/
|
|
76
|
+
function mapImportanceToPriority(
|
|
77
|
+
importance: "info" | "warning" | "critical",
|
|
78
|
+
): number {
|
|
79
|
+
const priorityMap: Record<string, number> = {
|
|
80
|
+
info: 0, // Normal
|
|
81
|
+
warning: 1, // High (bypasses quiet hours)
|
|
82
|
+
critical: 2, // Emergency (requires acknowledgment)
|
|
83
|
+
};
|
|
84
|
+
return priorityMap[importance];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
88
|
+
// Pushover API Constants
|
|
89
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
90
|
+
|
|
91
|
+
const PUSHOVER_API_URL = "https://api.pushover.net/1/messages.json";
|
|
92
|
+
|
|
93
|
+
// Emergency priority parameters
|
|
94
|
+
const EMERGENCY_RETRY_SECONDS = 60; // Retry every 60 seconds
|
|
95
|
+
const EMERGENCY_EXPIRE_SECONDS = 3600; // Expire after 1 hour
|
|
96
|
+
|
|
97
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
98
|
+
// Pushover Strategy Implementation
|
|
99
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Pushover notification strategy using REST API.
|
|
103
|
+
*/
|
|
104
|
+
const pushoverStrategy: NotificationStrategy<
|
|
105
|
+
PushoverConfig,
|
|
106
|
+
PushoverUserConfig
|
|
107
|
+
> = {
|
|
108
|
+
id: "pushover",
|
|
109
|
+
displayName: "Pushover",
|
|
110
|
+
description: "Send notifications via Pushover mobile app",
|
|
111
|
+
icon: "Smartphone",
|
|
112
|
+
|
|
113
|
+
config: new Versioned({
|
|
114
|
+
version: 1,
|
|
115
|
+
schema: pushoverConfigSchemaV1,
|
|
116
|
+
}),
|
|
117
|
+
|
|
118
|
+
// User-config resolution - users enter their user key
|
|
119
|
+
contactResolution: { type: "user-config", field: "userKey" },
|
|
120
|
+
|
|
121
|
+
userConfig: new Versioned({
|
|
122
|
+
version: 1,
|
|
123
|
+
schema: pushoverUserConfigSchema,
|
|
124
|
+
}),
|
|
125
|
+
|
|
126
|
+
adminInstructions,
|
|
127
|
+
userInstructions,
|
|
128
|
+
|
|
129
|
+
async send(
|
|
130
|
+
context: NotificationSendContext<PushoverConfig, PushoverUserConfig>,
|
|
131
|
+
): Promise<NotificationDeliveryResult> {
|
|
132
|
+
const { userConfig, notification, strategyConfig, logger } = context;
|
|
133
|
+
|
|
134
|
+
if (!strategyConfig.apiToken) {
|
|
135
|
+
return {
|
|
136
|
+
success: false,
|
|
137
|
+
error: "Pushover API token not configured",
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (!userConfig?.userKey) {
|
|
142
|
+
return {
|
|
143
|
+
success: false,
|
|
144
|
+
error: "User has not configured their Pushover user key",
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
const priority = mapImportanceToPriority(notification.importance);
|
|
150
|
+
|
|
151
|
+
// Build message body
|
|
152
|
+
const message = notification.body
|
|
153
|
+
? markdownToPlainText(notification.body)
|
|
154
|
+
: notification.title;
|
|
155
|
+
|
|
156
|
+
// Build request body
|
|
157
|
+
const body: Record<string, string | number> = {
|
|
158
|
+
token: strategyConfig.apiToken,
|
|
159
|
+
user: userConfig.userKey,
|
|
160
|
+
title: notification.title,
|
|
161
|
+
message,
|
|
162
|
+
priority,
|
|
163
|
+
html: 1, // Enable HTML formatting
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
// Add action URL if present
|
|
167
|
+
if (notification.action?.url) {
|
|
168
|
+
body.url = notification.action.url;
|
|
169
|
+
body.url_title = notification.action.label;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Emergency priority requires retry and expire parameters
|
|
173
|
+
if (priority === 2) {
|
|
174
|
+
body.retry = EMERGENCY_RETRY_SECONDS;
|
|
175
|
+
body.expire = EMERGENCY_EXPIRE_SECONDS;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Send to Pushover
|
|
179
|
+
const response = await fetch(PUSHOVER_API_URL, {
|
|
180
|
+
method: "POST",
|
|
181
|
+
headers: {
|
|
182
|
+
"Content-Type": "application/json",
|
|
183
|
+
},
|
|
184
|
+
body: JSON.stringify(body),
|
|
185
|
+
signal: AbortSignal.timeout(10_000),
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
if (!response.ok) {
|
|
189
|
+
const errorText = await response.text();
|
|
190
|
+
logger.error("Failed to send Pushover message", {
|
|
191
|
+
status: response.status,
|
|
192
|
+
error: errorText.slice(0, 500),
|
|
193
|
+
});
|
|
194
|
+
return {
|
|
195
|
+
success: false,
|
|
196
|
+
error: `Failed to send Pushover message: ${response.status}`,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const result = (await response.json()) as {
|
|
201
|
+
status: number;
|
|
202
|
+
request: string;
|
|
203
|
+
receipt?: string;
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
if (result.status !== 1) {
|
|
207
|
+
return {
|
|
208
|
+
success: false,
|
|
209
|
+
error: "Pushover API returned error status",
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
success: true,
|
|
215
|
+
externalId: result.receipt ?? result.request,
|
|
216
|
+
};
|
|
217
|
+
} catch (error) {
|
|
218
|
+
const message =
|
|
219
|
+
error instanceof Error ? error.message : "Unknown Pushover API error";
|
|
220
|
+
logger.error("Pushover notification error", { error: message });
|
|
221
|
+
return {
|
|
222
|
+
success: false,
|
|
223
|
+
error: `Failed to send Pushover notification: ${message}`,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
230
|
+
// Plugin Definition
|
|
231
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
232
|
+
|
|
233
|
+
export default createBackendPlugin({
|
|
234
|
+
metadata: pluginMetadata,
|
|
235
|
+
|
|
236
|
+
register(env) {
|
|
237
|
+
// Get the notification strategy extension point
|
|
238
|
+
const extensionPoint = env.getExtensionPoint(
|
|
239
|
+
notificationStrategyExtensionPoint,
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
// Register the Pushover strategy with our plugin metadata
|
|
243
|
+
extensionPoint.addStrategy(pushoverStrategy, pluginMetadata);
|
|
244
|
+
},
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// Export for testing
|
|
248
|
+
export {
|
|
249
|
+
pushoverConfigSchemaV1,
|
|
250
|
+
pushoverUserConfigSchema,
|
|
251
|
+
mapImportanceToPriority,
|
|
252
|
+
PUSHOVER_API_URL,
|
|
253
|
+
EMERGENCY_RETRY_SECONDS,
|
|
254
|
+
EMERGENCY_EXPIRE_SECONDS,
|
|
255
|
+
};
|
|
256
|
+
export type { PushoverConfig, PushoverUserConfig };
|
package/tsconfig.json
ADDED