Skip to content

Latest commit

 

History

8 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

KeyKosh Node.js SDK (@keykosh/sdk)

Read configuration from your self-hosted KeyKosh (K2) platform. Zero runtime dependencies — uses Node's built-in fetch and node:crypto. Node 18+.

The SDK talks to exactly one host — your own platform. There is no vendor default URL and no callback home; baseUrl is required.

Install

npm install @keykosh/sdk

Quick start

import { createClient } from '@keykosh/sdk';

// Reads K2_BASE_URL / K2_TOKEN / K2_ENV from the environment; options override.
const k2 = createClient({
  baseUrl: 'https://k2.acme.com',
  token: process.env.K2_TOKEN,     // SDK token minted in the admin UI → Tokens
  env: 'prod',
  cacheTtlMs: 30_000,              // optional in-memory TTL cache
});

const cfg = await k2.getConfiguration();          // full config for the default env
const dbUrl = cfg.getString('db.url', 'jdbc:postgresql://localhost/app');
const debug = cfg.getBoolean('feature.debug', false);

const poolSize = await k2.getProperty('prod', 'pool.size'); // single property

// Live updates: called whenever an admin changes this env's config.
const unsubscribe = k2.watch('prod', (fresh) => applyConfig(fresh));

Two ways to run

Everything below follows from which of these you are:

In production (the default — K2_OFFLINE=false): the platform is the source of truth. The SDK fetches from it, and writes what it got to k2config-<env>.json as a side effect. If the platform is later unreachable, that file is served instead of throwing, so a K2 outage cannot stop your app from booting. You don't manage the file; the SDK does.

On a laptop (K2_OFFLINE=true): there is no server at all. You own k2config-<env>.json, the SDK only reads it, and no network call is ever made.

K2_OFFLINE=false does not mean "no offline support". It is the mode that gives you the offline fallback — the SDK keeps the file current for you. K2_OFFLINE=true means "never contact the server", which is a different thing entirely.

To go from the first to the second, run once online and take ownership of what the SDK wrote:

K2_BASE_URL=https://k2.acme.com K2_TOKEN=… K2_ENV=dev node app.js   # writes the file
cp ~/.k2/config/k2config-dev.json ./k2config-dev.json               # repo-local wins
#   edit "_k2": { "managed": false } — the SDK will now never overwrite it
K2_OFFLINE=true node app.js                                          # no server

No CLI is needed for any of this: the SDK is the generator.

What it does

  • Token-scoped reads against GET /api/config/token/{env}/current and /properties/{key}, authenticated with Authorization: Bearer <token> (and X-API-Token).
  • Hot reload (watch()) — subscribes to the platform's change stream over Server-Sent Events and calls your handler when an admin edits config, with no polling. Reconnects with backoff, and falls back to polling on cacheTtlMs if a proxy strips SSE, so a blocked stream degrades instead of failing. Each push also refreshes the local file.
  • TTL cache (cacheTtlMs) so repeated reads don't hit the network each call; serves the last value through a failed refresh.
  • Local config file (k2config-<env>.json, on every tier) — plaintext, self-describing, mode 0600, written atomically. Auth errors (401/403/404/421) always surface: they are not availability blips, so a file on disk is deliberately declined.

The file

{
  "_k2": { "org": "acme", "app": "billing", "env": "prod",
           "managed": true, "fetchedAt": "2026-08-03T18:04:11Z", "sdk": "node/1.2.0" },
  "properties": { "db.url": "jdbc:postgresql://localhost:5432/billing", "db.pool": 20 }
}

Resolution order, first hit wins: $K2_CONFIG_FILE (alone, when set) → ./k2config-<env>.json$K2_CONFIG_DIR or ~/.k2/config. Naming an explicit location is exclusive of the machine default: K2_CONFIG_DIR replaces ~/.k2/config rather than preceding it, so a stale file in your home directory can never quietly satisfy a read. It does not suppress the working-directory candidate, which is always searched first — only K2_CONFIG_FILE does that. All three K2 SDKs order these identically.

The app identity lives inside the file rather than in its name, which keeps K2_APP optional and gives you one predictable string to gitignore. Set K2_APP anyway: it is what turns "which app is this?" into a checked invariant, so two apps sharing a config directory fail with K2_FILE_APP_MISMATCH naming both instead of silently serving each other's config.

