feat(http): attach user/device identity to swamp-club HTTP traffic (swamp-club#461)#1457
Conversation
Both ExtensionApiClient and SwampClubClient gain an optional identity
constructor arg ({ bearerToken?, distinctId? }). Their private fetch()
wrappers attach `Authorization: Bearer <token>` when logged in and
`Swamp-Distinct-Id: <uuid>` always — letting swamp-club's telemetry
attribute CLI traffic to a user/device instead of an anonymous IP+UA
hash. Identity is resolved once at the CLI composition root via
loadIdentity() and threaded through libswamp function signatures so
libswamp itself never reads auth.json/identity.json. Per-method apiKey
parameters are preserved for backwards compatibility; the new header
path is additive.
The swamp-club server-side middleware change (read Swamp-Distinct-Id,
prefer it over per-request anon_* hash) ships separately.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
AuthRepository.load() re-throws errors other than NotFound, including the "HOME environment variable is not set" thrown by getSwampConfigDir() on Windows test envs (which use USERPROFILE and may strip HOME in isolated test runners). Because loadIdentity() runs at CLI startup for every command — including `swamp --version` and `swamp --help` — that throw was crashing the CLI with empty stdout on windows-latest CI. Wrap the AuthRepository call in a try/catch that returns an empty identity on any failure. Identity resolution is best-effort: a missing config dir means anonymous traffic, never a CLI crash. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
There was a problem hiding this comment.
CLI UX Review
Blocking
None.
Suggestions
None.
Verdict
PASS — this PR makes no user-facing changes. Every modification is internal wiring: threading ClientIdentity through HTTP client constructors and adding a loadIdentity() composition-root helper. No flags, help text, output formats, error messages, or command behavior change. Errors are swallowed silently so identity resolution never crashes a command. Nothing here affects users.
There was a problem hiding this comment.
Code Review
Clean, well-designed PR that threads user/device identity through the HTTP clients via constructor injection. The architecture follows DDD principles correctly: identity resolution lives in the CLI composition root (loadIdentity), the domain and libswamp layers never touch the file system, and the ClientIdentity value object is threaded through function signatures cleanly.
Blocking Issues
None.
Suggestions
- Missing co-located test for
mergeIdentityHeaders: Per project convention (foo.ts→foo_test.ts),src/infrastructure/http/client_identity.tsshould have aclient_identity_test.ts. The merge function is well-covered indirectly through the 10+ identity tests across both client test files, so this is not blocking — but a direct unit test would make the merge logic independently verifiable (e.g., edge cases like emptycallerHeaders, conflicting header names with different casing).
What looks good
- S3 presigned URL isolation:
uploadArchiveanddownloadArchivecorrectly bypass the identity-injecting fetch wrapper, and the two-mock-server regression test (downloadArchive does NOT send identity headers to the S3 presigned URL) locks this contract in. No token leak to S3 access logs. - Caller-wins precedence: Identity headers spread first, caller headers second — preserving
createApiKey's session-tokenAuthorizationand per-methodx-api-keypaths. Both halves of the contract are tested for each client. - Resilient
loadIdentity: Catches allAuthRepositoryfailures (including missing HOME env var) and leans onUserIdentityRepository.getUserId()'s internal catch-all. Every CLI command calls this at startup — crashing here would break everything. - Clean domain boundary: libswamp never reads
auth.jsonoridentity.jsondirectly. The CLI composition root resolves identity and passes it through. - License headers, formatting, linting: All new files have the AGPLv3 header. CI passes on both Linux and Windows.
There was a problem hiding this comment.
Adversarial Review
Critical / High
None found.
Medium
-
Multiple
loadIdentity()calls per CLI invocation —src/cli/mod.ts:1154callsloadIdentity()for the auto-resolver, and then every individual command handler (e.g.,extension_push.ts:285,extension_update.ts:118) calls it again. Some paths likeextension updateload identity 3+ times (once inrunCli, once in the command handler, once in theinstallExtensioncallback viacreateInstallContext). Each call readsauth.jsonandidentity.jsonfrom disk. Not a correctness bug — the data won't change within a single CLI invocation — but it's redundant I/O. A single load at the composition root passed through would be cleaner. -
ClientIdentitynot re-exported fromlibswamp/mod.ts— The factory functionscreateExtensionInfoDeps,createExtensionPullDeps,createExtensionPushPrepareDeps, etc. are all re-exported fromsrc/libswamp/mod.tsand their signatures now acceptClientIdentity. ButClientIdentityitself is only exported fromsrc/infrastructure/http/client_identity.ts(and re-exported from the two client modules). CLI code imports it directly from the infrastructure path (src/cli/mod.ts:100). This works via structural typing, but exporting the type from the barrel would be more consistent with the project's import convention documented in CLAUDE.md.
Low
loadIdentitytest env var mutation (src/cli/load_identity_test.ts:31-33) — The test deletesHOME,USERPROFILE, andXDG_CONFIG_HOMEfrom the process environment. Thefinallyblock restores them, but if the test runner parallelizes across files, any concurrent test depending onHOME(which is most of them) will fail during the race window. This is safe under Deno's default sequential test execution, but fragile if parallel execution is ever enabled.
Verdict
PASS — Clean, additive change with correct header-merge precedence (identity-first, caller-wins), proper S3 presigned-URL isolation (both downloadArchive and uploadArchive use bare fetch not this.fetch), and thorough test coverage including the token-leak regression test. The medium items are minor inefficiency and API hygiene, not correctness issues.
Summary
ExtensionApiClientandSwampClubClientget an optionalidentityconstructor arg. Their privatefetch()wrappers now attachAuthorization: Bearer <token>when the user is logged in andSwamp-Distinct-Id: <uuid>always.loadIdentity()helper, then threaded through libswamp function signatures. libswamp itself never readsauth.jsonoridentity.json— keeps the domain boundary clean.init.headersspread second. Preserves the per-methodx-api-keypaths andSwampClubClient.getCurrentUser's session-token Bearer.apiKey?parameters are kept intact — this is purely additive.Follow-up (separate, swamp-club repo)
This PR is the CLI half. The swamp-club server needs:
routes/_middleware.ts) readsSwamp-Distinct-Id, stamps it on spans, prefers it over the per-requestanon_*hash. Authorization-derived identity still wins;Swamp-Distinct-Idis treated as untrusted hint data.content/manual/reference/api-key-authentication.md.Until that ships, the new header is silently dropped server-side. No CLI-visible breakage.
Test plan
deno checkcleandeno lintcleandeno fmtcleandeno run test: 6304 passed (1 unrelated pre-existing failure onextension quality: missing README, fails onmaintoo)deno run compile: binary buildsAuthorization+Swamp-Distinct-Idheaders on outbound requests once swamp-club middleware lands🤖 Generated with Claude Code