v2.0.0

Logging & Credential Redaction

What the SDK redacts automatically before any request information enters the logger or an AjaxError, what it does not redact, and how to wire a custom logger via setLogger(...) without re-introducing credential leaks.

Overview

The SDK redacts a fixed set of credential-bearing keys before request data can reach a logger sink or an error object. The redactor (redact.ts) is a single source of truth, shared across the SDK.

In the HTTP layer (since v1.1.2) it strips those keys from any request payload before it can reach:

  • the logger context of post/send, post/response, post/catchError info-level entries (and the lower-level http request starting / _logRequest debug entry),
  • the requestInfo.params field carried by AjaxError, which is what consumers see via AjaxError.toString() / toJSON().

The same redactor — plus a few safe-projection fixes — also guards the credential surfaces outside the HTTP layer: the pull client, the frame postMessage channel, and hook error messages. See Non-HTTP surfaces below.

This means a user wiring a custom logger through B24Hook.setLogger(...), B24Frame.setLogger(...) or B24OAuth.setLogger(...) does not need to add their own redaction for the standard credential keys listed below — the SDK already strips them on the way out.

Caller-supplied custom fields are not covered. Anything you stuff into params under a non-standard key (e.g. mySecretField) reaches the logger verbatim. See What the SDK does not redact.

What the SDK redacts automatically

The single source of truth is packages/jssdk/src/core/http/redact.ts.

// packages/jssdk/src/core/http/redact.ts
export const SENSITIVE_PARAM_KEYS: readonly string[] = [
  'auth',
  'password',
  'token',
  'secret',
  'access_token',
  'refresh_token',
  'client_secret',
  'application_token',
  'sessid',
  'key',
  'signature'
]

Wherever any of these keys appears — matched case-insensitively, so Auth / TOKEN / Access_Token are caught too — the value is replaced with '***REDACTED***' (the exported REDACTED_PLACEHOLDER) before the payload is serialised for logging or stored on an error object.

The walk descends two levels into nested objects and arrays. That covers the batch shape { cmd: [{ method, params: { ...creds... } }, ...] } where the credential lives at cmd[i].params.<key>, plus flat one-level shapes like { data: { token } }. Anything deeper than that is not walked — keep secrets close to the top of the payload if you want them caught, or redact at the callsite.

The redactor also scans string values for query-string credential pairs (<key>=<value>) and masks the value in place. This covers the serialised batch command shape cmd[i] = 'method?auth=<token>&…', where the credential is text rather than an object key. Only a key=value pair whose key is one of the canonical keys is masked, and a query boundary (? / & / start of string) is required — so a value-position = is left alone.

The key entry is deliberately broad: it masks any property literally named key (and any ?key=… query pair), not only API-key-shaped values. In Bitrix24 REST key is a credential parameter, so this is a conservative choice — be aware a non-credential field named key will also show as ***REDACTED*** in your logs.

