Skip to content

fix(client): redact credentials from SDK object diagnostics - #351

Merged
ffumero2003 merged 3 commits into
mainfrom
fix-client-redact-diagnostics
Nov 5, 2026
Merged

ffumero2003 merged 3 commits into
mainfrom
fix-client-redact-diagnostics

Conversation

@ffumero2003

Copy link
Copy Markdown
Contributor

Summary

Make SDK objects safe to log. console.log, util.inspect, JSON.stringify and object spread of B2Client, Bucket, B2Object, the account-info stores and the upload URL pools no longer include the application key or authorization tokens. This mirrors what PartnerClient already does.

Changes

  • B2Client holds the application key in a # private field and gains a toJSON() / util.inspect summary (B2ClientJson): type, key ID, a credentials placeholder, the realm URL (sanitized), authorized state, and URL-guard suffixes.
  • InMemoryAccountInfo and FileAccountInfo hold their stored authorization and upload URL pools in # fields, with matching summaries (InMemoryAccountInfoJson, FileAccountInfoJson).
  • Bucket and B2Object hold their client reference in a # field; their own data (names, IDs) is unchanged.
  • UploadUrlPool holds its entries in a # field.
  • B2Client.accountInfo is now a prototype accessor, so spreading or copying a client doesn't copy a custom store.

No API is removed:

  • client.accountInfo, authorize() and getAuth() work as before and return the full response.
  • FileAccountInfo still persists and reloads the real token.

Compatibility (CHANGELOG "Changed")

accountInfo is no longer an own property:

  • { ...client }, Object.keys, Object.assign, Object.hasOwn and own-property descriptors don't see it;
  • assigning it throws in strict mode;
  • a subclass redeclaring it as a field fails with TS2610.

Reading client.accountInfo and 'accountInfo' in client still work. As with PartnerClient, methods called through a Proxy wrapper (for example Vue reactive(client)) throw Cannot read private member.

Tests

src/redaction.test.ts (Node and browser) and src/redaction.node.test.ts (util.inspect, file persistence):

  • Every surface above is checked for each object, with sentinel credentials.
  • Each redaction layer is tested on its own: a custom enumerable AccountInfo checks the client summary, Object.keys and spread check the # fields, and the pool is tested directly.
  • A reviewed-export list (src/test-utils/redaction-export-review.ts, test-only and not built) fails when a new public export appears without a review, so a future credential-holding class can't skip this.

Each layer was checked by reverting it alone; the matching test failed. Reverted that way:

  • the # fields;
  • the client and store toJSON and inspect methods;
  • the realm sanitization;
  • the getter;
  • token preservation in getAuth() and file persistence.

Verification

pnpm typecheck, pnpm lint, pnpm lint:docs, pnpm lint:spelling, pnpm test, pnpm test:browser and pnpm test:coverage (gates unchanged) pass.

@ffumero2003 ffumero2003 added this to the v0.5.0 milestone Oct 30, 2026
@ffumero2003 ffumero2003 added area: auth Area: auth area: security Area: security labels Oct 30, 2026
@ffumero2003 ffumero2003 self-assigned this Oct 30, 2026
@quality-keeper

quality-keeper Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

Quality Keeper — Advisory Review

Source: backblaze-labs/b2-sdk-typescript

Advisory only — this is not a required status check. Quality Keeper reviewed this repository snapshot and is reporting what it found. It changes no status and merges nothing; suggested scaffolds are starting points, not proof.

Quality Keeper assessed the repository, executed checks, and collected results, covering 19 quality expectations.

Five repository-quality areas

Hard expectations keep their exact Evidence State. Advisory applicability questions are separate. Implementation or suite presence is not proof of a trusted run.

Area Hard expectations Evidence States Implementation status Advisory questions
Observability 0 — not_evaluated 1 0
Product Analytics 0 — not_evaluated 1 0
User Feedback 0 — not_evaluated 1 0
Reliability 3 pending_run 3 not_evaluated 3 0
Security 4 pending_run 2, verified 2 not_evaluated 4 0
Other registered quality 12 pending_run 10, verified 2 not_evaluated 12 0
Observability — 0 hard, 0 advisory

Can operators understand and diagnose repository-owned runtime behavior?

Hard expectations

None selected.

Applicability Review Gaps (advisory)

None.

Product Analytics — 0 hard, 0 advisory

Does repository-owned product analytics instrumentation behave safely and as declared?

Hard expectations

None selected.

Applicability Review Gaps (advisory)

None.

User Feedback — 0 hard, 0 advisory

Do repository-owned feedback and support-intake flows behave safely and accessibly?

Hard expectations

None selected.

Applicability Review Gaps (advisory)

None.

Reliability — 3 hard, 0 advisory

Does repository-owned software fail, recover, time out, and report health predictably?

Hard expectations

  • Cancellation or idempotency controls need behavior testing — Testing Type reliability_resilience_testing — Evidence State pending_run
    • Why it applies: Production source declares a cancellation primitive or idempotency control, so its externally observable behavior needs repository-owned verification.
    • Implementation presence: not_evaluated
    • Implementation explanation: Implementation detection was not evaluated because no applicable standards-owned detection contract was available. Evidence State remains pending_run; if verified, that verdict still comes only from Evaluate's accepted evidence.
    • Applicability signals: none recorded
    • Behavior suite / source: not required by the selected criteria
    • Native GitHub result: not observed
    • Structured summary: not_observed
    • Trusted run: not required by the selected criteria
    • Revision / freshness: 972def9d278b2e2cf7a1dbaaadb62a88b5cd4dba / unknown
    • Partial/unbound proof: implementation detection not evaluated; Evidence State remains pending_run
    • Owner / target: backblaze-labs/b2-sdk-typescript / src:c345774dbcc45204
    • Next: No seed proposal — an operator must design acceptable evidence for this expectation.
  • Outbound calling assets need timeout testing — Testing Type reliability_resilience_testing — Evidence State pending_run
    • Why it applies: Production source contains a recognized outbound network-client call site, so explicit timeout behavior needs repository-owned verification.
    • Implementation presence: not_evaluated
    • Implementation explanation: Implementation detection was not evaluated because no applicable standards-owned detection contract was available. Evidence State remains pending_run; if verified, that verdict still comes only from Evaluate's accepted evidence.
    • Applicability signals: none recorded
    • Behavior suite / source: not required by the selected criteria
    • Native GitHub result: not observed
    • Structured summary: not_observed
    • Trusted run: not required by the selected criteria
    • Revision / freshness: 972def9d278b2e2cf7a1dbaaadb62a88b5cd4dba / unknown
    • Partial/unbound proof: implementation detection not evaluated; Evidence State remains pending_run
    • Owner / target: backblaze-labs/b2-sdk-typescript / src:c345774dbcc45204
    • Next: No seed proposal — an operator must design acceptable evidence for this expectation.
  • Published sdk packages need reliability testing — Testing Type reliability_resilience_testing — Evidence State pending_run
    • Why it applies: The interpreted asset is a published package or library with a detected external service or network boundary — consumers depend on predictable error classification and failure-path behavior, so those paths need verification against the declared public contract. Timeout and retry expectations are selected separately when their owned call sites or policies are detected.
    • Implementation presence: not_evaluated
    • Implementation explanation: Implementation detection was not evaluated because no applicable standards-owned detection contract was available. Evidence State remains pending_run; if verified, that verdict still comes only from Evaluate's accepted evidence.
    • Applicability signals: none recorded
    • Behavior suite / source: not required by the selected criteria
    • Native GitHub result: not observed
    • Structured summary: not_observed
    • Trusted run: not required by the selected criteria
    • Revision / freshness: 972def9d278b2e2cf7a1dbaaadb62a88b5cd4dba / unknown
    • Partial/unbound proof: implementation detection not evaluated; Evidence State remains pending_run
    • Owner / target: backblaze-labs/b2-sdk-typescript / src:c345774dbcc45204
    • Next: No seed proposal — an operator must design acceptable evidence for this expectation.

