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

@qrvey/system-key-manager

v0.2.0-1369

Published

Keeps API_KEY in sync with the qrvey-system-key Kubernetes secret mounted as a read-only volume

Readme

@qrvey/system-key-manager

coverage

@qrvey/system-key-manager keeps the platform's API key environment variable aligned with the system key, the internal key used for backend-to-backend calls. That variable is named by the exported ENV.API_KEY, and is written <api-key-env> throughout this document.

Today the key reaches each service as an environment variable injected from a Kubernetes ConfigMap, generated once at environment creation. That makes the value visible in the pod spec and impossible to rotate without a redeploy. This package moves the source of truth to a read-only Kubernetes Secret mounted as a volume, and refreshes <api-key-env> in place when the Secret changes.

Consumers that read <api-key-env> per call need no change. Code that captures it once at import time — export const key = process.env[ENV.API_KEY] — does: it keeps the value it read at load time and never observes a rotation. See Usage for the ordering this implies.

Installation

npm install @qrvey/system-key-manager

The secret

Names and paths are environment-specific and are not reproduced here; the placeholders below stand in for the values your environment defines.

| | | | ----------- | --------------------- | | Secret name | <system-key-secret> | | Mount path | <mount-path> | | Mount mode | read-only |

A Secret volume mounts as a directory with one file per data key, and this package reads either of the two layouts that can produce:

One data key per slot. previous, current and next each appear as a file inside the mount directory, named after the data key and holding the key value. Slot values are trimmed, so a slot written with a trailing newline — what openssl rand -base64 32 produces — still matches the value callers send. This is the platform layout, and SYSTEM_KEY_PATH only has to name the mount directory:

<mount-path>/previous
<mount-path>/current
<mount-path>/next

One data key holding the whole document. The entry appears as a single file holding the JSON below, and SYSTEM_KEY_PATH has to name that file rather than the directory it sits in. This is also the shape a plain file takes in development.

{
    "previous": "<base64-encoded-key>",
    "current": "<base64-encoded-key>",
    "next": "<base64-encoded-key>"
}

The layout is decided on every read from stat, so one build serves both and moving between them needs no configuration change.

Three slots exist so a rotation can converge without downtime: callers always send current, and the authorizer accepts current and next so a rotation is accepted from either side of the promotion. previous is deliberately not accepted — it is retained for audit, and a leaked key has to stop working the moment it is rotated out. Only current is required; previous and next are optional and are dropped when empty.

An example volume mount (defaultMode: 292 is 0444 — pods may run under different uids, so the file has to be readable by all of them):

volumes:
    - name: system-key
      secret:
          secretName: <system-key-secret>
          defaultMode: 292
containers:
    - name: <service>
      volumeMounts:
          - name: system-key
            mountPath: <mount-path>
            readOnly: true

Usage

Start the manager once during bootstrap, before anything reads <api-key-env>:

// system-key.bootstrap.ts — side-effect only, no exports
import { startSystemKeyManagerSync } from '@qrvey/system-key-manager';

startSystemKeyManagerSync();
// main.ts — this import must come first
import './system-key.bootstrap';

import { key } from './config';

// From here on, the API key variable tracks the secret's `current` slot.

The separate module is not ceremony. Imports are hoisted: in the CommonJS output every require runs before any statement in the file, so a bare startSystemKeyManagerSync() in the entry point executes after the modules it was meant to precede. A side-effect-only module imported first is what actually lands the key before anything reads it, because CommonJS evaluates imports in source order. (node -r ./system-key.bootstrap does the same job from outside.)

That ordering only matters for modules that capture the key at import time. Code that reads <api-key-env> per call is unaffected and needs no preload.

The first read is blocking, so <api-key-env> holds the active key by the time the call returns. After that the mount is re-read every 5 seconds and <api-key-env> is rewritten whenever it drifts from current.

Services that already have an async bootstrap can await the manager there instead. Do not lift this to module top level: the platform compiles to CommonJS, where top-level await is a syntax error.

import { startSystemKeyManager } from '@qrvey/system-key-manager';

async function bootstrap() {
    await startSystemKeyManager();
    // ... start the server
}

void bootstrap();

Code that needs the key explicitly — for example to validate an incoming x-api-key, where the other rotation slots matter — can read the document directly:

import {
    getSystemKey,
    getSystemKeyDefinition,
} from '@qrvey/system-key-manager';

getSystemKey(); // the active key, falling back to the environment
getSystemKeyDefinition(); // { previous?, current, next? } | null

API

Functions

These operate on one process-wide instance, because the environment is process state.

  • startSystemKeyManager(options?): Promise<SystemKeyManagerService> — reads the secret once, then starts the poller. Idempotent.
  • startSystemKeyManagerSync(options?): SystemKeyManagerService — the same, with a blocking first read. For CommonJS entry points that have no await to give: <api-key-env> is in place before it returns.
  • stopSystemKeyManager(): void — stops the poller and drops the instance. <api-key-env> keeps whatever value it holds.
  • getSystemKeyManager(options?): SystemKeyManagerService — the shared instance. options only apply the first time, when the instance is created.
  • getSystemKey(): string | undefined — the active key, falling back to <api-key-env> before the first successful read.
  • getSystemKeyDefinition(): ISystemKeyDefinition | null — the last document read, or null if the secret was never readable.

