Skip to content

Release JavaScript SDK v17.6.0 - #1111

Draft
stas-schaller wants to merge 21 commits into
masterfrom
release/sdk/javascript/core/v17.6.0
Draft

Release JavaScript SDK v17.6.0#1111
stas-schaller wants to merge 21 commits into
masterfrom
release/sdk/javascript/core/v17.6.0

Conversation

@stas-schaller

@stas-schaller stas-schaller commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Summary

Release branch for JavaScript SDK v17.6.0: NSF folder-decryption parity, throttle hardening, per-item delete error surfacing, and key-rotation retry cap.

Changes

New Features

  • PAM settings dbConnectionMethod (KSM-1073): added dbConnectionMethod to PamSettingsConnection

Bug Fixes

  • getFolders() crash safety (KSM-1079): undecryptable folders are now skipped instead of throwing; the remaining folders are returned normally
  • deleteSecret()/deleteFolder() partial failure (KSM-1084): the SDK now surfaces per-item error messages from the server to the caller
  • Shared folder record decryption (KSM-748): records with innerFolderUid in the flat records[] array now use the folder key instead of the app key; this matches the behavior for records in folders[].records[]
  • Throttle retry jitter (KSM-1035): jitter is now one-sided (0 to +25%); server-supplied retry_after is capped at 176s
  • Key-rotation retry cap (KSM-1128): postQuery's {"error":"key"} branch now retries at most 3 times before throwing a typed KeeperError. The server-suggested key_id is validated for shape (positive integer) and membership in the bundled key table before being persisted; an unsupported id can no longer corrupt the stored configuration. PR fix(javascript): KSM-1128 bound server-key-rotation retries in postQuery #1078 merged 2026-08-18.

Maintenance

  • Updated minimatch, @babel/core, and handlebars dev dependencies

Breaking Changes

None.

Related Issues

  • KSM-1073, KSM-1079, KSM-1084, KSM-748, KSM-1035, KSM-1128
  • REL-5278

stas-schaller and others added 9 commits July 24, 2026 13:26
#1079)

* chore(javascript): humanize comments and test names on the release branch

