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.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.log("TruseraClient initialized", { baseUrl: this.baseUrl, batchSize: this.batchSize });
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: "0.1.0",
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: "0.1.0"
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) => new RegExp(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.