{"version":3,"file":"redact.cjs","sources":["../../../../src/core/http/redact.ts"],"sourcesContent":["/**\n * Bounded-depth redact for params that may contain credentials before they\n * enter any logger or error-rendering surface.\n *\n * Callers: `Http._sanitizeParams` (logger context), `_makeAxiosRequest`\n * (`post/send`, `post/response` and `post/catchError` info logs), `AjaxError`\n * constructor (stores `requestInfo.params` exposed by `toJSON()` /\n * `toString()`). Keeping a single source of truth means the redaction list\n * stays consistent across all of them.\n *\n * **This runs over response bodies as well as request params.** The\n * `post/response` callsite passes `response.data.result` through here, so\n * whatever the portal put in an answer is in scope too — a wider remit than\n * \"the parameters we sent\", and the reason pass 3 exists.\n *\n * Three complementary passes run over each value:\n *   1. Key match — a property whose (lower-cased) name is in\n *      {@link SENSITIVE_PARAM_KEYS} has its whole value replaced, so a nested\n *      credential object (e.g. `auth: { application_token }`) is masked\n *      wholesale (#151).\n *   2. Query-string scrub — a *string* value is scanned for\n *      `<sensitive-key>=<value>` pairs and the value is masked. This catches the\n *      batch `cmd[i]` shape (`method?auth=<token>&...`) where `_prepareParams`\n *      has already serialised the credential into text the key walk can't see\n *      (#229).\n *   3. Credential-in-path scrub — a *string* value is scanned for the Bitrix24\n *      incoming-webhook URL shape, `/rest/<userId>/<secret>/`, and the secret\n *      segment is masked. Neither of the passes above can see it: the secret is\n *      not a key, and it is not a `key=value` pair — it is a path segment, so\n *      neither pass above can see it. It turns up in a log because this runs\n *      over response bodies as well as over request params, and a portal method\n *      can return such a URL: `rest.deferredbatch.downloadresult` answers\n *      `{ result: { downloadUrl } }` built from the calling webhook, measured on\n *      a cloud portal.\n *\n * The object walk descends two levels into nested objects and arrays — the\n * minimum that covers batch payloads (`{ cmd: [{ method, params:\n * { ...credentials... } }, ...] }`) and flat one-level-nested payloads like\n * `{ data: { token } }`.\n *\n * Residual risk (documented, accepted):\n *   - credential keys nested deeper than two object levels are NOT masked —\n *     redact at the callsite for those;\n *   - the query-string scrub only masks a `key=value` pair whose key is itself\n *     a sensitive key; a bracketed/encoded query key (`auth[application_token]=`)\n *     is not matched by the string pass (its `auth` prefix object form is,\n *     though, via pass 1). This covers the v2 file links\n *     (`uf.php?…&auth[ap]=…`, e.g. `task.item.getfiles` `DOWNLOAD_URL`). The\n *     decision was made in PR #235 and is closed; do not reopen it.\n *   - `key` is deliberately broad: any property literally named `key` (and any\n *     `?key=…` query pair) is masked. In Bitrix24 REST `key` is a credential\n *     parameter (e.g. the Pull shared config), so this is a conservative,\n *     accepted trade-off — it can over-redact a non-credential field that\n *     happens to be named `key`.\n *   - `signature` is broad in the same way (added in #43 for the Pull channel\n *     HMAC, `TypeChanel.signature`): any property named `signature` and any\n *     `?signature=…` query pair is masked. In the Bitrix24 push/pull domain\n *     `signature` is the channel HMAC, so the breadth is accepted — at the cost\n *     of over-redacting a non-credential field that happens to be named so.\n *   - empty / nullish values are still treated as sensitive — an empty\n *     `access_token` is unusual but not safe to leave un-redacted.\n *   - the path scrub matches one shape — Bitrix24's own webhook URL — and not\n *     \"a credential somewhere in a path\" in general, which is not a decidable\n *     question. A credential that some other service puts in a path is not\n *     covered.\n */\n\nexport const SENSITIVE_PARAM_KEYS: readonly string[] = [\n  'auth',\n  'password',\n  'token',\n  'secret',\n  'access_token',\n  'refresh_token',\n  'client_secret',\n  'application_token',\n  'sessid',\n  'key',\n  'signature'\n]\n\nexport const REDACTED_PLACEHOLDER = '***REDACTED***'\n\n// Matches `<sep><sensitive-key>=<value>` inside a string, case-insensitively,\n// and masks the value. The `([?&]|^)` prefix anchors to a real query-param\n// boundary so a credential name appearing inside a value (`foo=token=x`) or as\n// the tail of a longer key (`access_token` vs `token`) is not mis-matched. The\n// value runs to the next `&`, `#`, or `;`, so a `;`-separated adjacent param is\n// not swallowed into the redacted span. An embedded `?token=…` inside a nested\n// URL value IS masked (intended — still a credential). Single-line: `^` carries\n// no `m` flag, so a credential after a newline in a multi-line string value is\n// not caught (accepted residual risk, same class as encoded/bracketed keys).\nconst QS_SENSITIVE_RE = new RegExp(\n  `([?&]|^)(${SENSITIVE_PARAM_KEYS.join('|')})=[^&#;]*`,\n  'gi'\n)\n\n/**\n * The Bitrix24 incoming-webhook URL shape: `/rest/<userId>/<secret>/`.\n *\n * Deliberately narrow, because a path segment carries no name to match on and\n * the only defence against over-masking is the shape itself:\n *\n * - `<userId>` is digits, as the portal builds it;\n * - `<secret>` carries no dot, so a REST method name never matches — those do\n *   (`crm.item.list`, `rest.deferredbatch.downloadresult`). Hyphen and\n *   underscore are inside the class even though Bitrix24 issues alphanumeric\n *   secrets: the SDK does not control that format, and guessing it too narrowly\n *   fails in the direction that matters;\n * - at least 8 characters, which no short path word (`batch`, `profile`,\n *   `download`) reaches, and every real webhook secret does — they are issued\n *   far longer.\n *\n * **Both API versions.** `restApi:v3` puts the webhook at `/rest/api/<id>/…`\n * rather than `/rest/<id>/…` (`B24Hook.fromWebhookUrl`), so the `api/` segment\n * is optional here — a pattern written for v2 alone would leave every v3 hook\n * unmasked.\n *\n * **The segment may end the string.** `.../rest/1/<secret>` with no trailing\n * slash is a legitimate webhook URL — it is the form `fromWebhookUrl` accepts —\n * so the boundary is `/`, `?`, `#` or end of input, not `/` alone. This is\n * where the shape stops being conservative: `/rest/1/somedotlessword` at the end\n * of a string is masked too. Accepted deliberately — dotless REST method names\n * are short (`batch`, `scope`, `profile`) and fall under the length floor, and\n * masking a method name costs a line of debugging detail where missing a secret\n * costs rather more.\n *\n * Case-insensitive, matching the query-string pass. The portal issues a\n * lower-case `/rest/`, but a URL that reached a log may have been copied,\n * proxied or hand-written, and `/REST/` guards nothing by staying literal.\n *\n * Only the secret is masked; the portal host and the user id stay readable,\n * because a redacted line still has to be usable for debugging.\n */\nconst WEBHOOK_PATH_RE = /(\\/rest\\/(?:api\\/)?\\d+\\/)[\\w-]{8,}(?=[/?#]|$)/gi\n\n// Safe to share at module scope despite the `g` flag: `String.replace` scans\n// from 0 and resets `lastIndex` when it finishes, and this regex is only ever\n// used that way. A `.test()` or `.exec()` call on it would carry `lastIndex`\n// between calls and start skipping matches — do not add one.\nfunction redactWebhookPath(value: string): string {\n  return value.replace(WEBHOOK_PATH_RE, `$1${REDACTED_PLACEHOLDER}`)\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n  return value !== null && typeof value === 'object' && !Array.isArray(value)\n}\n\nfunction redactQueryString(value: string): string {\n  if (!value.includes('=')) return value\n  return value.replace(\n    QS_SENSITIVE_RE,\n    (_match, sep: string, key: string) => `${sep}${key}=${REDACTED_PLACEHOLDER}`\n  )\n}\n\n/**\n * Both string passes, in the order that keeps each one's fast path honest.\n *\n * The query scrub returns early when the string holds no `=` at all, which is\n * correct for a query pair and wrong for a path: `.../rest/1/<secret>/download/`\n * has no `=` anywhere, so routing every string through the query pass alone\n * left exactly that URL untouched.\n */\nfunction redactString(value: string): string {\n  return redactWebhookPath(redactQueryString(value))\n}\n\n// String scrubbing runs before the `depth <= 0` guard on purpose: scanning a\n// string is cheap and bounded, so a serialised credential is masked even at a\n// level the object walk would stop descending into. Arrays do not consume a\n// depth slot (only object descent decrements `depth`), so an array nested in an\n// array is still walked.\nfunction redactValue(value: unknown, depth: number): unknown {\n  if (typeof value === 'string') return redactString(value)\n  if (depth <= 0) return value\n  if (isPlainObject(value)) return redactObject(value, depth - 1)\n  if (Array.isArray(value)) return value.map(item => redactValue(item, depth))\n  return value\n}\n\nfunction redactObject(\n  source: Record<string, unknown>,\n  depth: number\n): Record<string, unknown> {\n  const sanitized: Record<string, unknown> = { ...source }\n  for (const key of Object.keys(sanitized)) {\n    if (SENSITIVE_PARAM_KEYS.includes(key.toLowerCase())) {\n      sanitized[key] = REDACTED_PLACEHOLDER\n      continue\n    }\n    sanitized[key] = redactValue(sanitized[key], depth)\n  }\n  return sanitized\n}\n\nconst DEFAULT_REDACT_DEPTH = 2\n\n/**\n * Returns a copy of `params` with any known credential-bearing key replaced by\n * `REDACTED_PLACEHOLDER`, and any credential embedded in a string — a query\n * value or a webhook secret in a URL path — masked in place. Walks up to two\n * levels into nested objects/arrays so batch-shaped payloads\n * (`cmd[i].params.<key>` and `cmd[i]` query strings) are covered.\n *\n * **An array or a string at the top level is walked too.** It used to be\n * returned untouched: this function began `if (!isPlainObject(params)) return\n * params`, and `isPlainObject` excludes arrays. The walker underneath has\n * always handled both — only the door was shut. So the same content was masked\n * inside an object and printed verbatim when it arrived on its own, and #468\n * widened that gap rather than closing it, by teaching the string pass to mask\n * a webhook secret in a URL path that a top-level string never reached.\n *\n * That is not a corner: `_makeAxiosRequest` logs `response.data?.result`, and a\n * `restApi:v3` batch answers with an array there, so every successful v3 batch\n * wrote its response to the log unmasked.\n *\n * The plain-object branch is kept rather than folded into `redactValue`,\n * because entering through the value walker would spend a depth level on the\n * object itself and leave one for its contents — half the reach that\n * batch-shaped payloads need.\n */\nexport function redactSensitiveParams(\n  params: Record<string, unknown>\n): Record<string, unknown>\nexport function redactSensitiveParams<T>(params: T): T\nexport function redactSensitiveParams(params: unknown): unknown {\n  if (isPlainObject(params)) return redactObject(params, DEFAULT_REDACT_DEPTH)\n  return redactValue(params, DEFAULT_REDACT_DEPTH)\n}\n\n/**\n * Redact credentials in a URL string — e.g. a Pull `connectionPath` surfaced by\n * `getDebugInfo()`. Masks the webhook secret when the URL carries one in its\n * path (`/rest/<userId>/<secret>/`), and every\n * {@link SENSITIVE_PARAM_KEYS} value plus any caller-supplied `extraKeys`\n * (e.g. Pull's `CHANNEL_ID`, a private identifier that is not a global\n * credential key). `extraKeys` are regex-escaped, so any literal key name is\n * safe to pass. Anchored and bounded exactly like the in-object scrub, so a\n * value-position `=` and non-query strings are left intact. Non-string input is\n * returned unchanged (a defensive guard for untyped JS callers).\n */\nexport function redactSensitiveUrl(url: string, extraKeys: readonly string[] = []): string {\n  if (typeof url !== 'string') return url\n  // The webhook-path pass runs first and unconditionally: a webhook URL with no\n  // query string at all holds no `=`, and the query scrub's early return would\n  // otherwise hand it back with the secret intact.\n  const masked = redactWebhookPath(url)\n  if (!masked.includes('=')) return masked\n  if (extraKeys.length === 0) return redactQueryString(masked)\n  const escaped = extraKeys.map(key => key.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&'))\n  const re = new RegExp(\n    `([?&]|^)(${[...SENSITIVE_PARAM_KEYS, ...escaped].join('|')})=[^&#;]*`,\n    'gi'\n  )\n  return masked.replace(re, (_match, sep: string, key: string) => `${sep}${key}=${REDACTED_PLACEHOLDER}`)\n}\n"],"names":[],"mappings":"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"}