@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
package/lib/flag.js
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* b.flag — feature-flag client per the OpenFeature specification
|
|
4
|
+
* (https://openfeature.dev/specification/).
|
|
5
|
+
*
|
|
6
|
+
* var flag = b.flag.create({
|
|
7
|
+
* provider: b.flag.providers.localFile({ path: "./flags.json" }),
|
|
8
|
+
* defaultEvaluationContext: { environment: "production" },
|
|
9
|
+
* });
|
|
10
|
+
*
|
|
11
|
+
* var enabled = flag.getBoolean("new-checkout-flow", { targetingKey: req.user.id });
|
|
12
|
+
* var sample = flag.getString ("greeting-banner", { targetingKey: req.user.id }, "default-text");
|
|
13
|
+
* var rate = flag.getNumber ("upsell-rate", { targetingKey: req.user.id }, 0);
|
|
14
|
+
* var cfg = flag.getObject ("checkout-config", { targetingKey: req.user.id }, {});
|
|
15
|
+
*
|
|
16
|
+
* var details = flag.getDetails("new-checkout-flow", ctx);
|
|
17
|
+
* // → { value, variant, reason, metadata }
|
|
18
|
+
*
|
|
19
|
+
* flag.middleware() → request-time middleware that attaches a
|
|
20
|
+
* per-request `flag` accessor onto req.
|
|
21
|
+
*
|
|
22
|
+
* Per the validation-tier policy: create() throws on bad opts; the
|
|
23
|
+
* hot-path getValue / getBoolean / etc. NEVER throw — they return the
|
|
24
|
+
* operator-supplied default + emit `flag.evaluation.error` on the
|
|
25
|
+
* audit chain so the operator sees the problem without taking down
|
|
26
|
+
* the request.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
var validateOpts = require("./validate-opts");
|
|
30
|
+
var lazyRequire = require("./lazy-require");
|
|
31
|
+
var providersMod = require("./flag-providers");
|
|
32
|
+
var contextMod = require("./flag-evaluation-context");
|
|
33
|
+
var targeting = require("./flag-targeting");
|
|
34
|
+
var cacheMod = require("./flag-cache");
|
|
35
|
+
var { defineClass } = require("./framework-error");
|
|
36
|
+
var FlagError = defineClass("FlagError", { alwaysPermanent: true });
|
|
37
|
+
|
|
38
|
+
var audit = lazyRequire(function () { return require("./audit"); });
|
|
39
|
+
|
|
40
|
+
function _validateHooks(rawHooks) {
|
|
41
|
+
var out = { before: [], after: [], error: [], finally: [] };
|
|
42
|
+
if (rawHooks == null) return out;
|
|
43
|
+
if (typeof rawHooks !== "object") {
|
|
44
|
+
throw new FlagError("flag/bad-hooks",
|
|
45
|
+
"create: hooks must be an object { before, after, error, finally }");
|
|
46
|
+
}
|
|
47
|
+
var stages = ["before", "after", "error", "finally"];
|
|
48
|
+
for (var i = 0; i < stages.length; i += 1) {
|
|
49
|
+
var stage = stages[i];
|
|
50
|
+
if (rawHooks[stage] == null) continue;
|
|
51
|
+
var arr = Array.isArray(rawHooks[stage]) ? rawHooks[stage] : [rawHooks[stage]];
|
|
52
|
+
for (var j = 0; j < arr.length; j += 1) {
|
|
53
|
+
if (typeof arr[j] !== "function") {
|
|
54
|
+
throw new FlagError("flag/bad-hooks",
|
|
55
|
+
"create: hooks." + stage + "[" + j + "] must be a function");
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
out[stage] = arr.slice();
|
|
59
|
+
}
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function create(opts) {
|
|
64
|
+
opts = opts || {};
|
|
65
|
+
validateOpts(opts, [
|
|
66
|
+
"provider", "providers", "defaultEvaluationContext",
|
|
67
|
+
"audit", "errorHandler", "hooks",
|
|
68
|
+
], "flag.create");
|
|
69
|
+
var providers = [];
|
|
70
|
+
if (opts.provider) {
|
|
71
|
+
if (typeof opts.provider.evaluate !== "function") {
|
|
72
|
+
throw new FlagError("flag/bad-provider",
|
|
73
|
+
"create: provider must implement .evaluate(flagKey, ctx)");
|
|
74
|
+
}
|
|
75
|
+
providers.push(opts.provider);
|
|
76
|
+
}
|
|
77
|
+
if (Array.isArray(opts.providers)) {
|
|
78
|
+
for (var i = 0; i < opts.providers.length; i += 1) {
|
|
79
|
+
if (typeof opts.providers[i].evaluate !== "function") {
|
|
80
|
+
throw new FlagError("flag/bad-provider",
|
|
81
|
+
"create: providers[" + i + "] must implement .evaluate()");
|
|
82
|
+
}
|
|
83
|
+
providers.push(opts.providers[i]);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (providers.length === 0) {
|
|
87
|
+
throw new FlagError("flag/no-provider",
|
|
88
|
+
"create: at least one provider is required - pass `provider` or `providers`");
|
|
89
|
+
}
|
|
90
|
+
var defaultCtx = contextMod.create(opts.defaultEvaluationContext || {});
|
|
91
|
+
var auditOn = opts.audit !== false;
|
|
92
|
+
var errorHandler = (typeof opts.errorHandler === "function")
|
|
93
|
+
? opts.errorHandler : null;
|
|
94
|
+
var hooks = _validateHooks(opts.hooks);
|
|
95
|
+
|
|
96
|
+
function _emitErrorAudit(flagKey, err, ctx) {
|
|
97
|
+
if (!auditOn) return;
|
|
98
|
+
try {
|
|
99
|
+
audit().safeEmit({
|
|
100
|
+
action: "flag.evaluation.error",
|
|
101
|
+
outcome: "failure",
|
|
102
|
+
actor: { targetingKey: ctx && ctx.targetingKey || null },
|
|
103
|
+
metadata: {
|
|
104
|
+
flagKey: flagKey,
|
|
105
|
+
message: err && err.message || String(err),
|
|
106
|
+
code: err && err.code || "flag/unknown",
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
} catch (_e) { /* drop-silent */ }
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function _runHook(stage, info) {
|
|
113
|
+
var arr = hooks[stage];
|
|
114
|
+
if (!arr || arr.length === 0) return;
|
|
115
|
+
for (var i = 0; i < arr.length; i += 1) {
|
|
116
|
+
try { arr[i](info); } catch (_e) { /* drop-silent — hooks are observability, not blocking */ }
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function _evaluate(flagKey, ctx) {
|
|
121
|
+
var mergedCtx = contextMod.merge(defaultCtx, ctx || {});
|
|
122
|
+
var startMs = Date.now();
|
|
123
|
+
_runHook("before", { flagKey: flagKey, ctx: mergedCtx });
|
|
124
|
+
for (var i = 0; i < providers.length; i += 1) {
|
|
125
|
+
try {
|
|
126
|
+
var result = providers[i].evaluate(flagKey, mergedCtx);
|
|
127
|
+
if (result && result.reason !== "flag_not_found") {
|
|
128
|
+
if (auditOn) {
|
|
129
|
+
try {
|
|
130
|
+
audit().safeEmit({
|
|
131
|
+
action: "flag.evaluated",
|
|
132
|
+
outcome: "success",
|
|
133
|
+
actor: { targetingKey: mergedCtx.targetingKey || null },
|
|
134
|
+
metadata: {
|
|
135
|
+
flagKey: flagKey,
|
|
136
|
+
variant: result.variant,
|
|
137
|
+
reason: result.reason,
|
|
138
|
+
provider: providers[i].kind || null,
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
} catch (_e) { /* drop-silent */ }
|
|
142
|
+
}
|
|
143
|
+
_runHook("after", { flagKey: flagKey, ctx: mergedCtx, result: result, elapsedMs: Date.now() - startMs });
|
|
144
|
+
_runHook("finally", { flagKey: flagKey, ctx: mergedCtx, result: result });
|
|
145
|
+
return result;
|
|
146
|
+
}
|
|
147
|
+
} catch (err) {
|
|
148
|
+
_emitErrorAudit(flagKey, err, mergedCtx);
|
|
149
|
+
_runHook("error", { flagKey: flagKey, ctx: mergedCtx, err: err });
|
|
150
|
+
if (errorHandler) {
|
|
151
|
+
try { errorHandler({ flagKey: flagKey, err: err, ctx: mergedCtx }); }
|
|
152
|
+
catch (_e2) { /* drop-silent */ }
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
var notFound = {
|
|
157
|
+
value: undefined,
|
|
158
|
+
variant: null,
|
|
159
|
+
reason: "flag_not_found",
|
|
160
|
+
metadata: { flagKey: flagKey, providers: providers.map(function (p) { return p.kind; }) },
|
|
161
|
+
};
|
|
162
|
+
_runHook("after", { flagKey: flagKey, ctx: mergedCtx, result: notFound, elapsedMs: Date.now() - startMs });
|
|
163
|
+
_runHook("finally", { flagKey: flagKey, ctx: mergedCtx, result: notFound });
|
|
164
|
+
return notFound;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function _coerceBoolean(v) {
|
|
168
|
+
if (v === true || v === false) return v;
|
|
169
|
+
if (v === "true") return true;
|
|
170
|
+
if (v === "false") return false;
|
|
171
|
+
if (v === 1) return true;
|
|
172
|
+
if (v === 0) return false;
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
getValue: function (flagKey, ctx, defaultValue) {
|
|
178
|
+
var r = _evaluate(flagKey, ctx);
|
|
179
|
+
if (r.value === undefined) return defaultValue;
|
|
180
|
+
return r.value;
|
|
181
|
+
},
|
|
182
|
+
getDetails: function (flagKey, ctx) {
|
|
183
|
+
return _evaluate(flagKey, ctx);
|
|
184
|
+
},
|
|
185
|
+
getBoolean: function (flagKey, ctx, defaultValue) {
|
|
186
|
+
var r = _evaluate(flagKey, ctx);
|
|
187
|
+
if (r.value === undefined) return defaultValue === true;
|
|
188
|
+
var coerced = _coerceBoolean(r.value);
|
|
189
|
+
return coerced != null ? coerced : (defaultValue === true);
|
|
190
|
+
},
|
|
191
|
+
getString: function (flagKey, ctx, defaultValue) {
|
|
192
|
+
var r = _evaluate(flagKey, ctx);
|
|
193
|
+
if (typeof r.value === "string") return r.value;
|
|
194
|
+
return (typeof defaultValue === "string") ? defaultValue : "";
|
|
195
|
+
},
|
|
196
|
+
getNumber: function (flagKey, ctx, defaultValue) {
|
|
197
|
+
var r = _evaluate(flagKey, ctx);
|
|
198
|
+
if (typeof r.value === "number" && isFinite(r.value)) return r.value;
|
|
199
|
+
return (typeof defaultValue === "number") ? defaultValue : 0;
|
|
200
|
+
},
|
|
201
|
+
getObject: function (flagKey, ctx, defaultValue) {
|
|
202
|
+
var r = _evaluate(flagKey, ctx);
|
|
203
|
+
if (r.value && typeof r.value === "object") return r.value;
|
|
204
|
+
return defaultValue == null ? {} : defaultValue;
|
|
205
|
+
},
|
|
206
|
+
list: function () {
|
|
207
|
+
var keys = Object.create(null);
|
|
208
|
+
for (var i = 0; i < providers.length; i += 1) {
|
|
209
|
+
if (typeof providers[i].list === "function") {
|
|
210
|
+
var arr = providers[i].list();
|
|
211
|
+
for (var j = 0; j < arr.length; j += 1) keys[arr[j]] = true;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return Object.keys(keys);
|
|
215
|
+
},
|
|
216
|
+
providers: providers.slice(),
|
|
217
|
+
defaultEvaluationContext: defaultCtx,
|
|
218
|
+
getValues: function (flagKeys, ctx) {
|
|
219
|
+
var out = {};
|
|
220
|
+
if (!Array.isArray(flagKeys)) return out;
|
|
221
|
+
for (var i = 0; i < flagKeys.length; i += 1) {
|
|
222
|
+
var k = flagKeys[i];
|
|
223
|
+
if (typeof k !== "string") continue;
|
|
224
|
+
var r = _evaluate(k, ctx);
|
|
225
|
+
out[k] = r.value;
|
|
226
|
+
}
|
|
227
|
+
return out;
|
|
228
|
+
},
|
|
229
|
+
getDetailsAll: function (flagKeys, ctx) {
|
|
230
|
+
var out = {};
|
|
231
|
+
if (!Array.isArray(flagKeys)) return out;
|
|
232
|
+
for (var i = 0; i < flagKeys.length; i += 1) {
|
|
233
|
+
var k = flagKeys[i];
|
|
234
|
+
if (typeof k !== "string") continue;
|
|
235
|
+
out[k] = _evaluate(k, ctx);
|
|
236
|
+
}
|
|
237
|
+
return out;
|
|
238
|
+
},
|
|
239
|
+
addProvider: function (next) {
|
|
240
|
+
if (!next || typeof next.evaluate !== "function") {
|
|
241
|
+
throw new FlagError("flag/bad-provider",
|
|
242
|
+
"addProvider: provider must implement .evaluate()");
|
|
243
|
+
}
|
|
244
|
+
providers.push(next);
|
|
245
|
+
return providers.length;
|
|
246
|
+
},
|
|
247
|
+
removeProvider: function (target) {
|
|
248
|
+
var before = providers.length;
|
|
249
|
+
for (var i = providers.length - 1; i >= 0; i -= 1) {
|
|
250
|
+
if (providers[i] === target) providers.splice(i, 1);
|
|
251
|
+
}
|
|
252
|
+
return before - providers.length;
|
|
253
|
+
},
|
|
254
|
+
middleware: function (mwOpts) {
|
|
255
|
+
mwOpts = mwOpts || {};
|
|
256
|
+
validateOpts(mwOpts, ["userKey"], "flag.middleware");
|
|
257
|
+
var self = this;
|
|
258
|
+
return function flagMiddleware(req, res, next) {
|
|
259
|
+
var reqCtx = contextMod.fromRequest(req, {
|
|
260
|
+
userKey: mwOpts.userKey,
|
|
261
|
+
});
|
|
262
|
+
req.flag = {
|
|
263
|
+
getBoolean: function (k, def) { return self.getBoolean(k, reqCtx, def); },
|
|
264
|
+
getString: function (k, def) { return self.getString (k, reqCtx, def); },
|
|
265
|
+
getNumber: function (k, def) { return self.getNumber (k, reqCtx, def); },
|
|
266
|
+
getObject: function (k, def) { return self.getObject (k, reqCtx, def); },
|
|
267
|
+
getValue: function (k, def) { return self.getValue (k, reqCtx, def); },
|
|
268
|
+
getDetails: function (k) { return self.getDetails(k, reqCtx); },
|
|
269
|
+
ctx: reqCtx,
|
|
270
|
+
};
|
|
271
|
+
return next();
|
|
272
|
+
};
|
|
273
|
+
},
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
module.exports = {
|
|
278
|
+
create: create,
|
|
279
|
+
providers: providersMod,
|
|
280
|
+
context: contextMod,
|
|
281
|
+
targeting: targeting,
|
|
282
|
+
cache: cacheMod.cache,
|
|
283
|
+
FlagError: FlagError,
|
|
284
|
+
};
|
package/lib/guard-all.js
CHANGED
|
@@ -116,8 +116,16 @@ var SHARED_POSTURES = Object.freeze(["hipaa", "pci-dss", "gdpr", "soc2"]);
|
|
|
116
116
|
|
|
117
117
|
function _verifyParity() {
|
|
118
118
|
var failures = [];
|
|
119
|
-
|
|
120
|
-
|
|
119
|
+
// Walk both registries — content guards (with MIME_TYPES + EXTENSIONS)
|
|
120
|
+
// and standalone guards (filename / domain / uuid / cidr / time /
|
|
121
|
+
// mime / jwt / oauth / graphql / shell / regex / jsonpath / template /
|
|
122
|
+
// image / pdf / auth). Standalone guards skip the MIME/EXTENSION
|
|
123
|
+
// checks but every guard MUST declare the shared profile + posture
|
|
124
|
+
// vocabulary so b.guardAll.allGuards() returns a uniform surface.
|
|
125
|
+
var allGuards = GUARDS.concat(STANDALONE_GUARDS);
|
|
126
|
+
for (var i = 0; i < allGuards.length; i += 1) {
|
|
127
|
+
var g = allGuards[i];
|
|
128
|
+
var isContent = i < GUARDS.length;
|
|
121
129
|
if (!g || typeof g !== "object") {
|
|
122
130
|
failures.push("guard at index " + i + " is not an exported module object");
|
|
123
131
|
continue;
|
|
@@ -126,11 +134,13 @@ function _verifyParity() {
|
|
|
126
134
|
failures.push("guard at index " + i + ": missing NAME export");
|
|
127
135
|
continue;
|
|
128
136
|
}
|
|
129
|
-
if (
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
137
|
+
if (isContent) {
|
|
138
|
+
if (!Array.isArray(g.MIME_TYPES) || g.MIME_TYPES.length === 0) {
|
|
139
|
+
failures.push(g.NAME + ": missing or empty MIME_TYPES export");
|
|
140
|
+
}
|
|
141
|
+
if (!Array.isArray(g.EXTENSIONS) || g.EXTENSIONS.length === 0) {
|
|
142
|
+
failures.push(g.NAME + ": missing or empty EXTENSIONS export");
|
|
143
|
+
}
|
|
134
144
|
}
|
|
135
145
|
if (typeof g.gate !== "function") {
|
|
136
146
|
failures.push(g.NAME + ": missing gate(opts) function");
|
|
@@ -146,27 +156,34 @@ function _verifyParity() {
|
|
|
146
156
|
}
|
|
147
157
|
});
|
|
148
158
|
}
|
|
149
|
-
// Detect duplicate NAMEs
|
|
150
|
-
//
|
|
159
|
+
// Detect duplicate NAMEs across the full registry (both content +
|
|
160
|
+
// standalone) so a future guard with a NAME collision surfaces at
|
|
161
|
+
// boot instead of silently overriding _byName lookups. MIME / EXT
|
|
162
|
+
// collision detection stays scoped to content guards (standalone
|
|
163
|
+
// guards have no MIME/EXTENSIONS).
|
|
151
164
|
var nameSeen = Object.create(null);
|
|
152
165
|
var mimeSeen = Object.create(null);
|
|
153
166
|
var extSeen = Object.create(null);
|
|
154
|
-
for (var j = 0; j <
|
|
155
|
-
var gg =
|
|
167
|
+
for (var j = 0; j < allGuards.length; j += 1) {
|
|
168
|
+
var gg = allGuards[j];
|
|
156
169
|
if (gg && gg.NAME) {
|
|
157
|
-
if (nameSeen[gg.NAME]) failures.push("duplicate NAME " + JSON.stringify(gg.NAME)
|
|
170
|
+
if (nameSeen[gg.NAME]) failures.push("duplicate NAME " + JSON.stringify(gg.NAME) +
|
|
171
|
+
" across the full guard registry");
|
|
158
172
|
nameSeen[gg.NAME] = true;
|
|
159
173
|
}
|
|
160
|
-
|
|
161
|
-
|
|
174
|
+
}
|
|
175
|
+
for (var jc = 0; jc < GUARDS.length; jc += 1) {
|
|
176
|
+
var ggc = GUARDS[jc];
|
|
177
|
+
if (ggc && Array.isArray(ggc.MIME_TYPES)) {
|
|
178
|
+
ggc.MIME_TYPES.forEach(function (m) {
|
|
162
179
|
var k = String(m).toLowerCase();
|
|
163
180
|
if (mimeSeen[k]) failures.push("duplicate MIME_TYPE " + JSON.stringify(k) +
|
|
164
181
|
" across multiple guards");
|
|
165
182
|
mimeSeen[k] = true;
|
|
166
183
|
});
|
|
167
184
|
}
|
|
168
|
-
if (
|
|
169
|
-
|
|
185
|
+
if (ggc && Array.isArray(ggc.EXTENSIONS)) {
|
|
186
|
+
ggc.EXTENSIONS.forEach(function (e) {
|
|
170
187
|
var k = String(e).toLowerCase();
|
|
171
188
|
if (extSeen[k]) failures.push("duplicate EXTENSION " + JSON.stringify(k) +
|
|
172
189
|
" across multiple guards");
|
package/lib/guard-csv.js
CHANGED
|
@@ -360,7 +360,20 @@ function _detectIssues(text, opts) {
|
|
|
360
360
|
}
|
|
361
361
|
|
|
362
362
|
if (opts.formulaInjectionPolicy !== "audit-only" && opts.formulaInjectionPolicy !== "allow") {
|
|
363
|
-
|
|
363
|
+
// Strip ZWSP / RTLO / LRM / RLM / BOM at cell-start before the
|
|
364
|
+
// formula scan. Without this, a cell beginning with U+200B (zero-
|
|
365
|
+
// width space), U+202E (RTLO), U+200E/F (LTR/RTL marks), or U+FEFF
|
|
366
|
+
// (BOM) followed by `=` slips past the start-anchor check (the `^`
|
|
367
|
+
// sits before the codepoint, not after) and the formula reaches
|
|
368
|
+
// the spreadsheet evaluator. Browsers + Excel + Sheets all strip
|
|
369
|
+
// these silently — operator users see "=SUM(...)" rendered, the
|
|
370
|
+
// file shipped a hidden bidi prefix that bypassed the scanner.
|
|
371
|
+
// U+200B-200F (ZWSP / ZWNJ / ZWJ / LRM / RLM) +
|
|
372
|
+
// U+202A-202E (LRE / RLE / PDF / LRO / RLO) +
|
|
373
|
+
// U+2066-2069 (LRI / RLI / FSI / PDI) +
|
|
374
|
+
// U+FEFF (BOM) // allow:dynamic-regex — explicit codepoints, no operator input
|
|
375
|
+
var stripped = text.replace(new RegExp("^[\\u200B-\\u200F\\u202A-\\u202E\\u2066-\\u2069\\uFEFF]+"), "");
|
|
376
|
+
var formulaMatch = _firstMatch(stripped, FORMULA_SCAN_RE);
|
|
364
377
|
if (formulaMatch) {
|
|
365
378
|
issues.push({
|
|
366
379
|
kind: "formula-prefix-cell", severity: "critical",
|
|
@@ -368,7 +381,8 @@ function _detectIssues(text, opts) {
|
|
|
368
381
|
location: formulaMatch.index,
|
|
369
382
|
snippet: "cell beginning with formula trigger " +
|
|
370
383
|
JSON.stringify(formulaMatch.char.slice(-1)) +
|
|
371
|
-
" at byte " + formulaMatch.index
|
|
384
|
+
" at byte " + formulaMatch.index +
|
|
385
|
+
(stripped.length !== text.length ? " (after stripping leading bidi/zero-width prefix)" : ""),
|
|
372
386
|
});
|
|
373
387
|
}
|
|
374
388
|
}
|
package/lib/guard-html.js
CHANGED
|
@@ -402,6 +402,30 @@ function escapeAttr(value) {
|
|
|
402
402
|
.replace(/=/g, "=");
|
|
403
403
|
}
|
|
404
404
|
|
|
405
|
+
// HTML5 named entities that decode to ASCII codepoints — focused on
|
|
406
|
+
// the entries browsers honor inside URL contexts (whitespace, control
|
|
407
|
+
// chars, scheme-significant punctuation). The full WHATWG named-
|
|
408
|
+
// character-reference table is ~2,231 entries; this is the
|
|
409
|
+
// security-load-bearing subset documented in scheme-bypass writeups
|
|
410
|
+
// (CVE-2026-30838 class). High-codepoint named entities (e.g. mathematical
|
|
411
|
+
// symbols) don't affect URL scheme parsing, so they're omitted.
|
|
412
|
+
var NAMED_ENTITY_ASCII = {
|
|
413
|
+
// Whitespace + control chars browsers strip inside URL schemes
|
|
414
|
+
Tab: "\t", NewLine: "\n",
|
|
415
|
+
// Scheme-significant punctuation
|
|
416
|
+
colon: ":", semi: ";", period: ".", sol: "/", bsol: "\\",
|
|
417
|
+
num: "#", excl: "!", quest: "?", lpar: "(", rpar: ")",
|
|
418
|
+
lsqb: "[", rsqb: "]", lcub: "{", rcub: "}",
|
|
419
|
+
// Quotes / brackets
|
|
420
|
+
quot: "\"", apos: "'", lt: "<", gt: ">",
|
|
421
|
+
// Misc ASCII
|
|
422
|
+
amp: "&", commat: "@", dollar: "$", percnt: "%",
|
|
423
|
+
ast: "*", plus: "+", lowbar: "_", hyphen: "-",
|
|
424
|
+
// Whitespace markers (codepoints in the ASCII / Latin-1 range that
|
|
425
|
+
// browsers treat as URL-strippable)
|
|
426
|
+
nbsp: " ",
|
|
427
|
+
};
|
|
428
|
+
|
|
405
429
|
// _normalizeUrl — peel off entity-encoded leading whitespace and
|
|
406
430
|
// HTML/URL-encoded scheme prefix tricks, then return the lowercased
|
|
407
431
|
// scheme. Returns "" if no scheme.
|
|
@@ -415,6 +439,17 @@ function _extractScheme(rawUrl) {
|
|
|
415
439
|
s = s.replace(/&#(\d+);/g, function (_m, d) {
|
|
416
440
|
return String.fromCharCode(parseInt(d, 10));
|
|
417
441
|
});
|
|
442
|
+
// Decode HTML5 named entities that browsers honor inside URL
|
|
443
|
+
// contexts. Without this, payloads like `java	script:alert(1)`
|
|
444
|
+
// bypass the scheme allowlist (the literal `	` between `java`
|
|
445
|
+
// and `script:` doesn't match any denied scheme; the browser then
|
|
446
|
+
// decodes the entity, strips the tab, and executes javascript:).
|
|
447
|
+
s = s.replace(/&([A-Za-z][A-Za-z0-9]+);/g, function (m, name) {
|
|
448
|
+
if (Object.prototype.hasOwnProperty.call(NAMED_ENTITY_ASCII, name)) {
|
|
449
|
+
return NAMED_ENTITY_ASCII[name];
|
|
450
|
+
}
|
|
451
|
+
return m;
|
|
452
|
+
});
|
|
418
453
|
// Strip embedded whitespace + control chars + zero-widths the
|
|
419
454
|
// URL parser would tolerate.
|
|
420
455
|
s = s.replace(C0_CTRL_RE_G, "").replace(ZW_RE_G, "");
|
package/lib/guard-svg.js
CHANGED
|
@@ -352,6 +352,20 @@ function _resolveOpts(opts) {
|
|
|
352
352
|
});
|
|
353
353
|
}
|
|
354
354
|
|
|
355
|
+
// HTML5 named-entity ASCII subset — same shape as guard-html.
|
|
356
|
+
// Browsers honor these inside URL contexts; without decoding them,
|
|
357
|
+
// `java	script:` and friends bypass the scheme allowlist.
|
|
358
|
+
var SVG_NAMED_ENTITY_ASCII = {
|
|
359
|
+
Tab: "\t", NewLine: "\n",
|
|
360
|
+
colon: ":", semi: ";", period: ".", sol: "/", bsol: "\\",
|
|
361
|
+
num: "#", excl: "!", quest: "?", lpar: "(", rpar: ")",
|
|
362
|
+
lsqb: "[", rsqb: "]", lcub: "{", rcub: "}",
|
|
363
|
+
quot: "\"", apos: "'", lt: "<", gt: ">",
|
|
364
|
+
amp: "&", commat: "@", dollar: "$", percnt: "%",
|
|
365
|
+
ast: "*", plus: "+", lowbar: "_", hyphen: "-",
|
|
366
|
+
nbsp: " ",
|
|
367
|
+
};
|
|
368
|
+
|
|
355
369
|
function _extractScheme(rawUrl) {
|
|
356
370
|
var s = String(rawUrl || "").trim();
|
|
357
371
|
s = s.replace(/&#x([0-9a-f]+);/gi, function (_m, h) {
|
|
@@ -360,6 +374,12 @@ function _extractScheme(rawUrl) {
|
|
|
360
374
|
s = s.replace(/&#(\d+);/g, function (_m, d) {
|
|
361
375
|
return String.fromCharCode(parseInt(d, 10));
|
|
362
376
|
});
|
|
377
|
+
s = s.replace(/&([A-Za-z][A-Za-z0-9]+);/g, function (m, name) {
|
|
378
|
+
if (Object.prototype.hasOwnProperty.call(SVG_NAMED_ENTITY_ASCII, name)) {
|
|
379
|
+
return SVG_NAMED_ENTITY_ASCII[name];
|
|
380
|
+
}
|
|
381
|
+
return m;
|
|
382
|
+
});
|
|
363
383
|
s = s.replace(C0_CTRL_RE_G, "").replace(ZW_RE_G, "");
|
|
364
384
|
var m = s.match(/^([A-Za-z][A-Za-z0-9+.-]*):/);
|
|
365
385
|
return m ? m[1].toLowerCase() : "";
|
package/lib/http-client.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* body, // Buffer | string | Readable | undefined
|
|
17
17
|
* timeoutMs, // wall-clock cap (caller-chosen, no default)
|
|
18
18
|
* idleTimeoutMs, // zero-progress idle cap (default 30s)
|
|
19
|
-
* responseMode, // "buffer" (default) | "stream"
|
|
19
|
+
* responseMode, // "buffer" (default) | "stream" | "always-resolve"
|
|
20
20
|
* maxResponseBytes, // for buffer mode (default 16 MiB control,
|
|
21
21
|
* // 1 GiB GET — operators with > 1 GiB
|
|
22
22
|
* // stored objects must use stream mode)
|
|
@@ -618,6 +618,11 @@ function _requestWithRedirects(opts, hopsLeft) {
|
|
|
618
618
|
var u0 = safeUrl.parse(opts.url, { allowedProtocols: safeUrl.ALLOW_HTTP_ALL });
|
|
619
619
|
originalOrigin = u0.protocol + "//" + u0.host;
|
|
620
620
|
} catch (_e) { /* request() will reject on next hop's parse */ }
|
|
621
|
+
// onRedirect: function ({ from, to, hop, headersStripped, statusCode }) — called
|
|
622
|
+
// BEFORE each follow. Operator can mutate the next-hop URL or abort
|
|
623
|
+
// the redirect by throwing. Async hooks are awaited.
|
|
624
|
+
var onRedirect = typeof opts.onRedirect === "function" ? opts.onRedirect : null;
|
|
625
|
+
var hopCount = 0;
|
|
621
626
|
|
|
622
627
|
var current = Object.assign({}, opts, { _resolveOnRedirect: true });
|
|
623
628
|
function _follow() {
|
|
@@ -628,6 +633,7 @@ function _requestWithRedirects(opts, hopsLeft) {
|
|
|
628
633
|
var loc = res.headers && (res.headers.location || res.headers.Location);
|
|
629
634
|
if (!loc) return { finalOpts: current, res: res }; // 3xx with no Location — operator handles
|
|
630
635
|
hopsLeft -= 1;
|
|
636
|
+
hopCount += 1;
|
|
631
637
|
|
|
632
638
|
// Resolve relative Location against the just-fetched URL (the URL
|
|
633
639
|
// of the request that produced the redirect, which may itself be a
|
|
@@ -651,8 +657,10 @@ function _requestWithRedirects(opts, hopsLeft) {
|
|
|
651
657
|
var nu = safeUrl.parse(nextUrl, { allowedProtocols: safeUrl.ALLOW_HTTP_ALL });
|
|
652
658
|
nextOrigin = nu.protocol + "//" + nu.host;
|
|
653
659
|
} catch (_e) { /* request() will reject when it tries to parse */ }
|
|
660
|
+
var headersStripped = false;
|
|
654
661
|
if (originalOrigin && nextOrigin && nextOrigin !== originalOrigin) {
|
|
655
662
|
nextHeaders = _stripCrossOriginAuth(nextHeaders);
|
|
663
|
+
headersStripped = true;
|
|
656
664
|
}
|
|
657
665
|
|
|
658
666
|
// 303 → always GET; body dropped. 301/302 → historical clients
|
|
@@ -667,14 +675,42 @@ function _requestWithRedirects(opts, hopsLeft) {
|
|
|
667
675
|
nextBody = undefined;
|
|
668
676
|
}
|
|
669
677
|
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
+
function _continueFollow() {
|
|
679
|
+
current = Object.assign({}, current, {
|
|
680
|
+
url: nextUrl,
|
|
681
|
+
method: nextMethod,
|
|
682
|
+
body: nextBody,
|
|
683
|
+
headers: nextHeaders,
|
|
684
|
+
_resolveOnRedirect: true,
|
|
685
|
+
});
|
|
686
|
+
return _follow();
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// Caller-supplied redirect hook fires here. The hook can throw
|
|
690
|
+
// (sync) or reject (async) to abort the follow with a custom
|
|
691
|
+
// error; otherwise we proceed to the next hop. We pre-bind the
|
|
692
|
+
// values the hook gets and pass them in a frozen object so a
|
|
693
|
+
// caller can't mutate the in-flight pipeline by side-effect.
|
|
694
|
+
if (onRedirect) {
|
|
695
|
+
var hookEvent = Object.freeze({
|
|
696
|
+
from: current.url,
|
|
697
|
+
to: nextUrl,
|
|
698
|
+
hop: hopCount,
|
|
699
|
+
statusCode: res.statusCode,
|
|
700
|
+
headersStripped: headersStripped,
|
|
701
|
+
method: nextMethod,
|
|
702
|
+
});
|
|
703
|
+
try {
|
|
704
|
+
var hookResult = onRedirect(hookEvent);
|
|
705
|
+
if (hookResult && typeof hookResult.then === "function") {
|
|
706
|
+
return hookResult.then(function () { return _continueFollow(); });
|
|
707
|
+
}
|
|
708
|
+
} catch (e) {
|
|
709
|
+
return Promise.reject(_makeError(opts.errorClass, "REDIRECT_ABORTED",
|
|
710
|
+
"onRedirect hook refused redirect: " + ((e && e.message) || String(e)), true));
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
return _continueFollow();
|
|
678
714
|
});
|
|
679
715
|
}
|
|
680
716
|
void originalUrl;
|
|
@@ -885,7 +921,7 @@ function _requestH1(transport, u, opts) {
|
|
|
885
921
|
}
|
|
886
922
|
|
|
887
923
|
if (responseMode === "stream") {
|
|
888
|
-
if (res.statusCode >= 400) {
|
|
924
|
+
if (res.statusCode >= 400 && responseMode !== "always-resolve") {
|
|
889
925
|
res.resume();
|
|
890
926
|
return _reject(_makeError(opts.errorClass, "HTTP_ERROR",
|
|
891
927
|
"HTTP " + res.statusCode + " " + (res.statusMessage || ""),
|
|
@@ -936,6 +972,14 @@ function _requestH1(transport, u, opts) {
|
|
|
936
972
|
// request() never sets _resolveOnRedirect — operator code that
|
|
937
973
|
// didn't ask for redirect-following keeps seeing 3xx as errors.
|
|
938
974
|
_resolve({ statusCode: res.statusCode, headers: res.headers, body: buf });
|
|
975
|
+
} else if (responseMode === "always-resolve") {
|
|
976
|
+
// Operator opted in to "give me the full response object
|
|
977
|
+
// regardless of status." Caller branches on statusCode in
|
|
978
|
+
// their own code path — useful for proxies / forwarders /
|
|
979
|
+
// health-checkers / probe libraries that want to surface
|
|
980
|
+
// the upstream response structurally instead of via an
|
|
981
|
+
// error message string.
|
|
982
|
+
_resolve({ statusCode: res.statusCode, headers: res.headers, body: buf });
|
|
939
983
|
} else {
|
|
940
984
|
var msg = "HTTP " + res.statusCode + ": " + buf.toString("utf8").slice(0, 500);
|
|
941
985
|
_reject(_makeError(opts.errorClass, "HTTP_ERROR", msg,
|
|
@@ -1079,7 +1123,7 @@ function _requestH2(transport, u, opts) {
|
|
|
1079
1123
|
}
|
|
1080
1124
|
|
|
1081
1125
|
if (responseMode === "stream") {
|
|
1082
|
-
if (statusCode >= 400) {
|
|
1126
|
+
if (statusCode >= 400 && responseMode !== "always-resolve") {
|
|
1083
1127
|
stream.resume();
|
|
1084
1128
|
return _reject(_makeError(opts.errorClass, "HTTP_ERROR",
|
|
1085
1129
|
"HTTP " + statusCode, _isPermanentStatus(statusCode), statusCode));
|
|
@@ -1110,6 +1154,8 @@ function _requestH2(transport, u, opts) {
|
|
|
1110
1154
|
});
|
|
1111
1155
|
if (statusCode >= 200 && statusCode < 300) {
|
|
1112
1156
|
_resolve({ statusCode: statusCode, headers: responseHeaders, body: buf });
|
|
1157
|
+
} else if (responseMode === "always-resolve") {
|
|
1158
|
+
_resolve({ statusCode: statusCode, headers: responseHeaders, body: buf });
|
|
1113
1159
|
} else {
|
|
1114
1160
|
var msg = "HTTP " + statusCode + ": " + buf.toString("utf8").slice(0, 500);
|
|
1115
1161
|
_reject(_makeError(opts.errorClass, "HTTP_ERROR", msg,
|