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

configuard

v1.1.0

Published

Builds a nested, typed configuration object from a flat list of config items, with templating and accessor-based (ABAC) filtering. Built on notation.

Readme

Important: This module is ESM 🔆. Please read this.

© 2026, Onur Yıldırım (@onury). MIT License.

Builds a nested, typed configuration object from a flat list of configuration items (typically rows from a config database table) — with ${...} value templating, reusable option lists, and accessor-based (ABAC) filtering. Built on notation.

import { Configuard, AccessorType } from 'configuard';

const rows = [
  { accessor: 'system', key: 'company.name', type: 'string', listType: 'none', value: 'Acme', editable: true, requiresReboot: false, encrypt: false },
  { accessor: 'system', key: 'ai.vision.provider', type: 'string', listType: 'none', value: 'gemini', editable: true, requiresReboot: false, encrypt: false }
];

const cfg = new Configuard(rows, { accessor: AccessorType.SYSTEM });

cfg.data;                              // { company: { name: 'Acme' }, ai: { vision: { provider: 'gemini' } } }
cfg.get<string>('ai.vision.provider'); // 'gemini'  (a scalar, not ['gemini'])
cfg.has('company.name');               // true

Why?

Why a vertical key → value config table? Configuration is a long, ever-growing list of individual settings that changes across environments and over the life of a product. A tall key/value table lets you add, edit, or remove one setting at a time — each row carrying its own metadata (type, access level, options, requiresReboot, editable, …) — and administer it all from a UI without schema migrations. That's far more maintainable than wide, one-column-per-setting tables or settings scattered across files.

But flat rows are great to store and administer, not to consume: values are strings, related keys are scattered, and the same value is often duplicated. Configuard sits across the whole lifecycle:

  1. Store — keep your settings as IConfigItem rows in one config table.
  2. Build & use safelynew Configuard(rows) produces a nested, type-cast, ABAC-filtered object for your runtime. It is frozen by default (immutable), so backend code can't accidentally mutate live config. Read it via .data / .get() / .has().
  3. Edit in a UIConfiguard.parseFlat() returns the same flat list with templates resolved and option lists expanded — ideal for an admin/editor UI where each row is a form field with its own allowed values.
  4. Save backConfiguard.serializeFlat() validates the admin's edits, serializes them to DB strings (optionally re-encrypting), and returns the diff of changed rows to persist to the config table.

Configuard also fails loud: a corrupt config row throws immediately at construction rather than silently producing a partial object (see Validation).

Install

npm i configuard

notation is a runtime dependency and is installed automatically.

How it Works

  • Flat → nested + typed. Each item's key is a dot/bracket notation (ai.vision.provider); its string value is parsed according to type (see Value types) and assembled into a nested object via notation.
  • Scalar by default. A value is parsed to its declared type — "gemini" stays the string "gemini". Lists are opt-in via listType (see List types).
  • Templating. Values may reference other keys: "${files.content}/img" or "${port}". References are resolved recursively; circular references are detected (no infinite recursion) and missing references are reported.
  • Option lists. @-prefixed rows define reusable lists of allowed values that other rows point to from their options field (see Option lists).
  • ABAC. accessor (system / application / all) plus a bitwise appAccess vs the client's appLevel decide, per item, what a given client may see (see Access control).
  • Section markers. Keys whose last note starts with $ (e.g. cp.config.$title) are treated as section metadata and skipped.
  • Immutable by default. The built object is deep-frozen, so it can't be mutated at runtime. Pass { lock: false } for a mutable result; check .isLocked to see the current state.
  • Fail loud. A corrupt row (unparseable value, missing/circular template, or malformed item) throws at construction — never a silent partial build (see Validation).

Access Control (ABAC)

Each item declares an accessorsystem, application, or all — and the client constructing the Configuard declares its own accessor. Only items the client is allowed to see are included in the built object:

  • A system client sees system and all items.
  • An application client sees all items, plus application/all items whose appAccess bit flags intersect the client's appLevel (a bitwise &). application items must declare an appAccess, and an application client must be constructed with an appLevel.
import { Configuard, AccessorType } from 'configuard';

// Application client flags (bitwise).
const WEB = 1 << 0;    // 0b001
const MOBILE = 1 << 1; // 0b010
const KIOSK = 1 << 2;  // 0b100

