Node.js SDK

Read secrets from any Node.js or TypeScript application using Ed25519 machine authentication, on persistent hosts or serverless and ephemeral environments.

Installation

npm install @sikkerkey/sdk

Requires Node.js 18+. Zero runtime dependencies — uses only Node.js built-in modules.

Quick Start

import { SikkerKey } from '@sikkerkey/sdk'

const sk = SikkerKey.create('vault_abc123')
const secret = await sk.getSecret('sk_a1b2c3d4e5')

The SDK loads the machine identity from ~/.sikkerkey/vaults/{vaultId}/identity.json, signs every request with the machine's Ed25519 private key, and returns the decrypted value. All data methods are async and return Promises.

Client Creation

// Explicit vault ID
const sk = SikkerKey.create('vault_abc123')

// Direct path to identity file
const sk = SikkerKey.create('/etc/sikkerkey/vaults/vault_abc123/identity.json')

// Auto-detect from SIKKERKEY_IDENTITY env or single vault on disk
const sk = SikkerKey.create()

create() is synchronous — it reads identity and key files from disk. Throws ConfigurationError if the identity is missing, the key can't be loaded, or multiple vaults exist without a specified vault ID.

Serverless (Memory-Only Bootstrap)

On a long-lived host the SDK loads a persistent identity from disk. Serverless and other ephemeral or read-only-filesystem environments (Vercel, Netlify, AWS Lambda, Google Cloud Run, Fly.io, and similar) have no identity to persist. SikkerKey.bootstrap() handles that case: it generates an Ed25519 keypair in memory, enrolls a short-lived ephemeral machine with an enrollment token, and reads secrets, all without writing anything to disk.

import { SikkerKey } from '@sikkerkey/sdk'

const sk = SikkerKey.bootstrap(
  process.env.SIKKERKEY_VAULT_ID,
  process.env.SIKKERKEY_ENROLLMENT_TOKEN,
).inMemory()

const dbUrl = await sk.getSecret('sk_db_prod')

Create an enrollment token in the dashboard and supply its plaintext plus your vault ID. The token only registers an ephemeral machine scoped to the policy you set (projects, secrets, lifetime); it cannot read secrets on its own.

How It Works

  • bootstrap(vaultId, token, options?).inMemory() returns immediately and does no network work.
  • The first read enrolls lazily: it generates a keypair in memory, registers an ephemeral machine, and signs every request with the in-memory private key. Concurrent first reads share a single enrollment.
  • The identity is reused while the instance stays warm, and re-enrolled automatically shortly before the machine's lifetime expires.
  • Nothing is written to disk. The private key lives only in process memory and is gone when the instance is recycled.

Enrollment errors (bad token, sealed vault, IP not allowed) surface on the first read. Call await sk.ready() to enroll eagerly at startup instead.

Options

OptionDefaultDescription
hostname$HOSTNAME, then serverlessLabel recorded on the machine. Must match the token's hostname pattern if one is set
namenoneOptional machine name to request. Overridden when the enrollment token defines a name pattern (the server generates the name from it)
renewSkewMs60000Re-enroll this many milliseconds before the machine lifetime expires

The returned client exposes the same read methods as a disk-based client (getSecret, getFields, getField, listSecrets, listSecretsByProject, export), plus ready() (force enrollment and return the underlying client) and close().

Provisioning the Token for Serverless

When you create the enrollment token for a serverless deployment:

  • Set a short machine lifetime (minutes). Each cold start mints a fresh ephemeral machine, and short-lived ones free their slot quickly as they expire.
  • Set max-uses high enough for your cold-start and concurrency volume.
  • Leave the source-CIDR restriction unset, since serverless egress IPs are dynamic.
  • If the vault has an IP allowlist, make sure it permits the platform's egress or leave it off. Enrollment enforces the allowlist.
  • Set a name pattern on the token (for example vercel-{uuid8}) so each cold-start machine gets a clean, unique name in the dashboard instead of all sharing one hostname. A name pattern takes precedence over any name the SDK sends. {uuidN} inserts N random characters (4 to 32, default 8); {uuid} inserts 8.

Each live ephemeral machine counts against your plan's machine limit until it expires and is cleaned up, so size your plan for your expected concurrency.

Requires a Node.js runtime with outbound HTTPS. Edge runtimes, which lack Node's crypto and fs, are not supported yet.

Reading Secrets

Single Value

const apiKey = await sk.getSecret('sk_stripe_prod')

Structured (Multiple Fields)

const db = await sk.getFields('sk_db_prod')
const host = db.host       // "db.example.com"
const user = db.username   // "admin"
const pass = db.password   // "hunter2"

Throws SecretStructureError if the secret value is not a JSON object.

Single Field

const password = await sk.getField('sk_db_prod', 'password')

Throws FieldNotFoundError if the field doesn't exist. The error message includes available field names.

Listing Secrets

// All secrets this machine can access
const secrets = await sk.listSecrets()
for (const s of secrets) {
    console.log(`${s.id}: ${s.name}`)
}

// Secrets in a specific project
const projectSecrets = await sk.listSecretsByProject('proj_abc123')

Returns SecretListItem[] with id, name, fieldNames (string | null), and projectId (string | null).

Export

Export all accessible secrets as a flat key-value map in a single round trip:

