@kindgi/env-inmemory
npm install @kindgi/env-inmemory · source
Reference in-memory implementation of the EnvBinding contract from @kindgi/api: the store for non-sensitive, per-environment configuration values keyed by (scope, envName, name). Use it in tests and local development. It refuses to start under NODE_ENV=production; deployed environments use a durable EnvBinding implemented by the Kindgi runtime.
Purpose
Section titled “Purpose”Give every consumer of EnvBinding a store that needs no database but keeps the contract's semantics: values are partitioned by envName (a staging write is invisible to production reads), every write bumps a revision counter that supports optimistic concurrency, and resolve walks from the most specific scope to the tenant. With an AuditEventBinding attached it also emits the env-* audit events, so evidence trails can be exercised end to end in tests.
Exports
Section titled “Exports”createInMemoryEnvBinding(options?)— returns a fresh, isolatedEnvBindingbacked by process memory. Throws at construction whenprocess.env.NODE_ENV === 'production'andproductionSafeis nottrue. The returned binding:resolve— checks the requested scope, then the tenant scope (project → tenant,org → tenant). Returns{ kind: 'ok', value: { name, value, revision } }or anenv-not-founderror.get/list— exact scope only, no walk.listfilters bynamePrefixandtagFilter(every pair must match; untagged entries never match), sorts by name and returns at mostlimitentries.cursoris ignored andnextCursoris never set.set— creates or overwrites;revisionstarts at1and increments on every write. A mismatchedifRevisionreturns{ kind: 'revision-conflict', currentRevision }(0for a name that does not exist).enqueueTuplesis required by the contract but not called.delete— removes the entry and returns{ deleted }; a laterresolveat that scope falls through to the tenant value.
InMemoryEnvBindingOptions:productionSafe?: boolean— defaultfalse. Allows construction underNODE_ENV=production, for example in a parity-test container.now?: () => number— clock used forcreatedAt/updatedAt; defaults toDate.now.auditEvents?: AuditEventBinding,tenantId?: TenantId,projectId?: ProjectId— audit emission is enabled only when all three are set.setemitsenv-set(outcomefailedwithenv-write-conflicton a revision conflict),resolveemitsenv-resolved, and adeletethat removed something emitsenv-deleted. Every event is recorded under the configuredtenantId/projectId, and payloads never contain the value. Emission is awaited, but a failing audit sink is logged withconsole.warnand does not fail the operation.
Example
Section titled “Example”import { createInMemoryAuditEventBinding } from '@kindgi/audit-events-inmemory';import { createInMemoryEnvBinding } from '@kindgi/env-inmemory';import type { Scope } from '@kindgi/platform';import { makeEnvName, type ProjectId, type TenantId } from '@kindgi/types';
const tenantId = 'acme' as TenantId;const projectId = 'acme-support' as ProjectId;const staging = makeEnvName('staging');if (staging === null) throw new Error('invalid env name');
const tenant: Scope = { kind: 'tenant', tenantId };const project: Scope = { kind: 'project', tenantId, projectId };
const audit = createInMemoryAuditEventBinding();const env = createInMemoryEnvBinding({ auditEvents: audit, tenantId, projectId });
const kbUrl = { envName: staging, name: 'KB_URL' } as const;await env.set({ scope: tenant, ...kbUrl, value: 'https://kb.acme.test', enqueueTuples: () => [] });
// The project has no value of its own, so resolution falls back to the tenant.const resolved = await env.resolve({ scope: project, ...kbUrl, resolveContext: { caller: 'dispatch', runId: 'run-1' },});if (resolved.kind === 'ok') console.log(resolved.value.value, resolved.value.revision); // https://kb.acme.test 1
// Optimistic concurrency: this write applies only while the tenant value is at revision 1.const updated = await env.set({ scope: tenant, ...kbUrl, value: 'https://kb-v2.acme.test', ifRevision: 1, enqueueTuples: () => [],});if (updated.kind === 'revision-conflict') console.warn('stale write', updated.currentRevision);
const events = await audit.query({ tenantId }); // env-set, env-resolved, env-setNon-goals
Section titled “Non-goals”- Durability or cross-process sharing. State lives in one process and is lost on exit.
- Org-level resolution for project scopes. The in-memory store has no project → org relation, so a project scope falls back straight to the tenant. Durable implementations walk
project → org → tenant. - Secrets. Values are stored and returned in plaintext; sensitive values belong in a
SecretBinding. - Soft delete.
deleteremoves the entry; the contract defines env as a plain overwrite-shaped store.
Related
Section titled “Related”@kindgi/api— theEnvBindingcontract andResolveContext.@kindgi/platform— theScopeunion.@kindgi/types—EnvNameand themakeEnvNamevalidator.@kindgi/audit-events-inmemory— in-memoryAuditEventBindingfor the emitted events.