The file holds secret values in clear. Add k2config-*.json to your .gitignore. Commit one only when it holds no real secrets.

Errors

Every failure throws K2Error with a stable, greppable code (plus statusCode: the HTTP status, or -1 for transport/config/file errors). err.isAvailabilityError() is true only for K2_UNREACHABLE, K2_TIMEOUT and K2_SERVER_ERROR — the codes eligible for the local file.

Code Means
K2_MISSING_BASE_URL / K2_MISSING_TOKEN / K2_INVALID_MODE misconfiguration — raised at construction, not on first read
K2_TOKEN_FILE_UNREADABLE K2_TOKEN_FILE names a file that is missing, unreadable, or empty once trimmed — never downgraded to K2_MISSING_TOKEN
K2_MISSING_ENV no environment passed and no K2_ENV — raised on read, since one client can serve several envs
K2_FILE_NOT_FOUND K2_OFFLINE=true and no file; the message lists every path searched
K2_FILE_MALFORMED the file isn't valid K2 JSON — a bug, not an outage, so no fallback to the server
K2_FILE_APP_MISMATCH the file belongs to another app (set K2_CONFIG_DIR per app)
K2_FILE_STALE older than K2_OFFLINE_MAX_AGE
K2_FILE_UNMANAGED _k2.managed is false — you own it, so the SDK refused to overwrite
K2_FILE_NOT_WRITABLE read-only filesystem; logged once at WARN, never thrown
K2_UNAUTHORIZED / K2_FORBIDDEN / K2_NOT_FOUND / K2_HOST_NOT_LICENSED the platform refused — never served from the file
K2_UNREACHABLE / K2_TIMEOUT / K2_SERVER_ERROR the platform was unreachable — the file is served when present
K2_REQUEST_FAILED an unexpected non-2xx; reachable and refusing, so no fallback

Configuration via environment

Var Meaning
K2_BASE_URL platform URL, e.g. https://k2.acme.com
K2_TOKEN SDK token (the one secret — never commit it)
K2_TOKEN_FILE path to a file holding the token — the Docker/Kubernetes secret-mount shape (1.2.0+)
K2_ENV default environment for the no-arg reads
K2_APP app slug — optional, but set it: it makes the file's app a checked invariant
K2_OFFLINE true ⇒ never contact the server (default false)
K2_OFFLINE_CACHE false ⇒ keep nothing on disk (default true)
K2_HOT_RELOAD falsewatch() polls instead of subscribing (default: on when online)
K2_CONFIG_DIR directory holding k2config-<env>.json (default ~/.k2/config)
K2_CONFIG_FILE one exact path — wins over everything else
K2_OFFLINE_MAX_AGE e.g. 7d — hard-refuse a file older than this (default: no limit)
K2_CACHE_TTL in-memory TTL, and the watch() polling interval when SSE is unavailable

K2_OFFLINE=true with K2_OFFLINE_CACHE=false is contradictory and raises K2_INVALID_MODE at construction.

Credential precedence: an explicit token option → K2_TOKENK2_TOKEN_FILEK2_TOKEN_ENC. A token file is trimmed of surrounding whitespace (mounted secrets end in a newline) and read once, at token resolution — not per request. A missing, unreadable or blank file raises K2_TOKEN_FILE_UNREADABLE naming the path, never a silent fallthrough to "no token". K2_TOKEN_FILE is honoured from 1.2.0; earlier versions ignore it.

Cross-language note: the Java SDK throws K2Exception (not K2Error), classifies codes with a K2ErrorCode.Kind enum, adds snapshot(env) / getOfflineCacheAllowed(), does not honour the deprecated aliases above from its K2Client.fromEnv(), and is not zero-dependency (Jackson). Codes and current variable names are otherwise identical across all three. See USER_MANUAL.md → Cross-language differences.

Deprecated, honored for one minor release (each logs a WARN): K2_SOURCEK2_OFFLINE (filetrue, serverfalse, autotrue iff a file exists), and K2_CACHE_DIRK2_CONFIG_DIR.

Test

npm test   # file store, error taxonomy, SSE hot reload, config parsing — no network needed

About

KeyKosh Node.js client SDK (@keykosh/sdk) — token-scoped config reads, SSE hot reload, plaintext offline config file, zero deps

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages