@checkstack/notification-backstage-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 +25 -0
- package/package.json +22 -0
- package/src/index.test.ts +147 -0
- package/src/index.ts +292 -0
- package/src/plugin-metadata.ts +9 -0
- package/tsconfig.json +3 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# @checkstack/notification-backstage-backend
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 97c5a6b: Add Backstage notification provider plugin
|
|
8
|
+
|
|
9
|
+
This new plugin enables forwarding Checkstack notifications to external Backstage instances via the Backstage Notifications REST API.
|
|
10
|
+
|
|
11
|
+
**Features:**
|
|
12
|
+
|
|
13
|
+
- Admin configuration for Backstage instance URL and API token
|
|
14
|
+
- User configuration for custom entity reference (e.g., `user:default/john.doe`)
|
|
15
|
+
- Automatic entity reference generation from user email when not specified
|
|
16
|
+
- Severity mapping from Checkstack importance levels to Backstage severity
|
|
17
|
+
- Full admin and user setup instructions
|
|
18
|
+
|
|
19
|
+
### Patch Changes
|
|
20
|
+
|
|
21
|
+
- Updated dependencies [97c5a6b]
|
|
22
|
+
- Updated dependencies [8e43507]
|
|
23
|
+
- @checkstack/backend-api@0.2.0
|
|
24
|
+
- @checkstack/common@0.1.0
|
|
25
|
+
- @checkstack/notification-backend@0.0.4
|
package/package.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@checkstack/notification-backstage-backend",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "src/index.ts",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./src/index.ts"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"typecheck": "tsc --noEmit"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@checkstack/backend-api": "workspace:*",
|
|
14
|
+
"@checkstack/notification-backend": "workspace:*",
|
|
15
|
+
"@checkstack/common": "workspace:*",
|
|
16
|
+
"zod": "^4.2.1"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@checkstack/tsconfig": "workspace:*",
|
|
20
|
+
"typescript": "^5.9.3"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { describe, it, expect } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
backstageConfigSchemaV1,
|
|
4
|
+
userConfigSchemaV1,
|
|
5
|
+
mapImportanceToSeverity,
|
|
6
|
+
} from "./index";
|
|
7
|
+
|
|
8
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
9
|
+
// Config Schema Tests
|
|
10
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
11
|
+
|
|
12
|
+
describe("backstageConfigSchemaV1", () => {
|
|
13
|
+
it("should accept valid config with all fields", () => {
|
|
14
|
+
const config = {
|
|
15
|
+
baseUrl: "https://backstage.example.com",
|
|
16
|
+
token: "my-secret-token",
|
|
17
|
+
defaultEntityPrefix: "user:development/",
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const result = backstageConfigSchemaV1.safeParse(config);
|
|
21
|
+
expect(result.success).toBe(true);
|
|
22
|
+
if (result.success) {
|
|
23
|
+
expect(result.data.baseUrl).toBe("https://backstage.example.com");
|
|
24
|
+
expect(result.data.token).toBe("my-secret-token");
|
|
25
|
+
expect(result.data.defaultEntityPrefix).toBe("user:development/");
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("should accept config with only required fields (all optional)", () => {
|
|
30
|
+
const config = {};
|
|
31
|
+
|
|
32
|
+
const result = backstageConfigSchemaV1.safeParse(config);
|
|
33
|
+
expect(result.success).toBe(true);
|
|
34
|
+
if (result.success) {
|
|
35
|
+
expect(result.data.defaultEntityPrefix).toBe("user:default/");
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("should reject invalid URL", () => {
|
|
40
|
+
const config = {
|
|
41
|
+
baseUrl: "not-a-valid-url",
|
|
42
|
+
token: "my-token",
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const result = backstageConfigSchemaV1.safeParse(config);
|
|
46
|
+
expect(result.success).toBe(false);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("should use default entity prefix when not provided", () => {
|
|
50
|
+
const config = {
|
|
51
|
+
baseUrl: "https://backstage.example.com",
|
|
52
|
+
token: "my-token",
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const result = backstageConfigSchemaV1.safeParse(config);
|
|
56
|
+
expect(result.success).toBe(true);
|
|
57
|
+
if (result.success) {
|
|
58
|
+
expect(result.data.defaultEntityPrefix).toBe("user:default/");
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe("userConfigSchemaV1", () => {
|
|
64
|
+
it("should accept valid user config with entity reference", () => {
|
|
65
|
+
const config = {
|
|
66
|
+
entityRef: "user:default/john.doe",
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const result = userConfigSchemaV1.safeParse(config);
|
|
70
|
+
expect(result.success).toBe(true);
|
|
71
|
+
if (result.success) {
|
|
72
|
+
expect(result.data.entityRef).toBe("user:default/john.doe");
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("should accept empty user config", () => {
|
|
77
|
+
const config = {};
|
|
78
|
+
|
|
79
|
+
const result = userConfigSchemaV1.safeParse(config);
|
|
80
|
+
expect(result.success).toBe(true);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("should accept group entity references", () => {
|
|
84
|
+
const config = {
|
|
85
|
+
entityRef: "group:default/team-platform",
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const result = userConfigSchemaV1.safeParse(config);
|
|
89
|
+
expect(result.success).toBe(true);
|
|
90
|
+
if (result.success) {
|
|
91
|
+
expect(result.data.entityRef).toBe("group:default/team-platform");
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
97
|
+
// Severity Mapping Tests
|
|
98
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
99
|
+
|
|
100
|
+
describe("mapImportanceToSeverity", () => {
|
|
101
|
+
it("should map 'info' to 'normal'", () => {
|
|
102
|
+
expect(mapImportanceToSeverity("info")).toBe("normal");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("should map 'warning' to 'high'", () => {
|
|
106
|
+
expect(mapImportanceToSeverity("warning")).toBe("high");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("should map 'critical' to 'critical'", () => {
|
|
110
|
+
expect(mapImportanceToSeverity("critical")).toBe("critical");
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
115
|
+
// Strategy Send Function Tests
|
|
116
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
117
|
+
|
|
118
|
+
describe("backstageStrategy.send", () => {
|
|
119
|
+
// Note: Full integration tests would require mocking the plugin registration
|
|
120
|
+
// For now, we test the exported utility functions and schema validation
|
|
121
|
+
// The send function is tested indirectly through proper config validation
|
|
122
|
+
|
|
123
|
+
it("should validate that baseUrl and token are required for sending", () => {
|
|
124
|
+
// This test verifies the config validation logic
|
|
125
|
+
// The send function checks for baseUrl and token presence
|
|
126
|
+
const emptyConfig = backstageConfigSchemaV1.parse({});
|
|
127
|
+
expect(emptyConfig.baseUrl).toBeUndefined();
|
|
128
|
+
expect(emptyConfig.token).toBeUndefined();
|
|
129
|
+
// Strategy will return error when these are missing
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("should build correct entity ref from email when user config is empty", () => {
|
|
133
|
+
// Test the entity ref construction logic
|
|
134
|
+
const prefix = "user:default/";
|
|
135
|
+
const email = "john.doe@example.com";
|
|
136
|
+
const emailPart = email.split("@")[0]?.toLowerCase() ?? "";
|
|
137
|
+
const expectedRef = `${prefix}${emailPart}`;
|
|
138
|
+
|
|
139
|
+
expect(expectedRef).toBe("user:default/john.doe");
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("should strip trailing slash from baseUrl", () => {
|
|
143
|
+
const baseUrl = "https://backstage.example.com/";
|
|
144
|
+
const normalizedUrl = baseUrl.replace(/\/$/, "");
|
|
145
|
+
expect(normalizedUrl).toBe("https://backstage.example.com");
|
|
146
|
+
});
|
|
147
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createBackendPlugin,
|
|
3
|
+
type NotificationStrategy,
|
|
4
|
+
Versioned,
|
|
5
|
+
configString,
|
|
6
|
+
markdownToPlainText,
|
|
7
|
+
} from "@checkstack/backend-api";
|
|
8
|
+
import { notificationStrategyExtensionPoint } from "@checkstack/notification-backend";
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
import { pluginMetadata } from "./plugin-metadata";
|
|
11
|
+
|
|
12
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
13
|
+
// Admin Configuration Schema
|
|
14
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Backstage configuration schema with versioning support.
|
|
18
|
+
* Admins configure the Backstage instance URL and access token.
|
|
19
|
+
*/
|
|
20
|
+
const backstageConfigSchemaV1 = z.object({
|
|
21
|
+
baseUrl: configString({})
|
|
22
|
+
.url()
|
|
23
|
+
.optional()
|
|
24
|
+
.describe(
|
|
25
|
+
"Backstage instance base URL (e.g., https://backstage.example.com)"
|
|
26
|
+
),
|
|
27
|
+
token: configString({ "x-secret": true })
|
|
28
|
+
.optional()
|
|
29
|
+
.describe("Backstage API access token for external service authentication"),
|
|
30
|
+
defaultEntityPrefix: configString({})
|
|
31
|
+
.optional()
|
|
32
|
+
.default("user:default/")
|
|
33
|
+
.describe("Default entity prefix for user mapping (e.g., user:default/)"),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
type BackstageConfig = z.infer<typeof backstageConfigSchemaV1>;
|
|
37
|
+
|
|
38
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
39
|
+
// User Configuration Schema
|
|
40
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Per-user configuration for Backstage notifications.
|
|
44
|
+
* Users can specify their entity reference in Backstage.
|
|
45
|
+
*/
|
|
46
|
+
const userConfigSchemaV1 = z.object({
|
|
47
|
+
entityRef: configString({})
|
|
48
|
+
.optional()
|
|
49
|
+
.describe("Your Backstage entity reference (e.g., user:default/john.doe)"),
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
type BackstageUserConfig = z.infer<typeof userConfigSchemaV1>;
|
|
53
|
+
|
|
54
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
55
|
+
// Instructions
|
|
56
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
57
|
+
|
|
58
|
+
const adminInstructions = `
|
|
59
|
+
## Backstage Configuration
|
|
60
|
+
|
|
61
|
+
Connect Checkstack to your Backstage instance to forward notifications.
|
|
62
|
+
|
|
63
|
+
### Prerequisites
|
|
64
|
+
|
|
65
|
+
1. Your Backstage instance must have the **notifications plugin** installed
|
|
66
|
+
2. You need to enable **external access** for the notifications API
|
|
67
|
+
|
|
68
|
+
### Setup Steps
|
|
69
|
+
|
|
70
|
+
1. Enable external service access in your Backstage instance
|
|
71
|
+
2. Generate a static access token (see [Backstage docs](https://backstage.io/docs/auth/service-to-service-auth))
|
|
72
|
+
3. Enter your Backstage instance URL (e.g., \`https://backstage.example.com\`)
|
|
73
|
+
4. Paste the access token in the **Token** field
|
|
74
|
+
|
|
75
|
+
> **Note**: The default entity prefix is used when users don't specify their own entity reference.
|
|
76
|
+
`.trim();
|
|
77
|
+
|
|
78
|
+
const userInstructions = `
|
|
79
|
+
## Connect to Backstage
|
|
80
|
+
|
|
81
|
+
Receive Checkstack notifications in your Backstage notification inbox.
|
|
82
|
+
|
|
83
|
+
### Find Your Entity Reference
|
|
84
|
+
|
|
85
|
+
Your entity reference is how Backstage identifies you. It typically follows the format:
|
|
86
|
+
- \`user:default/your.username\`
|
|
87
|
+
- \`user:default/your-email\`
|
|
88
|
+
|
|
89
|
+
You can find this in your Backstage profile or catalog.
|
|
90
|
+
|
|
91
|
+
> **Tip**: If you leave this blank, the system will try to use your email with the default prefix.
|
|
92
|
+
`.trim();
|
|
93
|
+
|
|
94
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
95
|
+
// Severity Mapping
|
|
96
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Maps Checkstack importance levels to Backstage severity levels.
|
|
100
|
+
*/
|
|
101
|
+
function mapImportanceToSeverity(
|
|
102
|
+
importance: "info" | "warning" | "critical"
|
|
103
|
+
): "low" | "normal" | "high" | "critical" {
|
|
104
|
+
switch (importance) {
|
|
105
|
+
case "info": {
|
|
106
|
+
return "normal";
|
|
107
|
+
}
|
|
108
|
+
case "warning": {
|
|
109
|
+
return "high";
|
|
110
|
+
}
|
|
111
|
+
case "critical": {
|
|
112
|
+
return "critical";
|
|
113
|
+
}
|
|
114
|
+
default: {
|
|
115
|
+
return "normal";
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
121
|
+
// Backstage Strategy Implementation
|
|
122
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
123
|
+
|
|
124
|
+
const backstageStrategy: NotificationStrategy<
|
|
125
|
+
BackstageConfig,
|
|
126
|
+
BackstageUserConfig,
|
|
127
|
+
undefined
|
|
128
|
+
> = {
|
|
129
|
+
id: "backstage",
|
|
130
|
+
displayName: "Backstage",
|
|
131
|
+
description: "Send notifications to your Backstage developer portal",
|
|
132
|
+
icon: "LayoutDashboard",
|
|
133
|
+
|
|
134
|
+
config: new Versioned({
|
|
135
|
+
version: 1,
|
|
136
|
+
schema: backstageConfigSchemaV1,
|
|
137
|
+
}),
|
|
138
|
+
|
|
139
|
+
userConfig: new Versioned({
|
|
140
|
+
version: 1,
|
|
141
|
+
schema: userConfigSchemaV1,
|
|
142
|
+
}),
|
|
143
|
+
|
|
144
|
+
contactResolution: { type: "user-config", field: "entityRef" },
|
|
145
|
+
|
|
146
|
+
adminInstructions,
|
|
147
|
+
userInstructions,
|
|
148
|
+
|
|
149
|
+
async send({
|
|
150
|
+
contact,
|
|
151
|
+
notification,
|
|
152
|
+
strategyConfig,
|
|
153
|
+
userConfig,
|
|
154
|
+
user,
|
|
155
|
+
logger,
|
|
156
|
+
}) {
|
|
157
|
+
// Validate required admin config
|
|
158
|
+
if (!strategyConfig.baseUrl || !strategyConfig.token) {
|
|
159
|
+
return {
|
|
160
|
+
success: false,
|
|
161
|
+
error:
|
|
162
|
+
"Backstage is not configured. Please configure baseUrl and token.",
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Determine the entity reference
|
|
167
|
+
let entityRef = userConfig?.entityRef;
|
|
168
|
+
|
|
169
|
+
// Fallback: construct from email using default prefix
|
|
170
|
+
if (!entityRef && user.email) {
|
|
171
|
+
const prefix = strategyConfig.defaultEntityPrefix ?? "user:default/";
|
|
172
|
+
// Convert email to entity-safe format (replace @ and . with common patterns)
|
|
173
|
+
const emailPart = user.email.split("@")[0]?.toLowerCase() ?? "";
|
|
174
|
+
entityRef = `${prefix}${emailPart}`;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Still no entity ref? Use the contact as-is (might be set by system)
|
|
178
|
+
if (!entityRef) {
|
|
179
|
+
entityRef = contact;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (!entityRef) {
|
|
183
|
+
return {
|
|
184
|
+
success: false,
|
|
185
|
+
error: "No Backstage entity reference configured for this user.",
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Build the notification payload
|
|
190
|
+
const description = notification.body
|
|
191
|
+
? markdownToPlainText(notification.body)
|
|
192
|
+
: undefined;
|
|
193
|
+
|
|
194
|
+
const payload = {
|
|
195
|
+
recipients: {
|
|
196
|
+
type: "entity" as const,
|
|
197
|
+
entityRef,
|
|
198
|
+
},
|
|
199
|
+
payload: {
|
|
200
|
+
title: notification.title,
|
|
201
|
+
...(description && { description }),
|
|
202
|
+
...(notification.action?.url && { link: notification.action.url }),
|
|
203
|
+
severity: mapImportanceToSeverity(notification.importance),
|
|
204
|
+
...(notification.type && { topic: notification.type }),
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
// Send to Backstage
|
|
209
|
+
const url = `${strategyConfig.baseUrl.replace(
|
|
210
|
+
/\/$/,
|
|
211
|
+
""
|
|
212
|
+
)}/api/notifications/notifications`;
|
|
213
|
+
|
|
214
|
+
try {
|
|
215
|
+
logger?.debug?.("Sending notification to Backstage", {
|
|
216
|
+
url,
|
|
217
|
+
entityRef,
|
|
218
|
+
title: notification.title,
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
const response = await fetch(url, {
|
|
222
|
+
method: "POST",
|
|
223
|
+
headers: {
|
|
224
|
+
"Content-Type": "application/json",
|
|
225
|
+
Authorization: `Bearer ${strategyConfig.token}`,
|
|
226
|
+
},
|
|
227
|
+
body: JSON.stringify(payload),
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
if (!response.ok) {
|
|
231
|
+
const errorText = await response.text();
|
|
232
|
+
logger?.error?.("Backstage API error", {
|
|
233
|
+
status: response.status,
|
|
234
|
+
error: errorText,
|
|
235
|
+
});
|
|
236
|
+
return {
|
|
237
|
+
success: false,
|
|
238
|
+
error: `Backstage API error: ${response.status} - ${errorText}`,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Try to extract notification ID from response
|
|
243
|
+
let externalId: string | undefined;
|
|
244
|
+
try {
|
|
245
|
+
const result = (await response.json()) as { id?: string };
|
|
246
|
+
externalId = result.id;
|
|
247
|
+
} catch {
|
|
248
|
+
// Response might not be JSON, that's ok
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
logger?.info?.("Notification sent to Backstage", {
|
|
252
|
+
entityRef,
|
|
253
|
+
externalId,
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
success: true,
|
|
258
|
+
...(externalId && { externalId }),
|
|
259
|
+
};
|
|
260
|
+
} catch (error) {
|
|
261
|
+
logger?.error?.("Failed to send notification to Backstage", {
|
|
262
|
+
error: error instanceof Error ? error.message : String(error),
|
|
263
|
+
});
|
|
264
|
+
return {
|
|
265
|
+
success: false,
|
|
266
|
+
error: error instanceof Error ? error.message : String(error),
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
},
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
273
|
+
// Plugin Definition
|
|
274
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
275
|
+
|
|
276
|
+
export default createBackendPlugin({
|
|
277
|
+
metadata: pluginMetadata,
|
|
278
|
+
|
|
279
|
+
register(env) {
|
|
280
|
+
// Get the notification strategy extension point
|
|
281
|
+
const extensionPoint = env.getExtensionPoint(
|
|
282
|
+
notificationStrategyExtensionPoint
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
// Register the Backstage strategy with our plugin metadata
|
|
286
|
+
extensionPoint.addStrategy(backstageStrategy, pluginMetadata);
|
|
287
|
+
},
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
// Export for testing
|
|
291
|
+
export { backstageConfigSchemaV1, userConfigSchemaV1, mapImportanceToSeverity };
|
|
292
|
+
export type { BackstageConfig, BackstageUserConfig };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { definePluginMetadata } from "@checkstack/common";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Plugin metadata for the Backstage Notification backend.
|
|
5
|
+
* This is the single source of truth for the plugin ID.
|
|
6
|
+
*/
|
|
7
|
+
export const pluginMetadata = definePluginMetadata({
|
|
8
|
+
pluginId: "notification-backstage",
|
|
9
|
+
});
|
package/tsconfig.json
ADDED