@blamejs/core 0.7.107 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/CHANGELOG.md +17 -1
  2. package/NOTICE +17 -1
  3. package/README.md +4 -3
  4. package/index.js +15 -0
  5. package/lib/asyncapi-bindings.js +160 -0
  6. package/lib/asyncapi-traits.js +143 -0
  7. package/lib/asyncapi.js +531 -0
  8. package/lib/audit.js +6 -0
  9. package/lib/auth/acr-vocabulary.js +265 -0
  10. package/lib/auth/auth-time-tracker.js +111 -0
  11. package/lib/auth/elevation-grant.js +306 -0
  12. package/lib/auth/step-up-policy.js +335 -0
  13. package/lib/auth/step-up.js +445 -0
  14. package/lib/compliance-ai-act-logging.js +186 -0
  15. package/lib/compliance-ai-act-prohibited.js +205 -0
  16. package/lib/compliance-ai-act-risk.js +189 -0
  17. package/lib/compliance-ai-act-transparency.js +200 -0
  18. package/lib/compliance-ai-act.js +558 -0
  19. package/lib/compliance.js +2 -0
  20. package/lib/crypto.js +32 -0
  21. package/lib/flag-cache.js +136 -0
  22. package/lib/flag-evaluation-context.js +135 -0
  23. package/lib/flag-providers.js +279 -0
  24. package/lib/flag-targeting.js +210 -0
  25. package/lib/flag.js +284 -0
  26. package/lib/inbox.js +367 -0
  27. package/lib/mail-arc-sign.js +372 -0
  28. package/lib/mail-auth.js +2 -0
  29. package/lib/middleware/ai-act-disclosure.js +166 -0
  30. package/lib/middleware/asyncapi-serve.js +136 -0
  31. package/lib/middleware/flag-context.js +76 -0
  32. package/lib/middleware/index.js +15 -0
  33. package/lib/middleware/openapi-serve.js +143 -0
  34. package/lib/middleware/require-step-up.js +186 -0
  35. package/lib/openapi-paths-builder.js +248 -0
  36. package/lib/openapi-schema-walk.js +192 -0
  37. package/lib/openapi-security.js +169 -0
  38. package/lib/openapi-yaml.js +154 -0
  39. package/lib/openapi.js +443 -0
  40. package/lib/pqc-software.js +195 -0
  41. package/lib/vault/index.js +3 -0
  42. package/lib/vault-aad.js +259 -0
  43. package/lib/vendor/MANIFEST.json +29 -0
  44. package/lib/vendor/noble-post-quantum.cjs +18 -0
  45. package/lib/ws-client.js +829 -0
  46. package/package.json +1 -1
  47. package/sbom.cyclonedx.json +6 -6
