Repository navigation
fix: redact session credentials from user --json --full output - #78
Conversation
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
@doistbot /review |
doistbot
left a comment
There was a problem hiding this comment.
This PR redacts session credentials (token and cometChannel) from tdc user --json / --ndjson output, including --full and paginated paths, by applying an omitSecrets filter for top-level user entities in the shared formatter (filterEntityFields in src/lib/output.ts). Since formatJson, formatNdjson, and the paginated formatters all route through it, both the user and users commands are covered, curated output is preserved, and the filter is a no-op for non-user entities. All formatJson/formatNdjson call sites that render session-user data pass type: 'user', so the intended leak path is closed, and the documented shallow-redaction limitation has no concrete leak path today. No inline issues were flagged — the remaining open items worth confirming before merge are the backend sensitivity of cometServer/clientId and the follow-up verification of the #77 status commands once that PR lands.
I also included a few optional follow-up notes in the details below.
Optional follow-up notes (2)
src/lib/output.ts:156: For every entity type except
user,SECRET_FIELDS[type]isundefined, so thismaponly allocates a new array of references with no filtering benefit.--fulllist/paginated outputs (threads, channels, conversations, groups, workspaces) previously passed the original array straight through. Short-circuit before mapping when the type has no secrets, e.g.const secrets = SECRET_FIELDS[type]; if (!secrets) return data, and passsecretsintoomitSecretsto avoid the repeated lookup per item.src/lib/output.test.ts:150: This matrix asserts only that the
SECRET-values don't appear in the output, not that thetoken/cometChannelkeys are omitted. Since the PR deliberately omits these fields instead of emitting a"[redacted]"placeholder, a future switch to placeholders would still pass every case here (and theusers --fullcommand test). Parse the single-object outputs (forformatPaginatedJson, check.results[0]) or inspectfilterEntityFieldsdirectly, and assertnot.toHaveProperty('token')/not.toHaveProperty('cometChannel')to pin the omission contract.
… omission in tests Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
@doistbot /review |
doistbot
left a comment
There was a problem hiding this comment.
This PR routes all full JSON/NDJSON output for the top-level user entity through a new secret-omission filter in filterEntityFields, closing a credential leak where tdc user --json --full printed the session token and cometChannel. The redaction covers the curated, full, paginated, and NDJSON paths (all funnel through the shared formatter), curated output is preserved, and tests plus help/skill docs match the change. No inline issues were flagged — the implementation follows the existing per-entity filtering pattern, the omit-over-placeholder choice is sound, and the open question about cometChannel/clientId/cometServer sensitivity is already surfaced in the PR description for backend confirmation.
I also included a few optional follow-up notes in the details below.
Optional follow-up notes (2)
src/commands/user.test.ts:197: This block is low-value, synthetic coverage. The PR notes the SDK already strips
token/cometChannelfrom workspace users, sogetWorkspaceUsersnever returns the mockedleakyshape — the real carrier (getSessionUser) is already covered by theuser --fulltest above. It also never asserts a full-only field survives, so it would still pass if--fullwere ignored and curated output were returned (curated output omits these fields too). The array redaction is already pinned insrc/lib/output.test.ts. Suggest deleting this block; if thelistUserswiring is the concern, keep a single--jsoncase that asserts a full-only field and that the parsed result has notoken/cometChannelkeys.src/lib/output.test.ts:166: This asserts reference identity (
toBe), which pins the internal short-circuit optimization rather than behavior. IffilterEntityFieldsever returned an equal shallow copy for types without secrets — no observable change — this test would fail.toEqual(channels)(or just checking the items are unmodified) tests the same contract without coupling to the no-copy detail.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
@doistbot /review |
doistbot
left a comment
There was a problem hiding this comment.
This PR closes a real credential leak by omitting the session token and cometChannel fields from top-level user JSON/NDJSON output, centralized in filterEntityFields so the curated, --full, and paginated paths are all covered while other entity types and untyped output are untouched. No inline issues were flagged. The omit-over-placeholder choice and the shared-helper approach fit the existing filtering pattern well; the follow-ups in the description (confirming cometServer/clientId sensitivity with the backend owner, and re-verifying the #77 status commands once that PR merges) are good items to track.
I also included a few optional follow-up notes in the details below.
Optional follow-up notes (3)
src/lib/output.ts:121:
omitSecretsreturnsT, but the object it returns has hadtoken/cometChanneldeleted (return copy as T). The signature hides the deletion, so the helper's result is typed as a completeTeven though it is missing fields. ReturnPartial<T>(or a preciseOmitkeyed on the user secret fields) so the redaction is reflected in the type.src/lib/output.test.ts:166: This asserts reference identity (
toBe), which pins the no-copy short-circuit rather than the behavior the test name describes (fields unmodified). A behaviorally equivalentfilterEntityFieldsthat returned a shallow copy for types without secrets would fail here. UsetoEqual(channels)to keep the test on the contract.src/commands/user.test.ts:216: This block is synthetic and largely duplicates the
src/lib/output.test.tsmatrix. Per the PR description, the SDK already stripstoken/cometChannelfrom workspace users, sogetWorkspaceUsersnever returns the mockedleakyshape and this credential scenario can't occur throughlistUsers; the real carrier (getSessionUser) is covered by theuser --json --fulltest above. The only unique behavior here islistUserspassing--fullthrough to the formatter. Collapse it to a single--jsoncase assertinglangis present and the secret keys are absent, or delete the block.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
## [3.5.2](v3.5.1...v3.5.2) (2026-10-14) ### Bug Fixes * redact session credentials from user --json --full output ([#78](#78)) ([e768abf](e768abf))
The status commands print the user through formatJson, which since #78 drops token and cometChannel. This guards the call site. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* feat(status): add tdc status show, set and clear Needs a @doist/comms-sdk release that includes users.setStatus (Doist/comms-sdk-typescript#100); the dependency bump is not in this commit. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * build(deps): bump @doist/comms-sdk to 3.4.0 Brings in users.setStatus and users.clearStatus. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * docs(skill): sync SKILL.md with tdc status Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * docs: drop the tdc away lines from the README The command no longer exists. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * fix(status): reject empty expiry values and read date-only --until as local Addresses doistbot review: validate --until/--for presence instead of truthiness, skip markdown preload for status, add --full to set/clear, reuse the dates.ts unit constants, and cover the user Status line, the set text output and fake-timer cleanup in tests. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * fix(status): validate set input, register the user:write scope, report what the server returned From an offline review of the whole PR: reject impossible dates, past expiries, out-of-range messages and empty emoji before the dry-run branch, require user:write through the scope table, print the server's status after set and clear, add --full to show, and fix the stale AGENTS.md pointer. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * fix(status): reject an option-like value for --emoji Commander reads the next token as the value, so --emoji --dry-run ran the real write. An emoji never starts with a dash. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * fix(status): accept fractional seconds, reject DST gaps, exit 1 when the server disagrees Addresses the second doistbot round: parseLocalDate takes what Date.toISOString() produces, a nonexistent local time in a daylight-saving gap is rejected, clear and set exit 1 whenever the server's status does not match the request (JSON output included), the JSON rendering lives in one helper, and the read-only routing is asserted for both status methods. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * fix(status): accept a date whose midnight is skipped, check the year and offset range, fail through CliError From a revision review of 761dc90: the DST-gap rule rejected a date-only --until on the one day a year when a zone's clocks skip midnight, years 0 to 99 round-tripped to 1900 to 1999, and offsets up to +23:59 were accepted. set and clear now throw INVALID_STATE when the server disagrees, so --json prints the usual stderr envelope and no process.exitCode state leaks. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * fix(status): word the past-expiry hint for datetimes, share formatStatus, cover the curated user JSON From the third doistbot round: the midnight hint now appears only for a date with no time, formatStatus moves to src/lib so tdc user no longer imports from the status folder, and a test pins status in the curated tdc user --json output. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * docs(status): say pause-notifications is Comms-only for now; explain the no-op error From Scott's review: the help, README and skill text now say the pause holds back Comms notifications only, that each app has to read the status, and that held notifications are not sent later. An agent is told to pass the flag only when asked. The set error now says nothing changed, the usual cause, and what to run next. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * test(status): pin that --json --full redacts session credentials The status commands print the user through formatJson, which since #78 drops token and cometChannel. This guards the call site. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
## [3.6.0](v3.5.2...v3.6.0) (2026-10-14) ### Features * **status:** add tdc status show, set and clear ([#77](#77)) ([bdfcb16](bdfcb16)), closes [Doist/comms-sdk-typescript#100](Doist/comms-sdk-typescript#100) [#78](#78)
Overview
tdc user --json --fullprinted every field the API returns for the session user, including the SDK'stokenandcometChannel. The backend only includes session credentials when the bearer is a session-type token, soCOMMS_API_TOKEN=<session token> tdc user --json --fullwould print a live credential to stdout.The fix is in the shared formatter (
filterEntityFieldsinsrc/lib/output.ts): for a top-leveluserentity,tokenandcometChannelare omitted on the curated and--fullpaths.formatJson,formatNdjsonand the paginated formatters all go through it, so thestatuscommands in #77 (which callformatJson(user, 'user', true)) are covered once it lands. #77 is not merged, so I did not test them here. Redaction is top-level only: a user nested inside another object is not redacted, and nothing prints one today.Omit vs placeholder: omitted. A
"[redacted]"string would look like a real value to scripts, and absence is what an OAuth login already produces.Fields:
tokenis the session credential.cometChannelis a judgement call: it is the user's realtime subscription channel, paired withcometServer, so I treated it as secret, but nothing in the repo or SDK confirms that.cometServerandclientIdstay; worth a check with the backend owner that neither is sensitive.Reference
Follow-up to #77. SDK:
UserSchemain@doist/comms-sdk.Test plan
token/cometChannel, runtdc user --json --full; the credentials are printed.tdc user --json --fullhas notokenorcometChannel, other full fields (lang,shortName) remaintdc user --jsonoutput is unchangedThe
users --json/--ndjson --fulltests are defence in depth: the SDK already strips these fields from workspace users, sogetSessionUseris the only real carrier.Not verified: what the real backend returns for a session token. This rests on the
_include_session_credentialsbehaviour described in the task and the SDK schema.Adversarial review
0cf3deeDoistbot: 3 rounds, no P1 or P2, 7 P3 notes in total (4 fixed, 3 refuted, one of them raised twice). Last review was on
cd3c931;e3b5d2aonly changes a return type.🤖 Generated with Claude Code