@log-drop/web 1.0.0-alpha3 → 1.0.0-alpha4

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
@@ -9,7 +9,7 @@ LogDropConstants.DEFAULT_BASE_URL = "https://server.logdrop.io";
9
9
  LogDropConstants.DEFAULT_WEB_SECRET_KEY = "xuM9w0BAQEFx";
10
10
  LogDropConstants.FILE_EXPORT_INTERVAL_MS = 3e5;
11
11
  LogDropConstants.DEVICE_INFO_INTERVAL_MS = 1e4;
12
- LogDropConstants.SDK_VERSION = true ? "1.0.0-alpha3" : "1.0.0";
12
+ LogDropConstants.SDK_VERSION = true ? "1.0.0-alpha4" : "1.0.0";
13
13
  LogDropConstants.STORAGE_CHECK_INTERVAL_MS = 12e4;
14
14
  LogDropConstants.MAX_LOCAL_STORAGE_SIZE = 5242880;
15
15
  LogDropConstants.DATABASE_MAX_STORAGE_LIMIT = 16e6;
@@ -511,6 +511,168 @@ var NetworkInstrumentation = class {
511
511
  }
512
512
  };
513
513
 
514
+ // src/instrumentation/httpCapturePolicy.js
515
+ var ALWAYS_MASKED_HEADERS = /* @__PURE__ */ new Set([
516
+ "authorization",
517
+ "proxy-authorization",
518
+ "x-auth-token",
519
+ "x-api-key",
520
+ "x-session-token",
521
+ "cookie",
522
+ "set-cookie"
523
+ ]);
524
+ var MASK = "***";
525
+ var MAX_PATTERN_LENGTH = 500;
526
+ var DANGEROUS_PATTERN = /(a\+)\+|\(\.\*\)\+|\(\.\+\)\+/;
527
+ function normalizeHost(value) {
528
+ const host = String(value ?? "").trim().replace(/\.+$/, "").toLowerCase();
529
+ if (!host || /[/:?#@]/.test(host) || host.length > 253) return null;
530
+ return host;
531
+ }
532
+ function normalizePath(value) {
533
+ const trimmed = String(value ?? "").trim();
534
+ if (!trimmed) return "/";
535
+ const withSlash = trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
536
+ return withSlash.length > 1 ? withSlash.replace(/\/+$/, "") : withSlash;
537
+ }
538
+ function domainMatches(host, ruleHost) {
539
+ return host === ruleHost || host.endsWith(`.${ruleHost}`);
540
+ }
541
+ function pathMatches(path, rulePath, matchType) {
542
+ if (matchType === "EXACT") return path === rulePath;
543
+ if (matchType === "PREFIX") return rulePath === "/" || path === rulePath || path.startsWith(`${rulePath}/`);
544
+ return false;
545
+ }
546
+ function parseUrl(url) {
547
+ try {
548
+ const parsed = new URL(String(url ?? ""));
549
+ const host = normalizeHost(parsed.hostname);
550
+ return host ? { host, path: normalizePath(parsed.pathname) } : null;
551
+ } catch {
552
+ return null;
553
+ }
554
+ }
555
+ function compileSensitivePatterns(filters) {
556
+ const patterns = [];
557
+ for (const filter of Array.isArray(filters) ? filters : []) {
558
+ const source = String(filter ?? "");
559
+ if (!source || source.length > MAX_PATTERN_LENGTH) continue;
560
+ const parts = source.match(/^\/(.+)\/([gimsuy]*)$/);
561
+ const body = parts ? parts[1] : source;
562
+ if (DANGEROUS_PATTERN.test(body)) continue;
563
+ let flags = parts ? parts[2] : "";
564
+ if (!flags.includes("g")) flags += "g";
565
+ if (!flags.includes("i")) flags += "i";
566
+ try {
567
+ patterns.push(new RegExp(body, flags));
568
+ } catch {
569
+ }
570
+ }
571
+ return patterns;
572
+ }
573
+ function hashString(text) {
574
+ let hash = 2166136261;
575
+ for (let i = 0; i < text.length; i++) {
576
+ hash ^= text.charCodeAt(i);
577
+ hash = Math.imul(hash, 16777619);
578
+ }
579
+ return (hash >>> 0).toString(16);
580
+ }
581
+ function buildHttpCapturePolicy(config) {
582
+ const policy = {
583
+ domainBlacklist: (config?.httpDomainBlackList ?? []).map(normalizeHost).filter(Boolean),
584
+ pathBlacklist: Array.isArray(config?.httpPathBlackList) ? config.httpPathBlackList : [],
585
+ captureConfig: config?.httpPayloadCaptureConfig ?? null,
586
+ sensitivePatterns: compileSensitivePatterns(config?.sensitiveInfoFilter)
587
+ };
588
+ policy.fingerprint = hashString(JSON.stringify([
589
+ policy.domainBlacklist,
590
+ policy.pathBlacklist,
591
+ policy.captureConfig,
592
+ policy.sensitivePatterns.map((pattern) => `${pattern.source}/${pattern.flags}`)
593
+ ]));
594
+ return policy;
595
+ }
596
+ function shouldIgnoreHttpUrl(url, policy) {
597
+ const target = parseUrl(url);
598
+ if (!target || !policy) return false;
599
+ if (policy.domainBlacklist.some((domain) => domainMatches(target.host, domain))) return true;
600
+ return policy.pathBlacklist.some((rule) => {
601
+ const ruleHost = normalizeHost(rule?.host);
602
+ if (!ruleHost || !domainMatches(target.host, ruleHost)) return false;
603
+ return pathMatches(target.path, normalizePath(rule.path), rule.matchType);
604
+ });
605
+ }
606
+ function resolveDisabledPayloadFields(url, captureConfig) {
607
+ const disabled = {
608
+ requestHeaders: captureConfig?.disableRequestHeaders === true,
609
+ requestBody: captureConfig?.disableRequestBody === true,
610
+ responseHeaders: captureConfig?.disableResponseHeaders === true,
611
+ responseBody: captureConfig?.disableResponseBody === true
612
+ };
613
+ const target = parseUrl(url);
614
+ if (!captureConfig || !target) return disabled;
615
+ for (const rule of captureConfig.rules ?? []) {
616
+ const ruleHost = normalizeHost(rule?.host);
617
+ if (!ruleHost || !domainMatches(target.host, ruleHost)) continue;
618
+ if (rule.path && String(rule.path).trim() && !pathMatches(target.path, normalizePath(rule.path), rule.matchType)) {
619
+ continue;
620
+ }
621
+ disabled.requestHeaders || (disabled.requestHeaders = rule.disableRequestHeaders === true);
622
+ disabled.requestBody || (disabled.requestBody = rule.disableRequestBody === true);
623
+ disabled.responseHeaders || (disabled.responseHeaders = rule.disableResponseHeaders === true);
624
+ disabled.responseBody || (disabled.responseBody = rule.disableResponseBody === true);
625
+ }
626
+ return disabled;
627
+ }
628
+ function maskSensitive(text, patterns) {
629
+ if (typeof text !== "string" || !patterns?.length) return text;
630
+ let output = text;
631
+ for (const regex of patterns) {
632
+ regex.lastIndex = 0;
633
+ output = output.replace(regex, (...args) => {
634
+ const groups = args.slice(1, -2).filter((group) => typeof group === "string" || group === void 0);
635
+ const present = groups.filter((group) => group !== void 0 && group !== "");
636
+ if (present.length === 3) return `${groups[0]}${MASK}${groups[2]}`;
637
+ if (present.length === 2) return `${groups[0]}${MASK}`;
638
+ return MASK;
639
+ });
640
+ }
641
+ return output;
642
+ }
643
+ function maskStructured(value, patterns) {
644
+ if (value == null || !patterns.length) return value;
645
+ if (typeof value === "string") return maskSensitive(value, patterns);
646
+ try {
647
+ return JSON.parse(maskSensitive(JSON.stringify(value), patterns));
648
+ } catch {
649
+ return value;
650
+ }
651
+ }
652
+ function maskCredentialHeaders(headers) {
653
+ if (!headers || typeof headers !== "object") return {};
654
+ const out = {};
655
+ for (const [key, value] of Object.entries(headers)) {
656
+ out[key] = ALWAYS_MASKED_HEADERS.has(String(key).toLowerCase()) ? MASK : value;
657
+ }
658
+ return out;
659
+ }
660
+ function applyHttpCapturePolicy(payload, policy) {
661
+ if (!payload) return null;
662
+ const effective = policy ?? buildHttpCapturePolicy(null);
663
+ if (shouldIgnoreHttpUrl(payload.url, effective)) return null;
664
+ const disabled = resolveDisabledPayloadFields(payload.url, effective.captureConfig);
665
+ const patterns = effective.sensitivePatterns;
666
+ return {
667
+ ...payload,
668
+ url: maskSensitive(String(payload.url ?? ""), patterns),
669
+ requestHeaders: disabled.requestHeaders ? {} : maskStructured(maskCredentialHeaders(payload.requestHeaders), patterns),
670
+ requestBody: disabled.requestBody ? null : maskStructured(payload.requestBody ?? null, patterns),
671
+ responseHeaders: disabled.responseHeaders ? {} : maskStructured(maskCredentialHeaders(payload.responseHeaders), patterns),
672
+ responseBody: disabled.responseBody ? null : maskStructured(payload.responseBody ?? null, patterns)
673
+ };
674
+ }
675
+
514
676
  // src/push/pushModule.js
515
677
  function hasWindow2() {
516
678
  return typeof window !== "undefined";
@@ -1696,6 +1858,105 @@ function decodeWebPushConfig(buf) {
1696
1858
  }
1697
1859
  return result;
1698
1860
  }
1861
+ var HTTP_PATH_MATCH_TYPES = ["EXACT", "PREFIX"];
1862
+ function decodeHttpPathMatchType(value) {
1863
+ return HTTP_PATH_MATCH_TYPES[value] ?? null;
1864
+ }
1865
+ function decodeHttpPathBlacklistRule(buf) {
1866
+ const r = new ProtoReader(buf);
1867
+ const result = { host: "", path: "", matchType: "EXACT" };
1868
+ while (r.hasMore()) {
1869
+ const tag = r.readTag();
1870
+ switch (tag.fieldNumber) {
1871
+ case 1:
1872
+ result.host = r.readString();
1873
+ break;
1874
+ case 2:
1875
+ result.path = r.readString();
1876
+ break;
1877
+ case 3:
1878
+ result.matchType = decodeHttpPathMatchType(r.readVarint());
1879
+ break;
1880
+ default:
1881
+ r.skip(tag.wireType);
1882
+ }
1883
+ }
1884
+ return result;
1885
+ }
1886
+ function decodeHttpPayloadCaptureRule(buf) {
1887
+ const r = new ProtoReader(buf);
1888
+ const result = {
1889
+ host: "",
1890
+ path: null,
1891
+ matchType: "EXACT",
1892
+ disableRequestHeaders: false,
1893
+ disableRequestBody: false,
1894
+ disableResponseHeaders: false,
1895
+ disableResponseBody: false
1896
+ };
1897
+ while (r.hasMore()) {
1898
+ const tag = r.readTag();
1899
+ switch (tag.fieldNumber) {
1900
+ case 1:
1901
+ result.host = r.readString();
1902
+ break;
1903
+ case 2:
1904
+ result.path = r.readString();
1905
+ break;
1906
+ case 3:
1907
+ result.matchType = decodeHttpPathMatchType(r.readVarint());
1908
+ break;
1909
+ case 4:
1910
+ result.disableRequestHeaders = r.readBool();
1911
+ break;
1912
+ case 5:
1913
+ result.disableRequestBody = r.readBool();
1914
+ break;
1915
+ case 6:
1916
+ result.disableResponseHeaders = r.readBool();
1917
+ break;
1918
+ case 7:
1919
+ result.disableResponseBody = r.readBool();
1920
+ break;
1921
+ default:
1922
+ r.skip(tag.wireType);
1923
+ }
1924
+ }
1925
+ return result;
1926
+ }
1927
+ function decodeHttpPayloadCaptureConfig(buf) {
1928
+ const r = new ProtoReader(buf);
1929
+ const result = {
1930
+ disableRequestHeaders: false,
1931
+ disableRequestBody: false,
1932
+ disableResponseHeaders: false,
1933
+ disableResponseBody: false,
1934
+ rules: []
1935
+ };
1936
+ while (r.hasMore()) {
1937
+ const tag = r.readTag();
1938
+ switch (tag.fieldNumber) {
1939
+ case 1:
1940
+ result.disableRequestHeaders = r.readBool();
1941
+ break;
1942
+ case 2:
1943
+ result.disableRequestBody = r.readBool();
1944
+ break;
1945
+ case 3:
1946
+ result.disableResponseHeaders = r.readBool();
1947
+ break;
1948
+ case 4:
1949
+ result.disableResponseBody = r.readBool();
1950
+ break;
1951
+ case 5:
1952
+ result.rules.push(decodeHttpPayloadCaptureRule(r.readBytes()));
1953
+ break;
1954
+ default:
1955
+ r.skip(tag.wireType);
1956
+ }
1957
+ }
1958
+ return result;
1959
+ }
1699
1960
  function decodeSdkRiskConfig(buf) {
1700
1961
  const r = new ProtoReader(buf);
1701
1962
  const result = {
@@ -1732,6 +1993,8 @@ function decodeConfigResponse(buf) {
1732
1993
  isCrashTrackingEnabled: false,
1733
1994
  isEventTrackingEnabled: false,
1734
1995
  httpDomainBlackList: [],
1996
+ httpPathBlackList: [],
1997
+ httpPayloadCaptureConfig: null,
1735
1998
  sensitiveInfoFilter: [],
1736
1999
  sslPinning: null,
1737
2000
  accessToken: "",
@@ -1792,6 +2055,12 @@ function decodeConfigResponse(buf) {
1792
2055
  case 15:
1793
2056
  result.isEventTrackingEnabled = r.readBool();
1794
2057
  break;
2058
+ case 16:
2059
+ result.httpPathBlackList.push(decodeHttpPathBlacklistRule(r.readBytes()));
2060
+ break;
2061
+ case 17:
2062
+ result.httpPayloadCaptureConfig = decodeHttpPayloadCaptureConfig(r.readBytes());
2063
+ break;
1795
2064
  case 18:
1796
2065
  result.webPushConfig = decodeWebPushConfig(r.readBytes());
1797
2066
  break;
@@ -3475,6 +3744,7 @@ var LogDropClient = class {
3475
3744
  };
3476
3745
  this._activeIpReputationAction = this._loadStoredIpReputationAction();
3477
3746
  this._ipReputationExpiryTimer = null;
3747
+ this._httpCapturePolicy = null;
3478
3748
  this._loadCachedServerConfig();
3479
3749
  this._user = this._loadPersistedExternalUser();
3480
3750
  this._errorHandlersInstalled = false;
@@ -4301,6 +4571,7 @@ var LogDropClient = class {
4301
4571
  if (typeof parsed.data.isSDKDisabled === "boolean") {
4302
4572
  this._serverConfig.isSDKDisabled = parsed.data.isSDKDisabled;
4303
4573
  }
4574
+ this._httpCapturePolicy = buildHttpCapturePolicy(parsed.data);
4304
4575
  }
4305
4576
  }
4306
4577
  } catch {
@@ -4322,6 +4593,9 @@ var LogDropClient = class {
4322
4593
  if (typeof data.isHttpLoggingEnabled === "boolean") {
4323
4594
  this._serverConfig.isHttpLoggingEnabled = data.isHttpLoggingEnabled;
4324
4595
  }
4596
+ if ("httpDomainBlackList" in data || "httpPayloadCaptureConfig" in data || "sensitiveInfoFilter" in data) {
4597
+ this._httpCapturePolicy = buildHttpCapturePolicy(data);
4598
+ }
4325
4599
  if (typeof data.isCrashTrackingEnabled === "boolean" && this._crashTrackingOverride === null) {
4326
4600
  this._serverConfig.isCrashTrackingEnabled = data.isCrashTrackingEnabled;
4327
4601
  }
@@ -4381,7 +4655,7 @@ var LogDropClient = class {
4381
4655
  onEvent: (eventName, payload) => {
4382
4656
  if (eventName !== "network_activity") return;
4383
4657
  if (!this._serverConfig.isHttpLoggingEnabled || !this._config?.network?.enabled) return;
4384
- this._enqueue(LogLevel.NETWORK_ACTIVITY, payload, new LogFlow("NetworkActivity", payload.uuid));
4658
+ this._recordNetworkActivity(payload);
4385
4659
  },
4386
4660
  getBaseHref,
4387
4661
  excludeUrls: this._config.network.excludeUrls,
@@ -4400,6 +4674,31 @@ var LogDropClient = class {
4400
4674
  });
4401
4675
  this._net.install();
4402
4676
  }
4677
+ _recordNetworkActivity(payload) {
4678
+ const recorded = applyHttpCapturePolicy(payload, this._httpCapturePolicy);
4679
+ if (!recorded) return;
4680
+ this._enqueue(LogLevel.NETWORK_ACTIVITY, recorded, new LogFlow("NetworkActivity", recorded.uuid));
4681
+ }
4682
+ // Logs captured before the init response, or under rules that have changed
4683
+ // since, are filtered again with the current policy before upload; logs
4684
+ // stored under the same rules are sent as they are. Returns null when the
4685
+ // log must be dropped.
4686
+ _applyHttpCapturePolicyToStoredLog(log) {
4687
+ const policy = this._httpCapturePolicy;
4688
+ if (!policy || log.level === LogLevel.DEVICEINFO) return log.message;
4689
+ if (log.httpPolicyFingerprint === policy.fingerprint) return log.message;
4690
+ if (log.level !== LogLevel.NETWORK_ACTIVITY) {
4691
+ return maskSensitive(log.message, policy.sensitivePatterns);
4692
+ }
4693
+ let parsed;
4694
+ try {
4695
+ parsed = JSON.parse(log.message);
4696
+ } catch {
4697
+ return maskSensitive(log.message, policy.sensitivePatterns);
4698
+ }
4699
+ const recorded = applyHttpCapturePolicy(parsed, policy);
4700
+ return recorded ? safeStringify(recorded) : null;
4701
+ }
4403
4702
  _uninstallNetworkInstrumentation() {
4404
4703
  if (this._net) {
4405
4704
  this._net.uninstall();
@@ -5015,6 +5314,13 @@ var LogDropClient = class {
5015
5314
  // IMPORTANT: if message object convert => JSON string
5016
5315
  message: typeof message === "string" ? message : safeStringify(message)
5017
5316
  };
5317
+ const policy = this._httpCapturePolicy;
5318
+ if (policy && level !== LogLevel.DEVICEINFO) {
5319
+ if (level !== LogLevel.NETWORK_ACTIVITY) {
5320
+ entry.message = maskSensitive(entry.message, policy.sensitivePatterns);
5321
+ }
5322
+ entry.httpPolicyFingerprint = policy.fingerprint;
5323
+ }
5018
5324
  if (tag) {
5019
5325
  entry.tag = tag;
5020
5326
  }
@@ -5100,6 +5406,7 @@ var LogDropClient = class {
5100
5406
  message: entry.message
5101
5407
  };
5102
5408
  if (entry.tag) logRecord.tag = entry.tag;
5409
+ if (entry.httpPolicyFingerprint) logRecord.httpPolicyFingerprint = entry.httpPolicyFingerprint;
5103
5410
  if (logFlow && logFlow instanceof LogFlow) {
5104
5411
  logRecord.flowId = logFlow.id;
5105
5412
  logRecord.flowName = logFlow.name;
@@ -5401,11 +5708,16 @@ var LogDropClient = class {
5401
5708
  for (const [sessionId, logs] of Object.entries(sessionGroups)) {
5402
5709
  const sessionLogs = [];
5403
5710
  for (const log of logs) {
5711
+ const message = this._applyHttpCapturePolicyToStoredLog(log);
5712
+ if (message === null) {
5713
+ chunk.droppedLogIds.push(log.id);
5714
+ continue;
5715
+ }
5404
5716
  const logEntry = {
5405
5717
  id: log.id,
5406
5718
  ts: log.ts,
5407
5719
  level: log.level,
5408
- message: log.message,
5720
+ message,
5409
5721
  flowId: log.flowId,
5410
5722
  flowName: log.flowName,
5411
5723
  flowAttributes: log.flowAttributes
@@ -5623,7 +5935,8 @@ var LogDropClient = class {
5623
5935
  if (!this._dbPromise) return false;
5624
5936
  this._fileFlushInProgress = true;
5625
5937
  try {
5626
- const allLogs = await this._readAllLogs();
5938
+ const storedLogs = await this._readAllLogs();
5939
+ const allLogs = this._httpCapturePolicy ? storedLogs : storedLogs.filter((log) => log.level !== LogLevel.NETWORK_ACTIVITY);
5627
5940
  if (this._isInternalLoggingEnabled()) {
5628
5941
  this._logInternal(`[LogDrop DEBUG] _flushLogs called. DB log count: ${allLogs.length}`);
5629
5942
  }