@@ -0,0 +1,136 @@
1
+ "use strict";
2
+ /**
3
+ * Flag-evaluation cache — per-targetingKey TTL'd cache wrapping a
4
+ * downstream provider so a high-traffic request path does not hit
5
+ * the provider on every flag read.
6
+ *
7
+ * var raw = b.flag.providers.localFile({ path: "./flags.json", watch: true });
8
+ * var cached = b.flag.cache(raw, { ttlMs: 60_000, maxEntries: 10000 });
9
+ *
10
+ * var flag = b.flag.create({ provider: cached });
11
+ *
12
+ * Cache key: `${targetingKey}::${flagKey}`. Entries TTL out after
13
+ * `ttlMs` (default 30 s). When the cache hits its `maxEntries` cap,
14
+ * oldest entries are evicted (insertion-order via a Map).
15
+ *
16
+ * Cache is bypassed for evaluation contexts without a `targetingKey`
17
+ * (flag value depends on every attribute, not a stable key).
18
+ *
19
+ * Operators with a hot-reload need pass `bustOn: "flag-reload"` and
20
+ * call `cached.bust()` from their reload handler — clears the entire
21
+ * map.
22
+ */
23
+
24
+ var validateOpts = require("./validate-opts");
25
+ var lazyRequire = require("./lazy-require");
26
+ var C = require("./constants");
27
+ var { defineClass } = require("./framework-error");
28
+ var FlagError = defineClass("FlagError", { alwaysPermanent: true });
29
+
30
+ var audit = lazyRequire(function () { return require("./audit"); });
31
+
32
+ function cache(downstream, opts) {
33
+ opts = opts || {};
34
+ validateOpts(opts, ["ttlMs", "maxEntries", "audit"], "flag.cache");
35
+ if (!downstream || typeof downstream.evaluate !== "function") {
36
+ throw new FlagError("flag/bad-cache",
37
+ "cache: downstream provider must implement .evaluate()");
38
+ }
39
+ // allow:numeric-opt-Infinity — defaults clamped + ttl-floor enforced below
40
+ var ttlMs = (typeof opts.ttlMs === "number" && opts.ttlMs > 0)
41
+ ? opts.ttlMs
42
+ : C.TIME.seconds(30);
43
+ if (ttlMs < C.TIME.seconds(1)) {
44
+ throw new FlagError("flag/bad-cache",
45
+ "cache: ttlMs must be >= 1000ms - got " + ttlMs);
46
+ }
47
+ // allow:numeric-opt-Infinity — maxEntries default + Math.floor coerce; throws on bad type at config time
48
+ var maxEntries = (typeof opts.maxEntries === "number" && opts.maxEntries > 0)
49
+ ? Math.floor(opts.maxEntries)
50
+ : 10000; // allow:raw-byte-literal — entry-count default
51
+ var auditOn = opts.audit === true; // off by default — too chatty
52
+ var entries = new Map();
53
+ var hits = 0;
54
+ var misses = 0;
55
+ var evictions = 0;
56
+
57
+ function _evictExpired(nowMs) {
58
+ var iter = entries.entries();
59
+ var step = iter.next();
60
+ while (!step.done) {
61
+ if (step.value[1].expiresAt <= nowMs) {
62
+ entries.delete(step.value[0]);
63
+ evictions += 1;
64
+ }
65
+ step = iter.next();
66
+ }
67
+ }
68
+
69
+ function _evictOldest() {
70
+ var first = entries.keys().next();
71
+ if (!first.done) {
72
+ entries.delete(first.value);
73
+ evictions += 1;
74
+ }
75
+ }
76
+
77
+ return {
78
+ kind: "cache:" + (downstream.kind || "unknown"),
79
+ list: typeof downstream.list === "function"
80
+ ? function () { return downstream.list(); }
81
+ : function () { return []; },
82
+ evaluate: function (flagKey, ctx) {
83
+ var tk = (ctx && typeof ctx.targetingKey === "string") ? ctx.targetingKey : null;
84
+ if (!tk) {
85
+ misses += 1;
86
+ return downstream.evaluate(flagKey, ctx);
87
+ }
88
+ var key = tk + "::" + flagKey;
89
+ var now = Date.now();
90
+ var entry = entries.get(key);
91
+ if (entry && entry.expiresAt > now) {
92
+ hits += 1;
93
+ return entry.value;
94
+ }
95
+ if (entry) entries.delete(key);
96
+ var freshResult = downstream.evaluate(flagKey, ctx);
97
+ misses += 1;
98
+ // Don't cache flag-not-found — operator might add it later.
99
+ if (freshResult && freshResult.reason !== "flag_not_found") {
100
+ if (entries.size >= maxEntries) _evictOldest();
101
+ entries.set(key, { value: freshResult, expiresAt: now + ttlMs });
102
+ }
103
+ // Periodic sweep — evict expired on every 100th miss.
104
+ if (misses % 100 === 0) _evictExpired(now);
105
+ return freshResult;
106
+ },
107
+ bust: function () {
108
+ var prevSize = entries.size;
109
+ entries.clear();
110
+ if (auditOn) {
111
+ try {
112
+ audit().safeEmit({
113
+ action: "flag.cache.bust",
114
+ outcome: "success",
115
+ actor: null,
116
+ metadata: { prevSize: prevSize },
117
+ });
118
+ } catch (_e) { /* drop-silent */ }
119
+ }
120
+ return prevSize;
121
+ },
122
+ stats: function () {
123
+ return {
124
+ size: entries.size,
125
+ hits: hits,
126
+ misses: misses,
127
+ evictions: evictions,
128
+ hitRatio: (hits + misses) === 0 ? 0 : hits / (hits + misses),
129
+ ttlMs: ttlMs,
130
+ maxEntries: maxEntries,
131
+ };
132
+ },
133
+ };
134
+ }
135
+
136
+ module.exports = { cache: cache, FlagError: FlagError };
@@ -0,0 +1,135 @@
1
+ "use strict";
2
+ /**
3
+ * Flag evaluation context — the operator-supplied object describing
4
+ * the subject of a flag evaluation: targeting key, user attributes,
5
+ * tenant id, environment, custom attributes.
6
+ *
7
+ * Per the OpenFeature specification:
8
+ * - `targetingKey` is the canonical identity for percentage-bucket
9
+ * stickiness (so a 50% rollout consistently picks the SAME 50%
10
+ * of users across re-evaluations).
11
+ * - All other attributes flow through targeting rules.
12
+ *
13
+ * The framework's helper produces a frozen, normalised context object.
14
+ * Operators compose contexts incrementally: start from `fromRequest`
15
+ * (extracts user / tenant / locale from req), augment with `merge`,
16
+ * then evaluate.
17
+ */
18
+
19
+ var nodeCrypto = require("crypto");
20
+ var validateOpts = require("./validate-opts");
21
+ var lazyRequire = require("./lazy-require");
22
+ var { defineClass } = require("./framework-error");
23
+ var FlagError = defineClass("FlagError", { alwaysPermanent: true });
24
+
25
+ var fwCrypto = lazyRequire(function () { return require("./crypto"); });
26
+
27
+ function _normalize(input, label) {
28
+ if (input == null) return {};
29
+ if (typeof input !== "object" || Array.isArray(input)) {
30
+ throw new FlagError("flag/bad-context",
31
+ (label || "context") + ": must be a plain object");
32
+ }
33
+ var out = {};
34
+ for (var key in input) {
35
+ if (!Object.prototype.hasOwnProperty.call(input, key)) continue;
36
+ if (key === "__proto__" || key === "constructor" || key === "prototype") {
37
+ continue; // poisoned-keys defense
38
+ }
39
+ out[key] = input[key];
40
+ }
41
+ return out;
42
+ }
43
+
44
+ function create(input) {
45
+ var normalised = _normalize(input, "create");
46
+ if (normalised.targetingKey != null &&
47
+ typeof normalised.targetingKey !== "string") {
48
+ throw new FlagError("flag/bad-context",
49
+ "create: targetingKey must be a string");
50
+ }
51
+ return Object.freeze(normalised);
52
+ }
53
+
54
+ function merge(base, overlay) {
55
+ var b = _normalize(base, "merge.base");
56
+ var o = _normalize(overlay, "merge.overlay");
57
+ var out = {};
58
+ for (var k1 in b) {
59
+ if (Object.prototype.hasOwnProperty.call(b, k1)) out[k1] = b[k1];
60
+ }
61
+ for (var k2 in o) {
62
+ if (Object.prototype.hasOwnProperty.call(o, k2)) out[k2] = o[k2];
63
+ }
64
+ return Object.freeze(out);
65
+ }
66
+
67
+ function fromRequest(req, opts) {
68
+ opts = opts || {};
69
+ validateOpts(opts, ["userKey", "tenantKey", "extra"], "flag.context.fromRequest");
70
+ if (!req || typeof req !== "object") {
71
+ return create({});
72
+ }
73
+ var ctx = {};
74
+ if (req.user) {
75
+ if (typeof req.user.id === "string") ctx.userId = req.user.id;
76
+ if (typeof req.user.role === "string") ctx.role = req.user.role;
77
+ if (typeof req.user.email === "string") ctx.email = req.user.email;
78
+ if (req.user.tenantId != null) ctx.tenantId = req.user.tenantId;
79
+ }
80
+ var headers = req.headers || {};
81
+ if (typeof headers["accept-language"] === "string") {
82
+ ctx.locale = headers["accept-language"].split(",")[0].split(";")[0].trim();
83
+ }
84
+ if (typeof headers["user-agent"] === "string") {
85
+ ctx.userAgent = headers["user-agent"];
86
+ }
87
+ // Targeting key: prefer explicit userKey opt, then user.id, then a
88
+ // request-stable hash of (clientIp + userAgent) for anonymous flows.
89
+ var tk = null;
90
+ if (typeof opts.userKey === "string" && opts.userKey.length > 0) {
91
+ tk = opts.userKey;
92
+ } else if (req.user && typeof req.user.id === "string") {
93
+ tk = req.user.id;
94
+ } else {
95
+ var ip = (typeof headers["x-forwarded-for"] === "string" &&
96
+ headers["x-forwarded-for"].split(",")[0].trim()) ||
97
+ (req.connection && req.connection.remoteAddress) || "";
98
+ var ua = headers["user-agent"] || "";
99
+ tk = "anon:" + fwCrypto().sha3Hash(ip + ":" + ua).slice(0, 16); // allow:raw-byte-literal — base16 prefix len
100
+ }
101
+ ctx.targetingKey = tk;
102
+
103
+ if (opts.extra && typeof opts.extra === "object") {
104
+ for (var k in opts.extra) {
105
+ if (Object.prototype.hasOwnProperty.call(opts.extra, k)) {
106
+ if (k === "__proto__" || k === "constructor" || k === "prototype") continue;
107
+ ctx[k] = opts.extra[k];
108
+ }
109
+ }
110
+ }
111
+ return create(ctx);
112
+ }
113
+
114
+ // Percentage-bucket helper — deterministic hash of (targetingKey +
115
+ // flagKey) into [0, 100) for percentage-based rollouts.
116
+ function bucketOf(targetingKey, flagKey) {
117
+ if (typeof targetingKey !== "string" || typeof flagKey !== "string" ||
118
+ targetingKey.length === 0 || flagKey.length === 0) {
119
+ return 0;
120
+ }
121
+ var digest = nodeCrypto.createHash("sha3-512")
122
+ .update(flagKey + ":" + targetingKey).digest();
123
+ // Use first 4 bytes as a uint32, then mod 10000 → 0.00-99.99 with
124
+ // sub-percent granularity.
125
+ var n = digest.readUInt32BE(0);
126
+ return (n % 10000) / 100; // allow:raw-byte-literal — bucket-precision divisor
127
+ }
128
+
129
+ module.exports = {
130
+ create: create,
131
+ merge: merge,
132
+ fromRequest: fromRequest,
133
+ bucketOf: bucketOf,
134
+ FlagError: FlagError,
135
+ };
@@ -0,0 +1,279 @@
1
+ "use strict";
2
+ /**
3
+ * Flag providers — backend implementations that produce flag values
4
+ * for a given (flagKey, evaluationContext) tuple.
5
+ *
6
+ * Three first-party providers ship with the framework:
7
+ *
8
+ * localFile({ path, watch?, signature? })
9
+ * Reads a JSON file at boot and on change-events. Each flag entry
10
+ * describes default-variant + variants + targeting-rules +
11
+ * percentage-rollouts.
12
+ *
13
+ * memory({ flags })
14
+ * In-process map of flagKey to flag-spec. Useful for tests and
15
+ * for operators who treat flags as code (compiled into the boot
16
+ * image).
17
+ *
18
+ * environmentVariable({ envVarPattern, prefix })
19
+ * Reads a flag's value from process.env. Useful for boot-time
20
+ * toggles bound to deployment configuration.
21
+ *
22
+ * Operators with a remote-flag-management plane (LaunchDarkly, flagd,
23
+ * Unleash, OpenFeature gRPC) wire their own provider implementing the
24
+ * `evaluate(flagKey, ctx)` contract and pass it to b.flag.create.
25
+ *
26
+ * Provider contract:
27
+ *
28
+ * provider.evaluate(flagKey, ctx) -> {
29
+ * value: any, // resolved value (boolean / string / number / object)
30
+ * variant: string, // variant name ("on" / "off" / "treatment-A" / ...)
31
+ * reason: string, // "default" | "targeting_match" | "split" | ...
32
+ * metadata: object, // optional per-provider hints
33
+ * }
34
+ *
35
+ * provider.list() -> string[] list of registered flag keys (for tooling)
36
+ * provider.kind -> "local-file" | "memory" | "environment" | <operator-defined>
37
+ */
38
+
39
+ var fs = require("fs");
40
+ var validateOpts = require("./validate-opts");
41
+ var lazyRequire = require("./lazy-require");
42
+ var safeJson = require("./safe-json");
43
+ var C = require("./constants");
44
+ var { defineClass } = require("./framework-error");
45
+ var FlagError = defineClass("FlagError", { alwaysPermanent: true });
46
+
47
+ var targeting = require("./flag-targeting");
48
+ var contextMod = lazyRequire(function () { return require("./flag-evaluation-context"); });
49
+
50
+ function _validateFlagSpec(flagKey, spec) {
51
+ if (!spec || typeof spec !== "object") {
52
+ throw new FlagError("flag/bad-spec",
53
+ "flag spec for " + JSON.stringify(flagKey) + " must be an object");
54
+ }
55
+ validateOpts(spec, [
56
+ "default", "variants", "rules", "rollout",
57
+ "description", "tags", "kind",
58
+ ], "flag spec for " + flagKey);
59
+ if (spec.variants == null || typeof spec.variants !== "object") {
60
+ throw new FlagError("flag/bad-spec",
61
+ flagKey + ": variants object is required (variantName -> value)");
62
+ }
63
+ if (typeof spec.default !== "string" ||
64
+ !Object.prototype.hasOwnProperty.call(spec.variants, spec.default)) {
65
+ throw new FlagError("flag/bad-spec",
66
+ flagKey + ": default must be a variant name; got " + JSON.stringify(spec.default));
67
+ }
68
+ if (spec.rules != null) {
69
+ targeting.validateRules(spec.rules, flagKey + ".rules");
70
+ // Every rule's variant must be a registered variant.
71
+ for (var i = 0; i < spec.rules.length; i += 1) {
72
+ var v = spec.rules[i].variant;
73
+ if (!Object.prototype.hasOwnProperty.call(spec.variants, v)) {
74
+ throw new FlagError("flag/bad-spec",
75
+ flagKey + ".rules[" + i + "].variant: " + JSON.stringify(v) +
76
+ " is not a registered variant");
77
+ }
78
+ }
79
+ }
80
+ if (spec.rollout != null) {
81
+ if (!Array.isArray(spec.rollout)) {
82
+ throw new FlagError("flag/bad-spec",
83
+ flagKey + ".rollout: must be an array of { variant, percentage } entries");
84
+ }
85
+ var sum = 0;
86
+ for (var j = 0; j < spec.rollout.length; j += 1) {
87
+ var entry = spec.rollout[j];
88
+ if (!entry || typeof entry !== "object" ||
89
+ typeof entry.variant !== "string" ||
90
+ typeof entry.percentage !== "number" ||
91
+ entry.percentage < 0 || entry.percentage > 100) {
92
+ throw new FlagError("flag/bad-spec",
93
+ flagKey + ".rollout[" + j + "]: must be { variant: string, percentage: 0..100 }");
94
+ }
95
+ if (!Object.prototype.hasOwnProperty.call(spec.variants, entry.variant)) {
96
+ throw new FlagError("flag/bad-spec",
97
+ flagKey + ".rollout[" + j + "].variant: " + JSON.stringify(entry.variant) +
98
+ " is not a registered variant");
99
+ }
100
+ sum += entry.percentage;
101
+ }
102
+ if (sum > 100.0001) {
103
+ throw new FlagError("flag/bad-spec",
104
+ flagKey + ".rollout: percentage sum must be <= 100; got " + sum);
105
+ }
106
+ }
107
+ }
108
+
109
+ function memory(opts) {
110
+ opts = opts || {};
111
+ validateOpts(opts, ["flags"], "flag.providers.memory");
112
+ if (!opts.flags || typeof opts.flags !== "object") {
113
+ throw new FlagError("flag/bad-provider",
114
+ "providers.memory: flags object required (flagKey -> spec)");
115
+ }
116
+ var flags = {};
117
+ for (var key in opts.flags) {
118
+ if (!Object.prototype.hasOwnProperty.call(opts.flags, key)) continue;
119
+ _validateFlagSpec(key, opts.flags[key]);
120
+ flags[key] = opts.flags[key];
121
+ }
122
+ return _makeProvider("memory", flags);
123
+ }
124
+
125
+ function localFile(opts) {
126
+ opts = opts || {};
127
+ validateOpts(opts, ["path", "watch"], "flag.providers.localFile");
128
+ validateOpts.requireNonEmptyString(opts.path,
129
+ "providers.localFile: path", FlagError, "flag/bad-provider");
130
+ var raw;
131
+ try { raw = fs.readFileSync(opts.path, "utf8"); }
132
+ catch (e) {
133
+ throw new FlagError("flag/bad-provider",
134
+ "providers.localFile: cannot read file " + JSON.stringify(opts.path) +
135
+ " - " + e.message);
136
+ }
137
+ var parsed;
138
+ try { parsed = safeJson.parse(raw, { maxBytes: C.BYTES.mib(1) }); }
139
+ catch (e) {
140
+ throw new FlagError("flag/bad-provider",
141
+ "providers.localFile: invalid JSON in " + opts.path + " - " + e.message);
142
+ }
143
+ if (!parsed || typeof parsed !== "object" || !parsed.flags) {
144
+ throw new FlagError("flag/bad-provider",
145
+ "providers.localFile: file must export { flags: { flagKey: spec, ... } }");
146
+ }
147
+ for (var key in parsed.flags) {
148
+ if (!Object.prototype.hasOwnProperty.call(parsed.flags, key)) continue;
149
+ _validateFlagSpec(key, parsed.flags[key]);
150
+ }
151
+ var provider = _makeProvider("local-file", parsed.flags);
152
+ provider._path = opts.path;
153
+ if (opts.watch === true) {
154
+ try {
155
+ fs.watch(opts.path, { persistent: false }, function () {
156
+ try {
157
+ var nextRaw = fs.readFileSync(opts.path, "utf8");
158
+ var nextParsed = safeJson.parse(nextRaw, { maxBytes: C.BYTES.mib(1) });
159
+ if (nextParsed && nextParsed.flags) {
160
+ for (var k in nextParsed.flags) {
161
+ if (Object.prototype.hasOwnProperty.call(nextParsed.flags, k)) {
162
+ _validateFlagSpec(k, nextParsed.flags[k]);
163
+ }
164
+ }
165
+ provider._replace(nextParsed.flags);
166
+ }
167
+ } catch (_e) { /* drop-silent on hot-path reload */ }
168
+ });
169
+ } catch (_w) { /* watch unavailable - non-fatal */ }
170
+ }
171
+ return provider;
172
+ }
173
+
174
+ function environmentVariable(opts) {
175
+ opts = opts || {};
176
+ validateOpts(opts, ["prefix", "flags"], "flag.providers.environmentVariable");
177
+ var prefix = (typeof opts.prefix === "string" && opts.prefix.length > 0)
178
+ ? opts.prefix
179
+ : "FLAG_";
180
+ if (!opts.flags || typeof opts.flags !== "object") {
181
+ throw new FlagError("flag/bad-provider",
182
+ "providers.environmentVariable: flags object required to bound the surface");
183
+ }
184
+ var resolved = {};
185
+ for (var key in opts.flags) {
186
+ if (!Object.prototype.hasOwnProperty.call(opts.flags, key)) continue;
187
+ _validateFlagSpec(key, opts.flags[key]);
188
+ var envName = prefix + key.toUpperCase().replace(/[-.]/g, "_");
189
+ var envValue = process.env[envName];
190
+ var clone = Object.assign({}, opts.flags[key]);
191
+ if (typeof envValue === "string" && envValue.length > 0) {
192
+ // Map known env semantics: "true"/"false" override boolean flags
193
+ // by replacing the default variant. Operators wanting richer
194
+ // overrides ship a different provider.
195
+ var variantNames = Object.keys(opts.flags[key].variants);
196
+ if (variantNames.indexOf(envValue) !== -1) {
197
+ clone.default = envValue;
198
+ } else if (envValue === "true" && variantNames.indexOf("on") !== -1) {
199
+ clone.default = "on";
200
+ } else if (envValue === "false" && variantNames.indexOf("off") !== -1) {
201
+ clone.default = "off";
202
+ }
203
+ }
204
+ resolved[key] = clone;
205
+ }
206
+ return _makeProvider("environment", resolved);
207
+ }
208
+
209
+ function _makeProvider(kind, initialFlags) {
210
+ var flags = initialFlags;
211
+ var provider = {
212
+ kind: kind,
213
+ list: function () { return Object.keys(flags); },
214
+ get: function (flagKey) { return flags[flagKey] || null; },
215
+ _replace: function (newFlags) { flags = newFlags; },
216
+ evaluate: function (flagKey, ctx) {
217
+ var spec = flags[flagKey];
218
+ if (!spec) {
219
+ return {
220
+ value: undefined,
221
+ variant: null,
222
+ reason: "flag_not_found",
223
+ metadata: { flagKey: flagKey, provider: kind },
224
+ };
225
+ }
226
+ // 1. Targeting rules
227
+ var targetingResult = targeting.evaluateRules(spec.rules || [], ctx, spec.default);
228
+ if (targetingResult.reason === "targeting_match") {
229
+ return _buildResult(flagKey, spec, targetingResult.variant,
230
+ "targeting_match", { ruleIndex: targetingResult.ruleIndex });
231
+ }
232
+ // 2. Percentage rollout
233
+ if (Array.isArray(spec.rollout) && spec.rollout.length > 0) {
234
+ var tk = (ctx && typeof ctx.targetingKey === "string") ? ctx.targetingKey : "";
235
+ if (tk.length > 0) {
236
+ var bucket = contextMod().bucketOf(tk, flagKey);
237
+ var cumulative = 0;
238
+ for (var i = 0; i < spec.rollout.length; i += 1) {
239
+ cumulative += spec.rollout[i].percentage;
240
+ if (bucket < cumulative) {
241
+ return _buildResult(flagKey, spec, spec.rollout[i].variant,
242
+ "split", { bucket: bucket });
243
+ }
244
+ }
245
+ }
246
+ }
247
+ // 3. Default variant
248
+ return _buildResult(flagKey, spec, spec.default, "default", {});
249
+ },
250
+ };
251
+ return provider;
252
+ }
253
+
254
+ function _buildResult(flagKey, spec, variantName, reason, metaAdd) {
255
+ var value = spec.variants[variantName];
256
+ if (value === undefined) {
257
+ // Unknown variant from rollout/rule (validated at registration,
258
+ // so this only fires if a rule passed validation but referenced
259
+ // a since-deleted variant).
260
+ value = spec.variants[spec.default];
261
+ variantName = spec.default;
262
+ reason = "default_fallback";
263
+ }
264
+ var metadata = { flagKey: flagKey, provider: spec._provider || null };
265
+ if (metaAdd) {
266
+ for (var k in metaAdd) {
267
+ if (Object.prototype.hasOwnProperty.call(metaAdd, k)) metadata[k] = metaAdd[k];
268
+ }
269
+ }
270
+ return { value: value, variant: variantName, reason: reason, metadata: metadata };
271
+ }
272
+
273
+ module.exports = {
274
+ memory: memory,
275
+ localFile: localFile,
276
+ environmentVariable: environmentVariable,
277
+ _validateFlagSpec: _validateFlagSpec,
278
+ FlagError: FlagError,
279
+ };