trusera-sdk 1.0.0 → 1.1.1
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/dist/index.cjs +202 -12
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +21 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +202 -12
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -6,28 +6,56 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
|
|
|
6
6
|
});
|
|
7
7
|
|
|
8
8
|
// src/client.ts
|
|
9
|
+
var SDK_VERSION = "1.0.0";
|
|
9
10
|
var TruseraClient = class {
|
|
10
11
|
apiKey;
|
|
11
12
|
baseUrl;
|
|
12
13
|
batchSize;
|
|
14
|
+
maxQueueSize;
|
|
13
15
|
flushInterval;
|
|
14
16
|
debug;
|
|
15
17
|
agentId;
|
|
16
18
|
eventQueue = [];
|
|
17
19
|
flushTimer;
|
|
18
20
|
isClosed = false;
|
|
21
|
+
// Fleet auto-registration
|
|
22
|
+
autoRegister;
|
|
23
|
+
agentName;
|
|
24
|
+
agentType;
|
|
25
|
+
environment;
|
|
26
|
+
heartbeatInterval;
|
|
27
|
+
fleetAgentId;
|
|
28
|
+
heartbeatTimer;
|
|
19
29
|
constructor(options) {
|
|
20
30
|
this.apiKey = options.apiKey;
|
|
21
31
|
this.baseUrl = options.baseUrl ?? "https://api.trusera.io";
|
|
22
32
|
this.agentId = options.agentId;
|
|
23
33
|
this.batchSize = options.batchSize ?? 100;
|
|
34
|
+
this.maxQueueSize = options.maxQueueSize ?? 1e4;
|
|
24
35
|
this.flushInterval = options.flushInterval ?? 5e3;
|
|
25
36
|
this.debug = options.debug ?? false;
|
|
37
|
+
const envAuto = (typeof process !== "undefined" ? process.env?.TRUSERA_AUTO_REGISTER : void 0) ?? "";
|
|
38
|
+
if (envAuto.toLowerCase() === "true" || envAuto === "1") {
|
|
39
|
+
this.autoRegister = true;
|
|
40
|
+
} else if (envAuto.toLowerCase() === "false" || envAuto === "0") {
|
|
41
|
+
this.autoRegister = false;
|
|
42
|
+
} else {
|
|
43
|
+
this.autoRegister = options.autoRegister ?? false;
|
|
44
|
+
}
|
|
45
|
+
const hostname = typeof process !== "undefined" && process.env?.HOSTNAME ? process.env.HOSTNAME : typeof globalThis !== "undefined" && "navigator" in globalThis ? "browser" : "unknown";
|
|
46
|
+
this.agentName = options.agentName ?? (typeof process !== "undefined" ? process.env?.TRUSERA_AGENT_NAME : void 0) ?? hostname;
|
|
47
|
+
this.agentType = options.agentType ?? (typeof process !== "undefined" ? process.env?.TRUSERA_AGENT_TYPE : void 0) ?? "";
|
|
48
|
+
this.environment = options.environment ?? (typeof process !== "undefined" ? process.env?.TRUSERA_ENVIRONMENT : void 0) ?? "";
|
|
49
|
+
const envHb = typeof process !== "undefined" ? process.env?.TRUSERA_HEARTBEAT_INTERVAL : void 0;
|
|
50
|
+
this.heartbeatInterval = envHb ? parseInt(envHb, 10) * 1e3 : options.heartbeatInterval ?? 6e4;
|
|
26
51
|
if (!this.apiKey.startsWith("tsk_")) {
|
|
27
52
|
throw new Error("Invalid API key format. Must start with 'tsk_'");
|
|
28
53
|
}
|
|
29
54
|
this.startFlushTimer();
|
|
30
|
-
this.
|
|
55
|
+
if (this.autoRegister) {
|
|
56
|
+
void this.registerWithFleet();
|
|
57
|
+
}
|
|
58
|
+
this.log("TruseraClient initialized", { baseUrl: this.baseUrl, batchSize: this.batchSize, autoRegister: this.autoRegister });
|
|
31
59
|
}
|
|
32
60
|
/**
|
|
33
61
|
* Registers a new agent with Trusera backend.
|
|
@@ -49,7 +77,7 @@ var TruseraClient = class {
|
|
|
49
77
|
name,
|
|
50
78
|
framework,
|
|
51
79
|
metadata: {
|
|
52
|
-
sdk_version:
|
|
80
|
+
sdk_version: SDK_VERSION,
|
|
53
81
|
runtime: "node",
|
|
54
82
|
node_version: process.version
|
|
55
83
|
}
|
|
@@ -79,10 +107,14 @@ var TruseraClient = class {
|
|
|
79
107
|
metadata: {
|
|
80
108
|
...event.metadata,
|
|
81
109
|
agent_id: this.agentId,
|
|
82
|
-
sdk_version:
|
|
110
|
+
sdk_version: SDK_VERSION
|
|
83
111
|
}
|
|
84
112
|
};
|
|
85
113
|
this.eventQueue.push(enrichedEvent);
|
|
114
|
+
while (this.eventQueue.length > this.maxQueueSize) {
|
|
115
|
+
this.eventQueue.shift();
|
|
116
|
+
this.log("Event queue overflow, dropping oldest event");
|
|
117
|
+
}
|
|
86
118
|
this.log("Event tracked", { type: event.type, name: event.name, queueSize: this.eventQueue.length });
|
|
87
119
|
if (this.eventQueue.length >= this.batchSize) {
|
|
88
120
|
void this.flush();
|
|
@@ -131,6 +163,7 @@ var TruseraClient = class {
|
|
|
131
163
|
this.log("Closing client");
|
|
132
164
|
this.isClosed = true;
|
|
133
165
|
this.stopFlushTimer();
|
|
166
|
+
this.stopHeartbeat();
|
|
134
167
|
await this.flush();
|
|
135
168
|
this.log("Client closed");
|
|
136
169
|
}
|
|
@@ -146,6 +179,102 @@ var TruseraClient = class {
|
|
|
146
179
|
getAgentId() {
|
|
147
180
|
return this.agentId;
|
|
148
181
|
}
|
|
182
|
+
// -- Fleet auto-registration --
|
|
183
|
+
getProcessInfo() {
|
|
184
|
+
if (typeof process === "undefined") return {};
|
|
185
|
+
return {
|
|
186
|
+
pid: process.pid,
|
|
187
|
+
argv: process.argv?.slice(0, 2),
|
|
188
|
+
node_version: process.version,
|
|
189
|
+
platform: process.platform,
|
|
190
|
+
arch: process.arch
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
getNetworkInfo() {
|
|
194
|
+
const info = {};
|
|
195
|
+
if (typeof process !== "undefined") {
|
|
196
|
+
try {
|
|
197
|
+
const os = __require("os");
|
|
198
|
+
info.hostname = os.hostname?.();
|
|
199
|
+
} catch {
|
|
200
|
+
info.hostname = process.env?.HOSTNAME ?? "unknown";
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return info;
|
|
204
|
+
}
|
|
205
|
+
async registerWithFleet() {
|
|
206
|
+
const payload = {
|
|
207
|
+
name: this.agentName,
|
|
208
|
+
discovery_method: "sdk",
|
|
209
|
+
sdk_version: SDK_VERSION,
|
|
210
|
+
hostname: this.agentName,
|
|
211
|
+
process_info: this.getProcessInfo(),
|
|
212
|
+
network_info: this.getNetworkInfo()
|
|
213
|
+
};
|
|
214
|
+
if (this.agentType) payload.framework = this.agentType;
|
|
215
|
+
if (this.environment) payload.environment = this.environment;
|
|
216
|
+
try {
|
|
217
|
+
const response = await fetch(`${this.baseUrl}/api/v1/fleet/register`, {
|
|
218
|
+
method: "POST",
|
|
219
|
+
headers: {
|
|
220
|
+
"Content-Type": "application/json",
|
|
221
|
+
Authorization: `Bearer ${this.apiKey}`
|
|
222
|
+
},
|
|
223
|
+
body: JSON.stringify(payload)
|
|
224
|
+
});
|
|
225
|
+
if (!response.ok) {
|
|
226
|
+
this.log("Fleet auto-register failed", { status: response.status });
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
const data = await response.json();
|
|
230
|
+
const agentData = data.data ?? data;
|
|
231
|
+
const fleetId = agentData.id;
|
|
232
|
+
if (fleetId) {
|
|
233
|
+
this.fleetAgentId = String(fleetId);
|
|
234
|
+
this.startHeartbeat();
|
|
235
|
+
this.log("Fleet auto-register succeeded", { fleetAgentId: this.fleetAgentId });
|
|
236
|
+
}
|
|
237
|
+
} catch (err) {
|
|
238
|
+
this.log("Fleet auto-register error (continuing without)", { error: String(err) });
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
startHeartbeat() {
|
|
242
|
+
this.heartbeatTimer = setInterval(() => {
|
|
243
|
+
void this.sendHeartbeat();
|
|
244
|
+
}, this.heartbeatInterval);
|
|
245
|
+
if (this.heartbeatTimer.unref) {
|
|
246
|
+
this.heartbeatTimer.unref();
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
stopHeartbeat() {
|
|
250
|
+
if (this.heartbeatTimer) {
|
|
251
|
+
clearInterval(this.heartbeatTimer);
|
|
252
|
+
this.heartbeatTimer = void 0;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
async sendHeartbeat() {
|
|
256
|
+
if (!this.fleetAgentId) return;
|
|
257
|
+
try {
|
|
258
|
+
const payload = {
|
|
259
|
+
process_info: this.getProcessInfo(),
|
|
260
|
+
network_info: this.getNetworkInfo()
|
|
261
|
+
};
|
|
262
|
+
const response = await fetch(`${this.baseUrl}/api/v1/fleet/${this.fleetAgentId}/heartbeat`, {
|
|
263
|
+
method: "POST",
|
|
264
|
+
headers: {
|
|
265
|
+
"Content-Type": "application/json",
|
|
266
|
+
Authorization: `Bearer ${this.apiKey}`
|
|
267
|
+
},
|
|
268
|
+
body: JSON.stringify(payload)
|
|
269
|
+
});
|
|
270
|
+
if (!response.ok) {
|
|
271
|
+
this.log("Fleet heartbeat failed", { status: response.status });
|
|
272
|
+
}
|
|
273
|
+
} catch (err) {
|
|
274
|
+
this.log("Fleet heartbeat error", { error: String(err) });
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
// -- Timers --
|
|
149
278
|
startFlushTimer() {
|
|
150
279
|
this.flushTimer = setInterval(() => {
|
|
151
280
|
void this.flush();
|
|
@@ -203,13 +332,47 @@ function tryRequire(moduleName) {
|
|
|
203
332
|
return null;
|
|
204
333
|
}
|
|
205
334
|
}
|
|
335
|
+
var SENSITIVE_HEADERS = /* @__PURE__ */ new Set([
|
|
336
|
+
"authorization",
|
|
337
|
+
"cookie",
|
|
338
|
+
"set-cookie",
|
|
339
|
+
"x-api-key",
|
|
340
|
+
"x-n8n-api-key",
|
|
341
|
+
"proxy-authorization"
|
|
342
|
+
]);
|
|
343
|
+
function redactHeaders(headers) {
|
|
344
|
+
const redacted = {};
|
|
345
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
346
|
+
redacted[key] = SENSITIVE_HEADERS.has(key.toLowerCase()) ? "[REDACTED]" : value;
|
|
347
|
+
}
|
|
348
|
+
return redacted;
|
|
349
|
+
}
|
|
350
|
+
var MAX_PATTERN_LENGTH = 500;
|
|
351
|
+
var EVIL_REGEX_PATTERNS = /(\.\*){2,}|(\w\+){2,}|\(\[^[^\]]*\]\*\)\*|\(\.\+\)\+/;
|
|
352
|
+
function safeRegExp(pattern) {
|
|
353
|
+
if (pattern.length > MAX_PATTERN_LENGTH) {
|
|
354
|
+
console.warn(`[Trusera] Exclude pattern too long (${pattern.length} > ${MAX_PATTERN_LENGTH}), skipping`);
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
357
|
+
if (EVIL_REGEX_PATTERNS.test(pattern)) {
|
|
358
|
+
console.warn(`[Trusera] Potentially dangerous regex pattern detected, skipping: ${pattern.slice(0, 50)}`);
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
361
|
+
try {
|
|
362
|
+
return new RegExp(pattern);
|
|
363
|
+
} catch (e) {
|
|
364
|
+
console.warn(`[Trusera] Invalid regex pattern, skipping: ${pattern.slice(0, 50)}`);
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
206
368
|
var TruseraInterceptor = class {
|
|
207
369
|
client = null;
|
|
208
370
|
options = {
|
|
209
371
|
enforcement: "log",
|
|
210
372
|
policyUrl: "",
|
|
211
373
|
excludePatterns: [],
|
|
212
|
-
debug: false
|
|
374
|
+
debug: false,
|
|
375
|
+
failClosed: false
|
|
213
376
|
};
|
|
214
377
|
excludeRegexes = [];
|
|
215
378
|
isInstalled = false;
|
|
@@ -246,9 +409,10 @@ var TruseraInterceptor = class {
|
|
|
246
409
|
enforcement: options.enforcement ?? "log",
|
|
247
410
|
policyUrl: options.policyUrl ?? "",
|
|
248
411
|
excludePatterns: options.excludePatterns ?? [],
|
|
249
|
-
debug: options.debug ?? false
|
|
412
|
+
debug: options.debug ?? false,
|
|
413
|
+
failClosed: options.failClosed ?? false
|
|
250
414
|
};
|
|
251
|
-
this.excludeRegexes = this.options.excludePatterns.map((pattern) =>
|
|
415
|
+
this.excludeRegexes = this.options.excludePatterns.map((pattern) => safeRegExp(pattern)).filter((r) => r !== null);
|
|
252
416
|
if (originalFetch === null) {
|
|
253
417
|
originalFetch = globalThis.fetch;
|
|
254
418
|
}
|
|
@@ -295,7 +459,7 @@ var TruseraInterceptor = class {
|
|
|
295
459
|
const event = createEvent(
|
|
296
460
|
"api_call" /* API_CALL */,
|
|
297
461
|
eventName,
|
|
298
|
-
{ method, url, headers },
|
|
462
|
+
{ method, url, headers: redactHeaders(headers) },
|
|
299
463
|
{ interception_mode: this.options.enforcement }
|
|
300
464
|
);
|
|
301
465
|
if (this.options.policyUrl) {
|
|
@@ -570,7 +734,7 @@ var TruseraInterceptor = class {
|
|
|
570
734
|
response.headers.forEach((v, k) => {
|
|
571
735
|
h[k] = v;
|
|
572
736
|
});
|
|
573
|
-
return h;
|
|
737
|
+
return redactHeaders(h);
|
|
574
738
|
})()
|
|
575
739
|
}
|
|
576
740
|
);
|
|
@@ -615,7 +779,7 @@ var TruseraInterceptor = class {
|
|
|
615
779
|
body = "[Binary data]";
|
|
616
780
|
}
|
|
617
781
|
}
|
|
618
|
-
return { headers, body };
|
|
782
|
+
return { headers: redactHeaders(headers), body };
|
|
619
783
|
}
|
|
620
784
|
/**
|
|
621
785
|
* Evaluates request against Cedar policies.
|
|
@@ -642,11 +806,21 @@ var TruseraInterceptor = class {
|
|
|
642
806
|
});
|
|
643
807
|
if (!response.ok) {
|
|
644
808
|
console.error(`[Trusera] Policy evaluation failed: ${response.status}`);
|
|
809
|
+
if (this.options.failClosed && this.options.enforcement === "block") {
|
|
810
|
+
console.warn("[Trusera] FAIL-CLOSED: Policy service returned error, denying request");
|
|
811
|
+
return { decision: "Deny", reasons: [`Policy service error: ${response.status}`] };
|
|
812
|
+
}
|
|
813
|
+
console.warn("[Trusera] FAIL-OPEN: Policy service returned error, allowing request");
|
|
645
814
|
return { decision: "Allow" };
|
|
646
815
|
}
|
|
647
816
|
return await response.json();
|
|
648
817
|
} catch (error) {
|
|
649
818
|
console.error("[Trusera] Policy evaluation error:", error);
|
|
819
|
+
if (this.options.failClosed && this.options.enforcement === "block") {
|
|
820
|
+
console.warn("[Trusera] FAIL-CLOSED: Policy evaluation failed, denying request");
|
|
821
|
+
return { decision: "Deny", reasons: ["Policy evaluation failed (fail-closed mode)"] };
|
|
822
|
+
}
|
|
823
|
+
console.warn("[Trusera] FAIL-OPEN: Policy evaluation failed, allowing request");
|
|
650
824
|
return { decision: "Allow" };
|
|
651
825
|
}
|
|
652
826
|
}
|
|
@@ -851,6 +1025,24 @@ var CedarEvaluator = class {
|
|
|
851
1025
|
};
|
|
852
1026
|
|
|
853
1027
|
// src/standalone.ts
|
|
1028
|
+
var MAX_PATTERN_LENGTH2 = 500;
|
|
1029
|
+
var EVIL_REGEX_PATTERNS2 = /(\.\*){2,}|(\w\+){2,}|\(\[^[^\]]*\]\*\)\*|\(\.\+\)\+/;
|
|
1030
|
+
function safeRegExp2(pattern) {
|
|
1031
|
+
if (pattern.length > MAX_PATTERN_LENGTH2) {
|
|
1032
|
+
console.warn(`[Trusera Standalone] Exclude pattern too long, skipping`);
|
|
1033
|
+
return null;
|
|
1034
|
+
}
|
|
1035
|
+
if (EVIL_REGEX_PATTERNS2.test(pattern)) {
|
|
1036
|
+
console.warn(`[Trusera Standalone] Potentially dangerous regex pattern, skipping`);
|
|
1037
|
+
return null;
|
|
1038
|
+
}
|
|
1039
|
+
try {
|
|
1040
|
+
return new RegExp(pattern);
|
|
1041
|
+
} catch {
|
|
1042
|
+
console.warn(`[Trusera Standalone] Invalid regex pattern, skipping`);
|
|
1043
|
+
return null;
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
854
1046
|
var activeStandaloneInterceptor = null;
|
|
855
1047
|
var originalFetch2 = null;
|
|
856
1048
|
var StandaloneInterceptor = class {
|
|
@@ -867,9 +1059,7 @@ var StandaloneInterceptor = class {
|
|
|
867
1059
|
excludePatterns: options.excludePatterns ?? [],
|
|
868
1060
|
debug: options.debug ?? false
|
|
869
1061
|
};
|
|
870
|
-
this.excludeRegexes = this.options.excludePatterns.map(
|
|
871
|
-
(pattern) => new RegExp(pattern)
|
|
872
|
-
);
|
|
1062
|
+
this.excludeRegexes = this.options.excludePatterns.map((pattern) => safeRegExp2(pattern)).filter((r) => r !== null);
|
|
873
1063
|
}
|
|
874
1064
|
/**
|
|
875
1065
|
* Installs the standalone interceptor.
|