Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
79e0b62
fix(sdk/javascript): KSM-1073 add dbConnectionMethod to PamSettingsCo…
stas-schaller Jul 2, 2026
f711b85
fix(javascript): KSM-1079 skip undecryptable folders in getFolders in…
stas-schaller Jul 7, 2026
bc0ac29
test(js): KSM-1079 add getFolders crash-safety regression test
stas-schaller Jul 7, 2026
1c7d237
fix(js): KSM-1084 surface per-item error messages from deleteSecret a…
stas-schaller Jul 7, 2026
75c63bf
chore(javascript): humanize comments in core SDK on the release branc…
stas-schaller Jul 31, 2026
26264e5
fix(javascript): KSM-748 use folder key for shared-folder records in …
stas-schaller Aug 14, 2026
4bdfb7c
fix(javascript): KSM-1035 one-sided throttle jitter and retry_after c…
stas-schaller Aug 14, 2026
d06978b
fix(js/core): bump minimatch, @babel/core, handlebars dev-dependencies
saldoukhov Aug 13, 2026
8ccb7af
docs(javascript): STE pass on 17.6.0 changelog and share-client example
stas-schaller Aug 17, 2026
555cf09
fix(javascript): KSM-1128 bound server-key-rotation retries in postQu…
stas-schaller Aug 18, 2026
1ffba2b
JavaScript SDK: fix Node platform hash() ignoring tag parameter (KSM-…
mgallego-keeper Aug 27, 2026
6531c11
fix(ci): KSM-1334 run JS test matrix on release-branch PRs (#1140)
mgallego-keeper Aug 28, 2026
96b60f9
fix(javascript): KSM-1332 reject instead of hanging on an IndexedDB f…
mgallego-keeper Aug 28, 2026
d5de2bd
fix(javascript): require a real JSON parse in the readable-JSON heuri…
mgallego-keeper Sep 1, 2026
45cc6a5
Merge pull request #1147 from Keeper-Security/fix/js-record-link-json…
stas-schaller Sep 1, 2026
e863875
fix(javascript): config file permissions not corrected after each wri…
stas-schaller Sep 1, 2026
d8e2b2d
fix(javascript): KSM-1267 classify getFolders() decryption failures (…
mgallego-keeper Sep 1, 2026
6638568
fix(javascript): remove unconditional TLS verification bypass from ex…
stas-schaller Sep 1, 2026
9a4ebfe
feat(javascript): add notation, folders, file-upload, totp, and pam-l…
stas-schaller Sep 1, 2026
308b06e
fix(javascript): stop getSharedFolderUid hanging on a cycle (KSM-1297…
mgallego-keeper Sep 2, 2026
f7c33bc
chore(javascript): bump example core version pins, migrate share-clie…
stas-schaller Sep 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .github/workflows/test.js.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
name: Test-JS

on:
# Runs on the merge result (head merged into base), so it gates a change before it lands on
# a JS core release branch as well as on master. Deliberately not also on push to release/**,
# which would only re-test a commit this pull_request run already tested.
pull_request:
branches: [ master ]
branches:
- master
- 'release/sdk/javascript/core/**'
paths:
- 'sdk/javascript/packages/core/**'
- '.github/workflows/test.js.yml'
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -69,5 +69,6 @@ ansible.cfg
# Except typescript configuration
!tsconfig.json
!tsconfig.test.json
!tsconfig.node.json

.gradle/
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@
"run": "node hello.js"
},
"dependencies": {
"@keeper-security/secrets-manager-core": "17.3.0"
"@keeper-security/secrets-manager-core": "17.6.0"
}
}
3 changes: 3 additions & 0 deletions examples/javascript/file-upload/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules
config.json
upload-me.txt
24 changes: 24 additions & 0 deletions examples/javascript/file-upload/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# File upload

Uploads a local file to a record, then downloads it back and confirms the bytes round-trip.

## Function demonstrated

`uploadFile(options, ownerRecord, file)`: attaches `file` to `ownerRecord` and returns the new file's UID.
`file` is a `KeeperFileUpload`: `{ name, title, type?, data }`, where `data` is a `Uint8Array`.

Files always attach to a record - there's no way to upload a file to a folder directly. This complements
`downloadFile`, already shown in the `hello-secret` example, which only covers reading an existing file.

The script polls briefly after uploading: the server can take a moment to populate the new file's download
URL in a `getSecrets()` response, so fetching once immediately after `uploadFile()` returns can find a file
entry with no `url` yet. It also calls `process.exit(0)` explicitly at the end, since `uploadFile()`'s
underlying HTTP response is never read and leaves the socket (and the process) open otherwise.

## Running

1. Replace the placeholder token in `hello.js` with a real one-time access token for your vault.
2. `npm install`
3. `npm run run`

Expected output: the uploaded file's UID, then `true` confirming the downloaded bytes match what was uploaded.
58 changes: 58 additions & 0 deletions examples/javascript/file-upload/hello.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
const {
getSecrets,
initializeStorage,
localConfigStorage,
uploadFile,
downloadFile
} = require('@keeper-security/secrets-manager-core')
const fs = require('fs')

const main = async () => {
const storage = localConfigStorage("config.json")
// if your Keeper Account is in other region than US, update the hostname accordingly
await initializeStorage(storage, 'US:EXAMPLE_ONE_TIME_TOKEN', 'keepersecurity.com')

const {records} = await getSecrets({storage: storage})
const ownerRecord = records[0]

// Files attach to a record - there's no way to upload a file without an owner record.
const localFilePath = './upload-me.txt'
if (!fs.existsSync(localFilePath)) {
fs.writeFileSync(localFilePath, 'hello from the file-upload example\n')
}
const data = fs.readFileSync(localFilePath)

const fileUid = await uploadFile({storage: storage}, ownerRecord, {
name: 'upload-me.txt',
title: 'upload-me.txt',
type: 'text/plain',
data: data
})
console.log(`uploaded file UID: ${fileUid}`)

// Round-trip: re-fetch the record (uploadFile doesn't mutate the in-memory copy) and
// download the file we just uploaded to prove the bytes round-trip correctly.
//
// The server can take a moment after uploadFile() returns before the file's download
// URL is populated in a getSecrets() response, so poll briefly rather than fetching once.
let uploadedFile
for (let attempt = 1; attempt <= 5 && !uploadedFile?.url; attempt++) {
const {records: refreshedRecords} = await getSecrets({storage: storage}, [ownerRecord.recordUid])
uploadedFile = refreshedRecords[0].files.find(f => f.fileUid === fileUid)
if (!uploadedFile?.url) {
await new Promise(resolve => setTimeout(resolve, 1000))
}
}
if (!uploadedFile?.url) {
throw new Error(`Uploaded file ${fileUid} has no download URL yet after 5 attempts`)
}

const downloaded = await downloadFile(uploadedFile)
const matches = Buffer.compare(data, Buffer.from(downloaded)) === 0
console.log(`downloaded bytes match uploaded bytes: ${matches}`)
}

// uploadFile()'s underlying HTTP response is never drained, which leaves the process
// alive after main() resolves - exit explicitly rather than leave a script that appears
// to hang after printing its result.
main().finally(() => process.exit(0))
12 changes: 12 additions & 0 deletions examples/javascript/file-upload/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name": "file-upload",
"version": "1.0.0",
"description": "Secrets Manager file upload sample for Node",
"license": "ISC",
"scripts": {
"run": "node hello.js"
},
"dependencies": {
"@keeper-security/secrets-manager-core": "17.6.0"
}
}
2 changes: 2 additions & 0 deletions examples/javascript/folders/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules
config.json
21 changes: 21 additions & 0 deletions examples/javascript/folders/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Folders

Lists folders, creates a new one, renames it, then deletes it.

## Functions demonstrated

- `getFolders(options)`: returns every folder the application has access to, as `KeeperFolder[]` (`{ folderUid, parentUid?, name? }`).
- `createFolder(options, createOptions, folderName)`: creates a new folder. `createOptions` is `{ folderUid, subFolderUid? }`:
- `folderUid` must be the UID of a shared folder (a `KeeperFolder` entry with no `parentUid`, which is what distinguishes a shared folder from a regular sub-folder in the `getFolders()` result). It becomes the new folder's shared-folder association, not its direct visual parent.
- `subFolderUid` is optional; set it to an existing regular folder's UID to nest the new folder under it instead of directly under the shared folder.
- `updateFolder(options, folderUid, folderName)`: renames a folder.
- `deleteFolder(options, folderUids, forceDeletion?)`: deletes one or more folders by UID.

## Running

1. Replace the placeholder token in `hello.js` with a real one-time access token for your vault.
2. Make sure the vault has at least one shared folder.
3. `npm install`
4. `npm run run`

Expected output: the current folder list, the new folder's UID, a rename confirmation, then the delete result.
38 changes: 38 additions & 0 deletions examples/javascript/folders/hello.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
const {
getFolders,
createFolder,
updateFolder,
deleteFolder,
initializeStorage,
localConfigStorage
} = require('@keeper-security/secrets-manager-core')

const main = async () => {
const storage = localConfigStorage("config.json")
// if your Keeper Account is in other region than US, update the hostname accordingly
await initializeStorage(storage, 'US:EXAMPLE_ONE_TIME_TOKEN', 'keepersecurity.com')

const folders = await getFolders({storage: storage})
console.log(folders)

// A folder with no parentUid is itself a shared folder. New folders must be created
// inside one - pass its UID as createOptions.folderUid (it becomes the new folder's
// sharedFolderUid, not a direct parent; pass createOptions.subFolderUid too to nest
// under an existing regular folder instead of directly under the shared folder).
const sharedFolder = folders.find(f => !f.parentUid)
if (!sharedFolder) {
console.log('No shared folder found - create one in the vault first')
return
}

const newFolderUid = await createFolder({storage: storage}, {folderUid: sharedFolder.folderUid}, 'Example folder')
console.log(`created folder UID: ${newFolderUid}`)

await updateFolder({storage: storage}, newFolderUid, 'Example folder (renamed)')
console.log('renamed folder')

const deleteResult = await deleteFolder({storage: storage}, [newFolderUid])
console.log(`delete result: ${JSON.stringify(deleteResult)}`)
}

main().finally()
12 changes: 12 additions & 0 deletions examples/javascript/folders/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name": "folders",
"version": "1.0.0",
"description": "Secrets Manager folder CRUD sample for Node",
"license": "ISC",
"scripts": {
"run": "node hello.js"
},
"dependencies": {
"@keeper-security/secrets-manager-core": "17.6.0"
}
}
2 changes: 0 additions & 2 deletions examples/javascript/hello-secret/hello.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'

