READ-ONLY PACKAGE PREVIEW

getsentry-secret-serialization/references/javascript-typescript.md

Version d18b7aa8ba87.bb1 · Apache-2.0. This preview displays packaged text and does not execute code. Treat the contents as untrusted instructions.

← Return to resource and package checksum

JavaScript and TypeScript Secret Serialization Notes

Use this when reviewing JavaScript or TypeScript code. These notes refine the core skill; they do not add reporting scope.

Generated Serialization Paths

Shape Paths that include every own enumerable property Exclusion
Plain object or class instance JSON.stringify, util.inspect / console.log, spread {...obj}, Object.entries, structuredClone #private fields (excluded from all of these), Object.defineProperty(obj, k, { enumerable: false }), a toJSON() returning an allowlisted object, [util.inspect.custom]() for inspect only
Class with TypeScript private or protected Same as a plain class. The modifier is compile-time only. Not an exclusion. Use #private.
Zod or Valibot parsed config object Plain object; JSON.stringify and loggers see every key Split secrets into a separate object, or redact at the sink
Value held in a closure Not reachable by serialization Preferred for client credentials
WeakMap keyed by instance Not reachable by JSON.stringify or util.inspect of the instance Acceptable for credentials attached to instances

A toJSON() covers JSON.stringify only. console.log, util.inspect, pino, and the Sentry normalizer walk own enumerable properties and ignore toJSON. Treat a toJSON-only block as partial when an inspect or logger sink exists.

High-Signal Sinks

Sink Why it leaks Safe form
logger.info({ ...config }), logger.info('msg', client) in pino, winston, or bunyan Serializes own enumerable properties Log identifiers. Logger redact paths are defense in depth, not the fix.
console.log(obj) / util.inspect(obj) in shipped code Walks properties, including TypeScript private ones Remove, or log a summary
Sentry.setContext('client', obj), setExtra, captureException(err, { extra: { client } }) The normalizer walks properties Pass explicit scalar fields
span.setAttribute(key, JSON.stringify(obj)), span.setAttributes(obj) Stringifies whole objects Allowlist scalar attributes
Logging an HTTP client error with its request config (for example axios error.config.headers) The attached request carries Authorization Strip headers before logging
res.json(obj), Response.json(obj), a Server Action return value Serializes a config or client object into a response Return a DTO
for (const [k, v] of Object.entries(args)) attrs[k] = String(v) Stringifies arbitrary argument objects into telemetry Allowlist keys; record v.constructor.name for objects; redact credential-named keys

Examples

Report (high): token property on a client that instrumentation serializes

export class ApiClient {
  constructor(
    public readonly baseUrl: string,
    private readonly token: string,   // compile-time private only
  ) {}
}
// instrumentation.ts, already on the default branch
span.setAttribute('tool.args', JSON.stringify(args)); // args.client is an ApiClient

Evidence: private does not affect JSON.stringify, so args.client serializes with token. Fix: readonly #token: string or a closure, and have the sink skip object values.

Report (medium): unexcluded credential on a shared config object

export const config = {
  region: process.env.REGION,
  webhookSecret: process.env.WEBHOOK_SECRET,
};

export function createHandler(cfg: typeof config) { ... }

Evidence: config is exported and passed into handler factories. A repository search found no logger or Sentry context call on it yet. Fix: split into config and secrets, or read the secret inside the function that signs.

Report (medium): new wholesale sink

export function traced<T extends (...a: any[]) => any>(fn: T) {
  return (...args: Parameters<T>) =>
    Sentry.startSpan({ name: fn.name, attributes: { args: JSON.stringify(args) } }, () => fn(...args));
}

Evidence: every argument is stringified into span attributes with no filter. Fix: record primitive arguments and a type name for objects.

Do not report: #private field

export class ApiClient {
  readonly #token: string;
  constructor(readonly baseUrl: string, token: string) { this.#token = token; }
}

#private fields are not own enumerable properties. JSON.stringify, util.inspect, spread, and the Sentry normalizer do not see them.

Do not report: allowlisted toJSON with no inspect or logger sink

toJSON() { return { baseUrl: this.baseUrl }; }

This is enough when the only sinks are JSON.stringify or res.json. It becomes a partial exclusion if the instance is also passed to console.log or a logger.

Regression Test Shape

it('does not serialize the token', () => {
  const client = new ApiClient('https://example.invalid', 'token-value');
  expect(JSON.stringify(client)).not.toContain('token-value');
  expect(util.inspect(client)).not.toContain('token-value');
});

Assert against every serializer a sink in the repository uses.