const env = await sk.export()
// { API_KEY: "sk-live-...", DB_CREDS_HOST: "db.example.com", DB_CREDS_PASSWORD: "s3cret" }

Structured secrets are flattened: SECRET_NAME_FIELD_NAME. Secret names are converted to uppercase env format.

Scope to a Project

const env = await sk.export('proj_abc123')

Inject into Environment

const env = await sk.export()
Object.assign(process.env, env)

Watching for Changes

Watch secrets for real-time updates. When a secret is rotated, updated, or deleted, the callback fires with the new value.

sk.watch('sk_db_credentials', (event) => {
  if (event.status === 'changed') {
    // event.value has the new value
    // event.fields has parsed key-value pairs for structured secrets
    db.reconfigure({
      username: event.fields!.username,
      password: event.fields!.password,
    })
  } else if (event.status === 'deleted') {
    console.log('Secret deleted')
  } else if (event.status === 'access_denied') {
    console.log('Access revoked')
  } else if (event.status === 'error') {
    console.error(`Error: ${event.error}`)
  }
})

Polling starts automatically on the first watch() call using setInterval. Default interval is 15 seconds (server enforces a 10-second minimum).

sk.setPollInterval(30) // seconds
sk.unwatch('sk_db_credentials') // stop watching one secret
sk.close() // stop all watches

Multi-Vault

const prod = SikkerKey.create('vault_a1b2c3d4e5f6g7h8')
const staging = SikkerKey.create('vault_x9y8z7w6v5u4t3s2')

const prodDb = await prod.getSecret('sk_db_prod')
const stagingDb = await staging.getSecret('sk_db_staging')

List Registered Vaults

const vaults = SikkerKey.listVaults()
// ["vault_a1b2c3d4e5f6g7h8", "vault_x9y8z7w6v5u4t3s2"]

Static method, synchronous (reads the filesystem).

Identity Resolution

  1. Explicit path — starts with / or contains identity.json
  2. Vault ID — looks up ~/.sikkerkey/vaults/{vaultId}/identity.json
  3. SIKKERKEY_IDENTITY env — path to identity file
  4. Auto-detect — single vault on disk

The vault_ prefix is added automatically if not present. Override the base directory with SIKKERKEY_HOME.

Error Handling

The SDK uses typed exceptions. All extend SikkerKeyError:

import { SikkerKey, NotFoundError, AccessDeniedError, AuthenticationError } from '@sikkerkey/sdk'

try {
    const secret = await sk.getSecret('sk_nonexistent')
} catch (e) {
    if (e instanceof NotFoundError) {
        // 404 — secret doesn't exist
    } else if (e instanceof AccessDeniedError) {
        // 403 — machine not approved or no grant
    } else if (e instanceof AuthenticationError) {
        // 401 — invalid signature or unknown machine
    }
}

Exception Hierarchy

SikkerKeyError (extends Error)
├── ConfigurationError      — identity/key issues
├── SecretStructureError    — secret is not a JSON object
├── FieldNotFoundError      — field not in structured secret
└── ApiError                — HTTP error (has httpStatus: number)
    ├── AuthenticationError — 401
    ├── AccessDeniedError   — 403
    ├── NotFoundError       — 404
    ├── ConflictError       — 409
    ├── RateLimitedError    — 429
    └── ServerSealedError   — 503

Properties

PropertyTypeDescription
machineIdstringMachine UUID
machineNamestringHostname from bootstrap
vaultIdstringVault this identity belongs to
apiUrlstringSikkerKey API URL

All properties are read-only getters.

Method Reference

MethodReturnsDescription
SikkerKey.create(vaultOrPath?)SikkerKeyCreate client from disk identity (static, sync)
SikkerKey.bootstrap(vaultId, token, options?)SikkerKeyBootstrapMemory-only serverless bootstrap (static); call .inMemory()
SikkerKey.listVaults()string[]List registered vault IDs (static, sync)
getSecret(secretId)Promise<string>Read a secret value
getFields(secretId)Promise<Record<string, string>>Read structured secret
getField(secretId, field)Promise<string>Read single field
listSecrets()Promise<SecretListItem[]>List all accessible secrets
listSecretsByProject(projectId)Promise<SecretListItem[]>List secrets in a project
export(projectId?)Promise<Record<string, string>>Export as env map
watch(secretId, callback)voidWatch a secret for changes
unwatch(secretId)voidStop watching a secret
setPollInterval(seconds)voidSet poll interval (min 10s)
close()voidStop all watches, shut down polling

Exported Types

interface SecretListItem {
    id: string
    name: string
    fieldNames: string | null
    projectId: string | null
}

interface BootstrapOptions {
    hostname?: string
    name?: string
    renewSkewMs?: number
}

Retry Behavior

429 and 503 responses are retried up to 3 times with exponential backoff (1s, 2s, 4s). Each retry uses a fresh timestamp and nonce. Network errors are also retried.

Environment Variables

VariableDescription
SIKKERKEY_IDENTITYPath to identity.json — overrides vault lookup
SIKKERKEY_HOMEBase config directory (default: ~/.sikkerkey)

Dependencies

None at runtime. Node.js built-ins only: crypto, fs, path, http, https.

TypeScript types (@types/node) are a dev dependency only. Requires Node.js 18+.

All HTTP requests have a 15-second timeout. HTTPS is enforced for all non-localhost connections.