@checkstack/integration-jira-backend 0.0.2

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 ADDED
@@ -0,0 +1,53 @@
1
+ # @checkstack/integration-jira-backend
2
+
3
+ ## 0.0.2
4
+
5
+ ### Patch Changes
6
+
7
+ - d20d274: Initial release of all @checkstack packages. Rebranded from Checkmate to Checkstack with new npm organization @checkstack and domain checkstack.dev.
8
+ - Updated dependencies [d20d274]
9
+ - @checkstack/backend-api@0.0.2
10
+ - @checkstack/common@0.0.2
11
+ - @checkstack/integration-backend@0.0.2
12
+ - @checkstack/integration-common@0.0.2
13
+ - @checkstack/integration-jira-common@0.0.2
14
+
15
+ ## 0.0.3
16
+
17
+ ### Patch Changes
18
+
19
+ - 4c5aa9e: Fix `IntegrationProvider.testConnection` generic type
20
+
21
+ - **Breaking**: `testConnection` now receives `TConnection` (connection config) instead of `TConfig` (subscription config)
22
+ - **Breaking**: `RegisteredIntegrationProvider` now includes `TConnection` generic parameter
23
+ - Removed `testConnection` from webhook provider (providers without `connectionSchema` cannot have `testConnection`)
24
+ - Fixed Jira provider to use `JiraConnectionConfig` directly in `testConnection`
25
+
26
+ This aligns the interface with the actual behavior: `testConnection` tests connection credentials, not subscription configuration.
27
+
28
+ - Updated dependencies [4c5aa9e]
29
+ - Updated dependencies [b4eb432]
30
+ - Updated dependencies [a65e002]
31
+ - Updated dependencies [a65e002]
32
+ - @checkstack/integration-backend@0.1.0
33
+ - @checkstack/backend-api@1.1.0
34
+ - @checkstack/common@0.2.0
35
+ - @checkstack/integration-common@0.1.1
36
+ - @checkstack/integration-jira-common@0.0.3
37
+
38
+ ## 0.0.2
39
+
40
+ ### Patch Changes
41
+
42
+ - Updated dependencies [ffc28f6]
43
+ - Updated dependencies [4dd644d]
44
+ - Updated dependencies [71275dd]
45
+ - Updated dependencies [ae19ff6]
46
+ - Updated dependencies [b55fae6]
47
+ - Updated dependencies [b354ab3]
48
+ - Updated dependencies [81f3f85]
49
+ - @checkstack/common@0.1.0
50
+ - @checkstack/backend-api@1.0.0
51
+ - @checkstack/integration-common@0.1.0
52
+ - @checkstack/integration-backend@0.0.2
53
+ - @checkstack/integration-jira-common@0.0.2
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "@checkstack/integration-jira-backend",
3
+ "version": "0.0.2",
4
+ "type": "module",
5
+ "main": "src/index.ts",
6
+ "scripts": {
7
+ "typecheck": "tsc --noEmit",
8
+ "lint": "bun run lint:code",
9
+ "lint:code": "eslint . --max-warnings 0"
10
+ },
11
+ "dependencies": {
12
+ "@checkstack/backend-api": "workspace:*",
13
+ "@checkstack/integration-backend": "workspace:*",
14
+ "@checkstack/integration-common": "workspace:*",
15
+ "@checkstack/integration-jira-common": "workspace:*",
16
+ "@checkstack/common": "workspace:*",
17
+ "@orpc/server": "^1.13.2",
18
+ "zod": "^4.2.1"
19
+ },
20
+ "devDependencies": {
21
+ "@types/bun": "^1.0.0",
22
+ "typescript": "^5.0.0",
23
+ "@checkstack/tsconfig": "workspace:*",
24
+ "@checkstack/scripts": "workspace:*"
25
+ }
26
+ }
package/src/index.ts ADDED
@@ -0,0 +1,35 @@
1
+ import {
2
+ createBackendPlugin,
3
+ coreServices,
4
+ } from "@checkstack/backend-api";
5
+ import { integrationProviderExtensionPoint } from "@checkstack/integration-backend";
6
+ import { pluginMetadata } from "@checkstack/integration-jira-common";
7
+ import { createJiraProvider } from "./provider";
8
+
9
+ export const jiraPlugin = createBackendPlugin({
10
+ metadata: pluginMetadata,
11
+
12
+ register(env) {
13
+ // Register the Jira provider
14
+ env.registerInit({
15
+ deps: {
16
+ logger: coreServices.logger,
17
+ },
18
+ init: async ({ logger }) => {
19
+ logger.debug("🔌 Registering Jira Integration Provider...");
20
+
21
+ // Create and register the Jira provider
22
+ // No dependencies needed - connection access is provided through context/params
23
+ const jiraProvider = createJiraProvider();
24
+ const integrationExt = env.getExtensionPoint(
25
+ integrationProviderExtensionPoint
26
+ );
27
+ integrationExt.addProvider(jiraProvider, pluginMetadata);
28
+
29
+ logger.info("✅ Jira Integration Plugin registered");
30
+ },
31
+ });
32
+ },
33
+ });
34
+
35
+ export default jiraPlugin;
@@ -0,0 +1,332 @@
1
+ import type { Logger } from "@checkstack/backend-api";
2
+ import type {
3
+ JiraProject,
4
+ JiraIssueType,
5
+ JiraField,
6
+ JiraConnection,
7
+ } from "@checkstack/integration-jira-common";
8
+
9
+ /**
10
+ * Connection config for generic connection management.
11
+ * Mirrors the structure from provider.ts.
12
+ */
13
+ export interface JiraConnectionConfig {
14
+ baseUrl: string;
15
+ email: string;
16
+ apiToken: string;
17
+ }
18
+
19
+ /**
20
+ * Response from creating a Jira issue.
21
+ */
22
+ export interface CreateIssueResult {
23
+ /** Issue ID */
24
+ id: string;
25
+ /** Issue key (e.g., "PROJ-123") */
26
+ key: string;
27
+ /** Self URL */
28
+ self: string;
29
+ }
30
+
31
+ /**
32
+ * Priority from Jira API.
33
+ */
34
+ export interface JiraPriority {
35
+ id: string;
36
+ name: string;
37
+ iconUrl?: string;
38
+ }
39
+
40
+ /**
41
+ * Issue creation payload.
42
+ */
43
+ export interface CreateIssuePayload {
44
+ projectKey: string;
45
+ issueTypeId: string;
46
+ summary: string;
47
+ description?: string;
48
+ priorityId?: string;
49
+ additionalFields?: Record<string, unknown>;
50
+ }
51
+
52
+ /**
53
+ * Options for creating a Jira client.
54
+ */
55
+ interface JiraClientOptions {
56
+ baseUrl: string;
57
+ email: string;
58
+ apiToken: string;
59
+ logger: Logger;
60
+ }
61
+
62
+ /**
63
+ * Create a typed Jira REST API client.
64
+ */
65
+ export function createJiraClient(options: JiraClientOptions) {
66
+ const { baseUrl, email, apiToken, logger } = options;
67
+
68
+ // Build basic auth header
69
+ const authHeader = `Basic ${Buffer.from(`${email}:${apiToken}`).toString(
70
+ "base64"
71
+ )}`;
72
+
73
+ /**
74
+ * Make an authenticated request to the Jira API.
75
+ */
76
+ async function request<T>(path: string, init?: RequestInit): Promise<T> {
77
+ const url = `${baseUrl.replace(/\/$/, "")}/rest/api/3${path}`;
78
+
79
+ const response = await fetch(url, {
80
+ ...init,
81
+ headers: {
82
+ Authorization: authHeader,
83
+ "Content-Type": "application/json",
84
+ Accept: "application/json",
85
+ ...init?.headers,
86
+ },
87
+ });
88
+
89
+ if (!response.ok) {
90
+ const errorText = await response.text();
91
+ logger.error(
92
+ `Jira API error: ${response.status} ${response.statusText}`,
93
+ { url, error: errorText }
94
+ );
95
+ throw new Error(`Jira API error: ${response.status} - ${errorText}`);
96
+ }
97
+
98
+ return response.json() as Promise<T>;
99
+ }
100
+
101
+ return {
102
+ /**
103
+ * Test connection by fetching the current user.
104
+ */
105
+ async testConnection(): Promise<{ success: boolean; message?: string }> {
106
+ try {
107
+ await request<{ accountId: string }>("/myself");
108
+ return { success: true };
109
+ } catch (error) {
110
+ return {
111
+ success: false,
112
+ message: error instanceof Error ? error.message : "Unknown error",
113
+ };
114
+ }
115
+ },
116
+
117
+ /**
118
+ * Get all accessible projects.
119
+ */
120
+ async getProjects(): Promise<JiraProject[]> {
121
+ interface ProjectResponse {
122
+ id: string;
123
+ key: string;
124
+ name: string;
125
+ avatarUrls?: Record<string, string>;
126
+ }
127
+
128
+ const result = await request<ProjectResponse[]>("/project");
129
+ return result.map((p) => ({
130
+ id: p.id,
131
+ key: p.key,
132
+ name: p.name,
133
+ avatarUrls: p.avatarUrls,
134
+ }));
135
+ },
136
+
137
+ /**
138
+ * Get issue types for a project.
139
+ * Uses the /project/{projectIdOrKey}?expand=issueTypes endpoint.
140
+ */
141
+ async getIssueTypes(projectKey: string): Promise<JiraIssueType[]> {
142
+ interface ProjectWithIssueTypes {
143
+ id: string;
144
+ key: string;
145
+ name: string;
146
+ issueTypes: Array<{
147
+ id: string;
148
+ name: string;
149
+ description?: string;
150
+ iconUrl?: string;
151
+ subtask: boolean;
152
+ }>;
153
+ }
154
+
155
+ logger.debug(`Fetching issue types for project: ${projectKey}`);
156
+
157
+ const result = await request<ProjectWithIssueTypes>(
158
+ `/project/${encodeURIComponent(projectKey)}?expand=issueTypes`
159
+ );
160
+
161
+ logger.debug(
162
+ `Found ${
163
+ result.issueTypes?.length ?? 0
164
+ } issue types for project ${projectKey}`
165
+ );
166
+
167
+ // Filter out subtasks for simpler UX
168
+ return (result.issueTypes || [])
169
+ .filter((t) => !t.subtask)
170
+ .map((t) => ({
171
+ id: t.id,
172
+ name: t.name,
173
+ description: t.description,
174
+ iconUrl: t.iconUrl,
175
+ subtask: t.subtask,
176
+ }));
177
+ },
178
+
179
+ /**
180
+ * Get fields available for creating issues with a specific type.
181
+ * Uses the /issue/createmeta/{projectIdOrKey}/issuetypes/{issueTypeId} endpoint.
182
+ */
183
+ async getFields(
184
+ projectKey: string,
185
+ issueTypeId: string
186
+ ): Promise<JiraField[]> {
187
+ interface FieldsResponse {
188
+ startAt: number;
189
+ maxResults: number;
190
+ total: number;
191
+ fields: Array<{
192
+ key: string;
193
+ fieldId: string;
194
+ name: string;
195
+ required: boolean;
196
+ schema?: {
197
+ type: string;
198
+ system?: string;
199
+ custom?: string;
200
+ customId?: number;
201
+ };
202
+ allowedValues?: Array<{
203
+ id: string;
204
+ name?: string;
205
+ value?: string;
206
+ }>;
207
+ }>;
208
+ }
209
+
210
+ logger.debug(
211
+ `Fetching fields for project: ${projectKey}, issueType: ${issueTypeId}`
212
+ );
213
+
214
+ const result = await request<FieldsResponse>(
215
+ `/issue/createmeta/${encodeURIComponent(
216
+ projectKey
217
+ )}/issuetypes/${encodeURIComponent(issueTypeId)}`
218
+ );
219
+
220
+ logger.debug(
221
+ `Found ${
222
+ result.fields?.length ?? 0
223
+ } fields for project ${projectKey}, issueType ${issueTypeId}`
224
+ );
225
+
226
+ return (result.fields || []).map((field) => ({
227
+ key: field.key || field.fieldId,
228
+ name: field.name,
229
+ required: field.required,
230
+ schema: field.schema,
231
+ allowedValues: field.allowedValues,
232
+ }));
233
+ },
234
+
235
+ /**
236
+ * Get available priorities.
237
+ */
238
+ async getPriorities(): Promise<JiraPriority[]> {
239
+ interface PriorityResponse {
240
+ id: string;
241
+ name: string;
242
+ iconUrl?: string;
243
+ }
244
+
245
+ const result = await request<PriorityResponse[]>("/priority");
246
+ return result.map((p) => ({
247
+ id: p.id,
248
+ name: p.name,
249
+ iconUrl: p.iconUrl,
250
+ }));
251
+ },
252
+
253
+ /**
254
+ * Create a new issue.
255
+ */
256
+ async createIssue(payload: CreateIssuePayload): Promise<CreateIssueResult> {
257
+ const {
258
+ projectKey,
259
+ issueTypeId,
260
+ summary,
261
+ description,
262
+ priorityId,
263
+ additionalFields,
264
+ } = payload;
265
+
266
+ // Build the issue fields
267
+ const fields: Record<string, unknown> = {
268
+ project: { key: projectKey },
269
+ issuetype: { id: issueTypeId },
270
+ summary,
271
+ ...additionalFields,
272
+ };
273
+
274
+ if (description) {
275
+ // Use Atlassian Document Format for description
276
+ fields.description = {
277
+ type: "doc",
278
+ version: 1,
279
+ content: [
280
+ {
281
+ type: "paragraph",
282
+ content: [{ type: "text", text: description }],
283
+ },
284
+ ],
285
+ };
286
+ }
287
+
288
+ if (priorityId) {
289
+ fields.priority = { id: priorityId };
290
+ }
291
+
292
+ return request<CreateIssueResult>("/issue", {
293
+ method: "POST",
294
+ body: JSON.stringify({ fields }),
295
+ });
296
+ },
297
+ };
298
+ }
299
+
300
+ /**
301
+ * Create a Jira client from a generic connection config.
302
+ * Used with the generic connection management system.
303
+ */
304
+ export function createJiraClientFromConfig(
305
+ config: JiraConnectionConfig,
306
+ logger: Logger
307
+ ) {
308
+ return createJiraClient({
309
+ baseUrl: config.baseUrl,
310
+ email: config.email,
311
+ apiToken: config.apiToken,
312
+ logger,
313
+ });
314
+ }
315
+
316
+ /**
317
+ * Create a Jira client from a connection configuration.
318
+ * @deprecated Use createJiraClientFromConfig with generic connection management.
319
+ */
320
+ export function createJiraClientFromConnection(
321
+ connection: JiraConnection,
322
+ logger: Logger
323
+ ) {
324
+ return createJiraClient({
325
+ baseUrl: connection.baseUrl,
326
+ email: connection.email,
327
+ apiToken: connection.apiToken,
328
+ logger,
329
+ });
330
+ }
331
+
332
+ export type JiraClient = ReturnType<typeof createJiraClient>;
@@ -0,0 +1,377 @@
1
+ import { z } from "zod";
2
+ import { Versioned, configString } from "@checkstack/backend-api";
3
+ import type {
4
+ IntegrationProvider,
5
+ IntegrationDeliveryContext,
6
+ IntegrationDeliveryResult,
7
+ TestConnectionResult,
8
+ ConnectionOption,
9
+ GetConnectionOptionsParams,
10
+ } from "@checkstack/integration-backend";
11
+ import { createJiraClient, createJiraClientFromConfig } from "./jira-client";
12
+ import { expandTemplate } from "./template-engine";
13
+
14
+ /**
15
+ * Schema for Jira connection configuration.
16
+ * Uses configString with x-secret for API token encryption and automatic redaction.
17
+ */
18
+ export const JiraConnectionConfigSchema = z.object({
19
+ baseUrl: configString({}).url().describe("Jira Cloud base URL"),
20
+ email: configString({}).email().describe("Jira user email"),
21
+ apiToken: configString({ "x-secret": true }).describe("Jira API token"),
22
+ });
23
+
24
+ export type JiraConnectionConfig = z.infer<typeof JiraConnectionConfigSchema>;
25
+
26
+ /**
27
+ * Resolver names for dynamic dropdowns.
28
+ * Defined as constants to ensure consistency between schema and handler.
29
+ */
30
+ export const JIRA_RESOLVERS = {
31
+ PROJECT_OPTIONS: "projectOptions",
32
+ ISSUE_TYPE_OPTIONS: "issueTypeOptions",
33
+ PRIORITY_OPTIONS: "priorityOptions",
34
+ FIELD_OPTIONS: "fieldOptions",
35
+ } as const;
36
+
37
+ /**
38
+ * Dynamic field mapping schema with options resolver for field key.
39
+ * Uses configString with x-options-resolver to fetch available fields from Jira.
40
+ */
41
+ export const DynamicJiraFieldMappingSchema = z.object({
42
+ /** Jira field key - fetched dynamically from Jira */
43
+ fieldKey: configString({
44
+ "x-options-resolver": JIRA_RESOLVERS.FIELD_OPTIONS,
45
+ "x-depends-on": ["projectKey", "issueTypeId"],
46
+ "x-searchable": true,
47
+ }).describe("Jira field"),
48
+ /** Template string with {{payload.property}} placeholders */
49
+ template: configString({}).describe("Template value"),
50
+ });
51
+
52
+ /**
53
+ * Provider configuration for Jira subscriptions.
54
+ * Uses configString with x-options-resolver for dynamic dropdowns.
55
+ * Uses configString with x-hidden for connectionId which is auto-populated.
56
+ */
57
+ export const JiraSubscriptionConfigSchema = z.object({
58
+ /** ID of the site-wide Jira connection to use (auto-populated) */
59
+ connectionId: configString({ "x-hidden": true }).describe(
60
+ "Jira connection to use"
61
+ ),
62
+ /** Jira project key to create issues in */
63
+ projectKey: configString({
64
+ "x-options-resolver": JIRA_RESOLVERS.PROJECT_OPTIONS,
65
+ }).describe("Project key"),
66
+ /** Issue type ID for created issues */
67
+ issueTypeId: configString({
68
+ "x-options-resolver": JIRA_RESOLVERS.ISSUE_TYPE_OPTIONS,
69
+ "x-depends-on": ["projectKey"],
70
+ }).describe("Issue type"),
71
+ /** Summary template (required - uses {{payload.field}} syntax) */
72
+ summaryTemplate: configString({}).min(1).describe("Issue summary template"),
73
+ /** Description template (optional) */
74
+ descriptionTemplate: configString({})
75
+ .optional()
76
+ .describe("Issue description template"),
77
+ /** Priority ID (optional) */
78
+ priorityId: configString({
79
+ "x-options-resolver": JIRA_RESOLVERS.PRIORITY_OPTIONS,
80
+ })
81
+ .describe("Priority")
82
+ .optional(),
83
+ /** Additional field mappings */
84
+ fieldMappings: z
85
+ .array(DynamicJiraFieldMappingSchema)
86
+ .optional()
87
+ .describe("Additional field mappings"),
88
+ });
89
+
90
+ /**
91
+ * Jira subscription config type.
92
+ */
93
+ export type JiraProviderConfig = z.infer<typeof JiraSubscriptionConfigSchema>;
94
+
95
+ /**
96
+ * Create the Jira integration provider.
97
+ * Uses the generic connection management system for site-wide Jira connections.
98
+ * Connection access is provided through params/context at call time.
99
+ */
100
+ export function createJiraProvider(): IntegrationProvider<
101
+ JiraProviderConfig,
102
+ JiraConnectionConfig
103
+ > {
104
+ return {
105
+ id: "jira",
106
+ displayName: "Jira",
107
+ description: "Create Jira issues from integration events",
108
+ icon: "Ticket",
109
+
110
+ // Subscription configuration schema
111
+ config: new Versioned({
112
+ version: 1,
113
+ schema: JiraSubscriptionConfigSchema,
114
+ }),
115
+
116
+ // Connection configuration schema for generic connection management
117
+ connectionSchema: new Versioned({
118
+ version: 1,
119
+ schema: JiraConnectionConfigSchema,
120
+ }),
121
+
122
+ documentation: {
123
+ setupGuide: `
124
+ ## Jira Integration Setup
125
+
126
+ 1. **Create a Jira Connection**: First, set up a site-wide Jira connection with your Atlassian credentials.
127
+ 2. **Configure the Subscription**: Select your connection, project, and issue type.
128
+ 3. **Set Up Templates**: Use \`{{payload.property}}\` syntax to dynamically populate issue fields from event data.
129
+
130
+ ### Template Syntax
131
+
132
+ Use double curly braces to reference event payload properties:
133
+ - \`{{payload.title}}\` - Direct property access
134
+ - \`{{payload.system.name}}\` - Nested property access
135
+
136
+ If a property is missing, the placeholder will be preserved in the output for debugging.
137
+ `.trim(),
138
+ examplePayload: JSON.stringify(
139
+ {
140
+ eventType: "incident.created",
141
+ timestamp: "2024-01-15T10:30:00Z",
142
+ payload: {
143
+ title: "Database Connectivity Issue",
144
+ description: "Unable to connect to production database",
145
+ severity: "high",
146
+ system: {
147
+ id: "sys-123",
148
+ name: "Production Database",
149
+ },
150
+ },
151
+ },
152
+ undefined,
153
+ 2
154
+ ),
155
+ },
156
+
157
+ /**
158
+ * Get dynamic options for subscription configuration fields.
159
+ * Provides cascading dropdowns: connection -> projects -> issueTypes -> priorities
160
+ */
161
+ async getConnectionOptions(
162
+ params: GetConnectionOptionsParams
163
+ ): Promise<ConnectionOption[]> {
164
+ const {
165
+ connectionId,
166
+ resolverName,
167
+ context,
168
+ getConnectionWithCredentials,
169
+ logger,
170
+ } = params;
171
+
172
+ // Fetch the connection with credentials
173
+ const connection = await getConnectionWithCredentials(connectionId);
174
+ if (!connection) {
175
+ return [];
176
+ }
177
+
178
+ // Type-safe config access
179
+ const config = connection.config as JiraConnectionConfig;
180
+
181
+ const client = createJiraClientFromConfig(config, logger);
182
+
183
+ try {
184
+ switch (resolverName) {
185
+ case JIRA_RESOLVERS.PROJECT_OPTIONS: {
186
+ const projects = await client.getProjects();
187
+ return projects.map((p) => ({
188
+ value: p.key,
189
+ label: `${p.name} (${p.key})`,
190
+ }));
191
+ }
192
+
193
+ case JIRA_RESOLVERS.ISSUE_TYPE_OPTIONS: {
194
+ const projectKey = context?.projectKey as string | undefined;
195
+ if (!projectKey) {
196
+ return [];
197
+ }
198
+ const issueTypes = await client.getIssueTypes(projectKey);
199
+ return issueTypes.map((t) => ({
200
+ value: t.id,
201
+ label: t.name,
202
+ }));
203
+ }
204
+
205
+ case JIRA_RESOLVERS.PRIORITY_OPTIONS: {
206
+ const priorities = await client.getPriorities();
207
+ return priorities.map((p) => ({
208
+ value: p.id,
209
+ label: p.name,
210
+ }));
211
+ }
212
+
213
+ case JIRA_RESOLVERS.FIELD_OPTIONS: {
214
+ const projectKey = context?.projectKey as string | undefined;
215
+ const issueTypeId = context?.issueTypeId as string | undefined;
216
+ if (!projectKey || !issueTypeId) {
217
+ return [];
218
+ }
219
+ const fields = await client.getFields(projectKey, issueTypeId);
220
+ // Filter out standard fields that are handled separately
221
+ const excludedFields = new Set([
222
+ "summary",
223
+ "description",
224
+ "priority",
225
+ "issuetype",
226
+ "project",
227
+ "reporter",
228
+ "assignee",
229
+ ]);
230
+ return fields
231
+ .filter((f) => !excludedFields.has(f.key))
232
+ .map((f) => ({
233
+ value: f.key,
234
+ label: `${f.name}${f.required ? " *" : ""}`,
235
+ }));
236
+ }
237
+
238
+ default: {
239
+ logger.error(`Unknown resolver name: ${resolverName}`);
240
+ return [];
241
+ }
242
+ }
243
+ } catch (error) {
244
+ logger.error("Failed to get connection options", error);
245
+ return [];
246
+ }
247
+ },
248
+
249
+ /**
250
+ * Test the connection configuration.
251
+ */
252
+ async testConnection(
253
+ config: JiraConnectionConfig
254
+ ): Promise<TestConnectionResult> {
255
+ const minimalLogger = {
256
+ debug: () => {},
257
+ info: () => {},
258
+ warn: () => {},
259
+ error: () => {},
260
+ };
261
+
262
+ const client = createJiraClientFromConfig(config, minimalLogger);
263
+ return client.testConnection();
264
+ },
265
+
266
+ /**
267
+ * Deliver an event by creating a Jira issue.
268
+ */
269
+ async deliver(
270
+ context: IntegrationDeliveryContext<JiraProviderConfig>
271
+ ): Promise<IntegrationDeliveryResult> {
272
+ const { providerConfig, event, logger } = context;
273
+ const {
274
+ connectionId,
275
+ projectKey,
276
+ issueTypeId,
277
+ summaryTemplate,
278
+ descriptionTemplate,
279
+ priorityId,
280
+ fieldMappings,
281
+ } = providerConfig;
282
+
283
+ // Get the connection with credentials from the delivery context
284
+ if (!context.getConnectionWithCredentials) {
285
+ return {
286
+ success: false,
287
+ error: "Connection access not available in delivery context",
288
+ };
289
+ }
290
+ const connection = await context.getConnectionWithCredentials(
291
+ connectionId
292
+ );
293
+ if (!connection) {
294
+ return {
295
+ success: false,
296
+ error: `Jira connection not found: ${connectionId}`,
297
+ };
298
+ }
299
+
300
+ // Type-safe config access
301
+ const config = connection.config as JiraConnectionConfig;
302
+
303
+ // Create Jira client
304
+ const client = createJiraClient({
305
+ baseUrl: config.baseUrl,
306
+ email: config.email,
307
+ apiToken: config.apiToken,
308
+ logger,
309
+ });
310
+
311
+ // Expand templates using the event payload
312
+ const payload = event.payload as Record<string, unknown>;
313
+ const summary = expandTemplate(summaryTemplate, payload);
314
+ const description = descriptionTemplate
315
+ ? expandTemplate(descriptionTemplate, payload)
316
+ : undefined;
317
+
318
+ // Build additional fields from field mappings
319
+ const additionalFields: Record<string, unknown> = {};
320
+ if (fieldMappings) {
321
+ for (const mapping of fieldMappings) {
322
+ const value = expandTemplate(mapping.template, payload);
323
+ additionalFields[mapping.fieldKey] = value;
324
+ }
325
+ }
326
+
327
+ try {
328
+ // Create the issue
329
+ const result = await client.createIssue({
330
+ projectKey,
331
+ issueTypeId,
332
+ summary,
333
+ description,
334
+ priorityId,
335
+ additionalFields:
336
+ Object.keys(additionalFields).length > 0
337
+ ? additionalFields
338
+ : undefined,
339
+ });
340
+
341
+ logger.info(`Created Jira issue: ${result.key}`, {
342
+ issueId: result.id,
343
+ issueKey: result.key,
344
+ project: projectKey,
345
+ });
346
+
347
+ return {
348
+ success: true,
349
+ externalId: result.key,
350
+ };
351
+ } catch (error) {
352
+ const message =
353
+ error instanceof Error ? error.message : "Unknown error";
354
+ logger.error(`Failed to create Jira issue: ${message}`, { error });
355
+
356
+ // Check if it's a rate limit error
357
+ if (
358
+ message.includes("429") ||
359
+ message.toLowerCase().includes("rate limit")
360
+ ) {
361
+ return {
362
+ success: false,
363
+ error: `Rate limited by Jira: ${message}`,
364
+ retryAfterMs: 60_000, // Retry after 1 minute
365
+ };
366
+ }
367
+
368
+ return {
369
+ success: false,
370
+ error: `Failed to create Jira issue: ${message}`,
371
+ };
372
+ }
373
+ },
374
+ };
375
+ }
376
+
377
+ export type JiraProvider = ReturnType<typeof createJiraProvider>;
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Simple template engine for expanding {{payload.property}} placeholders.
3
+ * Supports nested property access (e.g., {{payload.system.name}}).
4
+ * Missing values are rendered as the original placeholder for easier debugging.
5
+ */
6
+
7
+ /**
8
+ * Get a nested property value from an object using dot notation.
9
+ * @param obj The object to retrieve from
10
+ * @param path The dot-separated path (e.g., "system.name")
11
+ * @returns The value at the path, or undefined if not found
12
+ */
13
+ function getNestedValue(obj: Record<string, unknown>, path: string): unknown {
14
+ const parts = path.split(".");
15
+ let current: unknown = obj;
16
+
17
+ for (const part of parts) {
18
+ if (current === null || current === undefined) {
19
+ return undefined;
20
+ }
21
+ if (typeof current !== "object") {
22
+ return undefined;
23
+ }
24
+ current = (current as Record<string, unknown>)[part];
25
+ }
26
+
27
+ return current;
28
+ }
29
+
30
+ /**
31
+ * Expand a template string using values from the payload.
32
+ *
33
+ * @param template The template string (e.g., "Alert: {{payload.title}}")
34
+ * @param payload The payload object containing replacement values
35
+ * @returns The expanded string with placeholders replaced
36
+ *
37
+ * @example
38
+ * expandTemplate("Issue: {{payload.title}}", { title: "Server down" })
39
+ * // Returns: "Issue: Server down"
40
+ *
41
+ * @example
42
+ * expandTemplate("{{payload.missing}}", {})
43
+ * // Returns: "{{payload.missing}}" (original placeholder for debugging)
44
+ */
45
+ export function expandTemplate(
46
+ template: string,
47
+ payload: Record<string, unknown>
48
+ ): string {
49
+ // Match {{payload.path.to.value}} patterns
50
+ const pattern = /\{\{payload\.([^}]+)\}\}/g;
51
+
52
+ return template.replaceAll(pattern, (match, path: string) => {
53
+ const value = getNestedValue(payload, path);
54
+
55
+ // If value is missing, return original placeholder for debugging
56
+ if (value === undefined || value === null) {
57
+ return match;
58
+ }
59
+
60
+ // Convert to string
61
+ if (typeof value === "object") {
62
+ return JSON.stringify(value);
63
+ }
64
+
65
+ return String(value);
66
+ });
67
+ }
68
+
69
+ /**
70
+ * Check if a template contains any placeholders.
71
+ */
72
+ export function hasPlaceholders(template: string): boolean {
73
+ return /\{\{payload\.[^}]+\}\}/.test(template);
74
+ }
75
+
76
+ /**
77
+ * Extract all placeholder paths from a template.
78
+ */
79
+ export function extractPlaceholders(template: string): string[] {
80
+ const pattern = /\{\{payload\.([^}]+)\}\}/g;
81
+ const paths: string[] = [];
82
+ let match: RegExpExecArray | null;
83
+
84
+ while ((match = pattern.exec(template)) !== null) {
85
+ paths.push(match[1]);
86
+ }
87
+
88
+ return paths;
89
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "extends": "@checkstack/tsconfig/backend.json",
3
+ "include": [
4
+ "src"
5
+ ]
6
+ }