Files
zeekayandHanzo Dev a1e811c5c3 rip: migrate secret surface off Infisical paths onto canonical /v1/kms
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]>
2026-07-26 08:30:23 -07:00

319 lines
13 KiB
JavaScript

"use strict";
// Contract tests against a stub that answers exactly like luxfi/kms v1.12.9:
// same routes, same envelopes, same last-slash split of the trailing path, same
// "env is required on write" rule. The SDK is driven over a real socket, so the
// URLs asserted here are the URLs axios puts on the wire.
//
// Run against the build output (npm test builds first) so the published artifact
// is what gets exercised.
const { test, before, after } = require("node:test");
const assert = require("node:assert/strict");
const http = require("node:http");
const fs = require("node:fs");
const path = require("node:path");
const { HanzoKmsSDK, HanzoKmsSDKError, HanzoKmsSDKRequestError } = require("../lib/index.js");
const TOKEN = "test-iam-bearer";
const CLIENT_ID = "hanzo-kms";
const CLIENT_SECRET = "s3cr3t";
/** Every request the stub received, across every test. */
const seen = [];
// Stored keys are NUL-joined so paths and names containing spaces or
// slashes stay distinguishable.
const SEP = "\u0000";
const key = (org, p, env, name) => [org, p, env, name].join(SEP);
const json = (res, status, body) => {
const payload = JSON.stringify(body);
res.writeHead(status, { "content-type": "application/json" });
res.end(payload);
};
/**
* Stub luxfi/kms. Mirrors cmd/kms/main.go:
* POST /v1/kms/auth/login
* GET /v1/kms/orgs/{org}/secrets?path=&env=
* GET /v1/kms/orgs/{org}/secrets/{rest...} rest split at its LAST slash
* POST /v1/kms/orgs/{org}/secrets env required, no default
* DELETE /v1/kms/orgs/{org}/secrets/{rest...}
* Everything else is an honest JSON 404 — the SPA catch-all that used to answer
* 200 text/html for unmatched paths is gone as of server 1.12.8.
*/
const store = new Map();
const server = http.createServer((req, res) => {
let raw = "";
req.on("data", (chunk) => (raw += chunk));
req.on("end", () => {
const url = new URL(req.url, "http://kms.test");
const body = raw ? JSON.parse(raw) : null;
seen.push({ method: req.method, url: req.url, auth: req.headers.authorization, body });
// Go's r.URL.Path is fully unescaped before routing, and PathValue("rest")
// hands the handler that unescaped remainder.
const pathname = decodeURIComponent(url.pathname);
if (req.method === "POST" && pathname === "/v1/kms/auth/login") {
if (body?.clientId !== CLIENT_ID || body?.clientSecret !== CLIENT_SECRET) {
return json(res, 401, { statusCode: 401, message: "invalid credentials" });
}
return json(res, 200, { accessToken: TOKEN, expiresIn: 86400, tokenType: "Bearer" });
}
const match = pathname.match(/^\/v1\/kms\/orgs\/([^/]+)\/secrets(?:\/(.*))?$/);
if (!match) {
return json(res, 404, { message: "not found", path: url.pathname });
}
if (req.headers.authorization !== `Bearer ${TOKEN}`) {
return json(res, 401, { statusCode: 401, message: "missing bearer token" });
}
const org = match[1];
const rest = match[2];
const env = url.searchParams.get("env") || "default";
if (rest === undefined) {
if (req.method === "GET") {
const prefix = key(org, url.searchParams.get("path") || "", env, "");
const names = [...store.keys()].filter((k) => k.startsWith(prefix)).map((k) => k.split(SEP)[3]);
return json(res, 200, { names });
}
if (req.method === "POST") {
if (!body?.name) return json(res, 400, { message: "name and value required" });
if (!body?.env?.trim()) return json(res, 400, { message: "env is required" });
store.set(key(org, body.path, body.env, body.name), body.value);
return json(res, 201, { ok: true });
}
return json(res, 405, { message: "method not allowed" });
}
const idx = rest.lastIndexOf("/");
if (idx < 0) {
return json(res, 400, { message: "path and name required" });
}
const k = key(org, rest.slice(0, idx), env, rest.slice(idx + 1));
if (req.method === "GET") {
if (!store.has(k)) return json(res, 404, { message: "not found" });
return json(res, 200, { secret: { value: store.get(k) } });
}
if (req.method === "DELETE") {
if (!store.delete(k)) return json(res, 404, { message: "not found" });
return json(res, 200, { ok: true });
}
return json(res, 405, { message: "method not allowed" });
});
});
let siteUrl;
before(async () => {
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
siteUrl = `http://127.0.0.1:${server.address().port}`;
});
after(() => server.close());
const sdk = (options) => new HanzoKmsSDK({ siteUrl, accessToken: TOKEN, ...options });
test("login posts credentials to /v1/kms/auth/login and sets the bearer", async () => {
const kms = new HanzoKmsSDK({ siteUrl, org: "hanzo" });
assert.equal(kms.auth().getAccessToken(), null);
const res = await kms.auth().login({ clientId: CLIENT_ID, clientSecret: CLIENT_SECRET });
assert.equal(res.accessToken, TOKEN);
assert.equal(res.expiresIn, 86400);
assert.equal(kms.auth().getAccessToken(), TOKEN);
const login = seen.at(-1);
assert.equal(login.method, "POST");
assert.equal(login.url, "/v1/kms/auth/login");
assert.deepEqual(login.body, { clientId: CLIENT_ID, clientSecret: CLIENT_SECRET });
assert.equal(login.auth, undefined, "login must not carry a bearer");
// The bearer from login is what the secret routes then see.
await kms.secrets().put({ path: "providers/hanzo", name: "AFTER_LOGIN", env: "main", value: "v" });
assert.equal(seen.at(-1).auth, `Bearer ${TOKEN}`);
});
test("bad credentials surface the server's message, not a decode error", async () => {
const kms = new HanzoKmsSDK({ siteUrl, org: "hanzo" });
await assert.rejects(() => kms.auth().login({ clientId: CLIENT_ID, clientSecret: "wrong" }), (err) => {
assert.ok(err instanceof HanzoKmsSDKRequestError);
assert.equal(err.statusCode, 401);
assert.match(err.message, /invalid credentials/);
return true;
});
});
test("put is the one upsert: create and update hit the same POST", async () => {
const kms = sdk({ org: "hanzo" });
const secret = { path: "providers/hanzo", name: "DEPLOY_MNEMONIC", env: "main" };
await kms.secrets().put({ ...secret, value: "first" });
const created = seen.at(-1);
assert.equal(created.method, "POST");
assert.equal(created.url, "/v1/kms/orgs/hanzo/secrets");
assert.deepEqual(created.body, { path: "providers/hanzo", name: "DEPLOY_MNEMONIC", env: "main", value: "first" });
assert.equal(await kms.secrets().get(secret), "first");
await kms.secrets().put({ ...secret, value: "second" });
assert.equal(seen.at(-1).method, "POST", "update is the same POST, not a PATCH to a versioned path");
assert.equal(await kms.secrets().get(secret), "second");
});
test("get reads /v1/kms/orgs/{org}/secrets/{path}/{name}", async () => {
const kms = sdk({ org: "hanzo" });
await kms.secrets().put({ path: "providers/hanzo", name: "API_KEY", env: "main", value: "k" });
assert.equal(await kms.secrets().get({ path: "providers/hanzo", name: "API_KEY", env: "main" }), "k");
assert.equal(seen.at(-1).url, "/v1/kms/orgs/hanzo/secrets/providers/hanzo/API_KEY?env=main");
});
test("each path segment is escaped individually, separators stay literal", async () => {
const kms = sdk({ org: "hanzo" });
const secret = { path: "providers/hanzo team/a+b", name: "K#1", env: "main" };
await kms.secrets().put({ ...secret, value: "escaped" });
// The value round-trips only if the server's last-slash split recovers the
// exact (path, name) the POST body carried.
assert.equal(await kms.secrets().get(secret), "escaped");
const url = seen.at(-1).url;
assert.equal(url, "/v1/kms/orgs/hanzo/secrets/providers/hanzo%20team/a%2Bb/K%231?env=main");
assert.ok(!url.includes("%2F"), "a %2F means the separators were escaped and the server reads one long name");
});
test("list returns the names under a path", async () => {
const kms = sdk({ org: "hanzo" });
await kms.secrets().put({ path: "listing", name: "ONE", env: "main", value: "1" });
await kms.secrets().put({ path: "listing", name: "TWO", env: "main", value: "2" });
await kms.secrets().put({ path: "listing", name: "OTHER_ENV", env: "test", value: "3" });
const names = await kms.secrets().list({ path: "listing", env: "main" });
assert.deepEqual(names.sort(), ["ONE", "TWO"]);
assert.equal(seen.at(-1).url, "/v1/kms/orgs/hanzo/secrets?path=listing&env=main");
});
test("delete removes the secret; reading it back is a 404", async () => {
const kms = sdk({ org: "hanzo" });
const secret = { path: "providers/hanzo", name: "TEMP", env: "main" };
await kms.secrets().put({ ...secret, value: "x" });
await kms.secrets().delete(secret);
assert.equal(seen.at(-1).method, "DELETE");
assert.equal(seen.at(-1).url, "/v1/kms/orgs/hanzo/secrets/providers/hanzo/TEMP?env=main");
await assert.rejects(() => kms.secrets().get(secret), (err) => {
assert.ok(err instanceof HanzoKmsSDKRequestError);
assert.equal(err.statusCode, 404);
return true;
});
});
test("env defaults to \"default\" and is never aliased", async () => {
const kms = sdk({ org: "hanzo" });
await kms.secrets().put({ path: "envs", name: "K", value: "no-env-given" });
assert.equal(seen.at(-1).body.env, "default", "the server rejects a write with no env, so the SDK must send one");
assert.equal(await kms.secrets().get({ path: "envs", name: "K" }), "no-env-given");
assert.equal(seen.at(-1).url, "/v1/kms/orgs/hanzo/secrets/envs/K?env=default");
// env is part of the key: the same name in another env is a different secret.
await assert.rejects(() => kms.secrets().get({ path: "envs", name: "K", env: "main" }), HanzoKmsSDKRequestError);
});
test("org comes from config, then KMS_ORG, then \"hanzo\"", async () => {
await sdk({ org: "zoo" }).secrets().list({ path: "listing" });
assert.match(seen.at(-1).url, /^\/v1\/kms\/orgs\/zoo\/secrets\?/);
const previous = process.env.KMS_ORG;
process.env.KMS_ORG = "pars";
try {
await sdk().secrets().list({ path: "listing" });
assert.match(seen.at(-1).url, /^\/v1\/kms\/orgs\/pars\/secrets\?/);
} finally {
if (previous === undefined) delete process.env.KMS_ORG;
else process.env.KMS_ORG = previous;
}
await sdk().secrets().list({ path: "listing" });
assert.match(seen.at(-1).url, /^\/v1\/kms\/orgs\/hanzo\/secrets\?/);
});
test("a versioned read throws instead of silently returning the current value", async () => {
const kms = sdk({ org: "hanzo" });
await kms.secrets().put({ path: "providers/hanzo", name: "VERSIONED", env: "main", value: "current" });
const before = seen.length;
await assert.rejects(
() => kms.secrets().get({ path: "providers/hanzo", name: "VERSIONED", env: "main", version: 1 }),
(err) => {
assert.ok(err instanceof HanzoKmsSDKError);
assert.match(err.message, /versions are not supported/);
return true;
}
);
assert.equal(seen.length, before, "no request may be issued for a versioned read");
});
test("a name containing \"/\" is refused before it can be written", async () => {
const kms = sdk({ org: "hanzo" });
const before = seen.length;
for (const call of [
() => kms.secrets().put({ path: "providers/hanzo", name: "a/b", env: "main", value: "v" }),
() => kms.secrets().get({ path: "providers/hanzo", name: "a/b", env: "main" }),
() => kms.secrets().delete({ path: "providers/hanzo", name: "a/b", env: "main" })
]) {
await assert.rejects(call, (err) => {
assert.ok(err instanceof HanzoKmsSDKError);
assert.match(err.message, /unreadable once written/);
return true;
});
}
assert.equal(seen.length, before);
});
test("an empty path is refused; a slash-wrapped one is normalized", async () => {
const kms = sdk({ org: "hanzo" });
for (const p of ["", "/", "///"]) {
await assert.rejects(() => kms.secrets().get({ path: p, name: "K" }), (err) => {
assert.ok(err instanceof HanzoKmsSDKError);
assert.match(err.message, /path is required/);
return true;
});
}
// "/providers/hanzo/" and "providers/hanzo" must resolve to one key —
// the server interpolates the path into its storage key verbatim.
await kms.secrets().put({ path: "/normalized/path/", name: "K", env: "main", value: "v" });
assert.equal(seen.at(-1).body.path, "normalized/path");
assert.equal(await kms.secrets().get({ path: "normalized/path", name: "K", env: "main" }), "v");
});
test("no request URL contains /api/ — Infisical paths are gone", () => {
assert.ok(seen.length >= 20, `expected the suite to have issued requests, saw ${seen.length}`);
const offenders = seen.filter((req) => req.url.includes("/api/"));
assert.deepEqual(offenders, [], "luxfi/kms has never served /api/ paths; it answers them with a JSON 404");
});
test("the published bundle contains no /api/ path", () => {
const lib = path.join(__dirname, "..", "lib");
const bundles = fs.readdirSync(lib).filter((f) => f.endsWith(".js") || f.endsWith(".mjs"));
assert.ok(bundles.length > 0, "build output missing");
for (const file of bundles) {
const source = fs.readFileSync(path.join(lib, file), "utf8");
assert.ok(!source.includes("/api/"), `${file} still references an /api/ path`);
}
});