const {
getSecrets,
initializeStorage,
Expand Down
2 changes: 1 addition & 1 deletion examples/javascript/hello-secret/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@
"run" : "node hello.js"
},
"dependencies": {
"@keeper-security/secrets-manager-core": "16.0.12"
"@keeper-security/secrets-manager-core": "17.6.0"
}
}
8 changes: 0 additions & 8 deletions examples/javascript/hello-secret/yarn.lock

This file was deleted.

2 changes: 2 additions & 0 deletions examples/javascript/notation/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules
config.json
26 changes: 26 additions & 0 deletions examples/javascript/notation/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Notation

Looks up a single secret value by notation instead of paging through a full `getSecrets()` result.

## Notation format

```
keeper://<record UID or title>/field/<field type>
keeper://<record UID or title>/custom_field/<field label>
keeper://<record UID or title>/file/<file name or UID>
```

The `keeper://` prefix is optional.

## Functions demonstrated

- `getNotationResults(options, notation)`: resolves a notation string to a list of values, throws if the notation is invalid or the target isn't found.
- `tryGetNotationResults(options, notation)`: same lookup, but logs and returns an empty array instead of throwing.

## Running

1. Replace the placeholder token in `hello.js` with a real one-time access token for your vault.
2. `npm install`
3. `npm run run`

