feat/ephemeral-keys #37
No reviewers
Labels
No labels
bug
dependencies
documentation
duplicate
enhancement
good first issue
help wanted
invalid
javascript
question
wontfix
bug
duplicate
enhancement
help wanted
invalid
question
renovate: stop-updating
security
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: nomyo-ai/nomyo-js#37
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/ephemeral-keys"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
create feature parity with main package
The dependency bumps on main left `npm test` and `npm run build:types` broken independently of any feature work; both fail on a clean checkout of origin/main. TypeScript 6 no longer auto-includes @types packages the way node10 resolution did, so every test suite failed to compile with "Cannot find name 'describe'/'expect'". Declare the needed @types explicitly instead: - tsconfig.json: types: ["node"] - jest.config.js: types: ["jest", "node"] TypeScript 6 also errors on two settings this config relied on: - moduleResolution "node" (node10) is deprecated -> "bundler", which matches how the package is actually consumed (rollup-bundled, with "module": "ESNext") - an implicit rootDir is now an error when outDir/declarationDir are set -> rootDir: "./src" Drop three unused imports that noUnusedLocals turns into hard errors, failing --emitDeclarationOnly. Finally, NodeSecureMemory logged to stdout unconditionally on construction. This was dormant while the native addon failed to load; once it loads, it broke the "no console.log when debug=false" test. A library must not write to stdout uninvited, and the client already reports this via getProtectionInfo() behind its own debug flag. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>🔴 NYX found 3 issue(s)
src/core/attestation/verifiers.ts:124:39[ts.code_exec.new_function] — new Function() constructor is equivalent to evalnative/src/mlock.cc:60:9[cfg-resource-leak] —Napi::Boolean::Newacquires heap object but not all exit paths release itnative/src/mlock.cc:88:9[cfg-resource-leak] —Napi::Boolean::Newacquires heap object but not all exit paths release itThree independent breakages, all present on main before this branch: 1. `npm ci` always exited non-zero. The root package.json carried "install": "node-gyp-build", but binding.gyp lives in native/, so the script ran in a directory with nothing to build: gyp: binding.gyp not found (cwd: <repo root>) native/package.json already declares that same install script in the right place, alongside its binding.gyp and "gypfile": true, so the root copy was a duplicate in the wrong package. Removing it also clears the now-inaccurate hasInstallScript flag from the lockfile. `npm ci` exits 0 again; the addon still builds from native/. 2. `npm run build` failed at the first step. @rollup/plugin-typescript requires tslib as a peer, and nothing depended on it directly — it was only present transitively as an optional dev dep, so a clean install could omit it entirely. Declared explicitly. 3. rollup.config.js used ESM syntax while package.json has no "type": "module", so Node parsed it as CommonJS and threw "Cannot use import statement outside a module". Node 24 recovers by reparsing (with a warning); Node 18 fails outright. Renamed to rollup.config.mjs, which is unambiguous on both. Setting "type": "module" instead would have broken jest.config.js, which is CommonJS. Verified on Node 18.19.1 and Node 24.18.0: npm ci exits 0, npm run build produces all three bundles plus declarations, and 76/76 tests pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>🔴 NYX found 3 issue(s)
src/core/attestation/verifiers.ts:124:39[ts.code_exec.new_function] — new Function() constructor is equivalent to evalnative/src/mlock.cc:60:9[cfg-resource-leak] —Napi::Boolean::Newacquires heap object but not all exit paths release itnative/src/mlock.cc:88:9[cfg-resource-leak] —Napi::Boolean::Newacquires heap object but not all exit paths release itBREAKING CHANGE: keyDir now defaults to null (ephemeral) instead of 'client_keys'. Clients that relied on keys persisting across restarts must now pass keyDir explicitly. The Python SDK defaults to key_dir=None: a key pair is generated in memory for the session and never written to disk. The JS port defaulted to 'client_keys' and always persisted, so merely constructing a client wrote an RSA private key into the working directory. That is a weaker default than the client it ports, and one users never asked for. - keyDir?: string | null, defaulting to undefined. null and undefined both mean ephemeral, matching Python's None. - Persistent mode is unchanged when keyDir is set: load the existing pair from that directory, otherwise generate and save one there. - Browsers are always ephemeral; they have no filesystem. Key rotation follows the same rule. It previously hardcoded 'client_keys' as its fallback directory, so an ephemeral client would have started writing private keys to disk on the first rotation tick. Rotated keys are now persisted only where keyDir or keyRotationDir is explicitly configured. Tests assert the intent (that saveKeys is never called) rather than probing the filesystem, since a leftover client_keys/ from the old default would otherwise make them pass or fail for the wrong reason. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>🔴 NYX found 3 issue(s)
src/core/attestation/verifiers.ts:124:39[ts.code_exec.new_function] — new Function() constructor is equivalent to evalnative/src/mlock.cc:60:9[cfg-resource-leak] —Napi::Boolean::Newacquires heap object but not all exit paths release itnative/src/mlock.cc:88:9[cfg-resource-leak] —Napi::Boolean::Newacquires heap object but not all exit paths release itMemory protection (step 3 of the Python parity work): NodeSecureMemory has implemented lockMemory/unlockMemory since the native addon landed, but nothing ever called them — mlock was dead code while getProtectionInfo() reported method: 'mlock' and canLock: true. The library claimed a protection it was not applying. - SecureByteContext now locks on entry and unlocks on exit, mirroring Python's secure_bytearray(lock=True). Zeroing happens *before* unlocking, so cleartext cannot reach swap in between. - lockMemory/unlockMemory are part of the SecureMemory interface, so the browser implementation must answer for them explicitly (false). - Locking is best-effort throughout: a refused or throwing lock degrades to zeroing only, which still has value. getProtectionInfo() now probes rather than assumes. Having the addon loaded is not the same as being allowed to lock: mlock is routinely refused by RLIMIT_MEMLOCK, which is small by default and 0 in some containers. canLock reflects a real mlock attempt, and 'mlock' is only claimed when locking genuinely works — otherwise it reports zero-only and says why. ProtectionInfo also carries platform, hasSecureZeroing and pageSize, closer to Python's get_protection_info(). SAST (ts.code_exec.new_function, ERROR): JwtQuoteVerifier loaded ESM-only jose via new Function('s', 'return import(s)'). No user input reached it, so it was not code injection — but new Function is blocked by any CSP without 'unsafe-eval', and this package ships a browser bundle, so the failure would land in the attestation path. Removed in favour of injection: pass the module as options.jose when require('jose') cannot work. The error message says so. No eval-equivalent remains in src/. .nyx/triage.json records the two native/src/mlock.cc cfg-resource-leak warnings as false positives, scoped to that file rather than the rule, so a genuine leak in future C++ still surfaces. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>