@sonde/packs 0.1.8 → 0.1.9
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/.turbo/turbo-build.log +1 -1
- package/.turbo/turbo-test.log +57 -1072
- package/CHANGELOG.md +9 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/integrations/keeper.d.ts +19 -0
- package/dist/integrations/keeper.d.ts.map +1 -0
- package/dist/integrations/keeper.js +95 -0
- package/dist/integrations/keeper.js.map +1 -0
- package/dist/integrations/servicenow.d.ts +2 -0
- package/dist/integrations/servicenow.d.ts.map +1 -1
- package/dist/integrations/servicenow.js +45 -6
- package/dist/integrations/servicenow.js.map +1 -1
- package/dist/integrations/servicenow.test.js +19 -8
- package/dist/integrations/servicenow.test.js.map +1 -1
- package/dist/integrations/unifi-access.d.ts +11 -0
- package/dist/integrations/unifi-access.d.ts.map +1 -0
- package/dist/integrations/unifi-access.js +130 -0
- package/dist/integrations/unifi-access.js.map +1 -0
- package/dist/integrations/unifi.d.ts +10 -0
- package/dist/integrations/unifi.d.ts.map +1 -0
- package/dist/integrations/unifi.js +215 -0
- package/dist/integrations/unifi.js.map +1 -0
- package/package.json +3 -2
- package/src/index.ts +3 -0
- package/src/integrations/keeper.ts +106 -0
- package/src/integrations/servicenow.test.ts +31 -8
- package/src/integrations/servicenow.ts +72 -6
- package/src/integrations/unifi-access.ts +194 -0
- package/src/integrations/unifi.ts +366 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
// --- API helper ---
|
|
2
|
+
/**
|
|
3
|
+
* Fetch a UniFi Network official API endpoint.
|
|
4
|
+
* Auth: X-API-KEY header. Base path: /proxy/network/integration
|
|
5
|
+
* Docs: Settings > Control Plane > Integrations on your controller,
|
|
6
|
+
* or https://developer.ui.com/network/v10.1.84/gettingstarted
|
|
7
|
+
*/
|
|
8
|
+
export async function unifiFetch(path, config, credentials, fetchFn) {
|
|
9
|
+
const apiKey = credentials.credentials.apiKey ?? '';
|
|
10
|
+
const endpoint = config.endpoint.replace(/\/$/, '');
|
|
11
|
+
const url = `${endpoint}/proxy/network/integration${path}`;
|
|
12
|
+
const res = await fetchFn(url, {
|
|
13
|
+
headers: {
|
|
14
|
+
'X-API-KEY': apiKey,
|
|
15
|
+
Accept: 'application/json',
|
|
16
|
+
...config.headers,
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
if (!res.ok) {
|
|
20
|
+
throw new Error(`UniFi Network API returned ${res.status}: ${res.statusText}`);
|
|
21
|
+
}
|
|
22
|
+
return (await res.json());
|
|
23
|
+
}
|
|
24
|
+
/** Collect all pages from a paginated endpoint (max 200 per page) */
|
|
25
|
+
async function fetchAllPages(path, config, credentials, fetchFn, maxItems = 1000) {
|
|
26
|
+
const results = [];
|
|
27
|
+
let offset = 0;
|
|
28
|
+
const limit = 200;
|
|
29
|
+
while (results.length < maxItems) {
|
|
30
|
+
const sep = path.includes('?') ? '&' : '?';
|
|
31
|
+
const page = await unifiFetch(`${path}${sep}offset=${offset}&limit=${limit}`, config, credentials, fetchFn);
|
|
32
|
+
results.push(...page.data);
|
|
33
|
+
if (results.length >= page.totalCount)
|
|
34
|
+
break;
|
|
35
|
+
offset += limit;
|
|
36
|
+
}
|
|
37
|
+
return results;
|
|
38
|
+
}
|
|
39
|
+
/** Resolve the siteId — uses the first site unless overridden */
|
|
40
|
+
async function resolveSiteId(config, credentials, fetchFn) {
|
|
41
|
+
const explicit = credentials.credentials.siteId ?? '';
|
|
42
|
+
if (explicit)
|
|
43
|
+
return explicit;
|
|
44
|
+
const sites = await unifiFetch('/v1/sites?limit=1', config, credentials, fetchFn);
|
|
45
|
+
const first = sites.data[0];
|
|
46
|
+
if (!first)
|
|
47
|
+
throw new Error('No sites found on this UniFi controller');
|
|
48
|
+
return first.id;
|
|
49
|
+
}
|
|
50
|
+
// --- Probe handlers ---
|
|
51
|
+
const appInfo = async (_params, config, credentials, fetchFn) => {
|
|
52
|
+
return unifiFetch('/v1/info', config, credentials, fetchFn);
|
|
53
|
+
};
|
|
54
|
+
const sites = async (_params, config, credentials, fetchFn) => {
|
|
55
|
+
const result = await fetchAllPages('/v1/sites', config, credentials, fetchFn);
|
|
56
|
+
return { sites: result, count: result.length };
|
|
57
|
+
};
|
|
58
|
+
const devices = async (_params, config, credentials, fetchFn) => {
|
|
59
|
+
const siteId = await resolveSiteId(config, credentials, fetchFn);
|
|
60
|
+
const result = await fetchAllPages(`/v1/sites/${siteId}/devices`, config, credentials, fetchFn);
|
|
61
|
+
return {
|
|
62
|
+
devices: result.map((d) => ({
|
|
63
|
+
id: d.id,
|
|
64
|
+
macAddress: d.macAddress,
|
|
65
|
+
ipAddress: d.ipAddress,
|
|
66
|
+
name: d.name,
|
|
67
|
+
model: d.model,
|
|
68
|
+
state: d.state,
|
|
69
|
+
firmwareVersion: d.firmwareVersion,
|
|
70
|
+
firmwareUpdatable: d.firmwareUpdatable,
|
|
71
|
+
features: d.features,
|
|
72
|
+
})),
|
|
73
|
+
count: result.length,
|
|
74
|
+
};
|
|
75
|
+
};
|
|
76
|
+
const deviceDetail = async (params, config, credentials, fetchFn) => {
|
|
77
|
+
const deviceId = params?.device_id ?? '';
|
|
78
|
+
if (!deviceId) {
|
|
79
|
+
throw new Error('device_id parameter is required (device UUID)');
|
|
80
|
+
}
|
|
81
|
+
const siteId = await resolveSiteId(config, credentials, fetchFn);
|
|
82
|
+
return unifiFetch(`/v1/sites/${siteId}/devices/${deviceId}`, config, credentials, fetchFn);
|
|
83
|
+
};
|
|
84
|
+
const deviceStats = async (params, config, credentials, fetchFn) => {
|
|
85
|
+
const deviceId = params?.device_id ?? '';
|
|
86
|
+
if (!deviceId) {
|
|
87
|
+
throw new Error('device_id parameter is required (device UUID)');
|
|
88
|
+
}
|
|
89
|
+
const siteId = await resolveSiteId(config, credentials, fetchFn);
|
|
90
|
+
return unifiFetch(`/v1/sites/${siteId}/devices/${deviceId}/statistics/latest`, config, credentials, fetchFn);
|
|
91
|
+
};
|
|
92
|
+
const clients = async (_params, config, credentials, fetchFn) => {
|
|
93
|
+
const siteId = await resolveSiteId(config, credentials, fetchFn);
|
|
94
|
+
const result = await fetchAllPages(`/v1/sites/${siteId}/clients`, config, credentials, fetchFn);
|
|
95
|
+
return {
|
|
96
|
+
clients: result.map((c) => ({
|
|
97
|
+
id: c.id,
|
|
98
|
+
name: c.name,
|
|
99
|
+
type: c.type,
|
|
100
|
+
ipAddress: c.ipAddress,
|
|
101
|
+
connectedAt: c.connectedAt,
|
|
102
|
+
})),
|
|
103
|
+
count: result.length,
|
|
104
|
+
};
|
|
105
|
+
};
|
|
106
|
+
const networks = async (_params, config, credentials, fetchFn) => {
|
|
107
|
+
const siteId = await resolveSiteId(config, credentials, fetchFn);
|
|
108
|
+
const result = await fetchAllPages(`/v1/sites/${siteId}/networks`, config, credentials, fetchFn);
|
|
109
|
+
return { networks: result, count: result.length };
|
|
110
|
+
};
|
|
111
|
+
const wans = async (_params, config, credentials, fetchFn) => {
|
|
112
|
+
const siteId = await resolveSiteId(config, credentials, fetchFn);
|
|
113
|
+
const result = await fetchAllPages(`/v1/sites/${siteId}/wans`, config, credentials, fetchFn);
|
|
114
|
+
return { wans: result, count: result.length };
|
|
115
|
+
};
|
|
116
|
+
// --- Pack definition ---
|
|
117
|
+
export const unifiPack = {
|
|
118
|
+
manifest: {
|
|
119
|
+
name: 'unifi',
|
|
120
|
+
type: 'integration',
|
|
121
|
+
version: '0.2.0',
|
|
122
|
+
description: 'Ubiquiti UniFi Network — devices, clients, networks, WAN, device stats (official API)',
|
|
123
|
+
requires: { groups: [], files: [], commands: [] },
|
|
124
|
+
probes: [
|
|
125
|
+
{
|
|
126
|
+
name: 'info',
|
|
127
|
+
description: 'Application version and basic info',
|
|
128
|
+
capability: 'observe',
|
|
129
|
+
params: {},
|
|
130
|
+
timeout: 10000,
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
name: 'sites',
|
|
134
|
+
description: 'List all sites on this controller',
|
|
135
|
+
capability: 'observe',
|
|
136
|
+
params: {},
|
|
137
|
+
timeout: 15000,
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
name: 'devices',
|
|
141
|
+
description: 'List adopted devices with state, model, firmware, features',
|
|
142
|
+
capability: 'observe',
|
|
143
|
+
params: {},
|
|
144
|
+
timeout: 15000,
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
name: 'device.detail',
|
|
148
|
+
description: 'Full device details including interfaces and uplink',
|
|
149
|
+
capability: 'observe',
|
|
150
|
+
params: {
|
|
151
|
+
device_id: {
|
|
152
|
+
type: 'string',
|
|
153
|
+
description: 'Device UUID',
|
|
154
|
+
required: true,
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
timeout: 15000,
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
name: 'device.stats',
|
|
161
|
+
description: 'Latest device statistics — CPU, memory, uptime, load averages',
|
|
162
|
+
capability: 'observe',
|
|
163
|
+
params: {
|
|
164
|
+
device_id: {
|
|
165
|
+
type: 'string',
|
|
166
|
+
description: 'Device UUID',
|
|
167
|
+
required: true,
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
timeout: 15000,
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
name: 'clients',
|
|
174
|
+
description: 'Connected clients with type, IP, connection time',
|
|
175
|
+
capability: 'observe',
|
|
176
|
+
params: {},
|
|
177
|
+
timeout: 15000,
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
name: 'networks',
|
|
181
|
+
description: 'List configured networks (VLANs, etc.)',
|
|
182
|
+
capability: 'observe',
|
|
183
|
+
params: {},
|
|
184
|
+
timeout: 15000,
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
name: 'wans',
|
|
188
|
+
description: 'WAN interface definitions',
|
|
189
|
+
capability: 'observe',
|
|
190
|
+
params: {},
|
|
191
|
+
timeout: 15000,
|
|
192
|
+
},
|
|
193
|
+
],
|
|
194
|
+
runbook: {
|
|
195
|
+
category: 'network',
|
|
196
|
+
probes: ['info', 'devices', 'clients'],
|
|
197
|
+
parallel: true,
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
handlers: {
|
|
201
|
+
info: appInfo,
|
|
202
|
+
sites,
|
|
203
|
+
devices,
|
|
204
|
+
'device.detail': deviceDetail,
|
|
205
|
+
'device.stats': deviceStats,
|
|
206
|
+
clients,
|
|
207
|
+
networks,
|
|
208
|
+
wans,
|
|
209
|
+
},
|
|
210
|
+
testConnection: async (config, credentials, fetchFn) => {
|
|
211
|
+
const result = await unifiFetch('/v1/info', config, credentials, fetchFn);
|
|
212
|
+
return typeof result.applicationVersion === 'string';
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
//# sourceMappingURL=unifi.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"unifi.js","sourceRoot":"","sources":["../../src/integrations/unifi.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sonde/packs",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
"typecheck": "tsc --noEmit"
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@sonde/shared": "*"
|
|
21
|
+
"@sonde/shared": "*",
|
|
22
|
+
"@keeper-security/secrets-manager-core": "17.4.0"
|
|
22
23
|
},
|
|
23
24
|
"devDependencies": {
|
|
24
25
|
"vitest": "^2.1.8"
|
package/src/index.ts
CHANGED
|
@@ -48,6 +48,9 @@ export { thousandeyesPack } from './integrations/thousandeyes.js';
|
|
|
48
48
|
export { merakiPack } from './integrations/meraki.js';
|
|
49
49
|
export { checkpointPack } from './integrations/checkpoint.js';
|
|
50
50
|
export { a10Pack } from './integrations/a10.js';
|
|
51
|
+
export { unifiPack } from './integrations/unifi.js';
|
|
52
|
+
export { unifiAccessPack } from './integrations/unifi-access.js';
|
|
53
|
+
export { keeperPack, initializeKeeper, regionToHostname } from './integrations/keeper.js';
|
|
51
54
|
export { proxmoxDiagnosticRunbooks } from './runbooks/proxmox.js';
|
|
52
55
|
export { nutanixDiagnosticRunbooks } from './runbooks/nutanix.js';
|
|
53
56
|
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { IntegrationPack } from '@sonde/shared';
|
|
2
|
+
|
|
3
|
+
const REGION_HOSTNAMES: Record<string, string> = {
|
|
4
|
+
US: 'keepersecurity.com',
|
|
5
|
+
EU: 'keepersecurity.eu',
|
|
6
|
+
AU: 'keepersecurity.com.au',
|
|
7
|
+
GOV: 'govcloud.keepersecurity.us',
|
|
8
|
+
JP: 'keepersecurity.jp',
|
|
9
|
+
CA: 'keepersecurity.ca',
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
async function loadSdk(): Promise<typeof import('@keeper-security/secrets-manager-core')> {
|
|
13
|
+
return await import('@keeper-security/secrets-manager-core');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Rebuild an in-memory KeyValueStorage from a previously
|
|
18
|
+
* serialized device config JSON string.
|
|
19
|
+
*/
|
|
20
|
+
export async function rebuildStorage(
|
|
21
|
+
deviceConfigJson: string,
|
|
22
|
+
): Promise<import('@keeper-security/secrets-manager-core').KeyValueStorage> {
|
|
23
|
+
const sdk = await loadSdk();
|
|
24
|
+
return sdk.inMemoryStorage(JSON.parse(deviceConfigJson));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Initialize a new Keeper device binding using a one-time access token.
|
|
29
|
+
* Returns the serialized device config JSON to store encrypted in the DB.
|
|
30
|
+
* The one-time token is consumed during this call and cannot be reused.
|
|
31
|
+
*/
|
|
32
|
+
export async function initializeKeeper(oneTimeToken: string, hostname?: string): Promise<string> {
|
|
33
|
+
const sdk = await loadSdk();
|
|
34
|
+
const configObj: Record<string, string> = {};
|
|
35
|
+
const storage = sdk.inMemoryStorage(configObj);
|
|
36
|
+
|
|
37
|
+
if (hostname) {
|
|
38
|
+
await storage.saveString('hostname', hostname);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
await sdk.initializeStorage(storage, oneTimeToken, hostname);
|
|
42
|
+
const { records } = await sdk.getSecrets({ storage });
|
|
43
|
+
|
|
44
|
+
// Verify the binding worked by checking we got a response
|
|
45
|
+
if (!records) {
|
|
46
|
+
throw new Error('Keeper initialization failed: no records returned');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return JSON.stringify(configObj);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Resolve the hostname for a given region code.
|
|
54
|
+
* Returns undefined for unknown regions (SDK will use its default).
|
|
55
|
+
*/
|
|
56
|
+
export function regionToHostname(region?: string): string | undefined {
|
|
57
|
+
if (!region) return undefined;
|
|
58
|
+
return REGION_HOSTNAMES[region.toUpperCase()];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export const keeperPack: IntegrationPack = {
|
|
62
|
+
manifest: {
|
|
63
|
+
name: 'keeper',
|
|
64
|
+
type: 'integration',
|
|
65
|
+
version: '0.1.0',
|
|
66
|
+
description: 'Keeper Secrets Manager — pull credentials from Keeper vault',
|
|
67
|
+
requires: { groups: [], files: [], commands: [] },
|
|
68
|
+
probes: [
|
|
69
|
+
{
|
|
70
|
+
name: 'list-records',
|
|
71
|
+
description: 'List accessible record UIDs and titles',
|
|
72
|
+
capability: 'observe',
|
|
73
|
+
timeout: 15000,
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
runbook: {
|
|
77
|
+
category: 'keeper',
|
|
78
|
+
probes: ['list-records'],
|
|
79
|
+
parallel: false,
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
handlers: {
|
|
84
|
+
'list-records': async (_params, _config, credentials) => {
|
|
85
|
+
const deviceConfig = credentials.credentials.deviceConfig;
|
|
86
|
+
if (!deviceConfig) throw new Error('Keeper device config not found');
|
|
87
|
+
const sdk = await loadSdk();
|
|
88
|
+
const storage = await rebuildStorage(deviceConfig);
|
|
89
|
+
const { records } = await sdk.getSecrets({ storage });
|
|
90
|
+
return records.map((r) => ({
|
|
91
|
+
uid: r.recordUid,
|
|
92
|
+
title: r.data.title,
|
|
93
|
+
type: r.data.type,
|
|
94
|
+
}));
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
testConnection: async (_config, credentials) => {
|
|
99
|
+
const deviceConfig = credentials.credentials.deviceConfig;
|
|
100
|
+
if (!deviceConfig) return false;
|
|
101
|
+
const sdk = await loadSdk();
|
|
102
|
+
const storage = await rebuildStorage(deviceConfig);
|
|
103
|
+
const { records } = await sdk.getSecrets({ storage });
|
|
104
|
+
return Array.isArray(records);
|
|
105
|
+
},
|
|
106
|
+
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { IntegrationConfig, IntegrationCredentials } from '@sonde/shared';
|
|
2
|
-
import { describe, expect, it, vi } from 'vitest';
|
|
3
|
-
import { servicenowPack } from './servicenow.js';
|
|
2
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { clearTokenCache, servicenowPack } from './servicenow.js';
|
|
4
4
|
|
|
5
5
|
const config: IntegrationConfig = {
|
|
6
6
|
endpoint: 'https://instance.service-now.com',
|
|
@@ -15,10 +15,11 @@ const basicCreds: IntegrationCredentials = {
|
|
|
15
15
|
const oauthCreds: IntegrationCredentials = {
|
|
16
16
|
packName: 'servicenow',
|
|
17
17
|
authMethod: 'oauth2',
|
|
18
|
-
credentials: {},
|
|
19
|
-
oauth2: { accessToken: 'my-token' },
|
|
18
|
+
credentials: { clientId: 'my-client-id', clientSecret: 'my-secret' },
|
|
20
19
|
};
|
|
21
20
|
|
|
21
|
+
afterEach(() => clearTokenCache());
|
|
22
|
+
|
|
22
23
|
const handler = (name: string) => {
|
|
23
24
|
const h = servicenowPack.handlers[name];
|
|
24
25
|
if (!h) throw new Error(`Handler ${name} not found`);
|
|
@@ -73,12 +74,34 @@ describe('servicenow pack', () => {
|
|
|
73
74
|
expect(init.headers.Authorization).toBe(expected);
|
|
74
75
|
});
|
|
75
76
|
|
|
76
|
-
it('
|
|
77
|
-
|
|
77
|
+
it('exchanges client_credentials for bearer token on oauth2 method', async () => {
|
|
78
|
+
let callCount = 0;
|
|
79
|
+
const fetchFn = vi.fn().mockImplementation((url: string) => {
|
|
80
|
+
callCount++;
|
|
81
|
+
if (callCount === 1) {
|
|
82
|
+
// Token exchange call to /oauth_token.do
|
|
83
|
+
expect(url).toBe('https://instance.service-now.com/oauth_token.do');
|
|
84
|
+
return Promise.resolve(
|
|
85
|
+
new Response(
|
|
86
|
+
JSON.stringify({ access_token: 'tok-123', expires_in: 1800 }),
|
|
87
|
+
{ status: 200, headers: { 'Content-Type': 'application/json' } },
|
|
88
|
+
),
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
// Table API call
|
|
92
|
+
return Promise.resolve(
|
|
93
|
+
new Response(
|
|
94
|
+
JSON.stringify({ result: [{ name: 'web01' }] }),
|
|
95
|
+
{ status: 200, headers: { 'Content-Type': 'application/json' } },
|
|
96
|
+
),
|
|
97
|
+
);
|
|
98
|
+
});
|
|
99
|
+
|
|
78
100
|
await handler('ci.lookup')({ query: 'web01' }, config, oauthCreds, fetchFn);
|
|
79
101
|
|
|
80
|
-
|
|
81
|
-
|
|
102
|
+
expect(fetchFn).toHaveBeenCalledTimes(2);
|
|
103
|
+
const apiInit = callArgs(fetchFn, 1)[1] as { headers: Record<string, string> };
|
|
104
|
+
expect(apiInit.headers.Authorization).toBe('Bearer tok-123');
|
|
82
105
|
});
|
|
83
106
|
});
|
|
84
107
|
|
|
@@ -6,12 +6,76 @@ import type {
|
|
|
6
6
|
IntegrationProbeHandler,
|
|
7
7
|
} from '@sonde/shared';
|
|
8
8
|
|
|
9
|
+
// --- OAuth2 client_credentials token cache ---
|
|
10
|
+
|
|
11
|
+
interface CachedToken {
|
|
12
|
+
accessToken: string;
|
|
13
|
+
expiresAt: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let tokenCache: CachedToken | null = null;
|
|
17
|
+
|
|
18
|
+
/** Clear the cached token (used for testing) */
|
|
19
|
+
export function clearTokenCache(): void {
|
|
20
|
+
tokenCache = null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Acquire or reuse a ServiceNow OAuth2 token via client_credentials grant.
|
|
25
|
+
* Requires Washington DC release or newer with the OAuth 2.0 plugin enabled
|
|
26
|
+
* and `glide.oauth.inbound.client.credential.grant_type.enabled = true`.
|
|
27
|
+
* Token endpoint: POST https://<instance>/oauth_token.do
|
|
28
|
+
*/
|
|
29
|
+
async function ensureAccessToken(
|
|
30
|
+
config: IntegrationConfig,
|
|
31
|
+
credentials: IntegrationCredentials,
|
|
32
|
+
fetchFn: FetchFn,
|
|
33
|
+
): Promise<string> {
|
|
34
|
+
if (tokenCache && Date.now() < tokenCache.expiresAt - 30_000) {
|
|
35
|
+
return tokenCache.accessToken;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const clientId = credentials.credentials.clientId ?? '';
|
|
39
|
+
const clientSecret = credentials.credentials.clientSecret ?? '';
|
|
40
|
+
const endpoint = config.endpoint.replace(/\/$/, '');
|
|
41
|
+
|
|
42
|
+
const res = await fetchFn(`${endpoint}/oauth_token.do`, {
|
|
43
|
+
method: 'POST',
|
|
44
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
45
|
+
body: new URLSearchParams({
|
|
46
|
+
grant_type: 'client_credentials',
|
|
47
|
+
client_id: clientId,
|
|
48
|
+
client_secret: clientSecret,
|
|
49
|
+
}).toString(),
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
if (!res.ok) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
`ServiceNow OAuth token request failed: ${res.status} ${res.statusText}`,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const data = (await res.json()) as {
|
|
59
|
+
access_token: string;
|
|
60
|
+
expires_in: number;
|
|
61
|
+
};
|
|
62
|
+
tokenCache = {
|
|
63
|
+
accessToken: data.access_token,
|
|
64
|
+
expiresAt: Date.now() + data.expires_in * 1000,
|
|
65
|
+
};
|
|
66
|
+
return tokenCache.accessToken;
|
|
67
|
+
}
|
|
68
|
+
|
|
9
69
|
/** Build Authorization header based on auth method */
|
|
10
|
-
function buildAuthHeaders(
|
|
11
|
-
|
|
12
|
-
|
|
70
|
+
async function buildAuthHeaders(
|
|
71
|
+
config: IntegrationConfig,
|
|
72
|
+
credentials: IntegrationCredentials,
|
|
73
|
+
fetchFn: FetchFn,
|
|
74
|
+
): Promise<Record<string, string>> {
|
|
75
|
+
if (credentials.authMethod === 'oauth2') {
|
|
76
|
+
const token = await ensureAccessToken(config, credentials, fetchFn);
|
|
77
|
+
return { Authorization: `Bearer ${token}` };
|
|
13
78
|
}
|
|
14
|
-
// api_key = basic auth with username:password
|
|
15
79
|
const { username, password } = credentials.credentials;
|
|
16
80
|
if (username && password) {
|
|
17
81
|
const encoded = Buffer.from(`${username}:${password}`).toString('base64');
|
|
@@ -45,9 +109,10 @@ async function snowFetch(
|
|
|
45
109
|
credentials: IntegrationCredentials,
|
|
46
110
|
fetchFn: FetchFn,
|
|
47
111
|
): Promise<{ result: unknown[] }> {
|
|
112
|
+
const authHeaders = await buildAuthHeaders(config, credentials, fetchFn);
|
|
48
113
|
const headers: Record<string, string> = {
|
|
49
114
|
Accept: 'application/json',
|
|
50
|
-
...
|
|
115
|
+
...authHeaders,
|
|
51
116
|
...config.headers,
|
|
52
117
|
};
|
|
53
118
|
const res = await fetchFn(url, { headers });
|
|
@@ -269,9 +334,10 @@ export const servicenowPack: IntegrationPack = {
|
|
|
269
334
|
const url = buildUrl(config.endpoint, 'sys_properties', undefined, ['name']);
|
|
270
335
|
const parsed = new URL(url);
|
|
271
336
|
parsed.searchParams.set('sysparm_limit', '1');
|
|
337
|
+
const authHeaders = await buildAuthHeaders(config, credentials, fetchFn);
|
|
272
338
|
const headers: Record<string, string> = {
|
|
273
339
|
Accept: 'application/json',
|
|
274
|
-
...
|
|
340
|
+
...authHeaders,
|
|
275
341
|
...config.headers,
|
|
276
342
|
};
|
|
277
343
|
const res = await fetchFn(parsed.toString(), { headers });
|