runenv-sdk
v0.1.2
Published
Node.js SDK for Runenv — load, manage, and push environment secrets directly in your application
Readme
runenv-sdk
Node.js SDK for RunEnv. Fetch authorized values in memory, inject them into a process, or update a project's secrets through the API.
Install and obtain a service token
npm install runenv-sdkNode.js 18+ and both CommonJS and ESM are supported.
- Open your project in the RunEnv dashboard and open Service Tokens. A project administrator can create tokens; ask your administrator if this control is unavailable.
- Create a named token for your workload. Choose READ for fetching or WRITE only when the workload must push changes. Select the intended environment and an expiry.
- Copy the displayed token once. Store it in your deployment platform's secret store as
RUNENV_SERVICE_TOKEN, injected into the process environment. Never commit it, put it in a browser bundle, or print it. - Copy the project's ID into
RUNENV_PROJECT_ID. SetRUNENV_ENV=productionfor production workloads. Keep the complete token when it contains an E2EE key suffix. - A lost token cannot be retrieved again. Create a replacement, update the workload, then revoke the old token in Service Tokens.
Runenv.fromEnv() resolves the token from explicit options, then RUNENV_TOKEN, then RUNENV_SERVICE_TOKEN. Both variable names work. projectId and project are supported; IDs avoid ambiguous project names. When both are supplied they must identify the same authorized project.
Fetch and load
import { Runenv } from 'runenv-sdk';
const runenv = Runenv.fromEnv();
const secrets = await runenv.fetchSecrets('production');
// Pass secrets directly to an in-memory consumer. Do not log their values.import Runenv from 'runenv-sdk';
const runenv = new Runenv({
token: process.env.RUNENV_SERVICE_TOKEN,
projectId: process.env.RUNENV_PROJECT_ID,
environment: 'production',
});
await runenv.load('production'); // preserves existing process.env values
await runenv.load('production', { override: true }); // explicitly replaces them
await runenv.load(false); // compatible boolean form: configured environment
await runenv.load(true); // configured environment, replace existing valuesEnvironment overrides apply to that call. Cache and in-flight requests are separated by environment inside each client. Injection is serialized within the client; applications loading multiple environments concurrently should use returned maps from fetchSecrets(environment) to avoid competing writes to process.env. Loading does not erase unrelated or previously injected process variables.
import { Runenv } from 'runenv-sdk';
const runenv = Runenv.fromEnv();
const optionalValue = await runenv.get('OPTIONAL_KEY');
const requiredValue = await runenv.getOrThrow('DATABASE_URL');
const selected = await runenv.getMany('DATABASE_URL', 'REDIS_URL');
const exists = await runenv.has('DATABASE_URL');
const names = await runenv.keys();
runenv.clearCache();The API omits expired secrets. Cache lifetimes stop at the earliest reported secret expiry. cacheTtl: 0 disables caching. clearCache() invalidates all cached environments and prevents older requests from repopulating the cache. A client cache cannot detect a server-side revocation until it next contacts the server; choose a shorter TTL when needed.
Push
import { Runenv, pushSecrets } from 'runenv-sdk';
const options = {
token: process.env.RUNENV_SERVICE_TOKEN,
projectId: process.env.RUNENV_PROJECT_ID,
environment: 'production',
};
const runenv = new Runenv(options);
await runenv.push({ EMPTY_ALLOWED: '', EXAMPLE: ' exact whitespace ' });
await pushSecrets(options, { EXAMPLE: 'updated example' });pushSecrets(options, secrets) requires two arguments. The instance form is runenv.push(secrets). Pass a flat record containing 1–1000 string-valued keys, with keys up to 256 characters and values up to 1 MiB; the API caps the complete request at 2 MiB. Missing records, arrays and non-string values fail locally. Successful responses contain success, project, environment, created, updated and total. An existing key is updated in place; quota checks apply to additions. Writes require a WRITE token and the target environment's access/approval policy.
JIT leases and watching
A project administrator must configure a supported JIT policy and its provider credentials first. A service token restricted to an environment cannot access an unbound project-wide JIT role. The API explains unsupported scope or provider configuration.
import { Runenv, leaseSecret, watchLeaseSecret } from 'runenv-sdk';
const options = {
token: process.env.RUNENV_SERVICE_TOKEN,
projectId: process.env.RUNENV_PROJECT_ID,
environment: 'production',
};
const runenv = new Runenv(options);
const lease = await runenv.lease('db-readonly', { ttl: 30 });
const anotherLease = await leaseSecret(options, 'db-readonly', { ttl: 30 });
const watcher = await watchLeaseSecret(options, 'db-readonly', newLease => {
// Reconfigure the in-memory consumer using newLease.credentials.
}, { ttl: 30, renewBufferSeconds: 60 });
watcher.stop(); // call during workload shutdownttl is an integer in minutes, 1–1440, additionally constrained by policy/provider limits. renewBufferSeconds is in seconds, defaults to 300 and accepts 0. Results contain id, policy, type, credentials, expiresAt, and createdAt; provider-specific convenience fields may also be present.
runenv.watchLease(policy, callback, options) has the same result as watchLeaseSecret: { initialLease, stop }. The callback runs for the initial lease and successful replacements. This watcher issues replacement credentials; it does not extend the original provider credential. Switch consumers when the callback runs. The original credential follows its provider expiry/revocation behavior.
Short leases cap the renewal buffer at half the remaining lifetime. Failed background issuance retries with bounded backoff and stops after four failures or when the current lease expires. Monitor the supplied logger. stop() cancels scheduling and suppresses in-flight callbacks; it does not revoke credentials already issued. Initial issuance errors reject the returned promise. Do not log lease credentials.
Polling and framework integration
import { Runenv, loadSecrets } from 'runenv-sdk';
const runenv = Runenv.fromEnv();
const stop = runenv.watch(updated => {
// Reconfigure an in-memory consumer; this does not inject process.env.
}, { interval: 60 }); // seconds; callback starts after the first observed change
stop();
const injected = await loadSecrets({
token: process.env.RUNENV_SERVICE_TOKEN,
projectId: process.env.RUNENV_PROJECT_ID,
environment: 'production',
});runenv.middleware() returns Express/Connect middleware that loads the configured environment once before continuing and passes initial errors to next(error). Register it with app.use(runenv.middleware()). It does not periodically reload secrets. Runenv.nestConfig(options) returns an async function for Nest's ConfigModule.forRoot({ load: [Runenv.nestConfig(options)] }); it returns a secret map without injecting it.
import { Runenv } from 'runenv-sdk';
const runenv = Runenv.fromEnv();
const middleware = runenv.middleware();
await new Promise((resolve, reject) => middleware({}, {}, error => error ? reject(error) : resolve()));
const configure = Runenv.nestConfig({
token: process.env.RUNENV_SERVICE_TOKEN,
projectId: process.env.RUNENV_PROJECT_ID,
});
const configuration = await configure();Keep this SDK server-side in Next.js. Never return secrets through next.config.js's env option, NEXT_PUBLIC_* variables, or client props.
Transit and E2EE
import { Runenv } from 'runenv-sdk';
const runenv = Runenv.fromEnv({ orgId: process.env.RUNENV_ORG_ID });
const ciphertext = await runenv.transitEncrypt('application-data', 'synthetic example');
const plaintext = await runenv.transitDecrypt('application-data', ciphertext);Transit requires orgId (or RUNENV_ORG_ID) and an explicit grant on the named Transit key for the calling credential. Methods accept/return UTF-8 strings; the SDK handles the API's base64 encoding. transitEncrypt returns the ciphertext envelope, and transitDecrypt returns plaintext. There is no runenv.transit.encrypt object API.
E2EE is automatic when the service token includes a project-key suffix. runenv.e2eeEnabled reports whether that suffix exists. The following lower-level exports are available for integrations; keep project keys in memory.
import { encryptE2EESecret, decryptE2EESecret, extractServiceTokenProjectKey } from 'runenv-sdk';
const projectKey = extractServiceTokenProjectKey(process.env.RUNENV_SERVICE_TOKEN);
if (projectKey) {
const encrypted = encryptE2EESecret('synthetic example', projectKey);
const decrypted = decryptE2EESecret(encrypted, projectKey);
}extractServiceTokenProjectKey(token) returns a base64 key or null. encryptE2EESecret(plaintext, base64ProjectKey) returns an authenticated e2ee: envelope; decryptE2EESecret(envelope, base64ProjectKey) returns the original string or throws on an invalid key/payload. Keys must decode to 32 bytes. Do not persist or print decrypted values.
Options and errors
| Option | Default | Meaning |
| --- | --- | --- |
| token | required | Service token, organization API key, or supported user session token. MCP tokens are not accepted. |
| projectId / project | one required | Stable ID / authorized project name. |
| orgId | RUNENV_ORG_ID for Transit | Organization owning the Transit grant. |
| environment | development | Default environment; fromEnv() also reads RUNENV_ENV. |
| serverUrl | see below | API origin. |
| resolve | true | Resolve secret references. Missing, expired or cyclic references fail; false returns literal placeholders. |
| cacheTtl | 300 | Non-negative seconds, limited by reported expiry. |
| retries | 2 | 0–10 retries for read network failures. Mutations are not retried after uncertain outcomes. |
| debug | false | Debug request diagnostics; never log values yourself. |
| logger | console | (level, message) => void, levels debug, info, warn, error. |
Server precedence is explicit serverUrl, RUNENV_LOCAL_URL, RUNENV_API_URL, RUNENV_SERVER_URL, then https://runenv.dev. fromEnv() additionally reads RUNENV_TOKEN, RUNENV_SERVICE_TOKEN, RUNENV_PROJECT_ID, RUNENV_PROJECT, RUNENV_ORG_ID, and RUNENV_ENV; explicit overrides win.
Async methods reject on invalid input, failed authentication/authorization, missing resources, API validation or provider failure. getOrThrow also rejects for a missing key; get returns undefined. A 400 requires correcting the input; a 401/403 requires checking token/role/environment/plan; 409 indicates a conflict, ambiguous project or unavailable reference. After a write network failure, verify server state before retrying. Cache data is never a fallback for an API error once a request has been made.
CommonJS and package checks
const { Runenv } = require('runenv-sdk');
const runenv = Runenv.fromEnv();
const secrets = await runenv.fetchSecrets(); // inside an async function in CommonJSThe default export and named Runenv export are the same class. Runtime exports are Runenv, default, loadSecrets, pushSecrets, leaseSecret, watchLeaseSecret, encryptE2EESecret, decryptE2EESecret, and extractServiceTokenProjectKey. TypeScript also exports RunenvOptions, RunenvSecrets, PushResult, and LeaseResult.
npm test builds CJS/ESM, executes behavior and documentation checks, and verifies package contents. Release qualification also installs the tarball into clean CJS/ESM consumers and exercises the running API.
0.1.2 release candidate
This unpublished patch follows the current latest release, 0.1.1. It fixes
explicit environment selection, push payload validation, lease/watch request
contracts and expiry handling. The documented projectId option remains a
compatible alias for project. Source and lockfile package identity now agree.
Publish only the qualified tarball after backend compatibility and release
acceptance pass; these source changes do not update the npm distribution tag.
License
MIT
