@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.
Files changed (100) hide show
  1. package/CHANGELOG.md +41 -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-sign.js +1 -1
  9. package/lib/audit.js +68 -2
  10. package/lib/auth/acr-vocabulary.js +265 -0
  11. package/lib/auth/auth-time-tracker.js +111 -0
  12. package/lib/auth/elevation-grant.js +306 -0
  13. package/lib/auth/jwt.js +13 -0
  14. package/lib/auth/lockout.js +16 -3
  15. package/lib/auth/oauth.js +15 -1
  16. package/lib/auth/password.js +22 -2
  17. package/lib/auth/sd-jwt-vc-issuer.js +2 -2
  18. package/lib/auth/sd-jwt-vc.js +7 -2
  19. package/lib/auth/step-up-policy.js +335 -0
  20. package/lib/auth/step-up.js +445 -0
  21. package/lib/break-glass.js +53 -14
  22. package/lib/cache-redis.js +1 -1
  23. package/lib/cache.js +6 -1
  24. package/lib/cli.js +3 -3
  25. package/lib/cluster.js +24 -1
  26. package/lib/compliance-ai-act-logging.js +190 -0
  27. package/lib/compliance-ai-act-prohibited.js +205 -0
  28. package/lib/compliance-ai-act-risk.js +189 -0
  29. package/lib/compliance-ai-act-transparency.js +200 -0
  30. package/lib/compliance-ai-act.js +558 -0
  31. package/lib/compliance.js +12 -2
  32. package/lib/config-drift.js +2 -2
  33. package/lib/crypto-field.js +21 -1
  34. package/lib/crypto.js +114 -1
  35. package/lib/db.js +35 -4
  36. package/lib/dev.js +30 -3
  37. package/lib/dual-control.js +19 -1
  38. package/lib/external-db.js +10 -0
  39. package/lib/file-upload.js +30 -3
  40. package/lib/flag-cache.js +136 -0
  41. package/lib/flag-evaluation-context.js +135 -0
  42. package/lib/flag-providers.js +279 -0
  43. package/lib/flag-targeting.js +210 -0
  44. package/lib/flag.js +284 -0
  45. package/lib/guard-all.js +33 -16
  46. package/lib/guard-csv.js +16 -2
  47. package/lib/guard-html.js +35 -0
  48. package/lib/guard-svg.js +20 -0
  49. package/lib/http-client.js +57 -11
  50. package/lib/inbox.js +391 -0
  51. package/lib/log-stream-syslog.js +8 -0
  52. package/lib/log-stream.js +1 -1
  53. package/lib/mail-arc-sign.js +372 -0
  54. package/lib/mail-auth.js +2 -0
  55. package/lib/mail.js +40 -0
  56. package/lib/middleware/ai-act-disclosure.js +166 -0
  57. package/lib/middleware/asyncapi-serve.js +136 -0
  58. package/lib/middleware/attach-user.js +25 -2
  59. package/lib/middleware/bearer-auth.js +71 -6
  60. package/lib/middleware/body-parser.js +13 -0
  61. package/lib/middleware/cors.js +10 -0
  62. package/lib/middleware/csrf-protect.js +34 -3
  63. package/lib/middleware/dpop.js +3 -3
  64. package/lib/middleware/flag-context.js +76 -0
  65. package/lib/middleware/host-allowlist.js +1 -1
  66. package/lib/middleware/index.js +15 -0
  67. package/lib/middleware/openapi-serve.js +143 -0
  68. package/lib/middleware/require-aal.js +2 -2
  69. package/lib/middleware/require-step-up.js +186 -0
  70. package/lib/middleware/trace-propagate.js +1 -1
  71. package/lib/mtls-ca.js +23 -29
  72. package/lib/mtls-engine-default.js +21 -1
  73. package/lib/network-tls.js +21 -6
  74. package/lib/object-store/sigv4-bucket-ops.js +41 -0
  75. package/lib/observability-otlp-exporter.js +35 -2
  76. package/lib/openapi-paths-builder.js +248 -0
  77. package/lib/openapi-schema-walk.js +192 -0
  78. package/lib/openapi-security.js +169 -0
  79. package/lib/openapi-yaml.js +154 -0
  80. package/lib/openapi.js +443 -0
  81. package/lib/outbox.js +3 -3
  82. package/lib/permissions.js +10 -1
  83. package/lib/pqc-agent.js +22 -1
  84. package/lib/pqc-software.js +195 -0
  85. package/lib/pubsub.js +8 -4
  86. package/lib/redact.js +26 -1
  87. package/lib/retention.js +26 -0
  88. package/lib/router.js +1 -0
  89. package/lib/scheduler.js +57 -1
  90. package/lib/session.js +3 -3
  91. package/lib/ssrf-guard.js +19 -4
  92. package/lib/static.js +12 -0
  93. package/lib/totp.js +16 -0
  94. package/lib/vault/index.js +3 -0
  95. package/lib/vault-aad.js +259 -0
  96. package/lib/vendor/MANIFEST.json +29 -0
  97. package/lib/vendor/noble-post-quantum.cjs +18 -0
  98. package/lib/ws-client.js +978 -0
  99. package/package.json +1 -1
  100. 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
+ };