@bettsnation/paperclip-plugin-servicenow 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/README.md +63 -0
- package/dist/manifest.d.ts +3 -0
- package/dist/manifest.js +83 -0
- package/dist/worker.d.ts +2 -0
- package/dist/worker.js +168 -0
- package/package.json +32 -0
package/README.md
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# @bettsnation/paperclip-plugin-servicenow
|
|
2
|
+
|
|
3
|
+
Paperclip plugin for bidirectional ServiceNow incident sync.
|
|
4
|
+
|
|
5
|
+
## Inbound (SN → Paperclip)
|
|
6
|
+
- SN Business Rule fires webhook on incident creation
|
|
7
|
+
- Plugin creates Paperclip issue assigned to target agent
|
|
8
|
+
- Sets correlation_id on SN incident for back-reference
|
|
9
|
+
|
|
10
|
+
## Outbound (Paperclip → SN)
|
|
11
|
+
- Issue completed → auto-resolve SN incident
|
|
12
|
+
- Issue status changes → SN work notes
|
|
13
|
+
- Agent comments → SN work notes
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
npm install @bettsnation/paperclip-plugin-servicenow
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Or: Instance Settings → Plugins → Install Plugin → `@bettsnation/paperclip-plugin-servicenow`
|
|
22
|
+
|
|
23
|
+
## Configuration
|
|
24
|
+
|
|
25
|
+
| Setting | Required | Description |
|
|
26
|
+
|---------|----------|-------------|
|
|
27
|
+
| `snInstanceUrl` | Yes | e.g. `https://dev199497.service-now.com` |
|
|
28
|
+
| `snUsernameRef` | Yes | Secret ref for SN username |
|
|
29
|
+
| `snPasswordRef` | Yes | Secret ref for SN password |
|
|
30
|
+
| `snWebhookSecretRef` | Yes | Secret ref for inbound webhook verification |
|
|
31
|
+
| `targetCompanyId` | Yes | Paperclip company for new issues |
|
|
32
|
+
| `targetAgentId` | Yes | Agent to assign new issues to |
|
|
33
|
+
| `targetProjectId` | Yes | Project for new issues |
|
|
34
|
+
| `autoResolve` | No | Auto-resolve SN incidents (default: true) |
|
|
35
|
+
| `syncWorkNotes` | No | Sync comments to SN (default: true) |
|
|
36
|
+
|
|
37
|
+
## SN Business Rule
|
|
38
|
+
|
|
39
|
+
Create on `incident` table, after insert:
|
|
40
|
+
```javascript
|
|
41
|
+
(function executeRule(current, previous) {
|
|
42
|
+
var rm = new sn_ws.RESTMessageV2();
|
|
43
|
+
rm.setEndpoint('<PLUGIN_WEBHOOK_URL>');
|
|
44
|
+
rm.setHttpMethod('POST');
|
|
45
|
+
rm.setRequestHeader('Content-Type', 'application/json');
|
|
46
|
+
rm.setRequestHeader('X-SN-Webhook-Secret', gs.getProperty('x_pc.webhook_secret'));
|
|
47
|
+
rm.setRequestBody(JSON.stringify({
|
|
48
|
+
number: current.getValue('number'),
|
|
49
|
+
sys_id: current.getValue('sys_id'),
|
|
50
|
+
short_description: current.getValue('short_description'),
|
|
51
|
+
description: current.getValue('description'),
|
|
52
|
+
priority: current.getValue('priority'),
|
|
53
|
+
caller_id: current.getDisplayValue('caller_id'),
|
|
54
|
+
assignment_group: current.getDisplayValue('assignment_group'),
|
|
55
|
+
category: current.getValue('category'),
|
|
56
|
+
subcategory: current.getValue('subcategory')
|
|
57
|
+
}));
|
|
58
|
+
rm.executeAsync();
|
|
59
|
+
})(current, previous);
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## License
|
|
63
|
+
MIT
|
package/dist/manifest.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
const manifest = {
|
|
2
|
+
id: "bettsnation.servicenow",
|
|
3
|
+
apiVersion: 1,
|
|
4
|
+
version: "0.1.0",
|
|
5
|
+
displayName: "ServiceNow Integration",
|
|
6
|
+
description: "Bidirectional ServiceNow incident sync. Receives SN webhooks to create Paperclip issues, auto-updates SN incidents as issues progress.",
|
|
7
|
+
author: "BettsNation",
|
|
8
|
+
categories: ["connector"],
|
|
9
|
+
capabilities: [
|
|
10
|
+
"events.subscribe",
|
|
11
|
+
"webhooks.receive",
|
|
12
|
+
"issues.create",
|
|
13
|
+
"issues.read",
|
|
14
|
+
"http.outbound",
|
|
15
|
+
"secrets.read-ref",
|
|
16
|
+
],
|
|
17
|
+
entrypoints: {
|
|
18
|
+
worker: "./dist/worker.js",
|
|
19
|
+
},
|
|
20
|
+
webhooks: [
|
|
21
|
+
{
|
|
22
|
+
endpointKey: "servicenow-incident",
|
|
23
|
+
displayName: "ServiceNow Incident Webhook",
|
|
24
|
+
description: "Receives incident create/update webhooks from ServiceNow Business Rules",
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
instanceConfigSchema: {
|
|
28
|
+
type: "object",
|
|
29
|
+
properties: {
|
|
30
|
+
snInstanceUrl: {
|
|
31
|
+
type: "string",
|
|
32
|
+
title: "ServiceNow Instance URL",
|
|
33
|
+
description: "e.g. https://dev199497.service-now.com",
|
|
34
|
+
},
|
|
35
|
+
snUsernameRef: {
|
|
36
|
+
type: "string",
|
|
37
|
+
title: "SN Username (Secret Ref)",
|
|
38
|
+
},
|
|
39
|
+
snPasswordRef: {
|
|
40
|
+
type: "string",
|
|
41
|
+
title: "SN Password (Secret Ref)",
|
|
42
|
+
},
|
|
43
|
+
snWebhookSecretRef: {
|
|
44
|
+
type: "string",
|
|
45
|
+
title: "Inbound Webhook Secret (Secret Ref)",
|
|
46
|
+
},
|
|
47
|
+
targetCompanyId: {
|
|
48
|
+
type: "string",
|
|
49
|
+
title: "Target Company ID",
|
|
50
|
+
description: "Paperclip company to create issues in",
|
|
51
|
+
},
|
|
52
|
+
targetAgentId: {
|
|
53
|
+
type: "string",
|
|
54
|
+
title: "Target Agent ID",
|
|
55
|
+
description: "Agent to assign new issues to",
|
|
56
|
+
},
|
|
57
|
+
targetProjectId: {
|
|
58
|
+
type: "string",
|
|
59
|
+
title: "Target Project ID",
|
|
60
|
+
},
|
|
61
|
+
autoResolve: {
|
|
62
|
+
type: "boolean",
|
|
63
|
+
title: "Auto-resolve SN incidents on issue completion",
|
|
64
|
+
default: true,
|
|
65
|
+
},
|
|
66
|
+
syncWorkNotes: {
|
|
67
|
+
type: "boolean",
|
|
68
|
+
title: "Sync agent comments to SN work notes",
|
|
69
|
+
default: true,
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
required: [
|
|
73
|
+
"snInstanceUrl",
|
|
74
|
+
"snUsernameRef",
|
|
75
|
+
"snPasswordRef",
|
|
76
|
+
"snWebhookSecretRef",
|
|
77
|
+
"targetCompanyId",
|
|
78
|
+
"targetAgentId",
|
|
79
|
+
"targetProjectId",
|
|
80
|
+
],
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
export default manifest;
|
package/dist/worker.d.ts
ADDED
package/dist/worker.js
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { definePlugin, runWorker, } from "@paperclipai/plugin-sdk";
|
|
2
|
+
const PRIORITY_MAP = {
|
|
3
|
+
"1": "critical",
|
|
4
|
+
"2": "high",
|
|
5
|
+
"3": "medium",
|
|
6
|
+
"4": "low",
|
|
7
|
+
};
|
|
8
|
+
async function snRequest(ctx, config, method, path, body) {
|
|
9
|
+
const username = await ctx.secrets.resolve(config.snUsernameRef);
|
|
10
|
+
const password = await ctx.secrets.resolve(config.snPasswordRef);
|
|
11
|
+
const auth = Buffer.from(`${username}:${password}`).toString("base64");
|
|
12
|
+
return ctx.http.fetch(`${config.snInstanceUrl}${path}`, {
|
|
13
|
+
method,
|
|
14
|
+
headers: {
|
|
15
|
+
"Content-Type": "application/json",
|
|
16
|
+
Accept: "application/json",
|
|
17
|
+
Authorization: `Basic ${auth}`,
|
|
18
|
+
},
|
|
19
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
function isSNSysId(value) {
|
|
23
|
+
return typeof value === "string" && /^[a-f0-9]{32}$/.test(value);
|
|
24
|
+
}
|
|
25
|
+
let _config = null;
|
|
26
|
+
let _ctx = null;
|
|
27
|
+
const plugin = definePlugin({
|
|
28
|
+
async setup(ctx) {
|
|
29
|
+
const config = (await ctx.config.get());
|
|
30
|
+
_config = config;
|
|
31
|
+
_ctx = ctx;
|
|
32
|
+
// =========================================================
|
|
33
|
+
// OUTBOUND: Paperclip events → update SN incident
|
|
34
|
+
// =========================================================
|
|
35
|
+
if (config.autoResolve !== false) {
|
|
36
|
+
// Auto-resolve SN incident when Paperclip issue completes
|
|
37
|
+
ctx.events.on("issue.updated", async (event) => {
|
|
38
|
+
const payload = event.payload;
|
|
39
|
+
const changes = payload?.changes;
|
|
40
|
+
const statusChange = changes?.status;
|
|
41
|
+
const newStatus = statusChange?.to ?? payload?.status;
|
|
42
|
+
if (newStatus !== "done")
|
|
43
|
+
return;
|
|
44
|
+
const originId = payload?.originId;
|
|
45
|
+
if (!isSNSysId(originId))
|
|
46
|
+
return;
|
|
47
|
+
const agentName = payload?.assigneeAgentName ?? "Paperclip agent";
|
|
48
|
+
const issueTag = payload?.issueTag ?? event.entityId;
|
|
49
|
+
const response = await snRequest(ctx, config, "PATCH", `/api/now/table/incident/${originId}`, {
|
|
50
|
+
state: "6",
|
|
51
|
+
close_code: "Solved (Permanently)",
|
|
52
|
+
close_notes: `Resolved by ${agentName}. Issue: ${issueTag}`,
|
|
53
|
+
});
|
|
54
|
+
if (response.ok) {
|
|
55
|
+
ctx.logger.info(`Resolved SN incident for ${issueTag}`);
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
ctx.logger.error(`Failed to resolve SN incident: ${response.status}`);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
// Sync status changes to SN work notes
|
|
62
|
+
ctx.events.on("issue.updated", async (event) => {
|
|
63
|
+
const payload = event.payload;
|
|
64
|
+
const changes = payload?.changes;
|
|
65
|
+
const statusChange = changes?.status;
|
|
66
|
+
const newStatus = statusChange?.to;
|
|
67
|
+
if (!newStatus || newStatus === "done")
|
|
68
|
+
return;
|
|
69
|
+
const originId = payload?.originId;
|
|
70
|
+
if (!isSNSysId(originId))
|
|
71
|
+
return;
|
|
72
|
+
const issueTag = payload?.issueTag ?? event.entityId;
|
|
73
|
+
const agentName = payload?.assigneeAgentName ?? "Agent";
|
|
74
|
+
const messages = {
|
|
75
|
+
in_progress: `${agentName} is working on this. Issue: ${issueTag}`,
|
|
76
|
+
in_review: `Code review in progress. Issue: ${issueTag}`,
|
|
77
|
+
blocked: `Issue ${issueTag} is blocked — awaiting board approval.`,
|
|
78
|
+
cancelled: `Issue ${issueTag} was cancelled.`,
|
|
79
|
+
};
|
|
80
|
+
const message = messages[newStatus];
|
|
81
|
+
if (!message)
|
|
82
|
+
return;
|
|
83
|
+
await snRequest(ctx, config, "PATCH", `/api/now/table/incident/${originId}`, {
|
|
84
|
+
work_notes: `[Paperclip] ${message}`,
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
// Sync agent comments to SN work notes
|
|
89
|
+
if (config.syncWorkNotes !== false) {
|
|
90
|
+
ctx.events.on("issue.comment.created", async (event) => {
|
|
91
|
+
const payload = event.payload;
|
|
92
|
+
const issue = payload?.issue;
|
|
93
|
+
const originId = issue?.originId;
|
|
94
|
+
if (!isSNSysId(originId))
|
|
95
|
+
return;
|
|
96
|
+
const agentName = payload?.authorAgentName ?? payload?.authorName ?? "Board";
|
|
97
|
+
const body = payload?.body ?? "";
|
|
98
|
+
const truncated = body.length > 3500 ? body.slice(0, 3500) + "\n\n[truncated]" : body;
|
|
99
|
+
await snRequest(ctx, config, "PATCH", `/api/now/table/incident/${originId}`, {
|
|
100
|
+
work_notes: `[Paperclip — ${agentName}] ${truncated}`,
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
ctx.logger.info("ServiceNow plugin initialized (outbound events active)");
|
|
105
|
+
},
|
|
106
|
+
// =========================================================
|
|
107
|
+
// INBOUND: SN webhook → create Paperclip issue
|
|
108
|
+
// =========================================================
|
|
109
|
+
async onWebhook(input) {
|
|
110
|
+
if (input.endpointKey !== "servicenow-incident")
|
|
111
|
+
return;
|
|
112
|
+
const ctx = _ctx;
|
|
113
|
+
const config = _config;
|
|
114
|
+
// Verify webhook secret
|
|
115
|
+
const providedSecret = Array.isArray(input.headers["x-sn-webhook-secret"])
|
|
116
|
+
? input.headers["x-sn-webhook-secret"][0]
|
|
117
|
+
: input.headers["x-sn-webhook-secret"];
|
|
118
|
+
const expectedSecret = await ctx.secrets.resolve(config.snWebhookSecretRef);
|
|
119
|
+
if (providedSecret !== expectedSecret) {
|
|
120
|
+
ctx.logger.warn("SN webhook rejected: invalid secret");
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const incident = input.parsedBody;
|
|
124
|
+
if (!incident?.number || !incident?.sys_id) {
|
|
125
|
+
ctx.logger.warn("SN webhook missing required fields");
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
// Create Paperclip issue
|
|
129
|
+
const description = [
|
|
130
|
+
`## ServiceNow Incident`,
|
|
131
|
+
``,
|
|
132
|
+
`**Number:** ${incident.number}`,
|
|
133
|
+
`**Priority:** ${incident.priority ?? "unknown"}`,
|
|
134
|
+
`**Category:** ${incident.category ?? ""}${incident.subcategory ? ` / ${incident.subcategory}` : ""}`,
|
|
135
|
+
`**Caller:** ${incident.caller_id ?? "unknown"}`,
|
|
136
|
+
`**Group:** ${incident.assignment_group ?? ""}`,
|
|
137
|
+
``,
|
|
138
|
+
`### Description`,
|
|
139
|
+
incident.description || "_No description provided_",
|
|
140
|
+
``,
|
|
141
|
+
`---`,
|
|
142
|
+
`_SN sys_id: ${incident.sys_id}_`,
|
|
143
|
+
].join("\n");
|
|
144
|
+
const issue = await ctx.issues.create({
|
|
145
|
+
companyId: config.targetCompanyId,
|
|
146
|
+
title: `[${incident.number}] ${incident.short_description}`,
|
|
147
|
+
description,
|
|
148
|
+
status: "todo",
|
|
149
|
+
priority: PRIORITY_MAP[incident.priority ?? "3"] ?? "medium",
|
|
150
|
+
assigneeAgentId: config.targetAgentId,
|
|
151
|
+
projectId: config.targetProjectId,
|
|
152
|
+
});
|
|
153
|
+
// Set correlation_id on SN incident
|
|
154
|
+
await snRequest(ctx, config, "PATCH", `/api/now/table/incident/${incident.sys_id}`, {
|
|
155
|
+
correlation_id: issue.id ?? "",
|
|
156
|
+
});
|
|
157
|
+
ctx.logger.info(`Created issue for ${incident.number}`);
|
|
158
|
+
},
|
|
159
|
+
async onHealth() {
|
|
160
|
+
return { status: "ok" };
|
|
161
|
+
},
|
|
162
|
+
async onShutdown() {
|
|
163
|
+
_config = null;
|
|
164
|
+
_ctx = null;
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
export default plugin;
|
|
168
|
+
runWorker(plugin, import.meta.url);
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bettsnation/paperclip-plugin-servicenow",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Paperclip plugin for bidirectional ServiceNow incident sync — webhook intake and auto-resolve",
|
|
6
|
+
"paperclipPlugin": {
|
|
7
|
+
"manifest": "./dist/manifest.js",
|
|
8
|
+
"worker": "./dist/worker.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"typecheck": "tsc --noEmit"
|
|
13
|
+
},
|
|
14
|
+
"peerDependencies": {
|
|
15
|
+
"@paperclipai/plugin-sdk": ">=0.1.0"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@paperclipai/plugin-sdk": "^2026.403.0",
|
|
19
|
+
"@types/node": "^22.0.0",
|
|
20
|
+
"typescript": "^5.7.0"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist/",
|
|
24
|
+
"README.md"
|
|
25
|
+
],
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"author": "BettsNation",
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "https://github.com/bettsnation/paperclip-plugin-servicenow.git"
|
|
31
|
+
}
|
|
32
|
+
}
|