fix: give each client its own API key instead of a shared global - #7
Merged
Conversation
The auth interceptor was registered on the module-global HTTP client that
sdk.gen.ts exports, so every `new Zernio({ apiKey })` stacked another
interceptor onto the same shared client. All of them ran on every request, each
overwriting Authorization, so the last-constructed key won for ALL in-flight
requests.
In a process serving more than one tenant, that means one tenant's call could go
out carrying another tenant's token and return their data. Two concurrent
requests in the same instance are enough. Interceptors were also never removed,
so they accumulated for the lifetime of the process.
Each Zernio instance now creates its own client via createClient/createConfig
and every namespace method is routed through it by `_bind`, which reads
`_client` at call time (class fields initialize before the constructor body).
baseURL and defaultHeaders move to that client for the same reason.
The raw operations and the global client are not exported from src/index.ts, so
no documented consumer depended on the global being configured.
scripts/generate-client.ts emits this shape, verified byte-identical to the
committed src/client.ts against the current live spec, so the next automated
regeneration cannot revert it.
tests/isolation.test.ts pins the behaviour; 4 of its 6 cases fail on the
pre-fix client.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
The auth interceptor was registered on the module-global HTTP client that
sdk.gen.tsexports, so everynew Zernio({ apiKey })stacked another interceptor onto the same shared client. All of them run on every request, each overwritingAuthorization, so the last-constructed key won for all in-flight requests.In a process serving more than one tenant, one tenant's call could go out carrying another tenant's token and return their data. Two concurrent requests in one instance is enough. Interceptors were also never removed, so they accumulated for the process lifetime.
Reported as zernio-dev/zernflow#18, found while fixing an unrelated issue there.
What
Zernioinstance creates its own client viacreateClient(createConfig(...)). Every namespace method is routed through it by_bind, which reads_clientat call time rather than bind time, because class fields initialize before the constructor body runs._bind<F>(op: F): Fpreserves each operation's declared signature, so no consumer-visible type changes.baseURLanddefaultHeadersmove onto that client for the same reason: they were global too.scripts/generate-client.tsemits this shape. Verified byte-identical to the committedsrc/client.tsagainst the current live spec, so the next automated regeneration cannot silently revert it.tests/isolation.test.tspins the behaviour: per-instance keys, an earlier instance surviving a later construction, no interceptor stacking across 25 instances, per-instance baseURL and defaultHeaders, and errors still surfacing.Compatibility
The raw operations and the global
clientare not exported fromsrc/index.ts(it exports onlyZernio, the error classes, andtypes.gen), so no documented consumer depended on the global being configured.npm run lint,typecheckand all 173 tests pass; 4 of the 6 new tests fail on the pre-fix client.🤖 Generated with Claude Code