JavaScript SDK: add configurable request timeout, fix Node hash() tag handling (KSM-1209, KSM-1254) - #1130
JavaScript SDK: add configurable request timeout, fix Node hash() tag handling (KSM-1209, KSM-1254)#1130stas-schaller wants to merge 1 commit into
Conversation
…g handling (KSM-1209, KSM-1254) Node requests set a socket timeout and reject with KeeperError when it fires; browser requests use AbortSignal.timeout. Default 30s, overridable via SecretManagerOptions.requestTimeoutMs or a direct timeoutMs argument on downloadFile/downloadThumbnail. Node's hash() now uses its tag parameter instead of a hardcoded string, matching the browser implementation and the Platform contract.
mgallego-keeper
left a comment
There was a problem hiding this comment.
Summary
Reviewed the timeout bounding and the Node hash() tag fix in detail, including empirical reproduction of the Node vs. browser timeout semantics against live servers. The core mechanism is sound and the hash() fix is correct, but a few concrete defects and a contradicted "Breaking Changes: None" claim should be addressed before merge.
Correctness / security
-
Node's
timeoutis an idle timer, not a deadline (medium).nodePlatform.ts(lines 213, 235, 272) passestimeouttohttps.request, which resets on every byte of socket activity.browserPlatform.ts(lines 335, 360, 386) usesAbortSignal.timeout(), a fixed wall clock deadline. Reproduced with a server trickling 1 byte every 150ms for 3s atrequestTimeoutMs=300: the Node request resolved successfully; the identical scenario under browser semantics rejected in ~300ms. A hostile server that stays just under the idle window can still hang a Node caller indefinitely, which is the exact scenario the changelog says this closes, on the SDK's primary server side target. -
timeoutMs: 0means opposite things on each platform, and nothing validates it (medium).timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MSonly substitutes onnull/undefined, so an explicit0passes through unchanged. On Node this fully disables the timeout (reopens the DoS this PR fixes); on browser,AbortSignal.timeout(0)aborts nearly every request instantly. Both reproduced live. Related, lower severity: negative values throw a rawRangeErrorinstead ofKeeperErroron both platforms; values above2^32-1throw on browser but only warn and clamp on Node. Recommend validating/clampingtimeoutMsonce, inkeeper.ts, before it reaches either platform. -
downloadFile/downloadThumbnailcannot inheritSecretManagerOptions.requestTimeoutMs(medium).uploadFileforwardsoptions.requestTimeoutMstoplatform.fileUpload, butdownloadFile/downloadThumbnail(keeper.ts:1398,1403) only accept an explicittimeoutMsargument, nooptionsparameter. A caller who setsrequestTimeoutMsonce, expecting it to bound downloads too, silently gets the 30s default instead. -
cachingPostFunction/createCachingFunctionsilently drop the new override (low). Both public exports (node/localConfigStorage.ts,browser/localConfigStorage.ts) declare only(url, transmissionKey, payload)and callplatform.postwith 3 args, sorequestTimeoutMsnever reachesplatform.postfor any consumer using the SDK's own offline cache helpers. -
Browser timeout errors are a raw
DOMException, neverKeeperError(low/medium). Node wraps its timeout innew KeeperError(...);browserPlatform.ts'sget/posthave no try/catch at all, andfileUpload's catch re-throws unchanged. Code written againstinstanceof KeeperError(the pattern the CHANGELOG implies) will silently miss timeouts on browser.
Breaking change risk
AbortSignal.timeout()is called unconditionally in every browser network call, with no feature detection (medium/high). Notypeofguard and no polyfill anywhere in the rollup browser build. On a runtime lacking this API (pre-2022 browsers, older embedded WebViews), everyget/post/fileUploadcall now fails immediately with aTypeError, a full break, not limited to calls that would have timed out. This contradicts "Breaking Changes: None": a runtime that previously worked fine with no timeout enforcement now hard fails on every call.
Test coverage
requestTimeoutMspropagation through the public API is completely untested (high). Zero occurrences ofrequestTimeoutMs/timeoutMsinkeeper.test.tsorthrottle.test.ts. Verified by mutation testing: droppingoptions.requestTimeoutMsfrom the call atkeeper.ts:800entirely still leaves the full suite at 73/73 passing.downloadFile/downloadThumbnail/uploadFileare never invoked by any test.- No test proves a timeout rejection isn't retried forever by
postQuery's retry loop (current behavior is correct, verified by repro, but it's unguarded and untested). nodePlatform.test.ts'shttpsmock never emits aresponseevent; a coverage run confirms the success path lines (fetchDataand the three callback bodies) are never executed.DEFAULT_REQUEST_TIMEOUT_MS's value is only checked against itself; mutating30000to30left the full suite green.hash()'s fix is only provable by the new isolated unit test; both real production call sites pass the same tag before and after the fix, so no integration test can distinguish buggy from fixed behavior.
Process note
- This PR's target branch never triggers the JS test workflow.
test.js.ymlruns only on PRs intomaster; this PR targetsrelease/sdk/javascript/core/v17.6.0. The only passing checks are Socket Security scans; no test execution ran in CI. Confirmed the suite does pass (73/73) by running it locally against this PR branch.
Not an issue
Confirmed the hash() fix itself is correct and non-breaking today (both call sites pass an identical tag), and no other package in the monorepo touches the changed surface.
Recommendation
Requesting changes on items 1, 2, 3, and 6: the Node/browser timeout semantics gap and the unvalidated 0 value both undercut the security rationale in the PR description, and the browser hard dependency contradicts the stated "no breaking changes." Items 7-12 would meaningfully reduce the chance of a silent regression in this exact area and are worth adding here or in a fast follow-up.
Summary
JavaScript SDK: bounds network request timeouts and fixes the Node platform's
hash()ignoring itstagargument.Changes
Fixed
KeeperErrorif it fires; browser usesAbortSignal.timeout. Default 30s, configurable viaSecretManagerOptions.requestTimeoutMs, or a directtimeoutMsargument ondownloadFile/downloadThumbnail.hash()hardcoded its HMAC tag instead of using the caller-suppliedtagargument, unlike the browser implementation and the documentedPlatformcontract (KSM-1254). No behavior change for the SDK's existing caller, which already passed that same string.Testing
Security Impact
hash()derives the client ID from the client key (getClientId, viaCLIENT_ID_HASH_TAG). The fix makes the Node platform honor itstagargument instead of a hardcoded string, closing a latent wrong-tag risk for any future caller that passes a tag other than the one currently in use; no change to today's derived client ID.Breaking Changes
None. New
requestTimeoutMs/timeoutMsparameters andPlatformcontract additions are additive and optional;downloadFile/downloadThumbnailkeep their existing call signature.Related Issues