Applicability Review Gaps (advisory)

None.

Security — 4 hard, 0 advisory

Do static controls and repository-owned security behaviors protect the intended boundaries?

Hard expectations

  • Authorization boundaries need permission behavior testing — Testing Type dynamic_security_testing — Evidence State pending_run
    • Why it applies: The interpreted asset contains a production authorization, role, permission, tenant, resource, or ownership boundary, so repository-owned tests must prove forbidden cross-boundary access is denied.
    • Implementation presence: not_evaluated
    • Implementation explanation: Implementation detection was not evaluated because no applicable standards-owned detection contract was available. Evidence State remains pending_run; if verified, that verdict still comes only from Evaluate's accepted evidence.
    • Applicability signals: none recorded
    • Behavior suite / source: not required by the selected criteria
    • Native GitHub result: not observed
    • Structured summary: not_observed
    • Trusted run: not required by the selected criteria
    • Revision / freshness: 972def9d278b2e2cf7a1dbaaadb62a88b5cd4dba / unknown
    • Partial/unbound proof: implementation detection not evaluated; Evidence State remains pending_run
    • Owner / target: backblaze-labs/b2-sdk-typescript / src:c345774dbcc45204
    • Next: No seed proposal — an operator must design acceptable evidence for this expectation.
  • Credential handling assets need non exposure testing — Testing Type dynamic_security_testing — Evidence State pending_run
    • Why it applies: The interpreted asset contains production credential, token, password, API-key, redaction, or sanitization handling, so repository-owned tests must prove sensitive values are not logged, returned, or durably exposed.
    • Implementation presence: not_evaluated
    • Implementation explanation: Implementation detection was not evaluated because no applicable standards-owned detection contract was available. Evidence State remains pending_run; if verified, that verdict still comes only from Evaluate's accepted evidence.
    • Applicability signals: none recorded
    • Behavior suite / source: not required by the selected criteria
    • Native GitHub result: not observed
    • Structured summary: not_observed
    • Trusted run: not required by the selected criteria
    • Revision / freshness: 972def9d278b2e2cf7a1dbaaadb62a88b5cd4dba / unknown
    • Partial/unbound proof: implementation detection not evaluated; Evidence State remains pending_run
    • Owner / target: backblaze-labs/b2-sdk-typescript / src:c345774dbcc45204
    • Next: No seed proposal — an operator must design acceptable evidence for this expectation.
  • Code and configuration assets need static security analysis — Testing Type static_security_analysis — Evidence State verified
    • Why it applies: The interpreted asset contains code, configuration, dependencies, secrets coordinates, infrastructure-as-code, or service boundaries, so static security analysis is required before runtime evidence is trusted.
    • Implementation presence: not_evaluated
    • Implementation explanation: Implementation detection was not evaluated because no applicable standards-owned detection contract was available. Evidence State remains verified; if verified, that verdict still comes only from Evaluate's accepted evidence.
    • Applicability signals: none recorded
    • Behavior suite / source: not required by the selected criteria
    • Native GitHub result: not observed
    • Structured summary: not_observed
    • Trusted run: not required by the selected criteria
    • Revision / freshness: 972def9d278b2e2cf7a1dbaaadb62a88b5cd4dba / current
    • Partial/unbound proof: implementation detection not evaluated
    • Owner / target: backblaze-labs/b2-sdk-typescript / src:c345774dbcc45204
    • Next: None — already verified before this run.
  • Published packages need provenance proof — Testing Type static_security_analysis — Evidence State verified
    • Why it applies: The interpreted asset is a published package with source code, so published release artifacts should carry provenance metadata (npm provenance, SLSA attestation, or sigstore signature) to protect the supply chain.
    • Implementation presence: not_evaluated
    • Implementation explanation: Implementation detection was not evaluated because no applicable standards-owned detection contract was available. Evidence State remains verified; if verified, that verdict still comes only from Evaluate's accepted evidence.
    • Applicability signals: none recorded
    • Behavior suite / source: not required by the selected criteria
    • Native GitHub result: not observed
    • Structured summary: not_observed
    • Trusted run: not required by the selected criteria
    • Revision / freshness: 972def9d278b2e2cf7a1dbaaadb62a88b5cd4dba / current
    • Partial/unbound proof: implementation detection not evaluated
    • Owner / target: backblaze-labs/b2-sdk-typescript / src:c345774dbcc45204
    • Next: None — already verified before this run.

Applicability Review Gaps (advisory)

None.

Other registered quality — 12 hard, 0 advisory

Registered Testing Types not assigned to one of the five focused areas.

