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 CHANGED
@@ -42,28 +42,56 @@ __export(index_exports, {
42
42
  module.exports = __toCommonJS(index_exports);
43
43
 
44
44
  // src/client.ts
45
+ var SDK_VERSION = "1.0.0";
45
46
  var TruseraClient = class {
46
47
  apiKey;
47
48
  baseUrl;
48
49
  batchSize;
50
+ maxQueueSize;
49
51
  flushInterval;
50
52
  debug;
51
53
  agentId;
52
54
  eventQueue = [];
53
55
  flushTimer;
54
56
  isClosed = false;
57
+ // Fleet auto-registration
58
+ autoRegister;
59
+ agentName;
60
+ agentType;
61
+ environment;
62
+ heartbeatInterval;
63
+ fleetAgentId;
64
+ heartbeatTimer;
55
65
  constructor(options) {
56
66
  this.apiKey = options.apiKey;
57
67
  this.baseUrl = options.baseUrl ?? "https://api.trusera.io";
58
68
  this.agentId = options.agentId;
59
69
  this.batchSize = options.batchSize ?? 100;
70
+ this.maxQueueSize = options.maxQueueSize ?? 1e4;
60
71
  this.flushInterval = options.flushInterval ?? 5e3;
61
72
  this.debug = options.debug ?? false;
73
+ const envAuto = (typeof process !== "undefined" ? process.env?.TRUSERA_AUTO_REGISTER : void 0) ?? "";
74
+ if (envAuto.toLowerCase() === "true" || envAuto === "1") {
75
+ this.autoRegister = true;
76
+ } else if (envAuto.toLowerCase() === "false" || envAuto === "0") {
77
+ this.autoRegister = false;
78
+ } else {
79
+ this.autoRegister = options.autoRegister ?? false;
80
+ }
81
+ const hostname = typeof process !== "undefined" && process.env?.HOSTNAME ? process.env.HOSTNAME : typeof globalThis !== "undefined" && "navigator" in globalThis ? "browser" : "unknown";
82
+ this.agentName = options.agentName ?? (typeof process !== "undefined" ? process.env?.TRUSERA_AGENT_NAME : void 0) ?? hostname;
83
+ this.agentType = options.agentType ?? (typeof process !== "undefined" ? process.env?.TRUSERA_AGENT_TYPE : void 0) ?? "";
84
+ this.environment = options.environment ?? (typeof process !== "undefined" ? process.env?.TRUSERA_ENVIRONMENT : void 0) ?? "";
85
+ const envHb = typeof process !== "undefined" ? process.env?.TRUSERA_HEARTBEAT_INTERVAL : void 0;
86
+ this.heartbeatInterval = envHb ? parseInt(envHb, 10) * 1e3 : options.heartbeatInterval ?? 6e4;
62
87
  if (!this.apiKey.startsWith("tsk_")) {
63
88
  throw new Error("Invalid API key format. Must start with 'tsk_'");
64
89
  }
65
90
  this.startFlushTimer();
66
- this.log("TruseraClient initialized", { baseUrl: this.baseUrl, batchSize: this.batchSize });
91
+ if (this.autoRegister) {
92
+ void this.registerWithFleet();
93
+ }
94
+ this.log("TruseraClient initialized", { baseUrl: this.baseUrl, batchSize: this.batchSize, autoRegister: this.autoRegister });
67
95
  }
68
96
  /**
69
97
  * Registers a new agent with Trusera backend.
@@ -85,7 +113,7 @@ var TruseraClient = class {
85
113
  name,
86
114
  framework,
87
115
  metadata: {
88
- sdk_version: "0.1.0",
116
+ sdk_version: SDK_VERSION,
89
117
  runtime: "node",
90
118
  node_version: process.version
91
119
  }
@@ -115,10 +143,14 @@ var TruseraClient = class {
115
143
  metadata: {
116
144
  ...event.metadata,
117
145
  agent_id: this.agentId,
118
- sdk_version: "0.1.0"
146
+ sdk_version: SDK_VERSION
119
147
  }
120
148
  };
121
149
  this.eventQueue.push(enrichedEvent);
150
+ while (this.eventQueue.length > this.maxQueueSize) {
151
+ this.eventQueue.shift();
152
+ this.log("Event queue overflow, dropping oldest event");
153
+ }
122
154
  this.log("Event tracked", { type: event.type, name: event.name, queueSize: this.eventQueue.length });
123
155
  if (this.eventQueue.length >= this.batchSize) {
124
156
  void this.flush();
@@ -167,6 +199,7 @@ var TruseraClient = class {
167
199
  this.log("Closing client");
168
200
  this.isClosed = true;
169
201
  this.stopFlushTimer();
202
+ this.stopHeartbeat();
170
203
  await this.flush();
171
204
  this.log("Client closed");
172
205
  }
@@ -182,6 +215,102 @@ var TruseraClient = class {
182
215
  getAgentId() {
183
216
  return this.agentId;
184
217
  }
218
+ // -- Fleet auto-registration --
219
+ getProcessInfo() {
220
+ if (typeof process === "undefined") return {};
221
+ return {
222
+ pid: process.pid,
223
+ argv: process.argv?.slice(0, 2),
224
+ node_version: process.version,
225
+ platform: process.platform,
226
+ arch: process.arch
227
+ };
228
+ }
229
+ getNetworkInfo() {
230
+ const info = {};
231
+ if (typeof process !== "undefined") {
232
+ try {
233
+ const os = require("os");
234
+ info.hostname = os.hostname?.();
235
+ } catch {
236
+ info.hostname = process.env?.HOSTNAME ?? "unknown";
237
+ }
238
+ }
239
+ return info;
240
+ }
241
+ async registerWithFleet() {
242
+ const payload = {
243
+ name: this.agentName,
244
+ discovery_method: "sdk",
245
+ sdk_version: SDK_VERSION,
246
+ hostname: this.agentName,
247
+ process_info: this.getProcessInfo(),
248
+ network_info: this.getNetworkInfo()
249
+ };
250
+ if (this.agentType) payload.framework = this.agentType;
251
+ if (this.environment) payload.environment = this.environment;
252
+ try {
253
+ const response = await fetch(`${this.baseUrl}/api/v1/fleet/register`, {
254
+ method: "POST",
255
+ headers: {
256
+ "Content-Type": "application/json",
257
+ Authorization: `Bearer ${this.apiKey}`
258
+ },
259
+ body: JSON.stringify(payload)
260
+ });
261
+ if (!response.ok) {
262
+ this.log("Fleet auto-register failed", { status: response.status });
263
+ return;
264
+ }
265
+ const data = await response.json();
266
+ const agentData = data.data ?? data;
267
+ const fleetId = agentData.id;
268
+ if (fleetId) {
269
+ this.fleetAgentId = String(fleetId);
270
+ this.startHeartbeat();
271
+ this.log("Fleet auto-register succeeded", { fleetAgentId: this.fleetAgentId });
272
+ }
273
+ } catch (err) {
274
+ this.log("Fleet auto-register error (continuing without)", { error: String(err) });
275
+ }
276
+ }
277
+ startHeartbeat() {
278
+ this.heartbeatTimer = setInterval(() => {
279
+ void this.sendHeartbeat();
280
+ }, this.heartbeatInterval);
281
+ if (this.heartbeatTimer.unref) {
282
+ this.heartbeatTimer.unref();
283
+ }
284
+ }
285
+ stopHeartbeat() {
286
+ if (this.heartbeatTimer) {
287
+ clearInterval(this.heartbeatTimer);
288
+ this.heartbeatTimer = void 0;
289
+ }
290
+ }
291
+ async sendHeartbeat() {
292
+ if (!this.fleetAgentId) return;
293
+ try {
294
+ const payload = {
295
+ process_info: this.getProcessInfo(),
296
+ network_info: this.getNetworkInfo()
297
+ };
298
+ const response = await fetch(`${this.baseUrl}/api/v1/fleet/${this.fleetAgentId}/heartbeat`, {
299
+ method: "POST",
300
+ headers: {
301
+ "Content-Type": "application/json",
302
+ Authorization: `Bearer ${this.apiKey}`
303
+ },
304
+ body: JSON.stringify(payload)
305
+ });
306
+ if (!response.ok) {
307
+ this.log("Fleet heartbeat failed", { status: response.status });
308
+ }
309
+ } catch (err) {
310
+ this.log("Fleet heartbeat error", { error: String(err) });
311
+ }
312
+ }
313
+ // -- Timers --
185
314
  startFlushTimer() {
186
315
  this.flushTimer = setInterval(() => {
187
316
  void this.flush();
@@ -239,13 +368,47 @@ function tryRequire(moduleName) {
239
368
  return null;
240
369
  }
241
370
  }
371
+ var SENSITIVE_HEADERS = /* @__PURE__ */ new Set([
372
+ "authorization",
373
+ "cookie",
374
+ "set-cookie",
375
+ "x-api-key",
376
+ "x-n8n-api-key",
377
+ "proxy-authorization"
378
+ ]);
379
+ function redactHeaders(headers) {
380
+ const redacted = {};
381
+ for (const [key, value] of Object.entries(headers)) {
382
+ redacted[key] = SENSITIVE_HEADERS.has(key.toLowerCase()) ? "[REDACTED]" : value;
383
+ }
384
+ return redacted;
385
+ }
386
+ var MAX_PATTERN_LENGTH = 500;
387
+ var EVIL_REGEX_PATTERNS = /(\.\*){2,}|(\w\+){2,}|\(\[^[^\]]*\]\*\)\*|\(\.\+\)\+/;
388
+ function safeRegExp(pattern) {
389
+ if (pattern.length > MAX_PATTERN_LENGTH) {
390
+ console.warn(`[Trusera] Exclude pattern too long (${pattern.length} > ${MAX_PATTERN_LENGTH}), skipping`);
391
+ return null;
392
+ }
393
+ if (EVIL_REGEX_PATTERNS.test(pattern)) {
394
+ console.warn(`[Trusera] Potentially dangerous regex pattern detected, skipping: ${pattern.slice(0, 50)}`);
395
+ return null;
396
+ }
397
+ try {
398
+ return new RegExp(pattern);
399
+ } catch (e) {
400
+ console.warn(`[Trusera] Invalid regex pattern, skipping: ${pattern.slice(0, 50)}`);
401
+ return null;
402
+ }
403
+ }
242
404
  var TruseraInterceptor = class {
243
405
  client = null;
244
406
  options = {
245
407
  enforcement: "log",
246
408
  policyUrl: "",
247
409
  excludePatterns: [],
248
- debug: false
410
+ debug: false,
411
+ failClosed: false
249
412
  };
250
413
  excludeRegexes = [];
251
414
  isInstalled = false;
@@ -282,9 +445,10 @@ var TruseraInterceptor = class {
282
445
  enforcement: options.enforcement ?? "log",
283
446
  policyUrl: options.policyUrl ?? "",
284
447
  excludePatterns: options.excludePatterns ?? [],
285
- debug: options.debug ?? false
448
+ debug: options.debug ?? false,
449
+ failClosed: options.failClosed ?? false
286
450
  };
287
- this.excludeRegexes = this.options.excludePatterns.map((pattern) => new RegExp(pattern));
451
+ this.excludeRegexes = this.options.excludePatterns.map((pattern) => safeRegExp(pattern)).filter((r) => r !== null);
288
452
  if (originalFetch === null) {
289
453
  originalFetch = globalThis.fetch;
290
454
  }
@@ -331,7 +495,7 @@ var TruseraInterceptor = class {
331
495
  const event = createEvent(
332
496
  "api_call" /* API_CALL */,
333
497
  eventName,
334
- { method, url, headers },
498
+ { method, url, headers: redactHeaders(headers) },
335
499
  { interception_mode: this.options.enforcement }
336
500
  );
337
501
  if (this.options.policyUrl) {
@@ -606,7 +770,7 @@ var TruseraInterceptor = class {
606
770
  response.headers.forEach((v, k) => {
607
771
  h[k] = v;
608
772
  });
609
- return h;
773
+ return redactHeaders(h);
610
774
  })()
611
775
  }
612
776
  );
@@ -651,7 +815,7 @@ var TruseraInterceptor = class {
651
815
  body = "[Binary data]";
652
816
  }
653
817
  }
654
- return { headers, body };
818
+ return { headers: redactHeaders(headers), body };
655
819
  }
