@diegotsi/flint-core 1.10.1 → 1.10.2
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 +105 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.js +105 -2
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -6,6 +6,8 @@ interface NetworkCollector {
|
|
|
6
6
|
interface NetworkCollectorOptions {
|
|
7
7
|
maxEntries?: number;
|
|
8
8
|
maxResponseBody?: number;
|
|
9
|
+
/** Extra key names (substring, case-insensitive) to redact from URLs, bodies and headers. */
|
|
10
|
+
redactKeys?: string[];
|
|
9
11
|
}
|
|
10
12
|
declare function createNetworkCollector(extraBlockedHosts?: string[], options?: NetworkCollectorOptions): NetworkCollector;
|
|
11
13
|
|
|
@@ -157,6 +159,8 @@ interface FlintConfig {
|
|
|
157
159
|
enableReplay?: boolean;
|
|
158
160
|
replayBufferMs?: number;
|
|
159
161
|
blockedHosts?: string[];
|
|
162
|
+
/** Extra key names (substring, case-insensitive) to redact from captured network URLs, bodies and headers, on top of the built-in defaults (password, token, secret, …). */
|
|
163
|
+
redactKeys?: string[];
|
|
160
164
|
frustration?: {
|
|
161
165
|
rageClickThreshold?: number;
|
|
162
166
|
rageClickWindow?: number;
|
|
@@ -184,7 +188,7 @@ interface FlintConfig {
|
|
|
184
188
|
/** @internal Inject platform-specific collectors (e.g. React Native) */
|
|
185
189
|
_collectors?: {
|
|
186
190
|
console?: () => ConsoleCollector;
|
|
187
|
-
network?: (blockedHosts: string[]) => NetworkCollector;
|
|
191
|
+
network?: (blockedHosts: string[], options?: NetworkCollectorOptions) => NetworkCollector;
|
|
188
192
|
environment?: () => EnvironmentInfo;
|
|
189
193
|
};
|
|
190
194
|
/** @internal Injected by framework packages to start replay without rrweb in core */
|
package/dist/index.js
CHANGED
|
@@ -763,6 +763,100 @@ function createFrustrationCollector(opts) {
|
|
|
763
763
|
};
|
|
764
764
|
}
|
|
765
765
|
|
|
766
|
+
// src/collectors/sanitize.ts
|
|
767
|
+
var REDACTED = "[REDACTED]";
|
|
768
|
+
var DEFAULT_REDACT_KEYS = [
|
|
769
|
+
"password",
|
|
770
|
+
"passwd",
|
|
771
|
+
"pwd",
|
|
772
|
+
"secret",
|
|
773
|
+
"token",
|
|
774
|
+
"auth",
|
|
775
|
+
"credential",
|
|
776
|
+
"apikey",
|
|
777
|
+
"api_key",
|
|
778
|
+
"session",
|
|
779
|
+
"cookie",
|
|
780
|
+
"ssn",
|
|
781
|
+
"creditcard",
|
|
782
|
+
"credit_card",
|
|
783
|
+
"card_number",
|
|
784
|
+
"cardnumber",
|
|
785
|
+
"cvv",
|
|
786
|
+
"cvc",
|
|
787
|
+
"pin"
|
|
788
|
+
];
|
|
789
|
+
function createKeyMatcher(extra = []) {
|
|
790
|
+
const needles = [...DEFAULT_REDACT_KEYS, ...extra.map((k) => k.toLowerCase())];
|
|
791
|
+
return (key) => {
|
|
792
|
+
const lower = key.toLowerCase();
|
|
793
|
+
return needles.some((n) => lower.includes(n));
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
function redactValue(value, match) {
|
|
797
|
+
if (Array.isArray(value)) return value.map((v) => redactValue(v, match));
|
|
798
|
+
if (value && typeof value === "object") {
|
|
799
|
+
const out = {};
|
|
800
|
+
for (const [k, v] of Object.entries(value)) {
|
|
801
|
+
out[k] = match(k) ? REDACTED : redactValue(v, match);
|
|
802
|
+
}
|
|
803
|
+
return out;
|
|
804
|
+
}
|
|
805
|
+
return value;
|
|
806
|
+
}
|
|
807
|
+
function redactQueryString(qs, match) {
|
|
808
|
+
if (!qs) return qs;
|
|
809
|
+
return qs.split("&").map((pair) => {
|
|
810
|
+
const eq = pair.indexOf("=");
|
|
811
|
+
if (eq === -1) return pair;
|
|
812
|
+
const rawKey = pair.slice(0, eq);
|
|
813
|
+
let key = rawKey;
|
|
814
|
+
try {
|
|
815
|
+
key = decodeURIComponent(rawKey.replace(/\+/g, " "));
|
|
816
|
+
} catch {
|
|
817
|
+
}
|
|
818
|
+
return match(key) ? `${rawKey}=${REDACTED}` : pair;
|
|
819
|
+
}).join("&");
|
|
820
|
+
}
|
|
821
|
+
function sanitizeBody(body, match) {
|
|
822
|
+
if (!body) return body;
|
|
823
|
+
const trimmed = body.trimStart();
|
|
824
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
|
825
|
+
try {
|
|
826
|
+
const parsed = JSON.parse(body);
|
|
827
|
+
return JSON.stringify(redactValue(parsed, match));
|
|
828
|
+
} catch {
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
if (body.includes("=") && !/\s/.test(body)) {
|
|
832
|
+
const redacted = redactQueryString(body, match);
|
|
833
|
+
if (redacted !== body) return redacted;
|
|
834
|
+
}
|
|
835
|
+
return body.replace(
|
|
836
|
+
/("?)([\w.-]+)\1\s*:\s*"(?:[^"\\]|\\.)*"/gi,
|
|
837
|
+
(full, q, key) => match(key) ? `${q}${key}${q}:"${REDACTED}"` : full
|
|
838
|
+
).replace(/([\w.-]+)=([^&\s]+)/gi, (full, key) => match(key) ? `${key}=${REDACTED}` : full);
|
|
839
|
+
}
|
|
840
|
+
function sanitizeUrl(url, match) {
|
|
841
|
+
const qIdx = url.indexOf("?");
|
|
842
|
+
if (qIdx === -1) return url;
|
|
843
|
+
const base = url.slice(0, qIdx);
|
|
844
|
+
const query = url.slice(qIdx + 1);
|
|
845
|
+
const hashIdx = query.indexOf("#");
|
|
846
|
+
const qs = hashIdx === -1 ? query : query.slice(0, hashIdx);
|
|
847
|
+
const hash = hashIdx === -1 ? "" : query.slice(hashIdx);
|
|
848
|
+
const redacted = redactQueryString(qs, match);
|
|
849
|
+
return `${base}?${redacted}${hash}`;
|
|
850
|
+
}
|
|
851
|
+
function sanitizeHeaders(headers, match) {
|
|
852
|
+
if (!headers) return headers;
|
|
853
|
+
const out = {};
|
|
854
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
855
|
+
out[k] = match(k) ? REDACTED : v;
|
|
856
|
+
}
|
|
857
|
+
return out;
|
|
858
|
+
}
|
|
859
|
+
|
|
766
860
|
// src/collectors/network.ts
|
|
767
861
|
var MAX_ENTRIES3 = 50;
|
|
768
862
|
var MAX_FULL_URL = 2e3;
|
|
@@ -938,6 +1032,7 @@ function extractContentType(headers) {
|
|
|
938
1032
|
function createNetworkCollector(extraBlockedHosts = [], options) {
|
|
939
1033
|
const maxEntries = options?.maxEntries ?? MAX_ENTRIES3;
|
|
940
1034
|
const maxResponseBody = options?.maxResponseBody ?? MAX_RESPONSE_BODY;
|
|
1035
|
+
const matchSecret = createKeyMatcher(options?.redactKeys);
|
|
941
1036
|
const entries = [];
|
|
942
1037
|
const blocked = /* @__PURE__ */ new Set([...DEFAULT_BLOCKED_HOSTS, ...extraBlockedHosts]);
|
|
943
1038
|
let origFetch = null;
|
|
@@ -949,6 +1044,12 @@ function createNetworkCollector(extraBlockedHosts = [], options) {
|
|
|
949
1044
|
const seenUrls = /* @__PURE__ */ new Set();
|
|
950
1045
|
function push(entry) {
|
|
951
1046
|
seenUrls.add(entry.fullUrl ?? entry.url);
|
|
1047
|
+
if (entry.fullUrl) entry.fullUrl = sanitizeUrl(entry.fullUrl, matchSecret);
|
|
1048
|
+
entry.url = sanitizeUrl(entry.url, matchSecret);
|
|
1049
|
+
entry.requestBody = sanitizeBody(entry.requestBody, matchSecret);
|
|
1050
|
+
entry.responseBody = sanitizeBody(entry.responseBody, matchSecret);
|
|
1051
|
+
entry.requestHeaders = sanitizeHeaders(entry.requestHeaders, matchSecret);
|
|
1052
|
+
entry.responseHeaders = sanitizeHeaders(entry.responseHeaders, matchSecret);
|
|
952
1053
|
entries.push(entry);
|
|
953
1054
|
if (entries.length > maxEntries) entries.shift();
|
|
954
1055
|
}
|
|
@@ -1014,7 +1115,8 @@ function createNetworkCollector(extraBlockedHosts = [], options) {
|
|
|
1014
1115
|
try {
|
|
1015
1116
|
const clone = res.clone();
|
|
1016
1117
|
clone.text().then((text) => {
|
|
1017
|
-
|
|
1118
|
+
const truncated = text.length > maxResponseBody ? text.slice(0, maxResponseBody) + "\u2026" : text;
|
|
1119
|
+
entry.responseBody = sanitizeBody(truncated, matchSecret);
|
|
1018
1120
|
if (!entry.responseSize && text.length) entry.responseSize = text.length;
|
|
1019
1121
|
}).catch(() => {
|
|
1020
1122
|
});
|
|
@@ -1244,6 +1346,7 @@ function init(config) {
|
|
|
1244
1346
|
enableReplay = false,
|
|
1245
1347
|
replayBufferMs = DEFAULT_REPLAY_BUFFER_MS,
|
|
1246
1348
|
blockedHosts = [],
|
|
1349
|
+
redactKeys,
|
|
1247
1350
|
frustration: frustrationOpts,
|
|
1248
1351
|
onFrustration,
|
|
1249
1352
|
_replayRecorder,
|
|
@@ -1267,7 +1370,7 @@ function init(config) {
|
|
|
1267
1370
|
const allBlockedHosts = [...blockedHosts, ...flintHost ? [flintHost] : []];
|
|
1268
1371
|
const consoleCol = enableConsole ? _collectors?.console?.() ?? createConsoleCollector() : null;
|
|
1269
1372
|
consoleCol?.start();
|
|
1270
|
-
const networkCol = enableNetwork ? _collectors?.network?.(allBlockedHosts) ?? createNetworkCollector(allBlockedHosts) : null;
|
|
1373
|
+
const networkCol = enableNetwork ? _collectors?.network?.(allBlockedHosts, { redactKeys }) ?? createNetworkCollector(allBlockedHosts, { redactKeys }) : null;
|
|
1271
1374
|
networkCol?.start();
|
|
1272
1375
|
const formErrorsCol = enableFormErrors ? createFormErrorCollector() : null;
|
|
1273
1376
|
if (formErrorsCol) {
|