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/catchErrorinfo-level entries (and the lower-levelhttp request starting/_logRequestdebug entry), - the
requestInfo.paramsfield carried byAjaxError, which is what consumers see viaAjaxError.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
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:
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 inSENSITIVE_PARAM_KEYS. - Credentials embedded inside string values, except canonical
query-string pairs — a
<canonical-key>=<value>pair inside a string (e.g. a batchcmd[i]) is masked, but a secret that is not inkey=valueform — a bare token pasted into adescription, or a bracketed/encoded query key likeauth[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 touchesparams, not headers, and headers are not part of the SDK's standard logger context anyway — but they do live onAxiosError.config.headers, see the next bullet.
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.
Recommended pattern: wiring a custom logger
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:
- Logging the original request object yourself. If your wrapper
captures
paramsbefore calling into the SDK and logs it, the SDK's redactor never runs on that copy. Always redact in your own log path too. - Reaching into
err.originalError/err.cause.AjaxError'srequestInfo.paramsis scrubbed, soString(err)andJSON.stringify(err)are safe by themselves. But if you logerr.originalError, you may be looking at a rawAxiosErrorwhoseconfig.urlstill holds the full webhook URL. See the warning block in What the SDK does not redact. - Custom or deeply-nested secrets in
post/response. Thepost/responsechannel now runsresponse.data.resultthrough 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:
packages/jssdk/src/core/http/redact.ts— the key list, placeholder, and walk depth.packages/jssdk/src/core/http/abstract-http.ts— every callsite that pipes data through the redactor.packages/jssdk/src/core/http/ajax-error.ts— error-construction redaction and therequestInfoshape.
See also the Logger page for
the logging framework itself (channels, handlers, processors,
formatters) and the Errors page
for AjaxError semantics.