const rows = [
  { accessor: 'application', appAccess: WEB | MOBILE, key: 'ui.theme', type: 'string', listType: 'none', value: 'dark', editable: true, requiresReboot: false, encrypt: false },
  { accessor: 'application', appAccess: KIOSK, key: 'kiosk.timeout', type: 'integer', listType: 'none', value: '30', editable: true, requiresReboot: false, encrypt: false },
  { accessor: 'all', appAccess: null, key: 'app.name', type: 'string', listType: 'none', value: 'Acme', editable: true, requiresReboot: false, encrypt: false }
];

// A mobile client (appLevel = MOBILE):
const cfg = new Configuard(rows, { accessor: AccessorType.APPLICATION, appAccess: MOBILE });

cfg.has('ui.theme');      // true   — (WEB|MOBILE) & MOBILE !== 0
cfg.has('kiosk.timeout'); // false  — KIOSK & MOBILE === 0
cfg.has('app.name');      // true   — `all` item, no appAccess

Combine with accesscontrol for property-level filtering of the built object.

Validation — Fail Loud

Configuration is foundational, so Configuard treats a corrupt row as a hard error: the constructor throws immediately rather than logging a warning and building a partial object. It throws when:

  • a value can't be parsed to its declared type (e.g. bad json/hexadecimal/ time/date, or a number that resolves to NaN);
  • a ${...} template reference is missing or circular;
  • an item is malformed (invalid accessor/listType/type, or a non-string key).
try {
  const cfg = new Configuard(rows, { accessor: AccessorType.SYSTEM });
  // use cfg.data / cfg.get(...)
} catch (err) {
  // e.g. 'Configuard: Value "abc" of key "port" is not a valid number.'
  // the original parser error (when any) is available as `err.cause`.
}

parseFlat() likewise throws on missing/circular templates, a missing option list, or a value outside its option list.

ConfiguardError

All failures throw a ConfiguardError (exported from the package root), so consumers can react to a configuration fault specifically:

import { Configuard, ConfiguardError } from 'configuard';

try {
  new Configuard(rows, { accessor: AccessorType.SYSTEM });
} catch (err) {
  if (err instanceof ConfiguardError) {
    err.key;    // the offending config item key, when known
    err.cause;  // the underlying error (e.g. the parser failure), when any
  }
}

Locking (Immutability)

The object returned by build() (the constructor) is deep-frozen by default — every nested object and array is recursively Object.freezed — so runtime config can't be mutated by accident. Opt out with { lock: false }:

const locked = new Configuard(rows, { accessor: AccessorType.SYSTEM });
locked.isLocked;                 // true
Object.isFrozen(locked.data);    // true
// locked.data.x = 1;            // throws in strict mode

const mutable = new Configuard(rows, { accessor: AccessorType.SYSTEM }, { lock: false });
mutable.isLocked;                // false

Encryption

Items flagged encrypt: true can be stored encrypted at rest. Configuard is crypto-agnostic — you supply a synchronous decrypt hook, which it applies (before templating/parsing) to those items while building:

const cfg = new Configuard(rows, { accessor: AccessorType.SYSTEM }, {
  decrypt: (value, item) => myDecrypt(value) // return the plaintext string
});

cfg.isEncrypted('db.password'); // true
cfg.get('db.password');         // the decrypted plaintext value

Decryption is opt-in: without a decrypt hook, encrypt: true values are used as-is. A failing hook throws a ConfiguardError. (Re-encrypting edited values on save is handled by serializeFlat() — see below.)

Value Types

type declares how a row's raw string value is parsed. See the ValueType enum.

| type | Parses into | Notes | |---------------|-------------|-------| | null | null | | | string | string | Value used as-is. | | boolean | boolean | "true"/"1"true. | | number | number | Integer or float. | | integer | number | Base-10 integer. | | float | number | Floating-point. | | hexadecimal | number | Hex string, with or without 0x. Invalid hex throws. | | datetime | Date | Date and time (RFC 2822 / ISO 8601). | | date | string | Calendar date without a time part (e.g. "2026-06-15"). Kept as the validated string; a value with a time, or an invalid date, throws. | | time | string | Clock time HH:mm or HH:mm:ss (e.g. "14:30"). Kept as the validated string; out-of-range values like "90:77" throw. | | regexp | RegExp | /pattern/flags or a plain pattern. | | json | any | JSON.parse (object, array, …). | | any | inferred | Best-effort auto-detection. |