Hard expectations

  • Packaged or runnable assets need build verification — Testing Type build_verification — Evidence State pending_run
    • Why it applies: The interpreted asset has package, script, workflow, UI, service, CLI, edge-runtime, or game-runtime signals, so it needs a reproducible build or equivalent construction check.
    • Implementation presence: not_evaluated
    • Implementation explanation: Implementation detection was not evaluated because no applicable standards-owned detection contract was available. Evidence State remains pending_run; if verified, that verdict still comes only from Evaluate's accepted evidence.
    • Applicability signals: none recorded
    • Behavior suite / source: not required by the selected criteria
    • Native GitHub result: not observed
    • Structured summary: not_observed
    • Trusted run: not required by the selected criteria
    • Revision / freshness: 972def9d278b2e2cf7a1dbaaadb62a88b5cd4dba / unknown
    • Partial/unbound proof: implementation detection not evaluated; Evidence State remains pending_run
    • Owner / target: backblaze-labs/b2-sdk-typescript / src:c345774dbcc45204
    • Next: None — exercised this run.
  • Published packages need sdk build artifact review — Testing Type build_verification — Evidence State pending_run
    • Why it applies: The interpreted asset is a published package with source code, so the distribution manifest completeness, pack tarball integrity, and declared file list need verification as part of the release artifact.
    • Implementation presence: not_evaluated
    • Implementation explanation: Implementation detection was not evaluated because no applicable standards-owned detection contract was available. Evidence State remains pending_run; if verified, that verdict still comes only from Evaluate's accepted evidence.
    • Applicability signals: none recorded
    • Behavior suite / source: not required by the selected criteria
    • Native GitHub result: not observed
    • Structured summary: not_observed
    • Trusted run: not required by the selected criteria
    • Revision / freshness: 972def9d278b2e2cf7a1dbaaadb62a88b5cd4dba / unknown
    • Partial/unbound proof: implementation detection not evaluated; Evidence State remains pending_run
    • Owner / target: backblaze-labs/b2-sdk-typescript / src:c345774dbcc45204
    • Next: None — exercised this run.
  • Consumer or platform boundaries need compatibility testing — Testing Type compatibility_testing — Evidence State pending_run
    • Why it applies: The interpreted asset is published or consumed across environments (published package, exported API, command-line tool, or edge target), so compatibility across platforms, versions, and consumers needs verification.
    • Implementation presence: not_evaluated
    • Implementation explanation: Implementation detection was not evaluated because no applicable standards-owned detection contract was available. Evidence State remains pending_run; if verified, that verdict still comes only from Evaluate's accepted evidence.
    • Applicability signals: none recorded
    • Behavior suite / source: not required by the selected criteria
    • Native GitHub result: not observed
    • Structured summary: not_observed
    • Trusted run: not required by the selected criteria
    • Revision / freshness: 972def9d278b2e2cf7a1dbaaadb62a88b5cd4dba / unknown
    • Partial/unbound proof: implementation detection not evaluated; Evidence State remains pending_run
    • Owner / target: backblaze-labs/b2-sdk-typescript / src:c345774dbcc45204
    • Next: None — exercised this run.
  • Published typescript packages need type declaration check — Testing Type compatibility_testing — Evidence State pending_run
    • Why it applies: The interpreted asset is a published TypeScript package (tsconfig.json or has_types in package.json detected), so generated .d.ts declaration files and type resolution must be verified from a consumer project's perspective.
    • Implementation presence: not_evaluated
    • Implementation explanation: Implementation detection was not evaluated because no applicable standards-owned detection contract was available. Evidence State remains pending_run; if verified, that verdict still comes only from Evaluate's accepted evidence.
    • Applicability signals: none recorded
    • Behavior suite / source: not required by the selected criteria
    • Native GitHub result: not observed
    • Structured summary: not_observed
    • Trusted run: not required by the selected criteria
    • Revision / freshness: 972def9d278b2e2cf7a1dbaaadb62a88b5cd4dba / unknown
    • Partial/unbound proof: implementation detection not evaluated; Evidence State remains pending_run
    • Owner / target: backblaze-labs/b2-sdk-typescript / src:c345774dbcc45204
    • Next: None — exercised this run.
  • Every asset needs configuration review — Testing Type configuration_artifact_review — Evidence State verified
    • Why it applies: Every interpreted asset needs a static review of its declared files, manifests, configuration, and artifacts so Quality Keeper can explain its shape before evidence is harvested.
    • Implementation presence: not_evaluated
    • Implementation explanation: Implementation detection was not evaluated because no applicable standards-owned detection contract was available. Evidence State remains verified; if verified, that verdict still comes only from Evaluate's accepted evidence.
    • Applicability signals: none recorded
    • Behavior suite / source: not required by the selected criteria
    • Native GitHub result: not observed
    • Structured summary: not_observed
    • Trusted run: not required by the selected criteria
    • Revision / freshness: 972def9d278b2e2cf7a1dbaaadb62a88b5cd4dba / current
    • Partial/unbound proof: implementation detection not evaluated
    • Owner / target: backblaze-labs/b2-sdk-typescript / src:c345774dbcc45204
    • Next: None — already verified before this run.
  • Public or network boundaries need contract testing — Testing Type contract_testing — Evidence State pending_run
    • Why it applies: The interpreted asset publishes or exposes a boundary that other software depends on (network API, CLI, package exports, or an importable Go external client), so the public interface contract needs verification.
    • Implementation presence: not_evaluated
    • Implementation explanation: Implementation detection was not evaluated because no applicable standards-owned detection contract was available. Evidence State remains pending_run; if verified, that verdict still comes only from Evaluate's accepted evidence.
    • Applicability signals: none recorded
    • Behavior suite / source: not required by the selected criteria
    • Native GitHub result: not observed
    • Structured summary: not_observed
    • Trusted run: not required by the selected criteria
    • Revision / freshness: 972def9d278b2e2cf7a1dbaaadb62a88b5cd4dba / unknown
    • Partial/unbound proof: implementation detection not evaluated; Evidence State remains pending_run
    • Owner / target: backblaze-labs/b2-sdk-typescript / src:c345774dbcc45204
    • Next: It was selected but produced no result — opt in (e.g. --include-expensive) or fix the command; do not just re-select it.
  • Published sdk packages need integration testing — Testing Type integration_testing — Evidence State pending_run
    • Why it applies: The interpreted asset is a published package or library that creates an integration boundary for consumers, so the package's integration paths, API contracts, and external service behaviors need integration-level verification.
    • Implementation presence: not_evaluated
    • Implementation explanation: Implementation detection was not evaluated because no applicable standards-owned detection contract was available. Evidence State remains pending_run; if verified, that verdict still comes only from Evaluate's accepted evidence.
    • Applicability signals: none recorded
    • Behavior suite / source: not required by the selected criteria
    • Native GitHub result: not observed
    • Structured summary: not_observed
    • Trusted run: not required by the selected criteria
    • Revision / freshness: 972def9d278b2e2cf7a1dbaaadb62a88b5cd4dba / unknown
    • Partial/unbound proof: implementation detection not evaluated; Evidence State remains pending_run
    • Owner / target: backblaze-labs/b2-sdk-typescript / src:c345774dbcc45204
    • Next: It was selected but produced no result — opt in (e.g. --include-expensive) or fix the command; do not just re-select it.
  • Every asset needs discoverability review — Testing Type metadata_discoverability_review — Evidence State verified
    • Why it applies: Every interpreted asset needs enough metadata and documentation for humans and systems to identify what it is and how to work with it.
    • Implementation presence: not_evaluated
    • Implementation explanation: Implementation detection was not evaluated because no applicable standards-owned detection contract was available. Evidence State remains verified; if verified, that verdict still comes only from Evaluate's accepted evidence.
    • Applicability signals: none recorded
    • Behavior suite / source: not required by the selected criteria
    • Native GitHub result: not observed
    • Structured summary: not_observed
    • Trusted run: not required by the selected criteria
    • Revision / freshness: 972def9d278b2e2cf7a1dbaaadb62a88b5cd4dba / current
    • Partial/unbound proof: implementation detection not evaluated
    • Owner / target: backblaze-labs/b2-sdk-typescript / src:c345774dbcc45204
    • Next: None — already verified before this run.
  • Documented assets need specification conformity review — Testing Type specification_conformity_review — Evidence State pending_run
    • Why it applies: The asset carries documentation, so documented commands, setup instructions, examples, and claims need to stay aligned with the implementation.
    • Implementation presence: not_evaluated
    • Implementation explanation: Implementation detection was not evaluated because no applicable standards-owned detection contract was available. Evidence State remains pending_run; if verified, that verdict still comes only from Evaluate's accepted evidence.
    • Applicability signals: none recorded
    • Behavior suite / source: not required by the selected criteria
    • Native GitHub result: not observed
    • Structured summary: not_observed
    • Trusted run: not required by the selected criteria
    • Revision / freshness: 972def9d278b2e2cf7a1dbaaadb62a88b5cd4dba / unknown
    • Partial/unbound proof: implementation detection not evaluated; Evidence State remains pending_run
    • Owner / target: backblaze-labs/b2-sdk-typescript / src:c345774dbcc45204
    • Next: None — exercised this run.
  • Published packages with docs need example validation — Testing Type specification_conformity_review — Evidence State pending_run
    • Why it applies: The interpreted asset is a published package with detected documentation, so code examples in README, docs, or examples directories should be compiled or type-checked to confirm they match the actual public API.
    • Implementation presence: not_evaluated
    • Implementation explanation: Implementation detection was not evaluated because no applicable standards-owned detection contract was available. Evidence State remains pending_run; if verified, that verdict still comes only from Evaluate's accepted evidence.
    • Applicability signals: none recorded
    • Behavior suite / source: not required by the selected criteria
    • Native GitHub result: not observed
    • Structured summary: not_observed
    • Trusted run: not required by the selected criteria
    • Revision / freshness: 972def9d278b2e2cf7a1dbaaadb62a88b5cd4dba / unknown
    • Partial/unbound proof: implementation detection not evaluated; Evidence State remains pending_run
    • Owner / target: backblaze-labs/b2-sdk-typescript / src:c345774dbcc45204
    • Next: None — exercised this run.
  • Cli sdk iac assets need system testing — Testing Type system_testing — Evidence State pending_run
    • Why it applies: The interpreted asset is a CLI, published package/SDK, or IaC provider that makes outbound calls to a real external dependency, so its own entry point needs an end-to-end test of that real flow — not just unit or integration coverage of individual pieces.
    • Implementation presence: not_evaluated
    • Implementation explanation: Implementation detection was not evaluated because no applicable standards-owned detection contract was available. Evidence State remains pending_run; if verified, that verdict still comes only from Evaluate's accepted evidence.
    • Applicability signals: none recorded
    • Behavior suite / source: not required by the selected criteria
    • Native GitHub result: not observed
    • Structured summary: not_observed
    • Trusted run: not required by the selected criteria
    • Revision / freshness: 972def9d278b2e2cf7a1dbaaadb62a88b5cd4dba / unknown
    • Partial/unbound proof: implementation detection not evaluated; Evidence State remains pending_run
    • Owner / target: backblaze-labs/b2-sdk-typescript / src:c345774dbcc45204
    • Next: It was selected but produced no result — opt in (e.g. --include-expensive) or fix the command; do not just re-select it.
  • Code assets need unit testing — Testing Type unit_testing — Evidence State pending_run
    • Why it applies: The interpreted asset contains runnable or reusable source code, so the smallest testable units need isolated verification.
    • Implementation presence: not_evaluated
    • Implementation explanation: Implementation detection was not evaluated because no applicable standards-owned detection contract was available. Evidence State remains pending_run; if verified, that verdict still comes only from Evaluate's accepted evidence.
    • Applicability signals: none recorded
    • Behavior suite / source: not required by the selected criteria
    • Native GitHub result: not observed
    • Structured summary: not_observed
    • Trusted run: not required by the selected criteria
    • Revision / freshness: 972def9d278b2e2cf7a1dbaaadb62a88b5cd4dba / unknown
    • Partial/unbound proof: implementation detection not evaluated; Evidence State remains pending_run
    • Owner / target: backblaze-labs/b2-sdk-typescript / src:c345774dbcc45204
    • Next: None — exercised this run.

