Every call in this SDK went to Infisical routes -- /api/v3/secrets/raw,
/api/v1/folders, /api/v2/workspace, /api/v1/dynamic-secrets, /api/v1/kms/keys.
kms.hanzo.ai runs luxfi/kms, which has NEVER served any of them.
The break stayed invisible because older server builds embedded a console SPA
under a root catch-all that answered every unmatched path with 200 text/html:
a wrong URL came back as a JSON decode error deep in the caller, not as a 404,
so it read like a parsing bug rather than a nonexistent endpoint. Server 1.12.8
removed the catch-all. Those paths now return honest JSON 404s -- verified live,
{"message":"not found","path":"/api/v3/secrets/raw"} -- so the SDK is visibly
broken, and this is the fix.
The canonical surface is the WHOLE secrets API; there is nothing else:
POST /v1/kms/auth/login {clientId, clientSecret}
GET /v1/kms/orgs/{org}/secrets?path=&env= -> {"names": [...]}
GET /v1/kms/orgs/{org}/secrets/{path}/{name}?env= -> {"secret":{"value"}}
POST /v1/kms/orgs/{org}/secrets {path, name, env, value}
DELETE /v1/kms/orgs/{org}/secrets/{path}/{name}?env=
So the SDK is now shaped like the server instead of like the fork it came from:
- One upsert. createSecret + updateSecret collapse into put(): the server has a
single POST for both, and aiming two methods at one route would misdescribe
what the store does.
- No versions. get() throws on a `version` option. Silently serving the current
value for a versioned read hands the caller a different secret than it asked
for.
- No folders / projects / environments / dynamic-secrets / tags / PITR. Those
routes map to nothing, so the clients, types and schemas are deleted rather
than left pointing at 404s. AWS-IAM login and token-renew go with them,
taking the entire AWS SigV4 dependency stack; zod went with the dynamic-secret
schemas. axios is the only runtime dependency left.
- `org` is a new required input -- constructor `org`, else KMS_ORG, else
"hanzo". Every secret route is org-scoped and the bearer's IAM owner claim
must resolve to it.
- /v1/kms/keys/* is deliberately NOT bound. It exists, but it is MPC validator
key-set management (generate/list/get/sign/rotate) -- a different contract
from the Infisical KMS-keys surface this SDK wrapped. kms() is gone rather
than guessed at.
Escaping: the server unescapes the trailing path and splits it at its LAST
slash into (path, name), so each segment is escaped individually and joined
with literal separators; encodeURIComponent over the joined string collapses
"a/b/C" into one opaque %2F-laden segment. For the same reason a name
containing "/" and an empty path are refused at the boundary -- POST would
accept both (they travel in the JSON body) and store a secret that GET and
DELETE could never address again.
Tests: a node:test contract suite (no new deps) drives the SDK over a real
socket against a stub that mirrors the Go handlers -- last-slash split,
env-required write, JSON 404s. Two guards keep the old paths from coming back:
no recorded request URL may contain "/api/", and no file in lib/ may either.
Proven by mutation: restoring /api/v3/secrets/raw fails 12 of 14 tests, and
escaping the joined path string fails 4.
4.0.6 -> 4.0.7.
Co-authored-by: Hanzo Dev <[email protected]>
Hanzo KMS Node.js SDK
The official Node.js SDK for Hanzo KMS — the org-scoped secret store served by luxfi/kms.
Installation
npm install @hanzo/kms-sdk
Quick Start
import { HanzoKmsSDK } from "@hanzo/kms-sdk";
const kms = new HanzoKmsSDK({
siteUrl: "https://kms.hanzo.ai", // optional, this is the default
org: "hanzo" // optional, defaults to process.env.KMS_ORG, then "hanzo"
});
await kms.auth().login({
clientId: process.env.KMS_CLIENT_ID!,
clientSecret: process.env.KMS_CLIENT_SECRET!
});
const mnemonic = await kms.secrets().get({
path: "providers/hanzo",
name: "deploy-mnemonic",
env: "main"
});
The API
Every secret is keyed by (path, name, env) inside an organization. That is the
whole model — there are no projects, folders, tags, versions or point-in-time
recovery, because the server has no routes for them.
| SDK call | Route |
|---|---|
auth().login({clientId, clientSecret}) |
POST /v1/kms/auth/login |
secrets().list({path, env?}) |
GET /v1/kms/orgs/{org}/secrets?path=&env= |
secrets().get({path, name, env?}) |
GET /v1/kms/orgs/{org}/secrets/{path}/{name}?env= |
secrets().put({path, name, value, env?}) |
POST /v1/kms/orgs/{org}/secrets |
secrets().delete({path, name, env?}) |
DELETE /v1/kms/orgs/{org}/secrets/{path}/{name}?env= |
env defaults to "default". It is part of the storage key, never an alias:
the same name under main and test is two different secrets.
Authentication
login() exchanges a machine identity for an IAM bearer (OAuth2
client_credentials, brokered by KMS) and sets it on the client:
const { expiresIn } = await kms.auth().login({ clientId, clientSecret });
There is no renew route — log in again when the token expires. A bearer obtained elsewhere (a projected service token, say) can be used directly:
const kms = new HanzoKmsSDK({ accessToken: process.env.KMS_TOKEN });
// or, on an existing client:
kms.auth().accessToken(process.env.KMS_TOKEN!);
The bearer's IAM owner claim must resolve to the client's org, or the
server answers 401.
Secrets
// Create AND update — the server has one upsert, so the SDK has one method.
await kms.secrets().put({ path: "providers/hanzo", name: "API_KEY", env: "main", value: "..." });
// Read a value.
const apiKey = await kms.secrets().get({ path: "providers/hanzo", name: "API_KEY", env: "main" });
// List the names under a path.
const names = await kms.secrets().list({ path: "providers/hanzo", env: "main" });
// Delete.
await kms.secrets().delete({ path: "providers/hanzo", name: "API_KEY", env: "main" });
Paths are normalized to slash-joined segments ("/a/b/" and "a/b" are the
same path) and each segment is percent-escaped on its own, so the separators
reach the server intact — it splits the URL at the LAST slash to recover
(path, name). For the same reason a name may not contain /: the SDK throws
rather than write a secret that could never be read back.
Errors
Both error types are exported. HanzoKmsSDKRequestError carries the server's
message plus statusCode; HanzoKmsSDKError covers everything the SDK refuses
locally (missing path, slash in a name, a version that cannot be honoured).
import { HanzoKmsSDKRequestError } from "@hanzo/kms-sdk";
try {
await kms.secrets().get({ path: "providers/hanzo", name: "MISSING" });
} catch (err) {
if (err instanceof HanzoKmsSDKRequestError && err.statusCode === 404) { /* ... */ }
}
Environment Variables
| Variable | Description |
|---|---|
KMS_ORG |
Organization used when org is not passed to the constructor. Defaults to hanzo. |
Development
npm install
npm test # builds, then runs the contract tests against a stub luxfi/kms
License
BSD-3-Clause — Hanzo AI Inc. See LICENSE and NOTICE.