@hustleops/n8n-nodes-hustleops 0.1.5

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.
@@ -0,0 +1,366 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MAX_JSON_PARAMETER_CHARS = void 0;
4
+ exports.normalizeBaseUrl = normalizeBaseUrl;
5
+ exports.isUuid = isUuid;
6
+ exports.assertUuid = assertUuid;
7
+ exports.parseIntegerInRange = parseIntegerInRange;
8
+ exports.safePathSegment = safePathSegment;
9
+ exports.compactObject = compactObject;
10
+ exports.redactSensitiveText = redactSensitiveText;
11
+ exports.assertPaginatedResponse = assertPaginatedResponse;
12
+ exports.parsePositiveInteger = parsePositiveInteger;
13
+ exports.parseJsonObject = parseJsonObject;
14
+ exports.parseJsonArray = parseJsonArray;
15
+ exports.testHustleOpsApiCredentials = testHustleOpsApiCredentials;
16
+ exports.createHustleOpsApiClient = createHustleOpsApiClient;
17
+ exports.hustleOpsApiRequest = hustleOpsApiRequest;
18
+ exports.hustleOpsApiRequestEachPage = hustleOpsApiRequestEachPage;
19
+ const n8n_workflow_1 = require("n8n-workflow");
20
+ const constants_1 = require("./constants");
21
+ exports.MAX_JSON_PARAMETER_CHARS = 100000;
22
+ const MAX_ERROR_MESSAGE_CHARS = 1000;
23
+ const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
24
+ const HELPER_NODE = {
25
+ id: 'hustleops-helper',
26
+ name: 'HustleOps',
27
+ type: 'hustleOps',
28
+ typeVersion: 1,
29
+ position: [0, 0],
30
+ parameters: {},
31
+ };
32
+ function isLocalHttpHost(hostname) {
33
+ return (hostname === 'localhost' ||
34
+ hostname === '127.0.0.1' ||
35
+ hostname === '::1' ||
36
+ hostname === '[::1]');
37
+ }
38
+ function normalizeBaseUrl(input) {
39
+ const trimmed = input.trim().replace(/\/+$/, '');
40
+ if (trimmed === '') {
41
+ throw new Error('Base URL is required.');
42
+ }
43
+ let url;
44
+ try {
45
+ url = new URL(trimmed);
46
+ }
47
+ catch {
48
+ throw new n8n_workflow_1.NodeOperationError(HELPER_NODE, 'Base URL must be a valid URL.');
49
+ }
50
+ if (url.protocol !== 'https:' && url.protocol !== 'http:') {
51
+ throw new Error('Base URL must use HTTP or HTTPS.');
52
+ }
53
+ if (url.protocol === 'http:' && !isLocalHttpHost(url.hostname)) {
54
+ throw new Error('HTTPS is required unless the Base URL host is localhost, 127.0.0.1, or ::1.');
55
+ }
56
+ if (url.username !== '' || url.password !== '') {
57
+ throw new Error('Base URL must not contain embedded credentials.');
58
+ }
59
+ if (url.search !== '') {
60
+ throw new Error('Base URL must not contain query strings.');
61
+ }
62
+ if (url.hash !== '') {
63
+ throw new Error('Base URL must not contain fragments.');
64
+ }
65
+ const path = url.pathname.replace(/\/+$/, '');
66
+ url.pathname = path.endsWith('/api/v1') ? path : `${path}/api/v1`;
67
+ return url.toString().replace(/\/$/, '');
68
+ }
69
+ function isUuid(value) {
70
+ return typeof value === 'string' && UUID_PATTERN.test(value.trim());
71
+ }
72
+ function assertUuid(value, label) {
73
+ if (!isUuid(value)) {
74
+ throw new Error(`${label} must be a valid UUID.`);
75
+ }
76
+ return value.trim();
77
+ }
78
+ function parseIntegerInRange(value, label, minimum, maximum, defaultValue) {
79
+ const numericValue = typeof value === 'number'
80
+ ? value
81
+ : typeof value === 'string' && value.trim() !== ''
82
+ ? Number(value)
83
+ : (defaultValue !== null && defaultValue !== void 0 ? defaultValue : Number.NaN);
84
+ if (!Number.isInteger(numericValue) || numericValue < minimum || numericValue > maximum) {
85
+ throw new Error(`${label} must be between ${minimum} and ${maximum}.`);
86
+ }
87
+ return numericValue;
88
+ }
89
+ function safePathSegment(value, label) {
90
+ return encodeURIComponent(assertUuid(value, label));
91
+ }
92
+ function compactObject(value) {
93
+ if (Array.isArray(value)) {
94
+ return value.filter((item) => item !== undefined).map((item) => compactObject(item));
95
+ }
96
+ if (value && typeof value === 'object') {
97
+ const output = {};
98
+ for (const [key, child] of Object.entries(value)) {
99
+ if (child !== undefined) {
100
+ output[key] = compactObject(child);
101
+ }
102
+ }
103
+ return output;
104
+ }
105
+ return value;
106
+ }
107
+ function buildRequestId(itemIndex) {
108
+ return `hustleops-n8n-${Date.now()}-${itemIndex}`;
109
+ }
110
+ function shouldIgnoreSslIssues(value) {
111
+ return value === true || value === 'true';
112
+ }
113
+ function getErrorBody(error) {
114
+ var _a, _b, _c, _d, _e, _f;
115
+ if (error && typeof error === 'object') {
116
+ const maybeError = error;
117
+ const statusCode = (_b = (_a = maybeError.response) === null || _a === void 0 ? void 0 : _a.statusCode) !== null && _b !== void 0 ? _b : (_c = maybeError.response) === null || _c === void 0 ? void 0 : _c.status;
118
+ const body = (_e = (_d = maybeError.response) === null || _d === void 0 ? void 0 : _d.body) !== null && _e !== void 0 ? _e : (_f = maybeError.response) === null || _f === void 0 ? void 0 : _f.data;
119
+ if (body && typeof body === 'object' && !Array.isArray(body)) {
120
+ return {
121
+ ...(typeof statusCode === 'number' ? { statusCode } : {}),
122
+ ...body,
123
+ };
124
+ }
125
+ if (typeof body === 'string' && body.trim() !== '') {
126
+ try {
127
+ const parsed = JSON.parse(body);
128
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
129
+ return {
130
+ ...(typeof statusCode === 'number' ? { statusCode } : {}),
131
+ ...parsed,
132
+ };
133
+ }
134
+ }
135
+ catch {
136
+ return {
137
+ ...(typeof statusCode === 'number' ? { statusCode } : {}),
138
+ message: body,
139
+ };
140
+ }
141
+ }
142
+ if (typeof statusCode === 'number') {
143
+ return { statusCode };
144
+ }
145
+ }
146
+ return {};
147
+ }
148
+ function redactUrlQuery(value) {
149
+ return value.replace(/(\/api\/v\d+\/[^?\s]+)\?[^,\s}]*/g, '$1?[REDACTED]');
150
+ }
151
+ function redactSensitiveText(value) {
152
+ return redactUrlQuery(value)
153
+ .replace(/Authorization:\s*Bearer\s+[A-Za-z0-9._~+/=-]+/gi, 'Authorization: Bearer [REDACTED]')
154
+ .replace(/Bearer\s+[A-Za-z0-9._~+/=-]+/gi, 'Bearer [REDACTED]')
155
+ .replace(/x-api-key\s*(?:[:=]|\s)\s*[A-Za-z0-9._~+/=-]+/gi, 'x-api-key [REDACTED]')
156
+ .replace(/ho_sk_[A-Za-z0-9._~+/=-]+/g, 'ho_sk_[REDACTED]')
157
+ .replace(/\b(apiKey|token|secret|password)\s*[:=]\s*[^,\s}]+/gi, '$1=[REDACTED]')
158
+ .slice(0, MAX_ERROR_MESSAGE_CHARS);
159
+ }
160
+ function formatApiError(error) {
161
+ const body = getErrorBody(error);
162
+ const statusCode = body.statusCode;
163
+ const message = Array.isArray(body.message)
164
+ ? body.message.join(', ')
165
+ : typeof body.message === 'string'
166
+ ? body.message
167
+ : error instanceof Error
168
+ ? error.message
169
+ : String(error);
170
+ const requestId = typeof body.requestId === 'string' ? ` requestId=${body.requestId}` : '';
171
+ const path = typeof body.path === 'string' ? ` path=${redactUrlQuery(body.path)}` : '';
172
+ const status = typeof statusCode === 'number' ? ` ${statusCode}` : '';
173
+ return redactSensitiveText(`HustleOps API error${status}: ${message}${requestId}${path}`);
174
+ }
175
+ function assertPaginatedResponse(value, label) {
176
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
177
+ throw new Error(`${label} must be a paginated response object.`);
178
+ }
179
+ const response = value;
180
+ if (!Array.isArray(response.data)) {
181
+ throw new Error(`${label} must include a data array.`);
182
+ }
183
+ for (const field of ['total', 'page', 'pageSize', 'totalPages']) {
184
+ const minimum = field === 'total' || field === 'totalPages' ? 0 : 1;
185
+ if (!Number.isInteger(response[field]) || response[field] < minimum) {
186
+ throw new Error(`${label} must include integer ${field}.`);
187
+ }
188
+ }
189
+ return response;
190
+ }
191
+ function parsePositiveInteger(context, value, label, itemIndex) {
192
+ const numericValue = typeof value === 'number'
193
+ ? value
194
+ : typeof value === 'string' && value.trim() !== ''
195
+ ? Number(value)
196
+ : Number.NaN;
197
+ if (!Number.isInteger(numericValue) || numericValue < 1) {
198
+ throw new n8n_workflow_1.NodeOperationError(context.getNode(), `${label} must be a positive integer.`, {
199
+ itemIndex,
200
+ });
201
+ }
202
+ return numericValue;
203
+ }
204
+ function parseJsonObject(context, value, fieldName, itemIndex) {
205
+ if (value === undefined || value === null || value === '') {
206
+ return {};
207
+ }
208
+ if (typeof value === 'object' && !Array.isArray(value)) {
209
+ return value;
210
+ }
211
+ if (typeof value !== 'string') {
212
+ throw new n8n_workflow_1.NodeOperationError(context.getNode(), `${fieldName} must be a JSON object.`, {
213
+ itemIndex,
214
+ });
215
+ }
216
+ if (value.length > exports.MAX_JSON_PARAMETER_CHARS) {
217
+ throw new n8n_workflow_1.NodeOperationError(context.getNode(), `${fieldName} is too large. Keep JSON parameters under ${exports.MAX_JSON_PARAMETER_CHARS} characters.`, { itemIndex });
218
+ }
219
+ try {
220
+ const parsed = JSON.parse(value);
221
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
222
+ throw new Error('not an object');
223
+ }
224
+ return parsed;
225
+ }
226
+ catch {
227
+ throw new n8n_workflow_1.NodeOperationError(context.getNode(), `${fieldName} must be a valid JSON object.`, {
228
+ itemIndex,
229
+ });
230
+ }
231
+ }
232
+ function parseJsonArray(context, value, fieldName, itemIndex) {
233
+ if (value === undefined || value === null || value === '') {
234
+ return [];
235
+ }
236
+ if (Array.isArray(value)) {
237
+ return value;
238
+ }
239
+ if (typeof value !== 'string') {
240
+ throw new n8n_workflow_1.NodeOperationError(context.getNode(), `${fieldName} must be a JSON array.`, {
241
+ itemIndex,
242
+ });
243
+ }
244
+ if (value.length > exports.MAX_JSON_PARAMETER_CHARS) {
245
+ throw new n8n_workflow_1.NodeOperationError(context.getNode(), `${fieldName} is too large. Keep JSON parameters under ${exports.MAX_JSON_PARAMETER_CHARS} characters.`, { itemIndex });
246
+ }
247
+ try {
248
+ const parsed = JSON.parse(value);
249
+ if (!Array.isArray(parsed)) {
250
+ throw new Error('not an array');
251
+ }
252
+ return parsed;
253
+ }
254
+ catch {
255
+ throw new n8n_workflow_1.NodeOperationError(context.getNode(), `${fieldName} must be a valid JSON array.`, {
256
+ itemIndex,
257
+ });
258
+ }
259
+ }
260
+ async function testHustleOpsApiCredentials(context, credentialData) {
261
+ var _a, _b, _c;
262
+ const baseUrl = normalizeBaseUrl(String((_a = credentialData.baseUrl) !== null && _a !== void 0 ? _a : ''));
263
+ const apiKey = String((_b = credentialData.apiKey) !== null && _b !== void 0 ? _b : '');
264
+ const ignoreSslIssues = shouldIgnoreSslIssues(credentialData.ignoreSslIssues);
265
+ const helpers = context.helpers;
266
+ const usesHttpRequest = !!helpers.httpRequest;
267
+ const request = (_c = helpers.httpRequest) !== null && _c !== void 0 ? _c : helpers.request;
268
+ if (!request) {
269
+ throw new Error('n8n credential-test HTTP helper is not available.');
270
+ }
271
+ try {
272
+ await request.call(helpers, {
273
+ method: 'GET',
274
+ url: `${baseUrl}/tags`,
275
+ headers: {
276
+ [constants_1.HUSTLEOPS_API_KEY_HEADER]: apiKey,
277
+ Accept: 'application/json',
278
+ 'x-request-id': buildRequestId(0),
279
+ },
280
+ json: true,
281
+ ...(ignoreSslIssues && usesHttpRequest ? { skipSslCertificateValidation: true } : {}),
282
+ ...(ignoreSslIssues && !usesHttpRequest ? { rejectUnauthorized: false } : {}),
283
+ });
284
+ }
285
+ catch (error) {
286
+ throw new n8n_workflow_1.NodeOperationError(HELPER_NODE, formatApiError(error));
287
+ }
288
+ }
289
+ async function createHustleOpsApiClient(context, itemIndex) {
290
+ const credentials = (await context.getCredentials('hustleOpsApi'));
291
+ const baseUrl = normalizeBaseUrl(credentials.baseUrl);
292
+ const ignoreSslIssues = shouldIgnoreSslIssues(credentials.ignoreSslIssues);
293
+ function buildRequestUrl(path, query) {
294
+ const url = new URL(`${baseUrl}${path}`);
295
+ for (const [key, value] of Object.entries(query !== null && query !== void 0 ? query : {})) {
296
+ if (value !== undefined && value !== null && value !== '') {
297
+ url.searchParams.set(key, String(value));
298
+ }
299
+ }
300
+ return url.toString();
301
+ }
302
+ const request = async (method, path, body, query) => {
303
+ const hasJsonBody = body !== undefined;
304
+ try {
305
+ return (await context.helpers.httpRequest({
306
+ method,
307
+ url: buildRequestUrl(path, query),
308
+ headers: {
309
+ [constants_1.HUSTLEOPS_API_KEY_HEADER]: credentials.apiKey,
310
+ Accept: 'application/json',
311
+ 'x-request-id': buildRequestId(itemIndex),
312
+ ...(hasJsonBody ? { 'Content-Type': 'application/json' } : {}),
313
+ },
314
+ body: hasJsonBody ? compactObject(body) : undefined,
315
+ json: true,
316
+ ...(ignoreSslIssues ? { skipSslCertificateValidation: true } : {}),
317
+ }));
318
+ }
319
+ catch (error) {
320
+ throw new n8n_workflow_1.NodeOperationError(context.getNode(), formatApiError(error), { itemIndex });
321
+ }
322
+ };
323
+ const requestEachPage = async (path, initialBody, options, onRow) => {
324
+ var _a, _b, _c;
325
+ const firstPagination = ((_a = initialBody.pagination) !== null && _a !== void 0 ? _a : {});
326
+ const pageSize = typeof firstPagination.pageSize === 'number' ? firstPagination.pageSize : 25;
327
+ const maxItems = (_b = options.maxItems) !== null && _b !== void 0 ? _b : Number.POSITIVE_INFINITY;
328
+ const maxPages = (_c = options.maxPages) !== null && _c !== void 0 ? _c : Number.POSITIVE_INFINITY;
329
+ let emitted = 0;
330
+ let pagesFetched = 0;
331
+ let page = typeof firstPagination.page === 'number' ? firstPagination.page : 1;
332
+ let totalPages = 1;
333
+ do {
334
+ if (emitted >= maxItems) {
335
+ return;
336
+ }
337
+ const response = assertPaginatedResponse(await request('POST', path, {
338
+ ...initialBody,
339
+ pagination: {
340
+ ...firstPagination,
341
+ page,
342
+ pageSize,
343
+ },
344
+ }), `${path} paginated response`);
345
+ for (const row of response.data) {
346
+ if (emitted >= maxItems) {
347
+ return;
348
+ }
349
+ onRow(row);
350
+ emitted += 1;
351
+ }
352
+ totalPages = response.totalPages;
353
+ page += 1;
354
+ pagesFetched += 1;
355
+ } while (page <= totalPages && pagesFetched < maxPages);
356
+ };
357
+ return { request, requestEachPage };
358
+ }
359
+ async function hustleOpsApiRequest(context, method, path, body, itemIndex, query) {
360
+ const client = await createHustleOpsApiClient(context, itemIndex);
361
+ return client.request(method, path, body, query);
362
+ }
363
+ async function hustleOpsApiRequestEachPage(context, path, initialBody, itemIndex, options, onRow) {
364
+ const client = await createHustleOpsApiClient(context, itemIndex);
365
+ await client.requestEachPage(path, initialBody, options, onRow);
366
+ }
@@ -0,0 +1,29 @@
1
+ import type { ICredentialDataDecryptedObject, ICredentialTestFunctions, IExecuteFunctions, ILoadOptionsFunctions, INodeCredentialTestResult, INodeExecutionData, INodePropertyOptions, INodeType, INodeTypeDescription } from 'n8n-workflow';
2
+ import { type CoreResource } from './resourceDefinitions';
3
+ import { type EntityTagOperation } from './tagDefinitions';
4
+ export type CoreOperation = 'search' | 'count' | 'get' | 'create' | 'update';
5
+ export type HustleOpsOperation = CoreOperation | EntityTagOperation;
6
+ export type HustleOpsResource = CoreResource | 'comment' | 'tag' | 'customField';
7
+ export declare class HustleOps implements INodeType {
8
+ description: INodeTypeDescription;
9
+ methods: {
10
+ credentialTest: {
11
+ hustleOps(this: ICredentialTestFunctions, credential: {
12
+ data?: ICredentialDataDecryptedObject;
13
+ }): Promise<INodeCredentialTestResult>;
14
+ };
15
+ loadOptions: {
16
+ getTagOptions(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]>;
17
+ getCustomFieldDefinitionOptions(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]>;
18
+ getAlertTypeOptions(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]>;
19
+ getAlertStatusOptions(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]>;
20
+ getIncidentStatusOptions(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]>;
21
+ getIncidentCategoryOptions(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]>;
22
+ getObservableTypeOptions(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]>;
23
+ getThreatLevelOptions(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]>;
24
+ getCriticalityOptions(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]>;
25
+ getKnowledgeTypeOptions(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]>;
26
+ };
27
+ };
28
+ execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]>;
29
+ }