Applicability Review Gaps (advisory)

None.

Result

  • 11 expectations are covered by acceptable evidence.
  • 8 still need attention — Quality Keeper cannot count them as covered yet.

Covered

Quality Keeper found acceptable evidence for one or more expectations in these Testing Types.

  • Build Verification — 2 expectations covered (Ran and passed).
    Confirms the software actually builds/packages into a runnable or publishable artifact.
    • What Quality Keeper ran:
      • pnpm run build → passed @. (asset scope) — declared in package.json
      • pnpm run typecheck → passed @. (asset scope) — declared in package.json
      • Full command output/logs are in the run artifacts (see Proof below).
  • Compatibility Testing — 2 expectations covered (Ran and passed).
    Checks the software works across the consumers, platforms, or versions it claims to support.
    • What Quality Keeper ran:
      • pnpm run verify:release → passed @. (asset scope) — declared in package.json
      • Full command output/logs are in the run artifacts (see Proof below).
  • Configuration & Artifact Review — 1 expectation covered (Verified before execution).
    Checks that config files, manifests, and build artifacts are present, well-formed, and say what they should.
  • Metadata & Discoverability Review — 1 expectation covered (Verified before execution).
    Checks that a repository or package is findable and understandable — description, topics, README, keywords.
  • Specification Conformity Review — 2 expectations covered (Ran and passed).
    Checks that documented assets actually match the specification or documented behavior they claim.
    • What Quality Keeper ran:
      • pnpm run lint:docs → passed @. (asset scope) — declared in package.json
      • Full command output/logs are in the run artifacts (see Proof below).
  • Static Security Analysis — 2 expectations covered (Verified before execution).
    Scans code and configuration for security problems without running it — secrets, unsafe patterns, misconfigurations.
    • What Quality Keeper ran:
      • pnpm run audit:deps → passed @. (asset scope) — declared in package.json
      • pnpm run test:audit:deps → passed — 7 passed @. (asset scope) — declared in package.json
      • Full command output/logs are in the run artifacts (see Proof below).
  • Unit Testing — 1 expectation covered (Ran and passed).
    Exercises individual functions or modules in isolation.
    • What Quality Keeper ran:
      • pnpm run test → passed — 2571 passed, 2 skipped @. (asset scope) — declared in package.json
      • pnpm run test:all → passed — 2571 passed, 2 skipped @. (asset scope) — declared in package.json
      • Full command output/logs are in the run artifacts (see Proof below).

Needs attention

Quality Keeper does not yet have acceptable evidence for these Testing Types.

Some have partial checks that need to run, be fixed, or be connected. Others need new proof or seeded tests. In both cases, Quality Keeper cannot count them as covered yet.

  • Contract Testing
    Checks that a public or network boundary (API, SDK surface, schema) matches its agreed shape.
    • Blocked before execution: expensive/invasive check gated for safety; opt in explicitly with --include-expensive to run it
  • Dynamic Security Testing
    Exercises security behavior before release or against a staged runtime, including access controls, secret safety, secure defaults, and sanitized audit records.
  • Integration Testing
    Checks that two or more components work together — a module plus a real dependency, service, or datastore.
    • Blocked before execution: expensive/invasive check gated for safety; opt in explicitly with --include-expensive to run it
  • Reliability & Resilience Testing
    Checks that the software behaves predictably when dependencies fail, time out, degrade, or recover — without mistaking retry libraries, health routes, or alert files for proof.
  • System / End-to-End Testing
    Drives a real user or system flow through the whole application, end to end.
    • Blocked before execution: expensive/invasive check gated for safety; opt in explicitly with --include-expensive to run it