656
820
  /**
657
821
  * Evaluates request against Cedar policies.
@@ -678,11 +842,21 @@ var TruseraInterceptor = class {
678
842
  });
679
843
  if (!response.ok) {
680
844
  console.error(`[Trusera] Policy evaluation failed: ${response.status}`);
845
+ if (this.options.failClosed && this.options.enforcement === "block") {
846
+ console.warn("[Trusera] FAIL-CLOSED: Policy service returned error, denying request");
847
+ return { decision: "Deny", reasons: [`Policy service error: ${response.status}`] };
848
+ }
849
+ console.warn("[Trusera] FAIL-OPEN: Policy service returned error, allowing request");
681
850
  return { decision: "Allow" };
682
851
  }
683
852
  return await response.json();
684
853
  } catch (error) {
685
854
  console.error("[Trusera] Policy evaluation error:", error);
855
+ if (this.options.failClosed && this.options.enforcement === "block") {
856
+ console.warn("[Trusera] FAIL-CLOSED: Policy evaluation failed, denying request");
857
+ return { decision: "Deny", reasons: ["Policy evaluation failed (fail-closed mode)"] };
858
+ }
859
+ console.warn("[Trusera] FAIL-OPEN: Policy evaluation failed, allowing request");
686
860
  return { decision: "Allow" };
687
861
  }
688
862
  }
@@ -887,6 +1061,24 @@ var CedarEvaluator = class {
887
1061
  };
888
1062
 
889
1063
  // src/standalone.ts
1064
+ var MAX_PATTERN_LENGTH2 = 500;
1065
+ var EVIL_REGEX_PATTERNS2 = /(\.\*){2,}|(\w\+){2,}|\(\[^[^\]]*\]\*\)\*|\(\.\+\)\+/;
1066
+ function safeRegExp2(pattern) {
1067
+ if (pattern.length > MAX_PATTERN_LENGTH2) {
1068
+ console.warn(`[Trusera Standalone] Exclude pattern too long, skipping`);
1069
+ return null;
1070
+ }
1071
+ if (EVIL_REGEX_PATTERNS2.test(pattern)) {
1072
+ console.warn(`[Trusera Standalone] Potentially dangerous regex pattern, skipping`);
1073
+ return null;
1074
+ }
1075
+ try {
1076
+ return new RegExp(pattern);
1077
+ } catch {
1078
+ console.warn(`[Trusera Standalone] Invalid regex pattern, skipping`);
1079
+ return null;
1080
+ }
1081
+ }
890
1082
  var activeStandaloneInterceptor = null;
891
1083
  var originalFetch2 = null;
892
1084
  var StandaloneInterceptor = class {
@@ -903,9 +1095,7 @@ var StandaloneInterceptor = class {
903
1095
  excludePatterns: options.excludePatterns ?? [],
904
1096
  debug: options.debug ?? false
905
1097
  };
906
- this.excludeRegexes = this.options.excludePatterns.map(
907
- (pattern) => new RegExp(pattern)
908
- );
1098
+ this.excludeRegexes = this.options.excludePatterns.map((pattern) => safeRegExp2(pattern)).filter((r) => r !== null);
909
1099
  }
910
1100
  /**
911
1101
  * Installs the standalone interceptor.