date and time are intentionally kept as validated strings (not Date objects), since they carry no full timestamp — use datetime when you need a Date.

List Types

listType controls whether a value is a single value or a list.

| listType | Result | Empty value → | |------------|--------|---------------| | none | A single parsed value of type. | null (or "" for string). | | array | An array of values, each parsed to type (value is split on commas). | [] | | csl | A normalized comma-separated string (whitespace around separators trimmed). | "" |

// listType: 'array', type: 'integer', value: '1, 2, 3'   →  [1, 2, 3]
// listType: 'csl',   type: 'string',  value: 'a , b ,c'  →  'a,b,c'

Option Lists (@-keys)

An admin UI often needs to constrain a field to a set of allowed values (dropdowns, multi-selects). Configuard models this with option lists:

  • A row whose key starts with @ is an option list definition, not a config value. Only its value matters, and it is always treated as a comma-separated list (regardless of type/listType).
  • Other rows reference an option list from their options field using a template: "${@UIColors}".

Option-list rows are excluded from build() output (they aren't real config). To resolve them, use parseFlat().

How a field's value relates to its option list:

  • listType: none → the value must be one member of the option list.
  • listType: csl / array → the value may contain several members.
  • A value outside the option list throws during parseFlat().

parseFlat() — flat output for admin UIs

Configuard.parseFlat(configList) returns the same flat list (not a nested object) with:

  • every ${...} placeholder in value resolved — the value stays a string (it is not cast to its type);
  • every @-key option list extracted into a separate @ object, each as a trimmed, uncast string array (ready to populate a UI control); and
  • every options reference expanded into that string array.
import { Configuard } from 'configuard';

const { '@': optionLists, configList } = Configuard.parseFlat([
  { accessor: 'system', key: '@UIColors', type: 'string', listType: 'csl', value: 'Blue,Red,Green', /* … */ },
  { accessor: 'system', key: 'device.ui.colors', type: 'string', listType: 'csl', value: 'Blue,Red', options: '${@UIColors}', /* … */ },
  { accessor: 'system', key: 'port', type: 'integer', listType: 'none', value: '8081', /* … */ },
  { accessor: 'system', key: 'environment.internalPort', type: 'integer', listType: 'none', value: '${port}', /* … */ }
]);

optionLists;
// { UIColors: ['Blue', 'Red', 'Green'] }

configList;
// [
//   { key: '@UIColors',                value: 'Blue,Red,Green', options: null,                   listType: 'csl',  … },
//   { key: 'device.ui.colors',         value: 'Blue,Red',       options: ['Blue','Red','Green'], listType: 'csl',  … },
//   { key: 'port',                     value: '8081',           options: null,                   listType: 'none', … },
//   { key: 'environment.internalPort', value: '8081',           options: null,                   listType: 'none', … }  // ${port} resolved
// ]

build() vs parseFlat():

| | build() (constructor) | parseFlat() | |---|---|---| | Output shape | Nested object | Flat list (+ @ option lists) | | value | Parsed/cast to type | Resolved template, kept as string | | @-keys | Excluded | Kept (in list and @) | | options | Ignored | Expanded to string arrays | | ABAC filtering | Yes | No |

Saving Edits — serializeFlat()

Configuard.serializeFlat(configList, edits, options?) is the inverse of parseFlat(): it turns the admin's edits back into DB-ready rows. For each edit it enforces editable, validates the value against its type and options, serializes it to the storage string, optionally re-encrypts encrypt: true values, and returns the diff of changed rows.

  • edits maps a config key to the changed fields (Partial<IConfigItem> — a value and/or metadata such as editable). Only keys you pass are processed; every other row is left untouched (so ${...} templates are preserved).
  • Returns { updates, requiresReboot }updates is the changed rows ({ key, id, value, requiresReboot }), requiresReboot is the aggregate. Pass { diffOnly: false } to also get the full merged rows.
const { updates, requiresReboot } = Configuard.serializeFlat(rows, {
  'ui.theme': { value: 'dark' },
  port:       { value: '9090' },
  'db.password': { value: 'newSecret' } // encrypt:true row → re-encrypted below
}, {
  encrypt: (value, item) => myEncrypt(value) // for encrypt:true items
});

// updates: [{ key:'ui.theme', id, value:'dark', requiresReboot:false }, …]
// requiresReboot: true   (if any changed row requires it)

Serialization is validate-then-store: numbers/booleans are canonicalized, lists are comma-joined, and other types (hex/date/time/regexp/json/…) are stored as the validated string. Invalid values, option-list violations, a non-editable value change, or an encrypt hook error throw a ConfiguardError.

API

new Configuard(rawConfigList, accessorInfo?, options?)

  • rawConfigList: IConfigItem[] — the flat config rows.
  • accessorInfo?: { accessor?: AccessorType; appAccess?: number } — defaults to application. Throws if accessor is all, or application without an appAccess level.
  • options?: { debugLogs?: boolean; lock?: boolean; decrypt? }debugLogs enables verbose console logging; lock (default true) deep-freezes the built object (set false for a mutable result); decrypt decrypts encrypt: true items (see Encryption).
  • Throws on a corrupt config (see Validation).

Instance Members

  • .data — the built, nested configuration object (frozen unless lock: false).
  • .get<T>(path, defaultValue?) — typed read of a property by notation.
  • .has(path) — whether a property exists at the given notation.
  • .accessor — the resolved accessor for this instance.
  • .isLocked — whether the built object is locked (deep-frozen).
  • .getMeta(key) — a read-only view of a visible item's metadata (type, editable, requiresReboot, encrypt, options, id, …), or undefined.
  • .isEncrypted(key) — whether the item is flagged to be encrypted at rest.
  • .requiresReboot(key) — whether changing the item requires a reboot.

The metadata accessors are ABAC-consistent: they only answer for keys visible to this instance's accessor (mirroring .has()), returning undefined/false otherwise.

Static Members

  • Configuard.parseFlat(configList) — resolve templates + option lists into a flat structure (see above). Returns { '@': Record<string, string[]>, configList: IFlatConfigItem[] }.
  • Configuard.serializeFlat(configList, edits, options?) — validate + serialize admin edits into a diff of DB-ready rows (the inverse of parseFlat; see Saving edits). Returns { updates: IConfigUpdate[], requiresReboot: boolean, rows? }.
  • Configuard.isConfigItem(o)IConfigItem type guard.

The Config Item

Each row implements IConfigItem, mirroring the reference config table (see docs/config.sql):

| Field | Type | Meaning | |-------|------|---------| | accessor | AccessorType | system / application / all. | | appAccess | number \| null | Bitwise flags for application clients (ABAC). | | key | string | Dot/bracket notation; leading @ marks an option list. | | type | ValueType | How value is parsed (see Value types). | | listType | ListType | none / array / csl (see List types). | | value | string \| null | The raw value to parse; may contain ${...} templates. | | options | string \| null | Allowed values, or a "${@name}" option-list reference. | | defaultValue | string \| null | Factory value for reverting. | | requiresReboot | boolean | Whether changing it requires a reboot. | | editable | boolean | Whether the accessor may edit it. | | encrypt | boolean | Whether the value should be encrypted when fetched. | | description | string \| null | Human-readable description. |

See docs/config.sql for the reference config table.

Documentation

Read the full documentation — guides, concepts and the API reference.

Quality

  • 100% test coverage (statements, branches, functions, lines) — enforced in CI via Vitest thresholds. Run npm run cover.
  • ~90% mutation score via StrykerJS. Run npm run mutation.

Mutation testing goes a step beyond coverage: it makes hundreds of tiny edits ("mutants") to the source — flipping > to >=, && to ||, returning undefined, etc. — and checks that a test fails for each. It catches tests that execute code without actually asserting its behavior (the trap where function getPositive(x){ return x } hits 100% coverage but never verifies anything). The remaining surviving mutants here are equivalent mutants (redundant fast-path guards that produce identical results). The general-purpose date utilities (parseDate/createUTCDate) are fully unit-tested but excluded from the mutation scope, as their date-format regexes are dominated by equivalent mutants.

License

MIT.