The signature entry (added in #43 for the Pull channel HMAC) is broad in the same way: any property named signature and any ?signature=… query pair is masked. In Bitrix24 push/pull signature is the channel HMAC, so the breadth is accepted — a non-credential field named signature will also render as ***REDACTED***.

HTTP callsites that consume the redactor

WhereLog channelLevelSource
post/send (request being dispatched)logger context paramsinfoabstract-http.ts:544
post/response (success body)logger context resultinfoabstract-http.ts:561
post/catchError (axios error)logger context responseDatainfoabstract-http.ts:506
http request starting (_logRequest)logger context paramsdebugabstract-http.ts:678
AjaxError constructorrequestInfo.params (visible via toJSON() / toString())n/aajax-error.ts:44

The other log callsites in abstract-http.ts (http request attempt, http request successful, http request failed, http refreshing auth token, http auth error detected, retry-wait logs) carry only requestId, method, api, attempt, duration, and AjaxError code/message/status — none of them touch params or the webhook URL, so they have no redaction concern.

The logged payloads are also length-capped: post/send params, post/response result, and post/catchError responseData are each truncated to a 100-character prefix + ... once they exceed 300 characters, so a large body (an HTML error page, a big validation dump) can't flood a wired logger sink. The cap is applied after redaction, so it never affects what is masked — only how much is written (#236).

The redaction is level-independent — it runs on the data before it is handed to the logger, so it applies whether your handler is filtering at DEBUG, INFO, ERROR, or anything else.

Non-HTTP surfaces

The #43 audit extended the same protection to the credential surfaces that live outside the HTTP layer. These do not pass through post/*, so they are guarded at their own callsites:

SurfaceWhat was carrying a credentialHow it's guarded
PulllogMessage (onPull*Event info logs), broadcastMessages error warning, attachCommandHandler debugapp-defined params / extra may hold a credential-shaped key (e.g. a channel signature)run through redactSensitiveParams before logging
Pull — JSON-RPC unknown id / unknown rpc packet / unknown rpc command in batch error logsan opaque server frame can carry a signaturerun through redactSensitiveParams
PullCHANNEL_EXPIRE "new config for … channel set" console logstringified the TypeChanel object, which includes its signatureemits [updated] instead of the object
Pull — unparseable raw wire framethe raw bytes could embed a signaturelogs only the frame's byteLength, never its content
FrameB24Frame.init() "init data" debug logthe handshake data carries AUTH_ID / REFRESH_ID / MEMBER_ID and the app *_OPTIONS storeslogs a fixed allowlist projection (PLACEMENT / LANG / INSTALL / IS_ADMIN / FIRST_RUN)
Frame — inbound MessageManager._runCallbackevent.data carries the refreshed AUTH_ID / REFRESH_IDlogs only the callback id and origin, never the payload
Frame — outbound MessageManager.sendthe assembled cmd string carries serialised setAppOption / setUserOption valueslogs only the command + callback key, never the cmd string
HookB24Hook.fromWebhookUrl parse / format errorsthe webhook URL embeds the secret in its path (/rest/<userId>/<secret>/)the thrown messages are generic, never echoing the URL

The pull localStorage config cache (push jwt + channel signatures) persists only for the life of the client: PullClient.destroy() removes it on teardown, and a stale entry is evicted on load. While the client is live the cache remains a documented, accepted shared-origin trade-off (it avoids a config round-trip on every reconnect) — see #242.

Webhook URL is no longer logged

The full webhook URL (https://<portal>/rest/<userId>/<secret>/<method>.json) used to enter the post/send info log. Since v1.1.2 only the bare REST method name (e.g. user.current, crm.item.add) is logged; the post/response and post/catchError callsites have always been URL-free and stay that way. AjaxQuery (the requestInfo shape carried by AjaxError) no longer types a url field at all — see ajax-result.ts:12-16 — so a future change cannot accidentally re-introduce the leak through error rendering.

Log archives from SDK versions >= 1.1.0, < 1.1.2 with a custom logger wired may contain the webhook URL pattern (/rest/{userId}/{secret}/) or the auth-token pattern ("auth":"...") that are no longer produced since v1.1.2. See Auditing your log archives below for patterns to verify your sinks are clean.

AjaxError — toJSON / toString

AjaxError accepts a requestInfo containing the caller-supplied params. The constructor passes those params through the same redactor, so the credential keys above never survive on the error object — neither in toJSON() output, nor in the stringified form, nor in any log line that subsequently includes the error.

// What you get out of AjaxError.toJSON().requestInfo.params,
// no matter what the caller passed in:
{
  ID: 42,
  access_token: '***REDACTED***',
  auth: '***REDACTED***'
}

What the SDK does not redact

The SDK knows about the canonical keys listed above, matched case-insensitively by exact key name. The redactor will not strip:

  • Custom payload fields you invented — e.g. mySecretField, apiKeyHeader, x_internal_token, anything whose key is not in SENSITIVE_PARAM_KEYS.
  • Credentials embedded inside string values, except canonical query-string pairs — a <canonical-key>=<value> pair inside a string (e.g. a batch cmd[i]) is masked, but a secret that is not in key=value form — a bare token pasted into a description, or a bracketed/encoded query key like auth[application_token]=… — is not.
  • Data more than two levels deep — anything past cmd[i].params.<key> depth is walked past, not into.
  • Custom request headers you set on the underlying axios client — e.g. an Authorization: Bearer <token> header configured via your own axios interceptor. The redactor only touches params, not headers, and headers are not part of the SDK's standard logger context anyway — but they do live on AxiosError.config.headers, see the next bullet.
Don't log err.originalError directly.AjaxError (via its SdkError base) exposes a public originalError field that, for transport failures, holds the rawAxiosError. That AxiosError.config.url contains the full webhook URL (/rest/{userId}/{secret}/method.json) and AxiosError.config.headers can contain Authorization. Neither is redacted by the SDK — they were never inside params. Use err.code, err.status, err.message, or err.requestInfo (which is redacted), and never blanket-stringify err.originalError into any logger sink.

Stripping the items above is caller responsibility. Either don't put secrets into non-standard keys / non-standard log channels to begin with, or redact in your custom logger's handler / processor.

The safe pattern is to leave the SDK's redaction in place and bolt your custom sink on top — never replace the SDK-side scrubbing with a homegrown one downstream of it.

// @check-ignore: full example — sendToRemoteAggregator is a placeholder, not declared

import {
  B24Hook,
  Logger,
  ConsoleV2Handler,
  LogLevel
} from '@bitrix24/b24jssdk'

const b24 = new B24Hook({ /* … */ })

const $logger = new Logger('app')
$logger.pushHandler(new ConsoleV2Handler(LogLevel.INFO))

// Optional belt-and-braces processor: redact any *custom* keys the SDK
// does not know about. Standard keys (auth / token / …) are already
// scrubbed before they reach this point.
$logger.pushProcessor((record) => {
  if (record.context?.params && typeof record.context.params === 'string') {
    record.context.params = record.context.params.replace(
      /"mySecretField":"[^"]*"/g,
      '"mySecretField":"***REDACTED***"'
    )
  }
  return record
})

b24.setLogger($logger)

What to avoid:

// @check-ignore: anti-pattern illustration — b24 and sendToRemoteAggregator are undeclared placeholders

// DON'T — re-emitting context without inspection forwards arbitrary
// caller-supplied data into a remote sink, and bypassing LoggerInterface
// with `as any` hides the fact that handlers must implement all 8
// log-level methods (log / debug / info / notice / warning / error /
// critical / alert / emergency). A partial implementation will silently
// drop entries at the unimplemented levels.
b24.setLogger({
  async info(message, context) {
    await sendToRemoteAggregator({
      msg: message,
      // `context.params` on `post/send` is already a redacted *string*,
      // but a custom action wrapper may also call this `info` with a
      // raw `params` object (or with an `error` whose `originalError`
      // is the unredacted `AxiosError`). Don't forward `context`
      // blindly — pick the fields you actually want.
      ctx: context
    })
  }
  // ...other 7 methods omitted — the cast below is what lets this
  // compile despite the incomplete interface; in real code, implement
  // `LoggerInterface` fully (or extend `AbstractLogger`).
} as any)

Three concrete failure modes to guard against:

  1. Logging the original request object yourself. If your wrapper captures params before calling into the SDK and logs it, the SDK's redactor never runs on that copy. Always redact in your own log path too.
  2. Reaching into err.originalError / err.cause. AjaxError's requestInfo.params is scrubbed, so String(err) and JSON.stringify(err) are safe by themselves. But if you log err.originalError, you may be looking at a raw AxiosError whose config.url still holds the full webhook URL. See the warning block in What the SDK does not redact.
  3. Custom or deeply-nested secrets in post/response. The post/response channel now runs response.data.result through the same redactor, so canonical credential keys at depth ≤ 2 are masked. But a secret under a custom key, or nested deeper than two levels, still reaches your sink — the same limits as everywhere else (see What the SDK does not redact).

Auditing your log archives

Scan your log archives to verify that no credential patterns are present — a useful routine check, especially if your setup pipes logs to a third-party aggregator or retains them long-term.

Webhook URL in request logs — grep (Linux/macOS) / ripgrep / PowerShell:

grep -rEi '/rest/[0-9]+/[a-zA-Z0-9]+/' /var/log/myapp/
rg -i '/rest/\d+/[a-zA-Z0-9]+/' /var/log/myapp/
Select-String -Path 'C:\logs\myapp\*.log' -Pattern '/rest/\d+/[a-zA-Z0-9]+/'

Credential keys in serialised params — grep (Linux/macOS) / ripgrep / PowerShell:

grep -rE '"(auth|password|token|secret|access_token|refresh_token|client_secret|application_token|sessid)":\s*"[^"]+"' /var/log/myapp/
rg '"(auth|password|token|secret|access_token|refresh_token|client_secret|application_token|sessid)":\s*"[^"]+"' /var/log/myapp/
Select-String -Path 'C:\logs\myapp\*.log' -Pattern '"(auth|password|token|secret|access_token|refresh_token|client_secret|application_token|sessid)":\s*"[^"]+"'

Replace /var/log/myapp/ (or C:\logs\myapp\) with your actual log paths. Common sinks to check: stdout captures (Docker logs, systemd journal, PM2), files written via StreamHandler, and third-party aggregators (Datadog, Logtail, Splunk, Papertrail, etc.).

The webhook URL pattern was produced by post/send entries in SDK versions >= 1.1.0, < 1.1.2 when a custom logger was wired. The credential-key patterns could appear in post/send for any version in that range where the corresponding key was present in request params. Both are suppressed by the SDK since v1.1.2.

client_secret, application_token and sessid were added to the redactor in v1.3 — include them (above) when scanning archives from endpoints that pass those params. The canonical list also covers key, omitted from the patterns above because "key": is too common to grep without heavy noise — scan for it manually if your integration uses key as a credential.

signature was added in #43 for the push/pull channel HMAC. Archives from older versions that wired a custom logger with the pull client active may contain "signature":"…" — scan for "signature":\s*"[^"]+" if you used the pull server, keeping in mind it (like key) can be noisy.

If either pattern matches in your archives, rotate the affected credentials: delete and recreate the webhook to generate a new secret, or revoke and reissue the OAuth / Frame token.

Advanced: reading the source

For details beyond what is summarised here, the canonical references are:

See also the Logger page for the logging framework itself (channels, handlers, processors, formatters) and the Errors page for AjaxError semantics.