Quality Keeper changes no Evidence State. Pending, missing, executed-but-not-exercised, and review items are not passing, and seeded scaffolds are next actions, not evidence, until applied and re-evaluated.

Full breakdown (19 expectations)
Area Status Expectation Why it applies
Build Verification Ran and passed packaged_or_runnable_assets_need_build_verification The interpreted asset has package, script, workflow, UI, service, CLI, edge-runtime, or game-runtime signals, so it needs a reproducible build or equivalent construction check.
Build Verification Ran and passed published_packages_need_sdk_build_artifact_review The interpreted asset is a published package with source code, so the distribution manifest completeness, pack tarball integrity, and declared file list need verification as part of the release artifact.
Compatibility Testing Ran and passed consumer_or_platform_boundaries_need_compatibility_testing The interpreted asset is published or consumed across environments (published package, exported API, command-line tool, or edge target), so compatibility across platforms, versions, and consumers needs verification.
Compatibility Testing Ran and passed published_typescript_packages_need_type_declaration_check The interpreted asset is a published TypeScript package (tsconfig.json or has_types in package.json detected), so generated .d.ts declaration files and type resolution must be verified from a consumer project's perspective.
Configuration & Artifact Review Verified before execution every_asset_needs_configuration_review Every interpreted asset needs a static review of its declared files, manifests, configuration, and artifacts so Quality Keeper can explain its shape before evidence is harvested.
Contract Testing Selected but not executed public_or_network_boundaries_need_contract_testing The interpreted asset publishes or exposes a boundary that other software depends on (network API, CLI, package exports, or an importable Go external client), so the public interface contract needs verification.
Dynamic Security Testing Missing — no proof yet authorization_boundaries_need_permission_behavior_testing The interpreted asset contains a production authorization, role, permission, tenant, resource, or ownership boundary, so repository-owned tests must prove forbidden cross-boundary access is denied.
Dynamic Security Testing Missing — no proof yet credential_handling_assets_need_non_exposure_testing The interpreted asset contains production credential, token, password, API-key, redaction, or sanitization handling, so repository-owned tests must prove sensitive values are not logged, returned, or durably exposed.
Integration Testing Selected but not executed published_sdk_packages_need_integration_testing The interpreted asset is a published package or library that creates an integration boundary for consumers, so the package's integration paths, API contracts, and external service behaviors need integration-level verification.
Metadata & Discoverability Review Verified before execution every_asset_needs_discoverability_review Every interpreted asset needs enough metadata and documentation for humans and systems to identify what it is and how to work with it.
Reliability & Resilience Testing Missing — no proof yet outbound_calling_assets_need_timeout_testing Production source contains a recognized outbound network-client call site, so explicit timeout behavior needs repository-owned verification.
Reliability & Resilience Testing Missing — no proof yet cancellation_or_idempotency_controls_need_behavior_testing Production source declares a cancellation primitive or idempotency control, so its externally observable behavior needs repository-owned verification.
Reliability & Resilience Testing Missing — no proof yet published_sdk_packages_need_reliability_testing The interpreted asset is a published package or library with a detected external service or network boundary — consumers depend on predictable error classification and failure-path behavior, so those paths need verification against the declared public contract. Timeout and retry expectations are selected separately when their owned call sites or policies are detected.
Specification Conformity Review Ran and passed documented_assets_need_specification_conformity_review The asset carries documentation, so documented commands, setup instructions, examples, and claims need to stay aligned with the implementation.
Specification Conformity Review Ran and passed published_packages_with_docs_need_example_validation The interpreted asset is a published package with detected documentation, so code examples in README, docs, or examples directories should be compiled or type-checked to confirm they match the actual public API.
Static Security Analysis Verified before execution code_and_configuration_assets_need_static_security_analysis The interpreted asset contains code, configuration, dependencies, secrets coordinates, infrastructure-as-code, or service boundaries, so static security analysis is required before runtime evidence is trusted.
Static Security Analysis Verified before execution published_packages_need_provenance_proof The interpreted asset is a published package with source code, so published release artifacts should carry provenance metadata (npm provenance, SLSA attestation, or sigstore signature) to protect the supply chain.
System / End-to-End Testing Selected but not executed cli_sdk_iac_assets_need_system_testing The interpreted asset is a CLI, published package/SDK, or IaC provider that makes outbound calls to a real external dependency, so its own entry point needs an end-to-end test of that real flow — not just unit or integration coverage of individual pieces.
Unit Testing Ran and passed code_assets_need_unit_testing The interpreted asset contains runnable or reusable source code, so the smallest testable units need isolated verification.

New to these terms? Start with the Quality Keeper Standards Wiki to learn what each testing type means, why it applies, and what counts as evidence.

@sophiecarreras

Copy link
Copy Markdown
Contributor

Thanks for working on this. We ran our repository's secrets-in-output check against the current head of this branch (905fea1), and it still reports remaining credential-bearing fields in the objects returned by authorize(), getUploadUrl() and getDownloadAuthorization(). Details are in our team's internal tracker. We have a small patch for the remaining fields and are happy to share it here if that would help.

@sophiecarreras sophiecarreras left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks Felipe. Review from Sofia, with Claude's help. No credential leak found, so this is a comment, not a blocker, but the framing needs changes before merge.

What was verified (by running, on head of this PR): tsc --noEmit passes, both redaction suites pass (44/44), full suite passes (87 files, 2555 passed, 9 skipped). Reverting one layer at a time makes the tests fail (in-memory.ts 9, upload-url-pool.ts 3, bucket.ts 1, file.ts 3, client inspect.custom 3), so the tests do detect each fix. Symbol.toStringTag, structuredClone, getOwnPropertyDescriptors and a subclass with an extra secret field showed no leak.

Please change before merge

  1. The Proxy break is under "Security", not "Changed" (CHANGELOG.md:27-31). new Proxy(client, {}).listBuckets() throws Cannot read private member #accountInfo, and new Proxy(new InMemoryAccountInfo(), {}).getApiUrl() throws for #auth. That breaks anyone wrapping these in Vue reactive, MobX or an instrumentation proxy. The note names only client; Bucket, B2Object, InMemoryAccountInfo, FileAccountInfo and UploadUrlPool are affected too.
  2. Semver and the toJSON change. The package is 0.4.0. The accessor change, the Proxy break and JSON.stringify(accountInfo) now returning {authorized} (in-memory.ts:396, file.ts:259) are breaking. Anyone who persisted a store with JSON.stringify silently loses data, and the changelog does not say so. Suggest a 0.5.0 bump, a "Breaking" label, and a line on the toJSON behavior.

