Skip to content

fix: contain async loggers and redact real preview tokens - #7

Merged
Sy-D merged 2 commits into
mainfrom
error-codes-hardening
Sep 2, 2026
Merged

fix: contain async loggers and redact real preview tokens#7
Sy-D merged 2 commits into
mainfrom
error-codes-hardening

Conversation

@Sy-D

@Sy-D Sy-D commented Sep 2, 2026

Copy link
Copy Markdown
Owner

Fixes the four findings from the GPT-5.6 Sol review of #6.

  • MAJOR — safeLogger did not contain a rejecting logger. Logger
    declares void and TypeScript accepts an async method there, so a log
    shipper whose methods reject left an unhandled rejection — the runtime ends
    the process for that, mid-handoff, before the relay sandbox is released. A
    handler is now attached to the runtime return value inside the existing
    try. Tests: a rejecting logger, a throwing property getter, and a full
    handoff (fake page, real relay process) driven with a rejecting logger.
  • MAJOR — the real preview token is a JWT, not pt_…. It survived both
    pt_token%3D<jwt> and a bare invalid preview token <jwt>. waitForHealth
    now takes the exact token out of the URL it is polling and redacts that value
    and its percent-encoded form by comparison; the pattern rules remain as the
    second net, with a JWT-shaped rule added. The fixture is a JWT, and Sol's two
    leak forms are tests that failed before the fix.
  • MINOR — the SDK cause kept the unredacted body and message. It is now a
    clone with the same prototype, name, status and code, and with
    message and the parsed body redacted. The 429 test asserts on
    cause.message and cause.body. README and CHANGELOG say that cause is
    the original error with credentials redacted.
  • MINOR — browser.isConnected() ran outside checkedPage's try. A
    browser proxy whose liveness accessor throws escaped as a plain Error
    instead of browser_unusable. It is read inside the try and branched on
    after.

Also in here: the health poll no longer issues a final request with no budget
left, whose abort used to overwrite the proxy's own answer — the one useful
thing in relay_not_ready's message — with "The operation timed out". The
CHANGELOG bullets Sol called overstated now claim exactly what is true.

Gates: lint, typecheck, 228 tests, build, dist-smoke (9 exports).

🤖 Generated with Claude Code

Sy-D and others added 2 commits September 2, 2026 21:08
Four findings from the GPT-5.6 Sol review of the typed-error-codes work.

safeLogger only wrapped a synchronous throw. `Logger` declares `void`, and
TypeScript accepts an `async` method there, so a log shipper whose methods
reject produced an unhandled rejection — the runtime ends the process for
that, mid-handoff, before the relay sandbox is released. The wrapper now
attaches a handler to the runtime return value in the same tick it is
created; a throwing property getter was already covered and now has a test.

Redaction guessed the credential's grammar as `pt_...`, but the preview
token is a ~362-character JWT (docs/measurements/01-preview-transport.md
§3), so a real one survived both `pt_token%3D<jwt>` and a bare `invalid
preview token <jwt>`. `waitForHealth` now takes the exact token out of the
URL it is polling and redacts that value and its percent-encoded form by
comparison; the pattern rules stay as the net for text where the value is
not known, with a JWT-shaped rule added. The test fixture is a JWT.

The SDK error attached as `cause` kept the unredacted body and message,
which every error serialiser prints. It is now a clone with the same
prototype, `name`, `status` and `code`, and with `message` and the parsed
`body` redacted; the same for the error `killSandbox` surfaces.

`browser.isConnected()` ran outside `checkedPage`'s try, so a browser proxy
whose liveness accessor throws escaped as a plain `Error` instead of
`browser_unusable`. It is read inside the try and branched on after.

Also: the health poll no longer issues a final request with no budget left,
whose abort used to overwrite the proxy's own answer — the one useful thing
in `relay_not_ready`'s message — with "The operation timed out". The
CHANGELOG bullets Sol called overstated now say what is true.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…oded dots

Round 1 on the verification pass of PR #7.

The clone was built with `Object.assign`, which copies own *enumerable*
properties only. `new Error(msg, { cause })` installs `cause` non-enumerable,
so an error chain was truncated exactly where the root reason lives —
undici's `TypeError: fetch failed` is that shape. The copy now starts from
`Object.getOwnPropertyDescriptors`, so every own property survives with its
descriptor, and `message`/`stack`/`body` are redefined rather than assigned,
which keeps them off `JSON.stringify(cause)` as they are on a real Error. The
nested `cause` is redacted recursively, bounded at eight links and cycle-safe.

Reading a foreign error is running foreign code, and this all happens inside
the `catch` that exists to produce a coded error. Descriptors are copied
rather than read, so an accessor is never invoked; building the message and
copying the error are both wrapped, so a throwing `message` getter or a body
that references itself now yields a plain redacted `Error` instead of a raw
`TypeError`.

The exact-value redaction was wired to the health poll alone. `startRelay`
holds the preview URL from the moment the sandbox answers, so it now passes
that credential to `relayStartError` and to `killSandbox` as well. Both nets
keyed on the JWT's two literal dots, which any escaping proxy removes: the
pattern accepts `%2E`, `%2e` and `&#46;` as separators, and the exact-value
comparison covers the same forms.

The `unhandledRejection` listener in the handoff test is inert under bun —
the runner claims the rejection first. The comment now says so instead of
implying the assertion is the gate.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@Sy-D
Sy-D merged commit 2b56961 into main Sep 2, 2026
2 checks passed
@Sy-D
Sy-D deleted the error-codes-hardening branch September 2, 2026 19:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant