cyrus-ai
Version:
AI-powered Linear issue automation using Claude
175 lines • 5.75 kB
JavaScript
/**
* Substring patterns (lowercased) that mark a key as sensitive. We err on the
* side of dropping: a redaction is recoverable from local logs, a leaked token
* is not.
*/
const SENSITIVE_KEY_PATTERNS = [
"token",
"secret",
"password",
"passwd",
"apikey",
"api_key",
"authorization",
"auth_header",
"cookie",
// NOTE: "session" intentionally omitted — it's too broad (matches the
// `sessionId` / `claudeSessionId` identifier attributes we *want* in
// Sentry for log slicing). Real session secrets are caught by the more
// specific patterns ("token", "cookie", "secret") via compounds like
// `session_token`, `session_cookie`, `session_secret`.
"private_key",
"privatekey",
"client_secret",
"clientsecret",
"refresh_token",
"access_token",
"bearer",
"dsn",
"webhook_secret",
"webhooksecret",
"signing_secret",
"signingsecret",
"linear_token",
"github_token",
"gitlab_token",
"slack_token",
];
const REDACTED = "[REDACTED]";
const MAX_DEPTH = 8;
function isSensitiveKey(key) {
const lower = key.toLowerCase();
return SENSITIVE_KEY_PATTERNS.some((p) => lower.includes(p));
}
/**
* Recursively redact values whose keys match a sensitive pattern. Strings that
* look like tokens (long, no whitespace, high entropy-ish) are also redacted
* even when their parent key is innocuous, so payload fields like
* `headers["x-foo"]: "ghp_…"` don't slip through.
*/
function scrubValue(value, depth = 0) {
if (depth > MAX_DEPTH)
return REDACTED;
if (value == null)
return value;
if (typeof value === "string") {
return looksLikeToken(value) ? REDACTED : redactBearerInString(value);
}
if (Array.isArray(value)) {
return value.map((v) => scrubValue(v, depth + 1));
}
if (typeof value === "object") {
const out = {};
for (const [k, v] of Object.entries(value)) {
if (isSensitiveKey(k)) {
out[k] = REDACTED;
}
else {
out[k] = scrubValue(v, depth + 1);
}
}
return out;
}
return value;
}
/**
* Heuristic for an opaque token: long, no whitespace, mostly URL-safe chars,
* and matches one of the well-known prefixes used by tokens we handle.
*/
function looksLikeToken(s) {
if (s.length < 20)
return false;
if (/\s/.test(s))
return false;
// Known prefixes
if (/^(ghp|gho|ghu|ghs|ghr|github_pat)_/i.test(s) ||
/^xox[abprs]-/i.test(s) ||
/^glpat-/i.test(s) ||
/^lin_(api|oauth)_/i.test(s) ||
/^sk-[A-Za-z0-9_-]{20,}$/i.test(s) ||
/^Bearer\s+\S{16,}$/i.test(s)) {
return true;
}
// JWT-shaped (three base64url segments)
if (/^[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}$/.test(s)) {
return true;
}
return false;
}
/**
* Redact `Authorization: Bearer …` and `?token=…` substrings inside otherwise
* innocuous strings (e.g. error messages echoing a request line).
*/
function redactBearerInString(s) {
return s
.replace(/Bearer\s+[A-Za-z0-9._\-+/=]{16,}/gi, "Bearer [REDACTED]")
.replace(/([?&](?:token|access_token|api_key|key|secret)=)[^&\s"']+/gi, "$1[REDACTED]");
}
/**
* Strip token-shaped substrings from a Sentry event. Mutates and returns the
* event so it can be used directly as a `beforeSend` hook.
*/
export function scrubSentryEvent(event, _hint) {
if (event.message) {
event.message = redactBearerInString(event.message);
}
if (event.extra) {
event.extra = scrubValue(event.extra);
}
if (event.contexts) {
event.contexts = scrubValue(event.contexts);
}
if (event.request) {
if (event.request.headers) {
event.request.headers = scrubValue(event.request.headers);
}
if (event.request.cookies) {
event.request.cookies =
REDACTED;
}
if (event.request.data !== undefined) {
event.request.data = scrubValue(event.request.data);
}
}
if (event.exception?.values) {
for (const ex of event.exception.values) {
if (ex.value)
ex.value = redactBearerInString(ex.value);
}
}
// Breadcrumbs come from `consoleIntegration` (every console.* line) and
// other auto-captured trails. They ride along on every event, so anything
// the app printed before the failure — request bodies, headers, repo paths
// — would otherwise leak with the next captured exception.
if (event.breadcrumbs) {
for (const bc of event.breadcrumbs) {
if (typeof bc.message === "string") {
bc.message = redactBearerInString(bc.message);
}
if (bc.data) {
bc.data = scrubValue(bc.data);
}
}
}
return event;
}
/**
* Scrub a Sentry Logs entry. Wired as `beforeSendLog` on `Sentry.init`. The
* Logs stream is a separate pipeline from Issues — `beforeSend` does not run
* on logs, so we need a dedicated hook to keep the redaction guarantees
* symmetric across both ingestion paths.
*
* Generic in `T` so the SDK's stricter `Log` shape (with `level:
* LogSeverityLevel`) flows through unchanged — TypeScript would otherwise
* narrow the return to a looser `string` level on assignment.
*/
export function scrubSentryLog(log) {
if (typeof log.message === "string") {
log.message = redactBearerInString(log.message);
}
if (log.attributes) {
log.attributes = scrubValue(log.attributes);
}
return log;
}
//# sourceMappingURL=sentryScrubber.js.map