Nits

  • {...client} still exposes raw.transport, the raw realmUrl and reauthCoalescer. No secret with the default setup, but a realm with userinfo (https://user:pass@host) shows in a spread while toJSON and inspect redact it. redaction.test.ts:1572 covers only toJSON.
  • client.accountInfo returns the user's own store, so logging a custom AccountInfo directly prints whatever it exposes (the JSDoc at client.ts:820 says so).
  • The reviewed-export list is a name snapshot: a contributor can append a name to pass it, and it checks a hand-picked fixture per class. A fine tripwire, not proof.

Not checked: error messages and RawClient request/response errors (untouched by this PR, not audited for token content); Bun, Deno and browser behavior of Symbol.for('nodejs.util.inspect.custom').

Needs a human: does any downstream package or the harness wrap SDK objects in a Proxy or read client.accountInfo as an own property? Does anyone rely on JSON.stringify(store)? Release owner to decide 0.5.0 vs patch.

@sophiecarreras

Copy link
Copy Markdown
Contributor

Patch for the remaining credential-bearing fields, as offered. Sofia, with Claude's help.

Fields our secrets-in-output check flags on 905fea1 (all authorizationToken, shown by JSON.stringify and util.inspect of the returned object):

  • authorize() → AuthorizeAccountResponse (src/types/auth.ts:192, returned at src/raw/index.ts:374)
  • getUploadUrl() → GetUploadUrlResponse (src/types/upload.ts:26, returned at src/raw/index.ts:498)
  • getDownloadAuthorization() → DownloadAuthorizationResponse (src/types/download.ts:148, returned at src/raw/index.ts:1210)
  • Not flagged by the check, same field, covered too: getUploadPartUrl() → GetUploadPartUrlResponse (src/types/upload.ts:151, src/raw/index.ts:914)

Check (from sdkharness/, pointing the clone dir at the PR checkout):
SDKHARNESS_REPO_TEST_CLONES_DIR=<dir containing b2-sdk-typescript> bin/run-security.sh --scenario logs.no_secrets b2-sdk-typescript

  • on 905fea1: FAIL (leaked -- account authorization token in a.x-objects:3; upload URL token in a.x-objects:145; download authorization token in a.x-objects:138 ...)
  • on the patched commit: SECURITY b2-sdk-typescript logs.no_secrets @simulator: PASS

What the patch does: the four raw-client responses get non-enumerable toJSON and inspect hooks that print [redacted token]. The authorizationToken property is unchanged, so the SDK still reads the real token. Biome and tsc are clean and the full suite passes (88 files, 2560 passed, 9 skipped). Reverting the redaction at each of the four sites fails exactly one new test each.

Two things to know before taking it

  1. src/auth/file.ts:336 persisted the response with JSON.stringify(auth). A hook alone would have written the placeholder to disk and silently broken the login cache. The patch changes it to JSON.stringify({ ...auth }); a new test fails if that is reverted.
  2. It is breaking and reverses your contract test (src/redaction.test.ts:194, and the CHANGELOG line saying authorize()/getAuth() return the full token including through JSON.stringify). I changed that test line and the CHANGELOG to match, and added a BREAKING entry (so the 0.5.0 bump). Serializing on purpose becomes JSON.stringify({ ...response }). Your call: take this, or keep your contract and adjust the check instead (the check's own scenario text exempts output the tool is documented to return).
Patch (git diff 905fea1..50d3a70, 6 files)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f477184..943b9b2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -19,13 +19,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
   `client.accountInfo` directly, and pass a custom store through the
   constructor's `accountInfo` option.
 
+- **BREAKING: responses that carry `authorizationToken` hide it when serialized or
+  printed.** `JSON.stringify`, `console.log`, and `util.inspect` of the objects
+  returned by `authorize()`, `getUploadUrl()`, `getUploadPartUrl()`, and
+  `getDownloadAuthorization()` (and of `client.accountInfo.getAuth()`) show
+  `[redacted token]` instead of the token. The `authorizationToken` property is
+  unchanged, so `response.authorizationToken` and every SDK code path still read
+  the real token. To serialize a response that carries a token on purpose (for
+  example to hand a download authorization to a browser), copy it first:
+  `JSON.stringify({ ...response })`. A copy made that way is a plain object with
+  the real token, so treat it as credential material. Code that persisted
+  `JSON.stringify(response)` now stores the placeholder and must switch to the
+  copy.
+
 ### Security
 
 - **SDK objects hide application keys and authorization tokens in diagnostics.**
   Logging, inspecting, or serializing clients, bucket and file handles, auth
-  stores, and upload URL pools no longer prints stored credentials. Explicit
-  `authorize()` and `getAuth()` responses still contain the real token, and file
-  auth caches still persist it.
+  stores, and upload URL pools no longer prints stored credentials. File auth
+  caches still persist the real token, and `response.authorizationToken` still
+  holds it.
   Methods called through a `Proxy` wrapper (for example Vue `reactive(client)`)
   now throw `Cannot read private member`, the same constraint as the Partner
   client. Keep these SDK instances outside
diff --git a/src/auth/file.ts b/src/auth/file.ts
index d183c95..b3308f5 100644
--- a/src/auth/file.ts
+++ b/src/auth/file.ts
@@ -333,7 +333,7 @@ export class FileAccountInfo implements AccountInfo, AuthContextAwareAccountInfo
                   : {}),
               },
             })
