@siax/secret-resolver (0.1.0)
Installation
@siax:registry=https://git.cloud.siax.io/api/packages/sax3l/npm/npm install @siax/secret-resolver@0.1.0"@siax/secret-resolver": "0.1.0"About this package
@siax/secret-resolver
P002 (Security / Secret / Provenance Incident Closure, Master Execution
Contract): resolveSecretReference() / resolveSecretReferences() /
resolveSecretReferencesToEnv() — takes a siax.secret-reference record
and resolves it through Infisical (the self-hosted secret engine at
vault.siax.io) at deploy/boot time. Zero external dependencies —
@siax/schemas is a workspace-internal link only. Node ESM (.mjs),
Node ≥ 20 — same convention as @siax/schemas, @siax/write-guard and
@siax/doctor.
Why this package exists
P002 recon (2026-09-02) asked two direct questions and got two honest "no" answers:
- Is
siax.secret-referenceactually used anywhere, or a stub? A stub. Zero runtime consumers anywhere in the estate — the schema existed, was well-formed, and was imported by nothing outside its own test file. - Does any code let apps request a secret by reference and resolve it
at deploy/boot? No real one existed. Two competing, non-production
conventions were found instead:
siax-integrations/packages/core/src/secret-ref.ts— its own header comment: "secret_refs were structurally-validated strings that were never actually resolved anywhere." The only implementation is explicitly labeled "DEV-ONLY resolver... NOT the production secret backend".siax-integrations/packages/infisical-adapter/src/adapter.ts— an explicit "P0 skeleton". NoresolveSecretmethod exists at all;getHealth()/testConnection()always return"unknown"/stub results;ADAPTER.mdstateslifecycle_status: "prototype".
This package is the first thing in the estate that actually performs the
resolve step: SecretReference in -> real Infisical HTTP call -> value out,
with the value never touching disk, git, or a log line.
Layout
packages/secret-resolver/
src/
infisical-client.mjs # zero-dep Infisical REST client (Universal Auth login + raw-secret GET)
resolve-secret.mjs # resolveSecretReference() / resolveSecretReferences() -- validates + resolves
boot.mjs # resolveSecretReferencesToEnv() -- the actual boot-time entrypoint, the ONLY function that mutates anything
index.mjs
test/
infisical-client.test.mjs
resolve-secret.test.mjs
boot.test.mjs
Core API
import { resolveSecretReference, resolveSecretReferencesToEnv } from '@siax/secret-resolver';
// One secret:
const { key, value } = await resolveSecretReference(
{ id: 'r1', repo: 'st0re', key: 'ST0RE_API_TOKEN', environment: 'prod', provider: 'infisical' },
{ projectId: process.env.SECRET_RESOLVER_INFISICAL_PROJECT_ID }, // or set the env var and omit this
);
// At boot, for a whole app:
await resolveSecretReferencesToEnv(myApp.requiredSecretReferences, {
projectId: '...',
}); // process.env.ST0RE_API_TOKEN etc. are now set, or the whole call rejected -- never a partial boot.
Every function is fail-closed: an invalid SecretReference (fails
@siax/schemas' validateSecretReference), a missing projectId, or any
Infisical auth/network/not-found failure throws a typed error. Nothing in
this package returns undefined on failure — see each module's own
"fail-closed" test cases.
Configuration (env vars)
| Env var | Purpose |
|---|---|
SECRET_RESOLVER_INFISICAL_API_URL |
Infisical base URL. Defaults to https://vault.siax.io. |
SECRET_RESOLVER_INFISICAL_CLIENT_ID / SECRET_RESOLVER_INFISICAL_CLIENT_SECRET |
Universal Auth machine-identity credential pair. Two env vars, not one — see "Why two env vars" below. |
SECRET_RESOLVER_INFISICAL_PROJECT_ID |
Infisical project/workspace ID. See "Known gap" below for why this can't come from the SecretReference itself. |
Why two env vars, not one pre-minted token
A single long-lived static bearer token in an env var is exactly the
pattern P002 recon flagged repeatedly as estate-wide architecture debt
(the aud0 DEPLOY.md static-token finding, the shared *_AUTH_TOKEN
convention across st0re/api0/m0b). Universal Auth's client-id +
client-secret exchange for a short-lived access token (cached in-memory,
never persisted) is the smaller blast radius of the two options actually
available today, without introducing a new architecture this pass wasn't
scoped to design.
Known gap: siax.secret-reference has no projectId field
P002 recon confirmed the schema's real, live shape is exactly
{id, repo, key, environment, provider, rotationPolicy?, lastRotatedAt?}
— not the {provider, path, key, version, tenantScope, projectScope, rotationPolicy} shape an earlier task framing implied. There is no
path, version, tenantScope, or projectScope field, and critically
no Infisical project/workspace ID field. Infisical's real API needs
one to resolve anything.
This package does not invent a guess (e.g. "derive projectId from
repo") — a wrong guess here would silently resolve the wrong secret,
strictly worse than refusing to resolve at all. projectId must be
supplied out-of-band, via options.projectId or the
SECRET_RESOLVER_INFISICAL_PROJECT_ID env var — see
MissingProjectIdError.
Not decided in this pass (flagged, not silently assumed): whether
siax.secret-reference should grow a projectId field (a P003 schema
change, estate-wide compatibility implications, out of this package's
scope) or whether a repo -> projectId lookup table belongs in this
package instead. A follow-up pass needs Simon's steer on which.
repo currently maps to an Infisical secret path (/${repo}) by
default — matching the --path=/coolify-style convention already live in
the siax-secrets operator CLI wrapper (see
../../standard/execution-reports/P002-SECURITY-SECRET-PROVENANCE-CLOSURE.md
§1). Override via options.secretPathForReference for a different layout.
Why Infisical, not OpenBao
P002 recon confirmed Infisical is the currently-selected self-hosted
secret engine — it's what the siax-secrets operator wrapper talks to,
what siax-integrations/packages/infisical-adapter (however skeletal) is
already scaffolded around, and what siax-integrations/CLAUDE.md's own
agent rule names explicitly ("NEVER hardcode secrets — use Infisical").
Introducing OpenBao instead would be a materially bigger, riskier
architecture change than this task's scope ("closing the resolver gap for
the already-selected engine") calls for — not attempted here.
Honest status — what is real vs. not yet verified
Same convention as packages/write-guard/src/aud0-client.mjs's "HONEST
STATUS" note: whether https://vault.siax.io is reachable from wherever a
given app actually boots, and whether its live Infisical version matches
the REST shape this client assumes (POST /api/v1/auth/universal-auth/login, GET /api/v3/secrets/raw/{name}), was
not verified from this repo/pass. Every test in test/*.test.mjs runs
against an injected fake fetch — zero real network calls, and this
pass used no live Infisical credentials (none were available, none were
requested from a live system). A follow-up pass must run one real
login + get-secret round trip against the actual deployed instance, with a
real, scoped-down machine identity requested from Simon, before this is
trusted in a real boot path.
What this package deliberately does NOT do
- Does not shell out to the
siax-secretsCLI wrapper. That wrapper (hardened in an earlier P002 pass, see the execution report referenced above) is an operator safety net for humans running ad-hoc commands. This package is the machine-to-machine boot-time path — a different layer, talking to Infisical's HTTP API directly. - Does not mint, rotate, or discover credentials. The machine identity it authenticates with must already exist and be supplied by the caller. Provisioning that identity is out of this package's scope.
- Does not log a resolved value or an access token anywhere, including
in thrown errors — every error class in
infisical-client.mjscarries status codes, URLs, and key names only.
Dependencies
Dependencies
| ID | Version |
|---|---|
| @siax/schemas | workspace:* |