Flag
Feature-flag primitive shaped to the OpenFeature specification (https://openfeature.dev/specification/). Per-tenant overrides ride on the targeting context ({ targetingKey, environment, ...attrs }); every flip of a flag value at the provider layer audits via flag.evaluated / flag.evaluation.error on the framework audit chain so operators see the change without scraping provider state. Kill-switch semantics fall out of the same resolution chain: a provider that returns value: false for a targeting key disables the gated path immediately — no client restart, no cache flush.
Validation policy: - create() throws on bad opts (provider missing .evaluate(flagKey, ctx), hooks not function-shaped, etc.). - The hot path (getBoolean / getString / getNumber / getObject / getValue) NEVER throws — a provider that errors emits flag.evaluation.error and the call returns the operator-supplied default. A request can't be taken down by a misconfigured flag.
Composability: flag.middleware({ userKey }) attaches a per-request req.flag accessor that bakes the request's targeting context in, so handlers call req.flag.getBoolean(key) without threading context through every site. Multiple providers can be stacked via opts.providers or addProvider/removeProvider — first non-flag_not_found response wins, so a local-file override beats the central provider for break-glass scenarios.
b.flag.cache(downstream, opts?) #
{
ttlMs: number, // entry lifetime, at least 1000; default 30000
maxEntries: number, // ceiling on entries held; default 10000
audit: boolean, // emit flag.cache.bust through b.audit
}
Wrap a flag provider in a TTL cache. Returns a provider of the same shape, { kind, list, evaluate, bust, stats }, so it goes wherever the wrapped one went.
The cache key is the flag key, the context's targetingKey, and a SHA3-512 digest of the whole evaluation context. A targeting rule reads any path through that context, so every field it could decide on is part of the key and one principal's decision is never answered for another's context. The digest keeps the key a fixed length, since a context carries values a client chooses, such as the agent string. A context that cannot be canonicalized, and one carrying no targetingKey, are passed straight to the wrapped provider rather than cached.
A flag_not_found result is not stored. bust() clears every entry and answers how many it held. Entries expire on read, the oldest is dropped when maxEntries is reached, and expired ones are swept every hundredth miss.
var b = require("blamejs");
var provider = b.flag.providers.memory({
flags: { beta: { default: "off", variants: { on: true, off: false } } },
});
var cached = b.flag.cache(provider, { ttlMs: b.constants.TIME.seconds(5) });
cached.evaluate("beta", { targetingKey: "alice" });
cached.evaluate("beta", { targetingKey: "alice" });
cached.stats().hits; // → 1
cached.kind; // → "cache:memory"
b.flag.context.create(input?) #
Freeze an operator-built evaluation context. Copies the own enumerable keys of input, dropping any key that would reach a prototype, and returns the result frozen. Raises flag/bad-context when input is not a plain object or when targetingKey is present and not a string.
var b = require("blamejs");
var ctx = b.flag.context.create({ targetingKey: "alice", role: "admin" });
ctx.role; // → "admin"
Object.isFrozen(ctx); // → true
b.flag.context.merge(base, overlay) #
Layer one evaluation context over another and freeze the result. A key present in overlay wins; a key only in base is carried through. Both sides are read for own enumerable keys alone, so neither can reach a prototype.
var b = require("blamejs");
var ctx = b.flag.context.merge({ role: "guest", locale: "en" },
{ role: "admin" });
ctx.role; // → "admin"
ctx.locale; // → "en"
b.flag.context.fromRequest(req, opts?) #
{
userKey: string, // use this as the targetingKey verbatim
actorKey: function, // (actor) → the name, where the actor's shape is the operator's
tenantKey: string, // overrides the tenant read from the actor
extra: object, // merged into the context
trustedProxies: string[], // CIDRs whose forwarded headers are honored
forwardedHeaders: string[], // named forwarded headers to read
clientIpResolver: function, // (req) → the client address
}
Build the evaluation context for a flag decision from a request. Reads the principal from req.user, the locale from Accept-Language and the agent from User-Agent, and returns a frozen context carrying targetingKey alongside userId, role, email, tenantId, locale and userAgent where the request supplies them. A targeting rule reads any path through that context, so attribute: "role" resolves role.
targetingKey places the principal in a percentage rollout and separates one principal's cached decision from another's. It is b.requestHelpers.actorIdentityKey of the actor, which tags the field the name came from, so { id: "x" } and { sub: "x" } are two principals. opts.userKey replaces it outright. An actor carrying no name that resolver reads, and an unauthenticated request, are keyed by the client address through b.requestHelpers.trustedClientIp, so a forwarded header moves that key only where the operator declared the proxy it arrives through.
var b = require("blamejs");
var ctx = b.flag.context.fromRequest({
user: { sub: "alice", role: "admin" },
headers: { "accept-language": "en-GB,en;q=0.9" },
});
ctx.userId; // → "alice"
ctx.role; // → "admin"
ctx.locale; // → "en-GB"
b.flag.context.bucketOf(targetingKey, flagKey) #
Place a principal in a percentage rollout for one flag: a number from 0 up to 100, two decimal places, from SHA3-512 over the flag key and the targeting key together. The flag key participates, so a principal in the first decile of one rollout is not thereby in the first decile of every rollout. The same pair always answers the same number, and a rollout of p percent admits the principal when the bucket is below p. Either argument missing or empty answers 0.
var b = require("blamejs");
b.flag.context.bucketOf("alice", "beta"); // → 25.04
b.flag.context.bucketOf("", "beta"); // → 0
b.flag.create(opts) #
{
provider: { evaluate: (flagKey, ctx) => result, kind?: string, list?: () => [string] },
providers: [provider], // additional providers (first non-not-found wins)
defaultEvaluationContext: { targetingKey?: string, [attr: string]: any },
audit: boolean, // emit flag.evaluated / flag.evaluation.error; default true
errorHandler: ({ flagKey, err, ctx }) => void,
hooks: { before?: fn, after?: fn, error?: fn, finally?: fn }, // OpenFeature hook stages
}
Build an OpenFeature-shaped flag client backed by one or more providers. The returned object exposes typed getters (getBoolean / getString / getNumber / getObject / getValue), a richer getDetails(flagKey, ctx) (returns { value, variant, reason, metadata }), batch helpers (getValues / getDetailsAll), provider mutation (addProvider / removeProvider), and middleware({ userKey }) for per-request binding.
A request with no identified user gets an anonymous targeting key derived from its client address ALONE. That key decides which bucket the caller lands in, so it is built only from what the caller cannot choose: the User-Agent does not contribute, and the address is resolved peer-gated — forwarded headers are ignored unless middleware is given trustedProxies (CIDRs of your reverse proxies), optionally with forwardedHeaders when the address arrives as something other than X-Forwarded-For, or clientIpResolver to own resolution outright.
Anonymous callers sharing an address therefore share a bucket. Behind a NAT, or behind a proxy with no trustedProxies configured, that can be all of them — pass userKey for callers whose distribution has to be per-user.
Throws FlagError at config time on a missing provider, hooks not shaped as { before?, after?, error?, finally? } functions, or a provider lacking .evaluate(flagKey, ctx).
var provider = {
kind: "memory",
evaluate: function (flagKey, ctx) {
if (flagKey === "new-checkout-flow") {
return { value: ctx.targetingKey === "user-42", variant: "on", reason: "TARGETING_MATCH" };
}
return { reason: "flag_not_found" };
},
};
var flag = b.flag.create({
provider: provider,
defaultEvaluationContext: { environment: "production" },
});
flag.getBoolean("new-checkout-flow", { targetingKey: "user-42" }); // → true
flag.getBoolean("new-checkout-flow", { targetingKey: "user-99" }); // → false
flag.getString ("missing-key", { targetingKey: "u" }, "fallback"); // → "fallback"
// Per-request binding via middleware:
// app.use(flag.middleware({ userKey: "id" }));
// // handler: if (req.flag.getBoolean("new-checkout-flow")) { ... }