Ticket refs belong in commit messages, not code; test names now describe the behavior
under test instead of the ticket that prompted it. No behavior change.
…flat response array (#1076)

Records created via non-SDK clients inside shared folders arrive in the flat
response.records[] with innerFolderUid set, but recordKey is wrapped with the
folder key, not the app key. The unconditional KEY_APP_KEY unwrap caused these
records to be silently skipped. Mirrors the folderKeyMap pattern already
shipped in the Java SDK (KSM-753).
…ap (#1077)

* fix(javascript): KSM-1035 one-sided throttle jitter and retry_after cap

throttleJitter previously returned [-0.25, 0.25), so a retry could fire before
the computed backoff floor and immediately re-trigger the same throttle window.
Narrowed to [0, 0.25), one-sided like the already-shipped Ruby fix (KSM-883).
Also caps a server-supplied retry_after at MAX_THROTTLE_DELAY_SEC (176s), the
same ceiling the exponential branch already reaches on its last retry.

* fix(javascript): address KSM-1035 review feedback

- Fix stale JSDoc on throttleDelay: jitter range is [0, 0.25) not [-0.25, 0.25)
- Update jitter-bounds unit test to reflect one-sided range (floor is 11s not 8.25s)
- Add 17.6.0 CHANGELOG entry for KSM-1035
All three are dev-only in this package (ts-jest transitive chain), never
shipped:
- minimatch -> 9.0.9 (CVE-2026-27903, CVE-2026-27904 ReDoS)
- @babel/core -> 7.29.7 (CVE-2026-49356, arbitrary file read via
  sourceMappingURL)
- handlebars -> 4.7.9 (CVE-2026-33938, CVE-2026-33941)

Lockfile-only, no package.json range changes. Cherry-picked and scoped
to sdk/javascript/packages/core/package-lock.json from 831b7b48,
efde007d, and b9aef2fa (Sergey Aldoukhov).

KSM-1217
@stas-schaller stas-schaller changed the title feat(javascript): JavaScript SDK v17.6.0 Release JavaScript SDK v17.6.0 Aug 18, 2026
stas-schaller and others added 10 commits August 18, 2026 12:03
…ery (#1078)

* fix(javascript): KSM-1128 bound server-key-rotation retries in postQuery

postQuery's error === 'key' branch (default, no custom server key pinned)
saved the server's suggested key_id and retried with no iteration cap. A
server that keeps rejecting the suggested key would retry forever. Adds a
keyRotationAttempt counter bounded by MAX_KEY_ROTATION_RETRIES (3), mirroring
the existing throttleAttempt/MAX_THROTTLE_RETRIES pattern in the same loop.

* fix(javascript): address KSM-1128 review feedback

- Import KeeperError for use at throw sites
- Validate key_id before persisting: reject non-integer or non-positive values with KeeperError
- Replace plain Error with KeeperError at all key-rotation throw sites
- Tighten rotation-bound test: assert calls === 4 (MAX_KEY_ROTATION_RETRIES + 1), remove loose guard
- Add happy-path test: single key rotation resolves on the retry

* fix(javascript): KSM-1128 complete review feedback

Blocking issue: Validate suggested key_id membership in keeperPublicKeys range.
Unsupported key ids (outside 7-18) now throw typed KeeperError instead of silently
persisting invalid config. Membership check is gated after custom-key check to
preserve IL5 deployment support.

Non-blocking improvements:
- Diagnostic message now names the transmission key id actually attempted, not a
  future suggestion that was never sent.
- Runaway loop guards in all key rotation tests prevent silent jest hang if retry
  bound breaks.
- Better adoption verification: second test uses key_id 8 (not default 7) and
  asserts both transmission key and storage reflect the new id.
- Helper functions (FAKE_ONE_TIME_TOKEN, keyErrorResponse) reduce duplicate
  literals and improve test maintainability.

All 57 tests pass.

* fix(javascript): KSM-1128 add membership check test and changelog

Add regression test for unsupported key_id rejection to prevent silent config
poisoning. Server response with unsupported key_id (e.g. 99) now correctly:
- Makes exactly 1 network request (no retry loop)
- Throws typed KeeperError with clear message
- Does not persist the invalid id to storage

Also add changelog entry documenting the key rotation bounding and validation
improvements in v17.6.0.

All 58 tests pass.

* fix(javascript): KSM-1128 move shape guard below customKey branch

When the server sends {"error":"key"} to a client with a pinned custom
server public key (IL5 config), the IL5 diagnostic now always fires
regardless of whether key_id is present or valid. Previously the shape
guard above the customKey check intercepted malformed key_id values and
produced a generic error instead of the actionable IL5 message.

* fix(javascript): KSM-1128 apply non-blocking cosmetic fixes

- IL5 diagnostic falls back to transmissionKey.publicKeyId when
  storage has no serverPublicKeyId yet, so the message never
  reads "id null"
- Membership error derives the supported range from keeperPublicKeys
  keys at runtime instead of the hardcoded literal "7-18", so the
  message stays accurate when a new key is added to the table

* fix(javascript): KSM-1128 rename rotation tests to behavior-based names

The three tests added for the retry bound and membership check were
prefixed "IL5 dynamic key - ..." but they test generic postQuery
rotation behavior, not IL5-specific code paths. Renamed to describe
what each test asserts.
…1254) (#1135)

* fix(javascript): Node platform hash() ignores tag parameter (KSM-1254)

hash() hardcoded 'KEEPER_SECRETS_MANAGER_CLIENT_ID' instead of hashing
with its tag parameter, unlike the browser implementation and the
Platform contract. No behavior change for the SDK's only caller, which
already passed that same string as the tag.

* test(javascript): pin the client id digest and guard Node/browser hash parity

The two tests already on this branch prove hash() honors its tag, but nothing
holds the result to a value computed outside the SDK, and nothing holds the two
platform implementations to each other. Both gaps are what let the Node
implementation hardcode its tag unnoticed since the initial commit: TypeScript
accepts a lower-arity function for a higher-arity signature, so the compiler
never objected, and every call site inside the SDK passes the one tag the buggy
code hardcoded, so no integration-level test could tell the two apart.

Add a separate file so the stacked KSM-1209 work, which appends to
nodePlatform.test.ts, rebases without a conflict.

- Pin the client id digest for a fixed key against a value cross-checked with
  the Python SDK's hmac.new(client_key_bytes, CLIENT_ID_HASH_TAG, 'sha512'),
  rather than recomputing it with the same call the implementation makes.
- Assert the Node and browser implementations agree on a tag that appears
  nowhere in the SDK, which is the only input that separates an implementation
  honoring its tag from one hardcoding CLIENT_ID_HASH_TAG.

Verified against the pre-fix implementation: the parity test fails, and the
pinned digest still passes, which is the evidence for this branch's claim that
the fix changes no behavior for existing callers.

---------

Co-authored-by: Mateo Gallego <mgallego@keepersecurity.com>
test.js.yml triggered only on pull_request into master, so every JS core fix
that goes to a release branch first, which is all of them, merged with no test
signal. The only checks on those PRs are the Socket Security scans, and a green
tick there says nothing about whether the SDK still passes.

Add release/sdk/javascript/core/** to the pull_request branch filter. The
workflow already lists itself under paths, so this change gates itself.

Deliberately not adding a push trigger on release/**: that is the duplicate-run
pattern KSM-1302 tracks in test.ruby.yml and the four JavaScript KMS workflows,
where the pull_request run has already tested the same commit.

Follows PR #1119, which made the same change to test.java.yml for KSM-1269.
Both are legs of KSM-1284, which tracks the remaining SDK core workflows.
…ailure (#1141)

Every IndexedDB call in the browser config storage wired only onsuccess, plus
onupgradeneeded on the two open calls. A failing IDBRequest fires onerror and
never onsuccess, so any storage failure left the returned promise pending
forever: the caller waited with no error, no rejection and no timeout. Four of
the eight wrappers even destructured reject from the Promise executor and never
called it.

secureStorage is the worst case. It awaits a read before it returns the storage
object, so a failure hung the constructor itself and never handed the caller
anything to catch.

- Wire onerror on all eight requests, rejecting with a typed KeeperError that
  carries the underlying DOMException name and message.
- Wire onblocked on both indexedDB.open calls. It fires when another live
  connection holds the database during a version change, and neither onsuccess
  nor onerror follows it.
- Catch the synchronous throw from transaction() when the object store is
  missing. A throw inside an event handler is not caught by the enclosing
  Promise executor, so that path left the promise pending too.

The cause is duck-typed on name and message rather than tested with instanceof
Error: a DOMException only satisfies instanceof Error in the realm it was
constructed in, so a cross-realm failure would otherwise lose the diagnostic.

Adds test/browserConfigStorage.test.ts, covering both exported storages with a
minimal IndexedDB double and no new dependency. Each failure test races a short
timer so a regression fails fast with "promise never settled" instead of
stalling the suite. Verified against the unfixed file: all eight failure tests
fail there, and both happy-path tests pass on either side.
…stic

_isReadableJson only checked whether the base64-decoded text started with
'{' or '['. Genuine ciphertext has a ~1-in-128 chance of coincidentally
decoding to a leading '{' or '[' byte, which made hasEncryptedData() (and
hasReadableData()) misclassify real encrypted data as not encrypted.

_isReadableJson now requires the text to actually parse as valid JSON, not
just start with the right character, closing the gap consistently across
hasReadableData, hasEncryptedData, and getLinkData.
…-heuristic-176

KSM-1351: JavaScript SDK: fix hasEncryptedData misreading ciphertext as JSON
…te (KSM-1263) (#1131)

fs.openSync's mode argument only takes effect when the file is created,
so a config file that already existed with looser permissions kept
them. Permissions are now explicitly reset to 0600 after every write.
…1152)

* fix(javascript): KSM-1267 classify and surface getFolders() decryption failures

getFolders() now classifies why an undecryptable folder was skipped
(integrity, format, missing-key, malformed-data, or unknown) instead of
logging an opaque, unclassified error, and logs one summary line naming
every folder UID it had to omit. Adds an optional onDecryptionError
callback to SecretManagerOptions, invoked once per skipped folder, so a
caller can react to or throw to fail closed on a partial result;
existing callers that do not set it see no behavior change.

Both the Node and browser platforms' unwrap() now reject an unwrapped
key of the wrong length immediately instead of caching it and failing
later at an unrelated call site.

The underlying finding (the shared-folder key wrap uses unauthenticated
AES-256-CBC, a format fixed server-side that the SDK cannot change
unilaterally) was reviewed and confirmed low-impact: a manipulated
folder key is still caught by the existing AES-GCM authentication on
the record keys inside that folder.

* fix(javascript): KSM-1267 remove nonexistent getFolders2 reference in comment

onDecryptionError's doc comment on SecretManagerOptions referenced
getFolders2, which does not exist (unlike getSecrets/getSecrets2,
getFolders has no "2" counterpart).
…amples (#1149)

hello-secret and proxy-support set NODE_TLS_REJECT_UNAUTHORIZED='0'
process-wide as their first line, with no self-signed-cert scenario in
either example to justify it. Neither example needs the bypass; delete
it rather than swap in a scoped alternative.

KSM-1316
…inked-records examples (#1151)

Five example areas the 2026-08-25 examples audit found genuinely
missing from examples/javascript - no existing directory had any
partial coverage of any of them. Each new directory mirrors
hello-secret's existing style (plain CommonJS, Node-only, no build
tooling) with its own README, since none of the three undocumented
existing directories had one either.

- notation: getNotationResults/tryGetNotationResults, throwing vs.
  non-throwing lookup against a missing field
- folders: getFolders/createFolder/updateFolder/deleteFolder, with a
  README note on createOptions.folderUid needing a shared-folder UID
- file-upload: uploadFile paired with downloadFile for a round-trip
  byte-compare, complementing hello-secret's existing download-only
  coverage
- totp: getTotpCode against a record's oneTimeCode field, including
  the unixTimeSeconds override for deterministic use
- pam-linked-records: getLinks()/KeeperRecordLink, the SDK's typed
  accessor for a PAM resource's linked credential/metadata/JIT/AI
  records via record.links - confirmed against the SDK's own
  record_link.test.ts rather than the recordRef-based mechanism the
  ticket description assumed, which doesn't exist as described

All five verified: destructured import names checked against the
published SDK, hello.js files pass node --check.

KSM-1328
@socket-security

socket-security Bot commented Sep 1, 2026

Copy link
Copy Markdown

mgallego-keeper and others added 2 commits September 2, 2026 11:10
#1153)

* fix(javascript): stop getSharedFolderUid from hanging on a folder parent cycle (KSM-1297)

getSharedFolderUid walked the folder parent chain in an unbounded while
loop with no visited set, so a cycle in server-supplied folder data (two
folders naming each other as parent, or a folder naming itself) spun
forever. Because the loop is synchronous with no await, this blocked the
entire JS event loop in Node and froze the tab in a browser, not just
the calling promise.

Bound the walk with a visited set and throw a descriptive error naming
the folder UID where the cycle closes. The existing per-folder try/catch
in fetchAndDecryptFolders (added by KSM-1079) already skips and
continues on any thrown error, so no caller changes are needed.

Add regression coverage for both the Node and browser platform builds:
self-cycle, two- and three-folder rings with exact log-message
assertions, a 500-folder ring proving the fix is genuinely bounded
rather than merely fast, a real two-level non-cyclic nested folder chain
to confirm unchanged behavior, and the pre-existing "parent not found"
case to confirm its message stays distinct from the new cycle message.

* test(javascript): fix KSM-1297 cycle tests after KSM-1267's error classification landed

KSM-1267 merged into this branch's base after KSM-1297 was opened. It added an
upfront missing-key check in fetchAndDecryptFolders that now runs before
getSharedFolderUid, plus a summary console.error line whenever any folder is
skipped. The cycle test fixtures used an empty folderKey placeholder, which the
new check now intercepts before the folder ever reaches the cycle-detection
code, and the new summary line shifted every exact call-count assertion by one.

Give the cyclic fixtures a non-empty placeholder folderKey so they still reach
and exercise the actual cycle-detection path instead of short-circuiting on the
new check, update call counts to account for the summary line, and update the
exact log-message assertions to match the new classification-tagged format.
The getSharedFolderUid fix itself is unchanged.
…nt to Vite (#1156)

hello-secret (16.0.12), proxy-support and custom-caching-function-support
(both 17.3.0) were stale relative to the SDK's current 17.6.0. hello-secret's
yarn.lock is dropped rather than regenerated against 17.6.0, which hasn't
published to npm yet, matching the other three example directories, none of
which commit a lockfile.

share-client also moves off Create React App (react-scripts 4.0.3, archived
upstream) onto Vite, bumping react-scripts/react 17/typescript 4.1 to
vite 8/react 19/typescript 5.9 and dropping the stale Node-12-era @types/node
pin. README.md's dead IndexedDB blog link is removed as part of the same
cleanup.

Supersedes #1150, which merged into the wrong base branch and never reached
this release branch.

KSM-1320
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.

3 participants