Validating an inbound key

  • isAcceptedSystemKey(candidate?, options?): boolean — the platform's inbound x-api-key rule. Accepts current and next from the mounted secret; then, only while legacy mode is on, the pre-migration key and any options.compatKeys the caller supplies. previous is never accepted — it is retained for audit, and a key must stop working the moment it is rotated out. This mirrors systemKeyManagement.ValidateSystemKey in qrvey_admin_security so the Go and Node validators cannot drift. Use this instead of comparing against <api-key-env> yourself.
  • isLegacyModeEnabled(): boolean — whether <legacy-mode-env> is exactly 'true'. No other value enables it.
  • getLegacyApiKey(): string | undefined — the value <api-key-env> carried before this package replaced it.
  • secureEquals(candidate?, expected?): boolean — constant-time comparison. An absent expected value never matches, so an unset key cannot authenticate a request that carries no key at all.

options.compatKeys values belong in the consuming repository, never in this package: it publishes with access: public, and npm ships README.md regardless of the files allowlist.

The pre-migration key

This package replaces <api-key-env> with the secret's current slot, so a validator that compares an inbound key against <api-key-env> becomes system-key-only from that moment. The value it replaced is preserved two ways:

  • getLegacyApiKey(), for consumers that import this package.
  • <legacy-api-key-env>, written once at module evaluation, for consumers that cannot — embedded libraries that never declare a dependency on it, or services resolving an older copy.

Both return the same value. Do not capture <api-key-env> into a module-scope constant in your own service to preserve it: importing anything that starts the manager first — qrvey-security does, at module scope — makes that capture record the system key and reject every legacy caller. The capture here happens at this package's own module evaluation, which no require order can get ahead of.

SystemKeyManagerService

Exported for tests and for the rare consumer that needs its own instance.

| Method | Description | | -------------------------------------------------------- | --------------------------------------------------------------- | | start(): Promise<void> | First read plus poller. No-op when already running or disabled. | | startSync(): void | As start(), with a blocking first read. | | stop(): void | Stops the poller. | | refresh(): Promise<ISystemKeyDefinition \| null> | One read-and-apply cycle. | | refreshSync(): ISystemKeyDefinition \| null | As refresh(), blocking. | | getSystemKey(): string \| undefined | Active key, falling back to <api-key-env>. | | getSystemKeyDefinition(): ISystemKeyDefinition \| null | Last document read. | | getStatus(): ISystemKeyStatus | Resolved configuration and current state, for health checks. |

ISystemKeyManagerOptions

| Option | Type | Default | | ---------------- | --------- | -------------------------------------------- | | path | string | SYSTEM_KEY_PATH, then the built-in default | | pollIntervalMs | number | SYSTEM_KEY_POLL_INTERVAL_MS, then 5000 | | disabled | boolean | SYSTEM_KEY_MANAGER_DISABLED |

Explicit options always win over the environment.

Environment

| Variable | Values | Default | Description | | ----------------------------- | -------------------------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | | <api-key-env> | string | — | The managed value, named by the exported ENV.API_KEY. Read by the rest of the platform; written by this package. | | <legacy-api-key-env> | string | — | The value <api-key-env> held before this package replaced it, named by ENV.LEGACY_API_KEY. Written once at module evaluation; an existing value is kept. | | <legacy-mode-env> | 'true' | unset | unset | Named by ENV.API_KEY_LEGACY_MODE. Exactly 'true' keeps the pre-migration key and any compat keys accepted. '1' does not enable it. | | SYSTEM_KEY_MANAGER_DISABLED | 'true' | '1' | unset | unset | Kill switch. When on, the poller never starts and <api-key-env> is never replaced. | | SYSTEM_KEY_PATH | string | built-in default | The mount to read: a directory holding one file per slot, or a single file holding the JSON document. Only needed when the mount is not at the built-in default. | | SYSTEM_KEY_POLL_INTERVAL_MS | positive number | 5000 | Overrides the polling cadence. An invalid value warns and falls back to the default. |

Setting SYSTEM_KEY_MANAGER_DISABLED is the escape hatch for an environment that has not been migrated yet, or for rolling back a bad rotation: the service falls back to whatever value the ConfigMap injected, with no code change.

Failure behaviour

The manager never throws into the host process and never removes a key that is already in place.

| Situation | Behaviour | | ------------------------------------------- | --------------------------------------------------------------------- | | Secret file absent | Warns that the secret was not found. <api-key-env> keeps its value. | | Secret unreadable (permissions, I/O) | Warns with the underlying reason. <api-key-env> keeps its value. | | Document malformed (not JSON, no current) | Warns. The last valid definition stays in effect. | | Mount carries no current entry | Warns. The last valid definition stays in effect. | | One slot exists but cannot be read | Warns and treats that slot as empty; the other slots still apply. | | Secret valid again after a failure | Picked up on the next poll; the warning state resets. | | A poll throws unexpectedly | Caught and logged. The poller keeps running. |

Warnings are emitted on state change rather than on every poll, so a missing secret produces one line instead of one line every 5 seconds.

Implementation notes

  • Polling, not fs.watch. The kubelet propagates Secret updates by atomically swapping the symlink tree behind the mount. fs.watch stops firing after the first swap, silently, so the mount is re-read on a timer instead.
  • Cheap steady state. A single-file secret is stat-fingerprinted (mtime, size, inode) and only re-read when it changes; the atomic-writer swap always lands on a new inode, so a rotation is never missed. A directory mount is re-read every poll instead — a directory's own stat does not change reliably when the kubelet swaps the tree underneath it, and three reads from tmpfs cost less than the risk of missing a rotation.
  • <api-key-env> is also re-checked on every poll, not just when the mount changes, so a value overwritten elsewhere in the process converges back.
  • Slots are read through the kubelet's ..data link, resolved once per poll, so all three come from one consistent snapshot even when a rotation lands between two of the reads. A mount without that link — a subPath projection, or a plain directory — is read directly.
  • The mount is never listed. The three slots are read by name, because the mount also holds the kubelet's own bookkeeping entries.
  • The poller is unref'd and will not hold the event loop open on its own.
  • Key values are never logged.