-          : JSON.stringify(auth)
+          : JSON.stringify({ ...auth })
       await this.writePrivateFileAtomically(contents)
     })
     this.writeQueue = next.catch((error: unknown) => {
diff --git a/src/raw/index.ts b/src/raw/index.ts
index 0d9b8d5..1fb6903 100644
--- a/src/raw/index.ts
+++ b/src/raw/index.ts
@@ -17,6 +17,7 @@ import { assertValidBucketConfiguration } from '../bucket-validation.ts'
 import { FinishLargeFileResponseBodyError, UploadResponseBodyError } from '../errors/index.ts'
 import type { RetryOptions } from '../http/retry.ts'
 import type { HttpTransport } from '../http/transport.ts'
+import { withRedactedTokenDiagnostics } from '../token-redaction.ts'
 import type {
   AllowedBucket,
   ApplicationKey,
@@ -371,7 +372,9 @@ export class RawClient {
       ...(options?.signal !== undefined ? { signal: options.signal } : {}),
       ...(options?.retry !== undefined ? { retry: options.retry } : {}),
     })
-    return normalizeAuthorizeAccountResponse(await response.json<WireAuthorizeAccountResponse>())
+    return withRedactedTokenDiagnostics(
+      normalizeAuthorizeAccountResponse(await response.json<WireAuthorizeAccountResponse>()),
+    )
   }
 
   // --- Buckets ---
@@ -495,12 +498,14 @@ export class RawClient {
     optionsOrSignal?: RawRequestOptions | AbortSignal,
     retry?: Partial<RetryOptions>,
   ): Promise<GetUploadUrlResponse> {
-    return this.postJson<GetUploadUrlResponse>(
-      apiUrl,
-      authToken,
-      'b2_get_upload_url',
-      request,
-      normalizeRawRequestOptions(optionsOrSignal, retry),
+    return withRedactedTokenDiagnostics(
+      await this.postJson<GetUploadUrlResponse>(
+        apiUrl,
+        authToken,
+        'b2_get_upload_url',
+        request,
+        normalizeRawRequestOptions(optionsOrSignal, retry),
+      ),
     )
   }
 
@@ -911,12 +916,14 @@ export class RawClient {
     optionsOrSignal?: RawRequestOptions | AbortSignal,
     retry?: Partial<RetryOptions>,
   ): Promise<GetUploadPartUrlResponse> {
-    return this.postJson<GetUploadPartUrlResponse>(
-      apiUrl,
-      authToken,
-      'b2_get_upload_part_url',
-      request,
-      normalizeRawRequestOptions(optionsOrSignal, retry),
+    return withRedactedTokenDiagnostics(
+      await this.postJson<GetUploadPartUrlResponse>(
+        apiUrl,
+        authToken,
+        'b2_get_upload_part_url',
+        request,
+        normalizeRawRequestOptions(optionsOrSignal, retry),
+      ),
     )
   }
 
@@ -1207,11 +1214,13 @@ export class RawClient {
     authToken: string,
     request: DownloadAuthorizationRequest,
   ): Promise<DownloadAuthorizationResponse> {
-    return this.postJson<DownloadAuthorizationResponse>(
-      apiUrl,
-      authToken,
-      'b2_get_download_authorization',
-      request,
+    return withRedactedTokenDiagnostics(
+      await this.postJson<DownloadAuthorizationResponse>(
+        apiUrl,
+        authToken,
+        'b2_get_download_authorization',
+        request,
+      ),
     )
   }
 
diff --git a/src/redaction.test.ts b/src/redaction.test.ts
index 2c097b2..f024a89 100644
--- a/src/redaction.test.ts
+++ b/src/redaction.test.ts
@@ -191,7 +191,8 @@ describe('credential redaction JSON and spread', () => {
     const response = await client.authorize()
     expect(response.authorizationToken).toBe(ACCOUNT_TOKEN)
     expect(store.getAuth()?.authorizationToken).toBe(ACCOUNT_TOKEN)
-    expect(JSON.parse(JSON.stringify(response)).authorizationToken).toBe(ACCOUNT_TOKEN)
+    expect(JSON.stringify(response)).not.toContain(ACCOUNT_TOKEN)
+    expect(JSON.parse(JSON.stringify({ ...response })).authorizationToken).toBe(ACCOUNT_TOKEN)
     expect(client.accountInfo).toBe(store)
     poolTokens(store)
     expect(store.checkoutUploadUrl(BID)?.authorizationToken).toBe(UPLOAD_TOKEN)
diff --git a/src/response-token-redaction.node.test.ts b/src/response-token-redaction.node.test.ts
new file mode 100644
index 0000000..96811d0
--- /dev/null
+++ b/src/response-token-redaction.node.test.ts
@@ -0,0 +1,128 @@
+import { mkdtemp, readFile, rm } from 'node:fs/promises'
+import { tmpdir } from 'node:os'
+import { join } from 'node:path'
+import { inspect } from 'node:util'
+import { afterEach, describe, expect, it } from 'vitest'
+import { FileAccountInfo } from './auth/file.ts'
+import type { HttpRequest, HttpResponse, HttpTransport } from './http/transport.ts'
+import { RawClient } from './raw/index.ts'
+import { jsonResponse } from './test-utils/index.ts'
+import { RESPONSE_TOKEN_REDACTED } from './token-redaction.ts'
+import { bucketId, largeFileId } from './types/ids.ts'
+
+const AUTH_TOKEN = 'sentinel-account-token-0001'
+const UPLOAD_TOKEN = 'sentinel-upload-token-0002'
+const PART_TOKEN = 'sentinel-part-token-0003'
+const DOWNLOAD_TOKEN = 'sentinel-download-token-0004'
+const API = 'https://api.example.test'
+
+function transport(): HttpTransport {
+  return {
+    async send(request: HttpRequest): Promise<HttpResponse> {
+      if (request.url.includes('b2_authorize_account')) {
+        return jsonResponse({
+          accountId: 'account-id',
+          authorizationToken: AUTH_TOKEN,
+          apiInfo: {
+            storageApi: {
+              apiUrl: API,
+              downloadUrl: 'https://download.example.test',
+              s3ApiUrl: 'https://s3.example.test',
+              absoluteMinimumPartSize: 5_000_000,
+              recommendedPartSize: 100_000_000,
+              allowed: { capabilities: [], buckets: null, namePrefix: null },
+            },
+          },
+          applicationKeyExpirationTimestamp: null,
+        })
+      }
+      if (request.url.includes('b2_get_upload_part_url')) {
+        return jsonResponse({
+          fileId: 'large-file',
+          uploadUrl: 'https://upload.example.test/part',
+          authorizationToken: PART_TOKEN,
+        })
+      }
+      if (request.url.includes('b2_get_upload_url')) {
+        return jsonResponse({
+          bucketId: 'bucket',
+          uploadUrl: 'https://upload.example.test/file',
+          authorizationToken: UPLOAD_TOKEN,
+        })
+      }
+      return jsonResponse({
+        bucketId: 'bucket',
+        fileNamePrefix: 'p',
+        authorizationToken: DOWNLOAD_TOKEN,
+      })
+    },
+  } as HttpTransport
+}
+
+const calls = {
+  'authorizeAccount (b2_authorize_account)': [
+    AUTH_TOKEN,
+    (raw: RawClient) => raw.authorizeAccount('key-id', 'key-secret', API),
+  ],
+  'getUploadUrl (b2_get_upload_url)': [
+    UPLOAD_TOKEN,
+    (raw: RawClient) => raw.getUploadUrl(API, 'auth', { bucketId: bucketId('bucket') }),
+  ],
+  'getUploadPartUrl (b2_get_upload_part_url)': [
+    PART_TOKEN,
+    (raw: RawClient) => raw.getUploadPartUrl(API, 'auth', { fileId: largeFileId('large-file') }),
+  ],
+  'getDownloadAuthorization (b2_get_download_authorization)': [
+    DOWNLOAD_TOKEN,
+    (raw: RawClient) =>
+      raw.getDownloadAuthorization(API, 'auth', {
+        bucketId: bucketId('bucket'),
+        fileNamePrefix: 'p',
+        validDurationInSeconds: 60,
+      }),
+  ],
+} as const
+
+const directories: string[] = []
+afterEach(async () => {
+  await Promise.all(directories.splice(0).map((d) => rm(d, { recursive: true, force: true })))
+})
+
+describe('B2 response token redaction', () => {
+  it.each(Object.entries(calls))(
+    '%s hides its token in diagnostics but keeps the property',
+    async (_name, [token, call]) => {
+      const response = await call(new RawClient({ transport: transport() }))
+
+      expect(response.authorizationToken).toBe(token)
+      for (const rendered of [
+        JSON.stringify(response),
+        inspect(response, { depth: Infinity, showHidden: true }),
+        inspect(response),
+      ]) {
+        expect(rendered).not.toContain(token)
+        expect(rendered).toContain(RESPONSE_TOKEN_REDACTED)
+      }
+      expect({ ...response }.authorizationToken).toBe(token)
+    },
+  )
+
+  it('FileAccountInfo without a realm still persists and reloads the real token', async () => {
+    const directory = await mkdtemp(join(tmpdir(), 'b2-response-token-'))
+    directories.push(directory)
+    const path = join(directory, 'auth.json')
+    const auth = await new RawClient({ transport: transport() }).authorizeAccount(
+      'key-id',
+      'key-secret',
+      API,
+    )
+    const store = new FileAccountInfo(path)
+    store.setAuth(auth)
+    await store.flushed()
+
+    expect(JSON.parse(await readFile(path, 'utf8')).authorizationToken).toBe(AUTH_TOKEN)
+    const reloaded = new FileAccountInfo(path)
+    await reloaded.load()
+    expect(reloaded.getAuthToken()).toBe(AUTH_TOKEN)
+  })
+})
diff --git a/src/token-redaction.ts b/src/token-redaction.ts
new file mode 100644
index 0000000..bbb2fd7
--- /dev/null
+++ b/src/token-redaction.ts
@@ -0,0 +1,32 @@
+/** Placeholder printed instead of a B2 authorization token in diagnostics. */
+export const RESPONSE_TOKEN_REDACTED = '[redacted token]'
+
+const INSPECT_CUSTOM = Symbol.for('nodejs.util.inspect.custom')
+
+/**
+ * Makes a B2 response that carries `authorizationToken` safe to print.
+ *
+ * `JSON.stringify`, `console.log` and `util.inspect` show the token as
+ * {@link RESPONSE_TOKEN_REDACTED}. The `authorizationToken` property itself is unchanged, so code
+ * that reads it (account info, upload pools, retries) works as before. The two hooks are
+ * non-enumerable, so a spread copy (`{ ...response }`) is a plain object with the real token:
+ * persist a copy like that, never `JSON.stringify(response)`.
+ *
+ * @internal
+ */
+export function withRedactedTokenDiagnostics<T extends { authorizationToken: string }>(
+  response: T,
+): T {
+  const redacted = (): T => ({ ...response, authorizationToken: RESPONSE_TOKEN_REDACTED })
+  Object.defineProperty(response, 'toJSON', {
+    value: redacted,
+    enumerable: false,
+    configurable: true,
+  })
+  Object.defineProperty(response, INSPECT_CUSTOM, {
+    value: redacted,
+    enumerable: false,
+    configurable: true,
+  })
+  return response
+}

@ffumero2003

Copy link
Copy Markdown
Contributor Author

@sophiecarreras — before I push the authorizationToken response redaction (the four response types from your patch), review flagged two problems, one of which is a scope question that's really yours to call since you defined the redaction surface.

1. Redacting the handed-out tokens breaks documented browser flows. The redactor hooks JSON.stringify/util.inspect on the response via toJSON. That's right for the account session token, but three of the four responses carry tokens that are meant to be serialized and handed to a client:

  • examples/browser-uploader/server.ts:42 does c.json(await client.raw.getUploadUrl(...))
  • examples/node-presigned-with-auth/mint.ts:38 does res.json(await bucket.getDownloadAuthorization(...))

With the redaction, both now serialize without the token, so the browser gets a tokenless response and the upload/download fails with 401. So GetUploadUrlResponse / GetUploadPartUrlResponse / DownloadAuthorizationResponse differ from AuthorizeAccountResponse: their token is designed to be handed out, not just a log-leak risk.

Scope question for you: redact only AuthorizeAccountResponse (the log-sensitive session token), or keep redacting all four and treat it as a breaking change (update the examples to read the token explicitly / spread, and document the migration)? Your logs.no_secrets check flags all four, but three of them are tokens we deliberately serialize to clients.

2. FileAccountInfo persistence drops the account token (regression, independent of the above). client.authorize() stores the redacted response via setAuth, and FileAccountInfo.flush() serializes it. The realm branch (file.ts:327) spreads {...auth} and is safe, but the no-realm branch (file.ts:336) does JSON.stringify(auth) directly, which hits the new toJSON and writes a cache file with no authorizationToken — so a reloaded store has no usable token, silently. Runtime repro confirms it. Fix is to spread {...auth} in that branch too and add a standalone-FileAccountInfo round-trip test; the Partner side already has an explicit-clone persistence helper for exactly this.

Minor, regardless of the scope decision: the 0.5.0 bump should move to the release commit (CHANGELOG is still [Unreleased]); the CHANGELOG claims toJSON() on Bucket/B2Object/UploadUrlPool, which only moved the client into a # field (no toJSON); and the PR needs a Closes #350 line.

Requesting your review on the scope question — I'll fold the persistence fix and the rest in once you've called it.

Assert that util.inspect, console.log, JSON.stringify and object spread of
clients, bucket and file handles, auth stores and upload URL pools never
contain the application key or authorization tokens, and that getAuth(),
authorize() and file auth caches still carry the real token.
Hold the application key, stored authorization and upload URL pools in
private fields, give B2Client and the account-info stores a redacted
toJSON/inspect summary, and expose accountInfo through a getter, mirroring
the Partner client.
@ffumero2003
ffumero2003 force-pushed the fix-client-redact-diagnostics branch from 905fea1 to 83c00b8 Compare November 5, 2026 18:55
@ffumero2003
ffumero2003 merged commit f3443cc into main Nov 5, 2026
46 of 48 checks passed
@ffumero2003
ffumero2003 deleted the fix-client-redact-diagnostics branch November 5, 2026 19:03
ffumero2003 added a commit that referenced this pull request Nov 10, 2026
)

* test: cover credential redaction in SDK object diagnostics

Assert that util.inspect, console.log, JSON.stringify and object spread of
clients, bucket and file handles, auth stores and upload URL pools never
contain the application key or authorization tokens, and that getAuth(),
authorize() and file auth caches still carry the real token.

* fix(client): redact credentials from SDK object diagnostics

Hold the application key, stored authorization and upload URL pools in
private fields, give B2Client and the account-info stores a redacted
toJSON/inspect summary, and expose accountInfo through a getter, mirroring
the Partner client.

* docs: note credential redaction and the accountInfo accessor change
ffumero2003 added a commit that referenced this pull request Nov 10, 2026
* fix(upload): record large-file SHA-1 and support resume (#354)

* fix(upload): retry upload POST connection failures by default (#344)

* fix(upload): preserve SHA-1 integrity during large-file resume (#365)

* chore: update vulnerable transitive dependencies (#340)

* chore: update development tooling for dependency audit (#361)

* chore: allowlist minimatch audit advisory (#363)

* chore(release): prepare isolated v0.5.0

* test(upload): verify connection-failure phases and SHA-1

* docs: make the upload retry migration example self-contained

* fix(client): redact credentials from SDK object diagnostics (#351) (#375)

* test: cover credential redaction in SDK object diagnostics

Assert that util.inspect, console.log, JSON.stringify and object spread of
clients, bucket and file handles, auth stores and upload URL pools never
contain the application key or authorization tokens, and that getAuth(),
authorize() and file auth caches still carry the real token.

* fix(client): redact credentials from SDK object diagnostics

Hold the application key, stored authorization and upload URL pools in
private fields, give B2Client and the account-info stores a redacted
toJSON/inspect summary, and expose accountInfo through a getter, mirroring
the Partner client.

* docs: note credential redaction and the accountInfo accessor change

* feat(upload): add single-file connection-fault retry opt-out (#376)

* feat(upload): add single-file connection-fault retry opt-out

* refactor(upload): resolve connection-fault retry options

---------

Co-authored-by: Sophie <49928680+sophiecarreras@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: auth Area: auth area: security Area: security

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants