npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@sikkerkey/sdk

v1.3.0

Published

SikkerKey Node.js SDK — read secrets with machine authentication

Readme

SikkerKey Node.js SDK

License: MIT npm Node.js TypeScript

The official Node.js SDK for SikkerKey. Read-only access to secrets using Ed25519 machine authentication. Zero external dependencies - Node.js built-in crypto, fs, http, https only. Runs on persistent hosts (identity on disk) and on serverless or ephemeral environments (memory-only bootstrap).

Installation

npm install @sikkerkey/sdk

Requires Node.js 18+.

Quick Start

import { SikkerKey } from '@sikkerkey/sdk'

const sk = SikkerKey.create('vault_abc123')
const apiKey = await sk.getSecret('sk_stripe_key')

The SDK reads the machine identity from ~/.sikkerkey/vaults/<vault-id>/identity.json, signs every request with the machine's Ed25519 private key, and returns the decrypted value.

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: uses SIKKERKEY_IDENTITY env, or finds the single vault on disk
const sk = SikkerKey.create()

Auto-detection throws ConfigurationError if multiple vaults are registered and no vault is specified.

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

| Option | Default | Description | |--------|---------|-------------| | hostname | $HOSTNAME, then serverless | Label recorded on the machine. Must match the token's hostname pattern if one is set | | name | none | Optional machine name to request. Overridden when the enrollment token defines a name pattern (the server generates the name from it) | | renewSkewMs | 60000 | Re-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 fields = await sk.getFields('sk_db_prod')
const host = fields.host       // "db.example.com"
const password = fields.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 the 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_production')

Each SecretListItem has id, name, fieldNames (null for single-value), and projectId.

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" }

// Scoped to a project
const env = await sk.export('proj_production')

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

Watching for Changes

Watch secrets for real-time updates. When a secret is rotated, updated, or deleted, the callback fires with the new value. Polling uses setInterval on Node's event loop - your application is never blocked.

sk.watch('sk_db_password', (event) => {
  switch (event.status) {
    case 'changed':
      console.log(`New value: ${event.value}`)
      // Structured secrets include parsed fields
      console.log(`Fields:`, event.fields)
      break
    case 'deleted':
      console.log('Secret was deleted')
      break
    case 'access_denied':
      console.log('Access revoked')
      break
    case 'error':
      console.log(`Error: ${event.error}`)
      break
  }
})

Practical Example

// Auto-rotate database credentials
sk.watch('sk_db_credentials', (event) => {
  if (event.status === 'changed') {
    db.reconfigure({
      username: event.fields!.username,
      password: event.fields!.password,
    })
  }
})

Poll Interval

The default poll interval is 15 seconds. The server enforces a minimum of 10 seconds.

sk.setPollInterval(30) // seconds

Stop Watching

// Stop watching a specific secret
sk.unwatch('sk_db_password')

// Stop all watches and shut down polling
sk.close()

Multi-Vault

const prod = SikkerKey.create('vault_a1b2c3')
const staging = SikkerKey.create('vault_x9y8z7')

const prodKey = await prod.getSecret('sk_api_key')
const stagingKey = await staging.getSecret('sk_api_key')

List Registered Vaults

const vaults = SikkerKey.listVaults()
// ["vault_a1b2c3", "vault_x9y8z7"]

Machine Info

sk.machineId    // "550e8400-e29b-41d4-a716-446655440000"
sk.machineName  // "api-server-1"
sk.vaultId      // "vault_abc123"
sk.apiUrl       // "https://api.sikkerkey.com"

Error Handling

The SDK uses typed exceptions for every error case:

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

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

Exception Hierarchy

SikkerKeyError
├── ConfigurationError      — identity file missing, bad key, invalid config
├── SecretStructureError    — secret is not a JSON object (getFields/getField)
├── FieldNotFoundError      — field not in structured secret
└── ApiError                — HTTP error from the API
    ├── AuthenticationError — 401
    ├── AccessDeniedError   — 403
    ├── NotFoundError       — 404
    ├── ConflictError       — 409
    ├── RateLimitedError    — 429
    └── ServerSealedError   — 503

ApiError has a httpStatus property with the HTTP status code.

Identity Resolution

The SDK resolves the identity file in this order:

  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.

Environment Variables

| Variable | Description | |----------|-------------| | SIKKERKEY_IDENTITY | Path to identity.json — overrides vault lookup | | SIKKERKEY_HOME | Base config directory (default: ~/.sikkerkey) | | SIKKERKEY_API_URL | Override the API base URL. Local development only (default: https://api.sikkerkey.com) |

Retry Behavior

429 (rate limited) and 503 (server sealed) 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.

Authentication

Every request includes Ed25519-signed headers:

  • X-Machine-Id — machine UUID
  • X-Timestamp — Unix timestamp
  • X-Nonce — random base64 nonce (replay protection)
  • X-Signature — signature of method:path:timestamp:nonce:bodyHash

HTTPS is enforced for all non-localhost connections. 15-second request timeout.

Method Reference

| Method | Returns | Description | |--------|---------|-------------| | SikkerKey.create(vaultOrPath?) | SikkerKey | Create client from disk identity (sync) | | SikkerKey.bootstrap(vaultId, token, options?) | SikkerKeyBootstrap | Memory-only serverless bootstrap (static); call .inMemory() | | SikkerKey.listVaults() | string[] | List registered vault IDs (static) | | 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) | void | Watch a secret for changes | | unwatch(secretId) | void | Stop watching a secret | | setPollInterval(seconds) | void | Set poll interval (min 10s) | | close() | void | Stop all watches, shut down polling |

Dependencies

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

Documentation

License

MIT - see LICENSE for details.