@futurewindai/wotchi 0.1.0-beta.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/LICENSE +201 -0
- package/README.md +128 -0
- package/SECURITY.md +17 -0
- package/dist/cjs/core/client.js +154 -0
- package/dist/cjs/core/config.js +114 -0
- package/dist/cjs/core/diagnostics.js +14 -0
- package/dist/cjs/core/errors.js +11 -0
- package/dist/cjs/core/fingerprint.js +37 -0
- package/dist/cjs/core/group-store.js +95 -0
- package/dist/cjs/core/incident-builder.js +46 -0
- package/dist/cjs/core/incident-policy.js +31 -0
- package/dist/cjs/core/normalize.js +138 -0
- package/dist/cjs/core/notification-queue.js +103 -0
- package/dist/cjs/core/process-monitor.js +30 -0
- package/dist/cjs/core/redact.js +141 -0
- package/dist/cjs/core/rolling-window.js +60 -0
- package/dist/cjs/core/stack-frame.js +33 -0
- package/dist/cjs/core/types.js +2 -0
- package/dist/cjs/index.js +13 -0
- package/dist/cjs/integrations/express/error-handler.js +15 -0
- package/dist/cjs/integrations/express/index.js +12 -0
- package/dist/cjs/integrations/express/request-context.js +19 -0
- package/dist/cjs/integrations/nest/exception-filter.js +36 -0
- package/dist/cjs/integrations/nest/index.js +9 -0
- package/dist/cjs/integrations/nest/register.js +13 -0
- package/dist/cjs/integrations/nest/request-context.js +41 -0
- package/dist/cjs/integrations/request-context.js +76 -0
- package/dist/cjs/notifiers/console.js +33 -0
- package/dist/cjs/notifiers/telegram-format.js +52 -0
- package/dist/cjs/notifiers/telegram-http.js +150 -0
- package/dist/cjs/notifiers/telegram.js +38 -0
- package/dist/cjs/package.json +1 -0
- package/dist/esm/core/client.js +151 -0
- package/dist/esm/core/config.js +110 -0
- package/dist/esm/core/diagnostics.js +10 -0
- package/dist/esm/core/errors.js +7 -0
- package/dist/esm/core/fingerprint.js +33 -0
- package/dist/esm/core/group-store.js +92 -0
- package/dist/esm/core/incident-builder.js +43 -0
- package/dist/esm/core/incident-policy.js +28 -0
- package/dist/esm/core/normalize.js +134 -0
- package/dist/esm/core/notification-queue.js +100 -0
- package/dist/esm/core/process-monitor.js +27 -0
- package/dist/esm/core/redact.js +136 -0
- package/dist/esm/core/rolling-window.js +57 -0
- package/dist/esm/core/stack-frame.js +29 -0
- package/dist/esm/core/types.js +1 -0
- package/dist/esm/index.js +6 -0
- package/dist/esm/integrations/express/error-handler.js +12 -0
- package/dist/esm/integrations/express/index.js +5 -0
- package/dist/esm/integrations/express/request-context.js +16 -0
- package/dist/esm/integrations/nest/exception-filter.js +33 -0
- package/dist/esm/integrations/nest/index.js +2 -0
- package/dist/esm/integrations/nest/register.js +10 -0
- package/dist/esm/integrations/nest/request-context.js +38 -0
- package/dist/esm/integrations/request-context.js +72 -0
- package/dist/esm/notifiers/console.js +29 -0
- package/dist/esm/notifiers/telegram-format.js +49 -0
- package/dist/esm/notifiers/telegram-http.js +144 -0
- package/dist/esm/notifiers/telegram.js +34 -0
- package/dist/types/core/client.d.ts +2 -0
- package/dist/types/core/config.d.ts +29 -0
- package/dist/types/core/diagnostics.d.ts +7 -0
- package/dist/types/core/errors.d.ts +3 -0
- package/dist/types/core/fingerprint.d.ts +3 -0
- package/dist/types/core/group-store.d.ts +15 -0
- package/dist/types/core/incident-builder.d.ts +3 -0
- package/dist/types/core/incident-policy.d.ts +15 -0
- package/dist/types/core/normalize.d.ts +17 -0
- package/dist/types/core/notification-queue.d.ts +16 -0
- package/dist/types/core/process-monitor.d.ts +5 -0
- package/dist/types/core/redact.d.ts +11 -0
- package/dist/types/core/rolling-window.d.ts +12 -0
- package/dist/types/core/stack-frame.d.ts +2 -0
- package/dist/types/core/types.d.ts +109 -0
- package/dist/types/index.d.ts +9 -0
- package/dist/types/integrations/express/error-handler.d.ts +4 -0
- package/dist/types/integrations/express/index.d.ts +7 -0
- package/dist/types/integrations/express/request-context.d.ts +5 -0
- package/dist/types/integrations/nest/exception-filter.d.ts +10 -0
- package/dist/types/integrations/nest/index.d.ts +4 -0
- package/dist/types/integrations/nest/register.d.ts +10 -0
- package/dist/types/integrations/nest/request-context.d.ts +5 -0
- package/dist/types/integrations/request-context.d.ts +13 -0
- package/dist/types/notifiers/console.d.ts +3 -0
- package/dist/types/notifiers/telegram-format.d.ts +2 -0
- package/dist/types/notifiers/telegram-http.d.ts +21 -0
- package/dist/types/notifiers/telegram.d.ts +4 -0
- package/package.json +111 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.normalizeDynamicSegments = normalizeDynamicSegments;
|
|
4
|
+
exports.fingerprintSafeErrorEvent = fingerprintSafeErrorEvent;
|
|
5
|
+
const node_crypto_1 = require("node:crypto");
|
|
6
|
+
const redact_js_1 = require("./redact.js");
|
|
7
|
+
const stack_frame_js_1 = require("./stack-frame.js");
|
|
8
|
+
const UUID_PATTERN = /\b[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\b/gi;
|
|
9
|
+
const HEX_PATTERN = /\b(?:0x)?[0-9a-f]{8,}\b/gi;
|
|
10
|
+
const NUMBER_PATTERN = /\b\d+\b/g;
|
|
11
|
+
function normalizeDynamicSegments(value) {
|
|
12
|
+
return value
|
|
13
|
+
.replace(UUID_PATTERN, "<uuid>")
|
|
14
|
+
.replace(HEX_PATTERN, "<hex>")
|
|
15
|
+
.replace(NUMBER_PATTERN, "<number>")
|
|
16
|
+
.replace(/\s+/g, " ")
|
|
17
|
+
.trim()
|
|
18
|
+
.toLowerCase();
|
|
19
|
+
}
|
|
20
|
+
const safeText = (value) => {
|
|
21
|
+
const redacted = (0, redact_js_1.redactValue)(value, { maxStringLength: 4_000 });
|
|
22
|
+
return typeof redacted === "string" ? redacted : "[unreadable value]";
|
|
23
|
+
};
|
|
24
|
+
const encodePart = (value) => `${value.length}:${value}`;
|
|
25
|
+
function fingerprintSafeErrorEvent(event) {
|
|
26
|
+
const service = normalizeDynamicSegments(safeText(event.service));
|
|
27
|
+
const environment = normalizeDynamicSegments(safeText(event.environment));
|
|
28
|
+
const errorName = normalizeDynamicSegments(safeText(event.error.name));
|
|
29
|
+
const message = normalizeDynamicSegments(safeText(event.error.message));
|
|
30
|
+
const method = normalizeDynamicSegments(safeText(event.request?.method ?? ""));
|
|
31
|
+
const route = normalizeDynamicSegments(safeText(event.request?.route ?? ""));
|
|
32
|
+
const frame = normalizeDynamicSegments((0, stack_frame_js_1.selectApplicationFrame)(event.error.stack) ?? "");
|
|
33
|
+
const canonical = [service, environment, errorName, message, method, route, frame]
|
|
34
|
+
.map(encodePart)
|
|
35
|
+
.join("|");
|
|
36
|
+
return (0, node_crypto_1.createHash)("sha256").update(canonical, "utf8").digest("hex");
|
|
37
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createGroupStore = createGroupStore;
|
|
4
|
+
const normalize_js_1 = require("./normalize.js");
|
|
5
|
+
const rolling_window_js_1 = require("./rolling-window.js");
|
|
6
|
+
const cloneSafeEvent = (event) => {
|
|
7
|
+
const context = event.context === undefined ? undefined : (0, normalize_js_1.normalizeUnknown)(event.context);
|
|
8
|
+
return {
|
|
9
|
+
id: event.id,
|
|
10
|
+
timestamp: event.timestamp,
|
|
11
|
+
service: event.service,
|
|
12
|
+
environment: event.environment,
|
|
13
|
+
...(event.release === undefined ? {} : { release: event.release }),
|
|
14
|
+
error: { ...event.error },
|
|
15
|
+
...(event.request === undefined ? {} : { request: { ...event.request } }),
|
|
16
|
+
...(context === undefined ? {} : { context: context }),
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
function createGroupStore(options) {
|
|
20
|
+
if (!Number.isSafeInteger(options.maxGroups) || options.maxGroups <= 0) {
|
|
21
|
+
throw new RangeError("maxGroups must be a positive integer");
|
|
22
|
+
}
|
|
23
|
+
const now = options.now ?? Date.now;
|
|
24
|
+
const groups = new Map();
|
|
25
|
+
let evicted = 0;
|
|
26
|
+
const snapshot = (group) => ({
|
|
27
|
+
fingerprint: group.fingerprint,
|
|
28
|
+
firstSeenAt: new Date(group.firstSeenMs).toISOString(),
|
|
29
|
+
lastSeenAt: new Date(group.lastSeenMs).toISOString(),
|
|
30
|
+
totalCount: group.totalCount,
|
|
31
|
+
windowCount: group.window.count(group.lastSeenMs),
|
|
32
|
+
sample: cloneSafeEvent(group.sample),
|
|
33
|
+
...(group.lastAlertedMs === undefined
|
|
34
|
+
? {}
|
|
35
|
+
: { lastAlertedAt: new Date(group.lastAlertedMs).toISOString() }),
|
|
36
|
+
severity: group.severity,
|
|
37
|
+
});
|
|
38
|
+
const evictLeastRecent = () => {
|
|
39
|
+
let candidate;
|
|
40
|
+
for (const group of groups.values()) {
|
|
41
|
+
if (candidate === undefined || group.lastSeenMs < candidate.lastSeenMs) {
|
|
42
|
+
candidate = group;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (candidate !== undefined) {
|
|
46
|
+
groups.delete(candidate.fingerprint);
|
|
47
|
+
evicted += 1;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
const record = (fingerprint, event, severity = "medium") => {
|
|
51
|
+
const timestamp = now();
|
|
52
|
+
let group = groups.get(fingerprint);
|
|
53
|
+
if (group === undefined) {
|
|
54
|
+
if (groups.size >= options.maxGroups) {
|
|
55
|
+
evictLeastRecent();
|
|
56
|
+
}
|
|
57
|
+
group = {
|
|
58
|
+
fingerprint,
|
|
59
|
+
firstSeenMs: timestamp,
|
|
60
|
+
lastSeenMs: timestamp,
|
|
61
|
+
totalCount: 0,
|
|
62
|
+
sample: cloneSafeEvent(event),
|
|
63
|
+
severity,
|
|
64
|
+
window: (0, rolling_window_js_1.createRollingWindow)({
|
|
65
|
+
maxEvents: options.maxEventsPerWindow,
|
|
66
|
+
windowMs: options.windowMs,
|
|
67
|
+
now,
|
|
68
|
+
}),
|
|
69
|
+
};
|
|
70
|
+
groups.set(fingerprint, group);
|
|
71
|
+
}
|
|
72
|
+
group.lastSeenMs = timestamp;
|
|
73
|
+
group.totalCount += 1;
|
|
74
|
+
group.severity = severity;
|
|
75
|
+
group.window.add(timestamp);
|
|
76
|
+
return snapshot(group);
|
|
77
|
+
};
|
|
78
|
+
return {
|
|
79
|
+
record,
|
|
80
|
+
get: (fingerprint) => {
|
|
81
|
+
const group = groups.get(fingerprint);
|
|
82
|
+
return group === undefined ? undefined : snapshot(group);
|
|
83
|
+
},
|
|
84
|
+
markAlerted: (fingerprint, timestamp = now()) => {
|
|
85
|
+
const group = groups.get(fingerprint);
|
|
86
|
+
if (group === undefined) {
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
group.lastAlertedMs = timestamp;
|
|
90
|
+
return snapshot(group);
|
|
91
|
+
},
|
|
92
|
+
size: () => groups.size,
|
|
93
|
+
groupsEvicted: () => evicted,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildIncidentAlert = buildIncidentAlert;
|
|
4
|
+
const titleCase = (value) => `${value.slice(0, 1).toUpperCase()}${value.slice(1)}`;
|
|
5
|
+
const suggestedActions = (group) => {
|
|
6
|
+
const text = `${group.sample.error.name} ${group.sample.error.message}`.toLowerCase();
|
|
7
|
+
if (text.includes("database") || text.includes("query") || text.includes("sql")) {
|
|
8
|
+
return [
|
|
9
|
+
"Check database availability, connection saturation, and recent schema changes.",
|
|
10
|
+
"Check the affected query and its dependency health before increasing capacity.",
|
|
11
|
+
];
|
|
12
|
+
}
|
|
13
|
+
if (text.includes("timeout") || text.includes("timed out")) {
|
|
14
|
+
return [
|
|
15
|
+
"Check upstream latency, timeout settings, and recent dependency changes.",
|
|
16
|
+
"Check whether retries or a dependency outage are increasing request pressure.",
|
|
17
|
+
];
|
|
18
|
+
}
|
|
19
|
+
if (text.includes("auth") || text.includes("permission") || text.includes("unauthor")) {
|
|
20
|
+
return [
|
|
21
|
+
"Check authentication configuration, credentials, and recent access-policy changes.",
|
|
22
|
+
"Check whether the affected route is receiving an unexpected caller or token.",
|
|
23
|
+
];
|
|
24
|
+
}
|
|
25
|
+
return [
|
|
26
|
+
"Check the application logs and the selected stack frame for the first failing operation.",
|
|
27
|
+
"Check recent deployments, dependency health, and the affected request path.",
|
|
28
|
+
];
|
|
29
|
+
};
|
|
30
|
+
function buildIncidentAlert(group, decision) {
|
|
31
|
+
const message = group.sample.error.message;
|
|
32
|
+
const errorName = group.sample.error.name;
|
|
33
|
+
return {
|
|
34
|
+
id: `wotchi-${group.fingerprint}-${group.lastSeenAt}`,
|
|
35
|
+
fingerprint: group.fingerprint,
|
|
36
|
+
title: `Wotchi — ${titleCase(decision.severity)} incident`,
|
|
37
|
+
severity: decision.severity,
|
|
38
|
+
summary: `Observed ${group.windowCount} occurrences of ${errorName}: ${message}.`,
|
|
39
|
+
suggestedActions: suggestedActions(group),
|
|
40
|
+
firstSeenAt: group.firstSeenAt,
|
|
41
|
+
lastSeenAt: group.lastSeenAt,
|
|
42
|
+
occurrences: group.windowCount,
|
|
43
|
+
service: group.sample.service,
|
|
44
|
+
environment: group.sample.environment,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.evaluateIncidentPolicy = evaluateIncidentPolicy;
|
|
4
|
+
const hasCooldown = (group, now, cooldownMs) => {
|
|
5
|
+
if (group.lastAlertedAt === undefined) {
|
|
6
|
+
return false;
|
|
7
|
+
}
|
|
8
|
+
const lastAlerted = Date.parse(group.lastAlertedAt);
|
|
9
|
+
return Number.isFinite(lastAlerted) && now - lastAlerted < cooldownMs;
|
|
10
|
+
};
|
|
11
|
+
const hasUnavailableResponse = (group) => group.sample.request?.statusCode === 503;
|
|
12
|
+
function evaluateIncidentPolicy(input) {
|
|
13
|
+
const eventKind = input.eventKind ?? "error";
|
|
14
|
+
const isCritical = eventKind === "process-monitor";
|
|
15
|
+
const isHigh = input.group.windowCount >= 20 || hasUnavailableResponse(input.group);
|
|
16
|
+
const severity = isCritical
|
|
17
|
+
? "critical"
|
|
18
|
+
: isHigh
|
|
19
|
+
? "high"
|
|
20
|
+
: eventKind === "manual"
|
|
21
|
+
? (input.manualSeverity ?? "low")
|
|
22
|
+
: "medium";
|
|
23
|
+
const eligible = isCritical ||
|
|
24
|
+
isHigh ||
|
|
25
|
+
eventKind === "manual" ||
|
|
26
|
+
input.group.windowCount >= input.alertThreshold;
|
|
27
|
+
return {
|
|
28
|
+
shouldAlert: eligible && !hasCooldown(input.group, input.now, input.cooldownMs),
|
|
29
|
+
severity,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.normalizeUnknown = normalizeUnknown;
|
|
4
|
+
exports.normalizeError = normalizeError;
|
|
5
|
+
const UNREADABLE_VALUE = "[unreadable value]";
|
|
6
|
+
const MAX_DEPTH_VALUE = "[MaxDepth]";
|
|
7
|
+
const CIRCULAR_VALUE = "[Circular]";
|
|
8
|
+
const MAX_KEYS_VALUE = "[MaxKeys]";
|
|
9
|
+
const defaultLimits = {
|
|
10
|
+
maxDepth: 5,
|
|
11
|
+
maxKeys: 100,
|
|
12
|
+
maxStringLength: 500,
|
|
13
|
+
maxStackLength: 4_000,
|
|
14
|
+
};
|
|
15
|
+
const boundedLimit = (value, fallback) => Number.isSafeInteger(value) && value > 0 ? value : fallback;
|
|
16
|
+
const normalizeLimits = (limits) => ({
|
|
17
|
+
maxDepth: boundedLimit(limits?.maxDepth ?? defaultLimits.maxDepth, defaultLimits.maxDepth),
|
|
18
|
+
maxKeys: boundedLimit(limits?.maxKeys ?? defaultLimits.maxKeys, defaultLimits.maxKeys),
|
|
19
|
+
maxStringLength: boundedLimit(limits?.maxStringLength ?? defaultLimits.maxStringLength, defaultLimits.maxStringLength),
|
|
20
|
+
maxStackLength: boundedLimit(limits?.maxStackLength ?? defaultLimits.maxStackLength, defaultLimits.maxStackLength),
|
|
21
|
+
});
|
|
22
|
+
const truncate = (value, limit) => value.slice(0, limit);
|
|
23
|
+
const readProperty = (value, key) => {
|
|
24
|
+
try {
|
|
25
|
+
return value[key];
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return UNREADABLE_VALUE;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
const normalizePrimitive = (value, limits) => {
|
|
32
|
+
if (value === null) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
switch (typeof value) {
|
|
36
|
+
case "string":
|
|
37
|
+
return truncate(value, limits.maxStringLength);
|
|
38
|
+
case "boolean":
|
|
39
|
+
return value;
|
|
40
|
+
case "number":
|
|
41
|
+
return Number.isFinite(value) ? value : "[non-finite number]";
|
|
42
|
+
case "undefined":
|
|
43
|
+
return "[undefined]";
|
|
44
|
+
case "bigint":
|
|
45
|
+
return "[bigint]";
|
|
46
|
+
case "symbol":
|
|
47
|
+
return "[symbol]";
|
|
48
|
+
case "function":
|
|
49
|
+
return "[function]";
|
|
50
|
+
default:
|
|
51
|
+
return UNREADABLE_VALUE;
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
const normalizeValue = (value, depth, state, limits) => {
|
|
55
|
+
if (value === null || typeof value !== "object") {
|
|
56
|
+
return normalizePrimitive(value, limits);
|
|
57
|
+
}
|
|
58
|
+
if (depth >= limits.maxDepth) {
|
|
59
|
+
return MAX_DEPTH_VALUE;
|
|
60
|
+
}
|
|
61
|
+
if (state.seen.has(value)) {
|
|
62
|
+
return CIRCULAR_VALUE;
|
|
63
|
+
}
|
|
64
|
+
state.seen.add(value);
|
|
65
|
+
if (Array.isArray(value)) {
|
|
66
|
+
const result = [];
|
|
67
|
+
let length;
|
|
68
|
+
try {
|
|
69
|
+
length = Math.min(value.length, limits.maxKeys - state.keysVisited);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return UNREADABLE_VALUE;
|
|
73
|
+
}
|
|
74
|
+
for (let index = 0; index < length; index += 1) {
|
|
75
|
+
if (state.keysVisited >= limits.maxKeys) {
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
state.keysVisited += 1;
|
|
79
|
+
result.push(normalizeValue(readProperty(value, String(index)), depth + 1, state, limits));
|
|
80
|
+
}
|
|
81
|
+
return result;
|
|
82
|
+
}
|
|
83
|
+
let keys;
|
|
84
|
+
try {
|
|
85
|
+
keys = Object.keys(value);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return UNREADABLE_VALUE;
|
|
89
|
+
}
|
|
90
|
+
const result = {};
|
|
91
|
+
for (const key of keys) {
|
|
92
|
+
if (state.keysVisited >= limits.maxKeys - 1) {
|
|
93
|
+
result["[truncated]"] = MAX_KEYS_VALUE;
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
state.keysVisited += 1;
|
|
97
|
+
result[truncate(key, limits.maxStringLength)] = normalizeValue(readProperty(value, key), depth + 1, state, limits);
|
|
98
|
+
}
|
|
99
|
+
return result;
|
|
100
|
+
};
|
|
101
|
+
function normalizeUnknown(value, limits) {
|
|
102
|
+
const normalizedLimits = normalizeLimits(limits);
|
|
103
|
+
try {
|
|
104
|
+
return normalizeValue(value, 0, { seen: new WeakSet(), keysVisited: 0 }, normalizedLimits);
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return UNREADABLE_VALUE;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const safeStringProperty = (value, key, fallback, limit) => {
|
|
111
|
+
const property = readProperty(value, key);
|
|
112
|
+
return typeof property === "string" ? truncate(property, limit) : fallback;
|
|
113
|
+
};
|
|
114
|
+
function normalizeError(error, limits) {
|
|
115
|
+
const normalizedLimits = normalizeLimits(limits);
|
|
116
|
+
let isError;
|
|
117
|
+
try {
|
|
118
|
+
isError = error instanceof Error;
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
isError = false;
|
|
122
|
+
}
|
|
123
|
+
if (isError && error !== null && typeof error === "object") {
|
|
124
|
+
const name = safeStringProperty(error, "name", "Error", normalizedLimits.maxStringLength);
|
|
125
|
+
const message = safeStringProperty(error, "message", UNREADABLE_VALUE, normalizedLimits.maxStringLength);
|
|
126
|
+
const stack = safeStringProperty(error, "stack", "", normalizedLimits.maxStackLength);
|
|
127
|
+
const code = safeStringProperty(error, "code", "", normalizedLimits.maxStringLength);
|
|
128
|
+
return {
|
|
129
|
+
name: name || "Error",
|
|
130
|
+
message,
|
|
131
|
+
...(stack ? { stack } : {}),
|
|
132
|
+
...(code ? { code } : {}),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
const normalized = normalizeUnknown(error, normalizedLimits);
|
|
136
|
+
const message = typeof normalized === "string" ? normalized : UNREADABLE_VALUE;
|
|
137
|
+
return { name: "UnknownError", message };
|
|
138
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createNotificationQueue = createNotificationQueue;
|
|
4
|
+
const DEFAULT_FLUSH_TIMEOUT_MS = 5_000;
|
|
5
|
+
function createNotificationQueue(options) {
|
|
6
|
+
if (!Number.isSafeInteger(options.maxPendingAlerts) || options.maxPendingAlerts <= 0) {
|
|
7
|
+
throw new RangeError("maxPendingAlerts must be a positive integer");
|
|
8
|
+
}
|
|
9
|
+
if (options.concurrency !== 1) {
|
|
10
|
+
throw new RangeError("concurrency must be 1");
|
|
11
|
+
}
|
|
12
|
+
const pendingJobs = [];
|
|
13
|
+
let drainPromise;
|
|
14
|
+
let resolveDrain;
|
|
15
|
+
let activeJobs = 0;
|
|
16
|
+
let queued = 0;
|
|
17
|
+
let dropped = 0;
|
|
18
|
+
let sent = 0;
|
|
19
|
+
let failures = 0;
|
|
20
|
+
const notifyFailure = (error, notifier) => {
|
|
21
|
+
failures += 1;
|
|
22
|
+
try {
|
|
23
|
+
options.onNotifierError?.(error, notifier);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
// Diagnostics callbacks cannot be allowed to affect the host application.
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
const resolveDrainWaiters = () => {
|
|
30
|
+
if (activeJobs !== 0 || pendingJobs.length !== 0) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
resolveDrain?.();
|
|
34
|
+
drainPromise = undefined;
|
|
35
|
+
resolveDrain = undefined;
|
|
36
|
+
};
|
|
37
|
+
const processJob = async (job) => {
|
|
38
|
+
for (const notifier of job.notifiers) {
|
|
39
|
+
try {
|
|
40
|
+
await notifier.send(job.alert);
|
|
41
|
+
sent += 1;
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
notifyFailure(error, notifier);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
const pump = () => {
|
|
49
|
+
while (activeJobs < options.concurrency && pendingJobs.length > 0) {
|
|
50
|
+
const job = pendingJobs.shift();
|
|
51
|
+
if (job === undefined) {
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
activeJobs += 1;
|
|
55
|
+
void processJob(job).finally(() => {
|
|
56
|
+
activeJobs -= 1;
|
|
57
|
+
pump();
|
|
58
|
+
resolveDrainWaiters();
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
const enqueue = (alert, notifiers) => {
|
|
63
|
+
if (pendingJobs.length >= options.maxPendingAlerts) {
|
|
64
|
+
dropped += 1;
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
pendingJobs.push({ alert, notifiers: [...notifiers] });
|
|
68
|
+
queued += 1;
|
|
69
|
+
pump();
|
|
70
|
+
return true;
|
|
71
|
+
};
|
|
72
|
+
const flush = (timeoutMs = DEFAULT_FLUSH_TIMEOUT_MS) => {
|
|
73
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0) {
|
|
74
|
+
return Promise.reject(new RangeError("timeoutMs must be a positive integer"));
|
|
75
|
+
}
|
|
76
|
+
if (activeJobs === 0 && pendingJobs.length === 0) {
|
|
77
|
+
return Promise.resolve();
|
|
78
|
+
}
|
|
79
|
+
const drain = drainPromise ??
|
|
80
|
+
new Promise((resolve) => {
|
|
81
|
+
resolveDrain = resolve;
|
|
82
|
+
});
|
|
83
|
+
drainPromise = drain;
|
|
84
|
+
return new Promise((resolve, reject) => {
|
|
85
|
+
const timer = setTimeout(() => {
|
|
86
|
+
reject(new Error(`Wotchi notification queue flush timed out after ${timeoutMs}ms`));
|
|
87
|
+
}, timeoutMs);
|
|
88
|
+
void drain.then(() => {
|
|
89
|
+
clearTimeout(timer);
|
|
90
|
+
resolve();
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
};
|
|
94
|
+
return {
|
|
95
|
+
enqueue,
|
|
96
|
+
flush,
|
|
97
|
+
pending: () => pendingJobs.length,
|
|
98
|
+
alertsQueued: () => queued,
|
|
99
|
+
alertsDropped: () => dropped,
|
|
100
|
+
alertsSent: () => sent,
|
|
101
|
+
notifierFailures: () => failures,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerWotchiProcessMonitor = registerWotchiProcessMonitor;
|
|
4
|
+
function registerWotchiProcessMonitor(client) {
|
|
5
|
+
const listener = (error, origin) => {
|
|
6
|
+
try {
|
|
7
|
+
client.captureEvent({
|
|
8
|
+
level: "error",
|
|
9
|
+
kind: "process-monitor",
|
|
10
|
+
message: "Uncaught process exception",
|
|
11
|
+
error,
|
|
12
|
+
context: { processOrigin: origin },
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
// Process monitoring must never alter the host process crash behavior.
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
globalThis.process.on("uncaughtExceptionMonitor", listener);
|
|
20
|
+
let registered = true;
|
|
21
|
+
return {
|
|
22
|
+
unregister() {
|
|
23
|
+
if (!registered) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
registered = false;
|
|
27
|
+
globalThis.process.off("uncaughtExceptionMonitor", listener);
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.REDACTED = void 0;
|
|
4
|
+
exports.redactValue = redactValue;
|
|
5
|
+
exports.redactError = redactError;
|
|
6
|
+
const normalize_js_1 = require("./normalize.js");
|
|
7
|
+
exports.REDACTED = "[REDACTED]";
|
|
8
|
+
const DEFAULT_KEYS = [
|
|
9
|
+
"authorization",
|
|
10
|
+
"cookie",
|
|
11
|
+
"set-cookie",
|
|
12
|
+
"password",
|
|
13
|
+
"passwd",
|
|
14
|
+
"secret",
|
|
15
|
+
"apiKey",
|
|
16
|
+
"api_key",
|
|
17
|
+
"token",
|
|
18
|
+
"accessToken",
|
|
19
|
+
"access_token",
|
|
20
|
+
"refreshToken",
|
|
21
|
+
"refresh_token",
|
|
22
|
+
"privateKey",
|
|
23
|
+
"private_key",
|
|
24
|
+
"clientSecret",
|
|
25
|
+
"client_secret",
|
|
26
|
+
"cardNumber",
|
|
27
|
+
"cvv",
|
|
28
|
+
"cvc",
|
|
29
|
+
"pin",
|
|
30
|
+
];
|
|
31
|
+
const normalizeKey = (key) => key.toLowerCase().replace(/[-_]/g, "");
|
|
32
|
+
const defaultKeySet = new Set(DEFAULT_KEYS.map(normalizeKey));
|
|
33
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
34
|
+
const luhnValid = (digits) => {
|
|
35
|
+
let sum = 0;
|
|
36
|
+
let double = false;
|
|
37
|
+
for (let index = digits.length - 1; index >= 0; index -= 1) {
|
|
38
|
+
let digit = Number(digits[index]);
|
|
39
|
+
if (double) {
|
|
40
|
+
digit *= 2;
|
|
41
|
+
if (digit > 9) {
|
|
42
|
+
digit -= 9;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
sum += digit;
|
|
46
|
+
double = !double;
|
|
47
|
+
}
|
|
48
|
+
return sum % 10 === 0;
|
|
49
|
+
};
|
|
50
|
+
const redactCardLikeValues = (value) => value.replace(/\b\d[\d -]{11,25}\d\b/g, (candidate) => {
|
|
51
|
+
const digits = candidate.replace(/[ -]/g, "");
|
|
52
|
+
return digits.length >= 13 && digits.length <= 19 && luhnValid(digits) ? exports.REDACTED : candidate;
|
|
53
|
+
});
|
|
54
|
+
const redactString = (value, maxStringLength) => {
|
|
55
|
+
let result = value;
|
|
56
|
+
result = result.replace(/-----BEGIN [^-]*PRIVATE KEY-----[\s\S]*?-----END [^-]*PRIVATE KEY-----/gi, exports.REDACTED);
|
|
57
|
+
result = result.replace(/\bBearer\s+[A-Za-z0-9._~+/=-]+/gi, `Bearer ${exports.REDACTED}`);
|
|
58
|
+
result = result.replace(/\b(?:authorization|password|passwd|secret|token|api[-_]?key)\s*[:=]\s*[^\s,;]+/gi, (match) => `${match.slice(0, match.search(/[:=]/))}: ${exports.REDACTED}`);
|
|
59
|
+
result = result.replace(/\b[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/g, exports.REDACTED);
|
|
60
|
+
result = result.replace(/\b(?:github_pat|ghp|xox[baprs]-|sk|pk|AKIA)[A-Za-z0-9_-]{8,}\b/gi, exports.REDACTED);
|
|
61
|
+
result = redactCardLikeValues(result);
|
|
62
|
+
return result.slice(0, maxStringLength);
|
|
63
|
+
};
|
|
64
|
+
const normalizedOption = (value, fallback) => Number.isSafeInteger(value) && value !== undefined && value > 0 ? value : fallback;
|
|
65
|
+
const redactNormalized = (value, sensitiveKeys, options, depth, keysVisited) => {
|
|
66
|
+
if (typeof value === "string") {
|
|
67
|
+
return redactString(value, options.maxStringLength);
|
|
68
|
+
}
|
|
69
|
+
if (value === null || typeof value !== "object") {
|
|
70
|
+
return value;
|
|
71
|
+
}
|
|
72
|
+
if (depth >= options.maxDepth) {
|
|
73
|
+
return "[MaxDepth]";
|
|
74
|
+
}
|
|
75
|
+
if (Array.isArray(value)) {
|
|
76
|
+
const result = [];
|
|
77
|
+
for (const item of value) {
|
|
78
|
+
if (keysVisited.value >= options.maxKeys) {
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
keysVisited.value += 1;
|
|
82
|
+
result.push(redactNormalized(item, sensitiveKeys, options, depth + 1, keysVisited));
|
|
83
|
+
}
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
const result = {};
|
|
87
|
+
for (const [key, child] of Object.entries(value)) {
|
|
88
|
+
if (keysVisited.value >= options.maxKeys) {
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
keysVisited.value += 1;
|
|
92
|
+
const normalizedKey = normalizeKey(key);
|
|
93
|
+
result[key] = sensitiveKeys.has(normalizedKey)
|
|
94
|
+
? exports.REDACTED
|
|
95
|
+
: typeof child === "string" && normalizedKey === "stack"
|
|
96
|
+
? redactString(child, options.maxStackLength)
|
|
97
|
+
: redactNormalized(child, sensitiveKeys, options, depth + 1, keysVisited);
|
|
98
|
+
}
|
|
99
|
+
return result;
|
|
100
|
+
};
|
|
101
|
+
const redactionLimits = (options) => ({
|
|
102
|
+
maxDepth: normalizedOption(options.maxDepth, 5),
|
|
103
|
+
maxKeys: normalizedOption(options.maxKeys, 100),
|
|
104
|
+
maxStringLength: normalizedOption(options.maxStringLength, 500),
|
|
105
|
+
maxStackLength: normalizedOption(options.maxStackLength, 4_000),
|
|
106
|
+
});
|
|
107
|
+
const sensitiveKeySet = (options) => {
|
|
108
|
+
const keys = new Set(defaultKeySet);
|
|
109
|
+
for (const key of options.redactKeys ?? []) {
|
|
110
|
+
if (typeof key === "string" && key.trim().length > 0) {
|
|
111
|
+
keys.add(normalizeKey(key.trim()));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return keys;
|
|
115
|
+
};
|
|
116
|
+
function redactValue(value, options = {}) {
|
|
117
|
+
const limits = redactionLimits(options);
|
|
118
|
+
const normalized = (0, normalize_js_1.normalizeUnknown)(value, {
|
|
119
|
+
maxDepth: limits.maxDepth,
|
|
120
|
+
maxKeys: limits.maxKeys,
|
|
121
|
+
maxStringLength: limits.maxStringLength,
|
|
122
|
+
maxStackLength: limits.maxStackLength,
|
|
123
|
+
});
|
|
124
|
+
return redactNormalized(normalized, sensitiveKeySet(options), limits, 0, { value: 0 });
|
|
125
|
+
}
|
|
126
|
+
function redactError(error, options = {}) {
|
|
127
|
+
const redacted = redactValue(error, options);
|
|
128
|
+
if (!isRecord(redacted)) {
|
|
129
|
+
return { name: "UnknownError", message: exports.REDACTED };
|
|
130
|
+
}
|
|
131
|
+
const name = typeof redacted.name === "string" ? redacted.name : "UnknownError";
|
|
132
|
+
const message = typeof redacted.message === "string" ? redacted.message : exports.REDACTED;
|
|
133
|
+
const stack = typeof redacted.stack === "string" ? redacted.stack : undefined;
|
|
134
|
+
const code = typeof redacted.code === "string" ? redacted.code : undefined;
|
|
135
|
+
return {
|
|
136
|
+
name,
|
|
137
|
+
message,
|
|
138
|
+
...(stack === undefined ? {} : { stack }),
|
|
139
|
+
...(code === undefined ? {} : { code }),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createRollingWindow = createRollingWindow;
|
|
4
|
+
function createRollingWindow(options) {
|
|
5
|
+
if (!Number.isSafeInteger(options.maxEvents) || options.maxEvents <= 0) {
|
|
6
|
+
throw new RangeError("maxEvents must be a positive integer");
|
|
7
|
+
}
|
|
8
|
+
if (!Number.isSafeInteger(options.windowMs) || options.windowMs <= 0) {
|
|
9
|
+
throw new RangeError("windowMs must be a positive integer");
|
|
10
|
+
}
|
|
11
|
+
const now = options.now ?? Date.now;
|
|
12
|
+
const buffer = new Array(options.maxEvents);
|
|
13
|
+
let head = 0;
|
|
14
|
+
let length = 0;
|
|
15
|
+
const readNow = (timestamp) => {
|
|
16
|
+
const value = timestamp ?? now();
|
|
17
|
+
return Number.isFinite(value) ? value : now();
|
|
18
|
+
};
|
|
19
|
+
const purge = (timestamp) => {
|
|
20
|
+
while (length > 0) {
|
|
21
|
+
const oldest = buffer[head];
|
|
22
|
+
if (oldest === undefined || timestamp - oldest < options.windowMs) {
|
|
23
|
+
break;
|
|
24
|
+
}
|
|
25
|
+
buffer[head] = undefined;
|
|
26
|
+
head = (head + 1) % options.maxEvents;
|
|
27
|
+
length -= 1;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
const add = (timestamp) => {
|
|
31
|
+
const value = readNow(timestamp);
|
|
32
|
+
purge(value);
|
|
33
|
+
const index = (head + length) % options.maxEvents;
|
|
34
|
+
if (length === options.maxEvents) {
|
|
35
|
+
buffer[head] = undefined;
|
|
36
|
+
head = (head + 1) % options.maxEvents;
|
|
37
|
+
length -= 1;
|
|
38
|
+
}
|
|
39
|
+
buffer[index] = value;
|
|
40
|
+
length += 1;
|
|
41
|
+
};
|
|
42
|
+
const timestamps = (timestamp) => {
|
|
43
|
+
const value = readNow(timestamp);
|
|
44
|
+
purge(value);
|
|
45
|
+
const result = [];
|
|
46
|
+
for (let index = 0; index < length; index += 1) {
|
|
47
|
+
const entry = buffer[(head + index) % options.maxEvents];
|
|
48
|
+
if (entry !== undefined) {
|
|
49
|
+
result.push(entry);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return result;
|
|
53
|
+
};
|
|
54
|
+
return {
|
|
55
|
+
add,
|
|
56
|
+
count: (timestamp) => timestamps(timestamp).length,
|
|
57
|
+
size: (timestamp) => timestamps(timestamp).length,
|
|
58
|
+
timestamps,
|
|
59
|
+
};
|
|
60
|
+
}
|