Expected output: the first record's `login` field value resolved via notation, an empty-array result from `tryGetNotationResults` against a field that doesn't exist, and a caught error from `getNotationResults` against the same missing field.
37 changes: 37 additions & 0 deletions examples/javascript/notation/hello.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
const {
getSecrets,
initializeStorage,
localConfigStorage,
getNotationResults,
tryGetNotationResults
} = require('@keeper-security/secrets-manager-core')

const main = async () => {
const storage = localConfigStorage("config.json")
// if your Keeper Account is in other region than US, update the hostname accordingly
await initializeStorage(storage, 'US:EXAMPLE_ONE_TIME_TOKEN', 'keepersecurity.com')

const {records} = await getSecrets({storage: storage})
const firstRecord = records[0]

// Notation addresses a single value by record UID/title + selector, without paging
// through a full getSecrets() result yourself: keeper://<uid-or-title>/field/<type>
const loginNotation = `keeper://${firstRecord.recordUid}/field/login`
const [login] = await getNotationResults({storage: storage}, loginNotation)
console.log(`login via notation: ${login}`)

// tryGetNotationResults() never throws - it logs and returns an empty array on error,
// so it's safe to call speculatively against a field that may not be present.
const missingNotation = `keeper://${firstRecord.recordUid}/field/does_not_exist`
const missing = await tryGetNotationResults({storage: storage}, missingNotation)
console.log(`missing field via tryGetNotationResults: ${JSON.stringify(missing)} (empty array, no throw)`)

// getNotationResults() is the throwing variant - same lookup, surfaced as a real error.
try {
await getNotationResults({storage: storage}, missingNotation)
} catch (e) {
console.log(`getNotationResults threw as expected: ${e.message}`)
}
}

