@blamejs/core 0.7.107 → 0.8.4
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.
- package/CHANGELOG.md +41 -1
- package/NOTICE +17 -1
- package/README.md +4 -3
- package/index.js +15 -0
- package/lib/asyncapi-bindings.js +160 -0
- package/lib/asyncapi-traits.js +143 -0
- package/lib/asyncapi.js +531 -0
- package/lib/audit-sign.js +1 -1
- package/lib/audit.js +68 -2
- package/lib/auth/acr-vocabulary.js +265 -0
- package/lib/auth/auth-time-tracker.js +111 -0
- package/lib/auth/elevation-grant.js +306 -0
- package/lib/auth/jwt.js +13 -0
- package/lib/auth/lockout.js +16 -3
- package/lib/auth/oauth.js +15 -1
- package/lib/auth/password.js +22 -2
- package/lib/auth/sd-jwt-vc-issuer.js +2 -2
- package/lib/auth/sd-jwt-vc.js +7 -2
- package/lib/auth/step-up-policy.js +335 -0
- package/lib/auth/step-up.js +445 -0
- package/lib/break-glass.js +53 -14
- package/lib/cache-redis.js +1 -1
- package/lib/cache.js +6 -1
- package/lib/cli.js +3 -3
- package/lib/cluster.js +24 -1
- package/lib/compliance-ai-act-logging.js +190 -0
- package/lib/compliance-ai-act-prohibited.js +205 -0
- package/lib/compliance-ai-act-risk.js +189 -0
- package/lib/compliance-ai-act-transparency.js +200 -0
- package/lib/compliance-ai-act.js +558 -0
- package/lib/compliance.js +12 -2
- package/lib/config-drift.js +2 -2
- package/lib/crypto-field.js +21 -1
- package/lib/crypto.js +114 -1
- package/lib/db.js +35 -4
- package/lib/dev.js +30 -3
- package/lib/dual-control.js +19 -1
- package/lib/external-db.js +10 -0
- package/lib/file-upload.js +30 -3
- package/lib/flag-cache.js +136 -0
- package/lib/flag-evaluation-context.js +135 -0
- package/lib/flag-providers.js +279 -0
- package/lib/flag-targeting.js +210 -0
- package/lib/flag.js +284 -0
- package/lib/guard-all.js +33 -16
- package/lib/guard-csv.js +16 -2
- package/lib/guard-html.js +35 -0
- package/lib/guard-svg.js +20 -0
- package/lib/http-client.js +57 -11
- package/lib/inbox.js +391 -0
- package/lib/log-stream-syslog.js +8 -0
- package/lib/log-stream.js +1 -1
- package/lib/mail-arc-sign.js +372 -0
- package/lib/mail-auth.js +2 -0
- package/lib/mail.js +40 -0
- package/lib/middleware/ai-act-disclosure.js +166 -0
- package/lib/middleware/asyncapi-serve.js +136 -0
- package/lib/middleware/attach-user.js +25 -2
- package/lib/middleware/bearer-auth.js +71 -6
- package/lib/middleware/body-parser.js +13 -0
- package/lib/middleware/cors.js +10 -0
- package/lib/middleware/csrf-protect.js +34 -3
- package/lib/middleware/dpop.js +3 -3
- package/lib/middleware/flag-context.js +76 -0
- package/lib/middleware/host-allowlist.js +1 -1
- package/lib/middleware/index.js +15 -0
- package/lib/middleware/openapi-serve.js +143 -0
- package/lib/middleware/require-aal.js +2 -2
- package/lib/middleware/require-step-up.js +186 -0
- package/lib/middleware/trace-propagate.js +1 -1
- package/lib/mtls-ca.js +23 -29
- package/lib/mtls-engine-default.js +21 -1
- package/lib/network-tls.js +21 -6
- package/lib/object-store/sigv4-bucket-ops.js +41 -0
- package/lib/observability-otlp-exporter.js +35 -2
- package/lib/openapi-paths-builder.js +248 -0
- package/lib/openapi-schema-walk.js +192 -0
- package/lib/openapi-security.js +169 -0
- package/lib/openapi-yaml.js +154 -0
- package/lib/openapi.js +443 -0
- package/lib/outbox.js +3 -3
- package/lib/permissions.js +10 -1
- package/lib/pqc-agent.js +22 -1
- package/lib/pqc-software.js +195 -0
- package/lib/pubsub.js +8 -4
- package/lib/redact.js +26 -1
- package/lib/retention.js +26 -0
- package/lib/router.js +1 -0
- package/lib/scheduler.js +57 -1
- package/lib/session.js +3 -3
- package/lib/ssrf-guard.js +19 -4
- package/lib/static.js +12 -0
- package/lib/totp.js +16 -0
- package/lib/vault/index.js +3 -0
- package/lib/vault-aad.js +259 -0
- package/lib/vendor/MANIFEST.json +29 -0
- package/lib/vendor/noble-post-quantum.cjs +18 -0
- package/lib/ws-client.js +978 -0
- package/package.json +1 -1
- package/sbom.cyclonedx.json +6 -6
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Flag targeting — rule-based evaluation against an operator's
|
|
4
|
+
* evaluation-context object (subject id, role, region, custom
|
|
5
|
+
* attributes). Operators describe targeting in declarative JSON; the
|
|
6
|
+
* framework evaluates without expression-injection risk.
|
|
7
|
+
*
|
|
8
|
+
* Rule shape:
|
|
9
|
+
*
|
|
10
|
+
* { variant: "on", conditions: [
|
|
11
|
+
* { attribute: "user.role", op: "eq", value: "admin" },
|
|
12
|
+
* { attribute: "user.region", op: "in", value: ["EU", "UK"] },
|
|
13
|
+
* { attribute: "user.tier", op: "gte", value: 2 },
|
|
14
|
+
* ] }
|
|
15
|
+
*
|
|
16
|
+
* Operators are: eq / neq / in / nin / gt / gte / lt / lte / startsWith
|
|
17
|
+
* / endsWith / contains / regex / exists / not_exists / between.
|
|
18
|
+
*
|
|
19
|
+
* Evaluation is conjunctive across `conditions` (all must pass for
|
|
20
|
+
* the variant to apply). Multiple rules are evaluated in declaration
|
|
21
|
+
* order; first match wins. If no rule matches, the flag's default
|
|
22
|
+
* variant is returned.
|
|
23
|
+
*
|
|
24
|
+
* Per the validation-tier policy: rule-shape validation throws at
|
|
25
|
+
* boot (config-time entry-point); evaluation hot-path returns
|
|
26
|
+
* structured falsey on bad-shape rather than throwing.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
var validateOpts = require("./validate-opts");
|
|
30
|
+
var { defineClass } = require("./framework-error");
|
|
31
|
+
var FlagError = defineClass("FlagError", { alwaysPermanent: true });
|
|
32
|
+
|
|
33
|
+
var VALID_OPS = [
|
|
34
|
+
"eq", "neq", "in", "nin", "gt", "gte", "lt", "lte",
|
|
35
|
+
"starts_with", "ends_with", "contains",
|
|
36
|
+
"regex", "exists", "not_exists", "between",
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
function _readPath(ctx, attribute) {
|
|
40
|
+
if (typeof attribute !== "string" || attribute.length === 0) return undefined;
|
|
41
|
+
if (!ctx || typeof ctx !== "object") return undefined;
|
|
42
|
+
var parts = attribute.split(".");
|
|
43
|
+
var current = ctx;
|
|
44
|
+
for (var i = 0; i < parts.length; i += 1) {
|
|
45
|
+
if (current == null || typeof current !== "object") return undefined;
|
|
46
|
+
current = current[parts[i]];
|
|
47
|
+
}
|
|
48
|
+
return current;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function _evaluateCondition(condition, ctx) {
|
|
52
|
+
if (!condition || typeof condition !== "object") return false;
|
|
53
|
+
if (typeof condition.op !== "string") return false;
|
|
54
|
+
if (VALID_OPS.indexOf(condition.op) === -1) return false;
|
|
55
|
+
|
|
56
|
+
var presented = _readPath(ctx, condition.attribute);
|
|
57
|
+
switch (condition.op) {
|
|
58
|
+
case "eq": return presented === condition.value;
|
|
59
|
+
case "neq": return presented !== condition.value;
|
|
60
|
+
case "in": return Array.isArray(condition.value) &&
|
|
61
|
+
condition.value.indexOf(presented) !== -1;
|
|
62
|
+
case "nin": return Array.isArray(condition.value) &&
|
|
63
|
+
condition.value.indexOf(presented) === -1;
|
|
64
|
+
case "gt": return typeof presented === "number" &&
|
|
65
|
+
typeof condition.value === "number" &&
|
|
66
|
+
presented > condition.value;
|
|
67
|
+
case "gte": return typeof presented === "number" &&
|
|
68
|
+
typeof condition.value === "number" &&
|
|
69
|
+
presented >= condition.value;
|
|
70
|
+
case "lt": return typeof presented === "number" &&
|
|
71
|
+
typeof condition.value === "number" &&
|
|
72
|
+
presented < condition.value;
|
|
73
|
+
case "lte": return typeof presented === "number" &&
|
|
74
|
+
typeof condition.value === "number" &&
|
|
75
|
+
presented <= condition.value;
|
|
76
|
+
case "starts_with": return typeof presented === "string" &&
|
|
77
|
+
typeof condition.value === "string" &&
|
|
78
|
+
presented.indexOf(condition.value) === 0;
|
|
79
|
+
case "ends_with": return typeof presented === "string" &&
|
|
80
|
+
typeof condition.value === "string" &&
|
|
81
|
+
presented.length >= condition.value.length &&
|
|
82
|
+
presented.slice(-condition.value.length) === condition.value;
|
|
83
|
+
case "contains": return typeof presented === "string" &&
|
|
84
|
+
typeof condition.value === "string" &&
|
|
85
|
+
presented.indexOf(condition.value) !== -1;
|
|
86
|
+
case "regex":
|
|
87
|
+
// Regex bounded — operator-supplied regex compiled at rule-validate
|
|
88
|
+
// time and re-used here. Refuse to evaluate if regex isn't pre-
|
|
89
|
+
// compiled (defense against runtime regex compilation per call).
|
|
90
|
+
if (!(condition._compiledRegex instanceof RegExp)) return false;
|
|
91
|
+
return typeof presented === "string" &&
|
|
92
|
+
condition._compiledRegex.test(presented);
|
|
93
|
+
case "exists": return presented !== undefined;
|
|
94
|
+
case "not_exists": return presented === undefined;
|
|
95
|
+
case "between": return Array.isArray(condition.value) &&
|
|
96
|
+
condition.value.length === 2 &&
|
|
97
|
+
typeof presented === "number" &&
|
|
98
|
+
presented >= condition.value[0] &&
|
|
99
|
+
presented <= condition.value[1];
|
|
100
|
+
default: return false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function evaluateRules(rules, ctx, defaultVariant) {
|
|
105
|
+
if (!Array.isArray(rules)) return { variant: defaultVariant, ruleIndex: -1, reason: "default" };
|
|
106
|
+
for (var i = 0; i < rules.length; i += 1) {
|
|
107
|
+
var rule = rules[i];
|
|
108
|
+
if (!rule || typeof rule !== "object") continue;
|
|
109
|
+
if (!Array.isArray(rule.conditions)) continue;
|
|
110
|
+
var allPass = true;
|
|
111
|
+
for (var j = 0; j < rule.conditions.length; j += 1) {
|
|
112
|
+
if (!_evaluateCondition(rule.conditions[j], ctx)) {
|
|
113
|
+
allPass = false; break;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (allPass) {
|
|
117
|
+
return { variant: rule.variant, ruleIndex: i, reason: "targeting_match" };
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return { variant: defaultVariant, ruleIndex: -1, reason: "default" };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function validateRules(rules, label) {
|
|
124
|
+
label = label || "rules";
|
|
125
|
+
if (rules == null) return [];
|
|
126
|
+
if (!Array.isArray(rules)) {
|
|
127
|
+
throw new FlagError("flag/bad-rules",
|
|
128
|
+
label + ": rules must be an array of rule objects");
|
|
129
|
+
}
|
|
130
|
+
var validated = [];
|
|
131
|
+
for (var i = 0; i < rules.length; i += 1) {
|
|
132
|
+
var rule = rules[i];
|
|
133
|
+
if (!rule || typeof rule !== "object") {
|
|
134
|
+
throw new FlagError("flag/bad-rule",
|
|
135
|
+
label + "[" + i + "]: rule must be an object");
|
|
136
|
+
}
|
|
137
|
+
validateOpts(rule, ["variant", "conditions", "weight"], label + "[" + i + "]");
|
|
138
|
+
validateOpts.requireNonEmptyString(rule.variant, label + "[" + i + "].variant",
|
|
139
|
+
FlagError, "flag/bad-rule");
|
|
140
|
+
if (!Array.isArray(rule.conditions)) {
|
|
141
|
+
throw new FlagError("flag/bad-rule",
|
|
142
|
+
label + "[" + i + "].conditions: must be an array");
|
|
143
|
+
}
|
|
144
|
+
var validatedConds = [];
|
|
145
|
+
for (var j = 0; j < rule.conditions.length; j += 1) {
|
|
146
|
+
var cond = rule.conditions[j];
|
|
147
|
+
var clabel = label + "[" + i + "].conditions[" + j + "]";
|
|
148
|
+
if (!cond || typeof cond !== "object") {
|
|
149
|
+
throw new FlagError("flag/bad-condition",
|
|
150
|
+
clabel + ": condition must be an object");
|
|
151
|
+
}
|
|
152
|
+
validateOpts(cond, ["attribute", "op", "value"], clabel);
|
|
153
|
+
validateOpts.requireNonEmptyString(cond.attribute, clabel + ".attribute",
|
|
154
|
+
FlagError, "flag/bad-condition");
|
|
155
|
+
if (VALID_OPS.indexOf(cond.op) === -1) {
|
|
156
|
+
throw new FlagError("flag/bad-condition",
|
|
157
|
+
clabel + ".op: must be one of " + VALID_OPS.join(", ") +
|
|
158
|
+
" - got " + JSON.stringify(cond.op));
|
|
159
|
+
}
|
|
160
|
+
var validatedCond = {
|
|
161
|
+
attribute: cond.attribute,
|
|
162
|
+
op: cond.op,
|
|
163
|
+
value: cond.value,
|
|
164
|
+
};
|
|
165
|
+
if (cond.op === "regex") {
|
|
166
|
+
if (typeof cond.value !== "string") {
|
|
167
|
+
throw new FlagError("flag/bad-condition",
|
|
168
|
+
clabel + ".value: regex op requires a string value");
|
|
169
|
+
}
|
|
170
|
+
if (cond.value.length > 200) {
|
|
171
|
+
throw new FlagError("flag/bad-condition",
|
|
172
|
+
clabel + ".value: regex pattern must be <= 200 chars (DoS defense)");
|
|
173
|
+
}
|
|
174
|
+
try {
|
|
175
|
+
// allow:dynamic-regex — operator-supplied targeting pattern, length-bounded to 200 chars above
|
|
176
|
+
validatedCond._compiledRegex = new RegExp(cond.value);
|
|
177
|
+
} catch (e) {
|
|
178
|
+
throw new FlagError("flag/bad-condition",
|
|
179
|
+
clabel + ".value: invalid regex - " + e.message);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (cond.op === "between") {
|
|
183
|
+
if (!Array.isArray(cond.value) || cond.value.length !== 2 ||
|
|
184
|
+
typeof cond.value[0] !== "number" || typeof cond.value[1] !== "number") {
|
|
185
|
+
throw new FlagError("flag/bad-condition",
|
|
186
|
+
clabel + ".value: between op requires [number, number]");
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if ((cond.op === "in" || cond.op === "nin") && !Array.isArray(cond.value)) {
|
|
190
|
+
throw new FlagError("flag/bad-condition",
|
|
191
|
+
clabel + ".value: " + cond.op + " op requires an array value");
|
|
192
|
+
}
|
|
193
|
+
validatedConds.push(validatedCond);
|
|
194
|
+
}
|
|
195
|
+
validated.push({
|
|
196
|
+
variant: rule.variant,
|
|
197
|
+
conditions: validatedConds,
|
|
198
|
+
weight: (typeof rule.weight === "number") ? rule.weight : null,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
return validated;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
module.exports = {
|
|
205
|
+
evaluateRules: evaluateRules,
|
|
206
|
+
validateRules: validateRules,
|
|
207
|
+
VALID_OPS: VALID_OPS,
|
|
208
|
+
_readPath: _readPath,
|
|
209
|
+
FlagError: FlagError,
|
|
210
|
+
};
|