Commit graph

13 commits

Author SHA1 Message Date
659ef5a62b
fix: repair platform resolution in the built bundles
All checks were successful
NYX Security Scan / nyx-scan (pull_request) Successful in 5m53s
The published package could not be used at all. `require('nomyo-js')`
succeeded, but constructing a client threw:

    Cannot find module './node'

Platform selection was done with a runtime require:

    const NodeSecureMemory = require('./node').NodeSecureMemory;

Rollup flattens every module into one file, so './node' and './browser'
no longer exist at runtime — and because the calls sit inside function
bodies, rollup left them as literal runtime requires rather than
resolving them. The failure was therefore deferred to first use: module
load and Object.keys() both looked fine, so nothing noticed. The
SecureCompletionClient constructor calls createSecureMemory() and
createHttpClient(), which made every client unconstructable.

Confirmed present at 057ff6c, this branch's merge base, so the npm
package has never worked.

Platform implementations are now injected by the entry points, which is
what src/node.ts and src/browser.ts always claimed to do (they merely
re-exported ./index). createSecureMemory/createHttpClient consult a
registered factory and throw a directive error if none was registered.
No require fallback is kept: leaving one would put an unresolvable
relative require back in the bundle, and bundlers resolve requires
statically, so webpack/vite would fail on a path that does not exist in
dist/. Jest registers the platform via tests/setup.ts instead.

This also keeps the Node HTTP client and the optional native addon out
of the browser bundle, which previously carried both.

Second defect found while verifying: dist/esm/index.mjs contained 13
require() calls (crypto, fs, path, jose, nomyo-native) that the source
loads lazily. `require` does not exist in ES module scope, so an ESM
consumer crashed with "require is not defined" as soon as one ran —
using keyDir for key persistence would have hit it on every Node
version. Node 24 masked the crypto case by having a global crypto. The
ESM output now carries a createRequire shim.

tests/integration/bundle.test.ts covers the artefact that actually
ships: both bundles construct a client, expose the API, resolve the
platform layer, keep Node-only modules out of the browser build, and the
ESM entry is imported and used by a real spawned Node process. Every
other suite runs against src/ through ts-jest, where these paths resolve
normally — which is precisely why this went unnoticed.

Verified end to end by installing the packed tarball into a clean
project: CommonJS and ESM both construct a client and run fs-backed key
generation on Node 18.19.1 and 24.18.0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 12:59:24 +02:00
eafab3d9ac
feat: align timeout, error types and docs with the Python SDK
All checks were successful
NYX Security Scan / nyx-scan (pull_request) Successful in 5m43s
Completes the parity work (step 4).

Request timeout now defaults to 900 s, matching Python, instead of 60 s.
Encrypted inference cannot stream, so an entire completion arrives in one
response; a long generation on a busy backend legitimately takes minutes
and was timing out here while succeeding in the Python client.

Error types now distinguish malformed data from integrity failures.
Python raises ValueError for a bad package, a non-200 or unparseable
/pki/public_key, and plaintext that will not parse, reserving
SecurityError for crypto failures. This port wrapped nearly all of it in
SecurityError — so a server sending malformed JSON was reported as an
authentication failure, pointing debugging in exactly the wrong
direction. Malformed data is now a plain Error (the JS equivalent of
ValueError), carried past the deliberately opaque catch-all by a symbol
marker rather than a new exported class. Genuine crypto failures still
report a single vague message so they cannot serve as a decryption
oracle.

Also adds the missing guard Python has: decrypting without a private key
now says so, instead of failing later and being reported as an integrity
failure.

doc/attestation.md ports the Python attestation guide to the JS API, and
documents the two deliberate divergences: no verify_ssl escape hatch, and
jose injection instead of a runtime dynamic import.

Version 0.1.0 -> 0.3.0 to match the Python client's feature level, now
that the two are at parity.

Not ported: Python's warning when secure_memory=True but the SecureMemory
module is unavailable. There is no JS equivalent — zeroing is always
available, and the weaker case (mlock unavailable) is already reported
honestly by getProtectionInfo().

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 12:17:52 +02:00
00388ccb01
feat: lock sensitive buffers in memory; drop new Function from jose loading
All checks were successful
NYX Security Scan / nyx-scan (pull_request) Successful in 5m49s
Memory 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>
2026-07-19 11:34:18 +02:00
987acf8816
feat!: make RSA keys ephemeral by default
Some checks failed
NYX Security Scan / nyx-scan (pull_request) Failing after 5m51s
BREAKING 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>
2026-07-19 11:19:03 +02:00
84eae58317
fix: restore build and tests under TypeScript 6
Some checks failed
NYX Security Scan / nyx-scan (pull_request) Failing after 5m55s
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>
2026-07-19 10:59:28 +02:00
75867ef85a
feat: SGX attestation 2026-07-19 10:49:59 +02:00
43165f86f2
fix: base_url
doc: created
2026-04-16 16:44:26 +02:00
6e02559f4e
fix: imports and SecureByteContext in node 2026-04-16 15:40:41 +02:00
76b2a284d5
feat:
- added retry logic with exponential backoff
- per request base_url setting
- configurable key_dir
- protocol downgrade protection
- public secure memory API
2026-04-16 15:36:20 +02:00
d9d2ec98db fix:
Added DisposedError
Wrapped zeroMemory() in its own try/catch in finally
Generic error message; fixed ArrayBufferLike TypeScript type issue
Generic error message; password/salt/IV wrapped in SecureByteContext
Password ≥8 chars enforced; zeroKeys(); rotateKeys(); debug-gated logs; TS type fix
Zero source ArrayBuffer after req.write()
Added timeout, debug, keyRotationInterval, keyRotationDir, keyRotationPassword
dispose(), assertNotDisposed(), startKeyRotationTimer(), rotateKeys(); Promise-mutex on ensureKeys(); new URL() validation; CR/LF API key check; server error detail truncation; response schema validation; all console.log behind debugMode
Propagates new config fields; dispose()
Tests for dispose, timer, header injection, URL validation, error sanitization, debug flag
Tests for generic error messages, password validation, zeroKeys()
2026-04-01 14:28:05 +02:00
76703e2e3e fix: base_url port
feat: add types for reasoning_content and _metadata

fix: key format incompatibility
2026-04-01 13:38:45 +02:00
c7601b2270 fix:
- AES GCM protocol mismatch
- better, granular error handling
- UUID now uses crypto.randomUUID()
- added native mlock addon to improve security
- ZeroBuffer uses explicit_bzero now
- fixed imports

feat:
-  added unit tests
2026-03-04 11:30:44 +01:00
fd1a3b50cb feature: port from python client lib 2026-01-17 12:02:08 +01:00