main().finally()
12 changes: 12 additions & 0 deletions examples/javascript/notation/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name": "notation",
"version": "1.0.0",
"description": "Secrets Manager notation lookup sample for Node",
"license": "ISC",
"scripts": {
"run": "node hello.js"
},
"dependencies": {
"@keeper-security/secrets-manager-core": "17.6.0"
}
}
2 changes: 2 additions & 0 deletions examples/javascript/pam-linked-records/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules
config.json
36 changes: 36 additions & 0 deletions examples/javascript/pam-linked-records/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# PAM linked records

Reads a PAM record's linked records - the mechanism PAM resources use to associate a
credential, connection/rotation metadata, JIT elevation settings, and AI risk settings
with a resource record.

## Concept

In the Keeper vault, a PAM resource (e.g. a machine or database) links to other records rather than
embedding their data directly. Those links are exposed on `record.links` as a raw
`{ recordUid, data?, path? }[]`. `path` identifies what kind of link it is:

- `'meta'`: rotation/connection permission metadata (plain JSON)
- `'jit_settings'`: just-in-time elevation settings (encrypted)
- `'ai_settings'`: AI risk-level settings (encrypted)
- no path: a credential link (admin/IAM/launch-credential flags)

## Functions demonstrated

- `getLinks(record)`: wraps `record.links` as `KeeperRecordLink[]`, one typed accessor per link. Decryption
keys are pulled automatically from the SDK's internal key cache (already populated by the preceding
`getSecrets()` call), so none of the accessor methods below need a key argument in normal use.
- `KeeperRecordLink` accessors used here: `getMetaData()`, `allowsRotation()`, `allowsConnections()`,
`getJitSettingsData()`, `getAiSettingsData()`, `isAdminUser()`, `isLaunchCredential()`. Several more
exist (`isIamUser()`, `belongsTo()`, `allowsPortForwards()`, `getRotationSettings()`, `getAllowedSettings()`,
and the generic `getLinkData()`/`getDecryptedData()` for reading a link's raw payload directly).

## Running

1. Replace the placeholder token in `hello.js` with a real one-time access token for your vault.
2. Make sure the vault has at least one PAM record with linked records.
3. `npm install`
4. `npm run run`

Expected output: the linked-record count for the first record that has any, followed by each link's UID,
path, and the relevant typed accessor values for that path.
47 changes: 47 additions & 0 deletions examples/javascript/pam-linked-records/hello.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
const {
getSecrets,
getLinks,
initializeStorage,
localConfigStorage
} = require('@keeper-security/secrets-manager-core')

const main = async () => {
const storage = localConfigStorage("config.json")
// if your Keeper Account is in other region than US, update the hostname accordingly
await initializeStorage(storage, 'US:EXAMPLE_ONE_TIME_TOKEN', 'keepersecurity.com')

const {records} = await getSecrets({storage: storage})

// A PAM record's linked records (a credential, rotation/connection metadata, JIT
// elevation settings, AI risk settings, ...) live in record.links, not a dedicated
// field. getLinks() wraps each raw link in a typed accessor. Decryption keys are
// pulled automatically from the same key cache getSecrets() just populated, so
// there's no need to supply one explicitly for any of the calls below.
const record = records.find(r => (r.links || []).length > 0)
if (!record) {
console.log('No record with linked records found')
return
}

const links = getLinks(record)
console.log(`${record.recordUid} has ${links.length} linked record(s)`)

for (const link of links) {
console.log(`- linked record ${link.recordUid} (path: ${link.path ?? 'none'})`)

if (link.path === 'meta') {
console.log(` metadata: ${JSON.stringify(await link.getMetaData())}`)
console.log(` allows rotation: ${link.allowsRotation()}, allows connections: ${link.allowsConnections()}`)
} else if (link.path === 'jit_settings') {
console.log(` JIT elevation settings: ${JSON.stringify(await link.getJitSettingsData())}`)
} else if (link.path === 'ai_settings') {
console.log(` AI risk settings: ${JSON.stringify(await link.getAiSettingsData())}`)
} else {
// A credential-type link has no path - flags are read directly off its
// decoded link data via the boolean accessors.
console.log(` is admin user: ${link.isAdminUser()}, is launch credential: ${link.isLaunchCredential()}`)
}
}
}

main().finally()
Loading
Loading