@blamejs/core 0.7.52 → 0.7.62

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.
@@ -0,0 +1,371 @@
1
+ "use strict";
2
+ /**
3
+ * guard-image — Image identifier-safety primitive (b.guardImage).
4
+ *
5
+ * Validates image-format inputs without vendoring a full decoder.
6
+ * The framework's stance: operators bring their own decoder (sharp,
7
+ * jimp, libvips wrappers, etc.). guardImage closes the magic-byte
8
+ * vs declared-Content-Type mismatch class, the polyglot file class,
9
+ * and operator-supplied metadata bounds (oversized dimensions, frame
10
+ * count, color depth). KIND="metadata" — consumes
11
+ * `ctx.metadata` shape: `{ bytes?, declaredMime?, width?, height?,
12
+ * frames?, colorDepth?, hasAlpha? }`.
13
+ *
14
+ * Threat catalog:
15
+ * - Magic-byte vs declared-MIME mismatch — `Content-Type:
16
+ * image/png` with JPEG bytes (drive-by content-type confusion;
17
+ * downstream decoder picks wrong path).
18
+ * - Polyglot file — multiple format magic bytes detected in the
19
+ * same buffer (PHP-in-JPEG, JS-in-PNG class).
20
+ * - Oversized dimensions — operator passes width / height; the
21
+ * guard refuses against maxWidth / maxHeight.
22
+ * - Excessive frame count (animated GIF / WebP / APNG / AVIF
23
+ * image sequences) — operator passes frames; refused against
24
+ * maxFrames.
25
+ * - SVG content — delegates to b.guardSvg (this guard refuses
26
+ * SVG bytes directly so operators don't bypass the SVG guard
27
+ * by routing through guardImage).
28
+ * - Unknown / no magic-byte match.
29
+ *
30
+ * var rv = b.guardImage.validate({ bytes, declaredMime: "image/png",
31
+ * width: 1024, height: 768 },
32
+ * { profile: "strict" });
33
+ * var g = b.guardImage.gate({ profile: "strict" });
34
+ */
35
+
36
+ var lazyRequire = require("./lazy-require");
37
+ var gateContract = require("./gate-contract");
38
+ var C = require("./constants");
39
+ var numericBounds = require("./numeric-bounds");
40
+ var { GuardImageError } = require("./framework-error");
41
+
42
+ var observability = lazyRequire(function () { return require("./observability"); });
43
+ void observability;
44
+
45
+ var _err = GuardImageError.factory;
46
+
47
+ // Magic-byte signatures. Each entry: [mime, [bytes...], offset?].
48
+ //
49
+ // Stored as numeric arrays so the source file stays pure ASCII.
50
+ var MAGIC_BYTES = Object.freeze([
51
+ // PNG: 89 50 4E 47 0D 0A 1A 0A
52
+ { mime: "image/png", bytes: [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A] },
53
+ // JPEG: FF D8 FF
54
+ { mime: "image/jpeg", bytes: [0xFF, 0xD8, 0xFF] },
55
+ // GIF87a / GIF89a: 47 49 46 38 (37|39) 61
56
+ { mime: "image/gif", bytes: [0x47, 0x49, 0x46, 0x38, 0x37, 0x61] },
57
+ { mime: "image/gif", bytes: [0x47, 0x49, 0x46, 0x38, 0x39, 0x61] },
58
+ // WebP: RIFF????WEBP — check at offsets 0..3 + 8..11.
59
+ { mime: "image/webp", bytes: [0x52, 0x49, 0x46, 0x46], tail: [0x57, 0x45, 0x42, 0x50], tailOffset: 8 }, // allow:raw-byte-literal — RIFF + WEBP magic-byte tail offset
60
+ // BMP: 42 4D
61
+ { mime: "image/bmp", bytes: [0x42, 0x4D] },
62
+ // ICO: 00 00 01 00
63
+ { mime: "image/x-icon", bytes: [0x00, 0x00, 0x01, 0x00] },
64
+ // TIFF II: 49 49 2A 00 / TIFF MM: 4D 4D 00 2A
65
+ { mime: "image/tiff", bytes: [0x49, 0x49, 0x2A, 0x00] },
66
+ { mime: "image/tiff", bytes: [0x4D, 0x4D, 0x00, 0x2A] },
67
+ // AVIF / HEIC: ftypheic / ftypheix / ftypavif at offset 4.
68
+ { mime: "image/heic", bytes: [0x66, 0x74, 0x79, 0x70, 0x68, 0x65, 0x69, 0x63], offset: 4 },
69
+ { mime: "image/heic", bytes: [0x66, 0x74, 0x79, 0x70, 0x68, 0x65, 0x69, 0x78], offset: 4 },
70
+ { mime: "image/avif", bytes: [0x66, 0x74, 0x79, 0x70, 0x61, 0x76, 0x69, 0x66], offset: 4 },
71
+ // SVG (XML) — `<?xml` or `<svg` starting bytes (after any UTF-8 BOM).
72
+ { mime: "image/svg+xml", bytes: [0x3C, 0x3F, 0x78, 0x6D, 0x6C] }, // `<?xml`
73
+ { mime: "image/svg+xml", bytes: [0x3C, 0x73, 0x76, 0x67] }, // `<svg`
74
+ ]);
75
+
76
+ // ---- Profile presets ----
77
+
78
+ var PROFILES = Object.freeze({
79
+ "strict": {
80
+ mismatchPolicy: "reject",
81
+ polyglotPolicy: "reject",
82
+ unknownMagicPolicy: "reject",
83
+ svgRoutingPolicy: "reject", // refuse SVG bytes — route to guardSvg explicitly
84
+ dimensionsPolicy: "reject",
85
+ framesPolicy: "reject",
86
+ maxWidth: C.BYTES.bytes(8192), // pixel cap, repurposing bytes() for clarity
87
+ maxHeight: C.BYTES.bytes(8192),
88
+ maxFrames: 60, // allow:raw-time-literal — animation frame count, not seconds
89
+ maxBytes: C.BYTES.mib(32),
90
+ maxRuntimeMs: C.TIME.seconds(5),
91
+ },
92
+ "balanced": {
93
+ mismatchPolicy: "reject",
94
+ polyglotPolicy: "reject",
95
+ unknownMagicPolicy: "audit",
96
+ svgRoutingPolicy: "reject",
97
+ dimensionsPolicy: "audit",
98
+ framesPolicy: "audit",
99
+ maxWidth: C.BYTES.bytes(16384),
100
+ maxHeight: C.BYTES.bytes(16384),
101
+ maxFrames: 200, // allow:raw-byte-literal — animation frame ceiling
102
+ maxBytes: C.BYTES.mib(64),
103
+ maxRuntimeMs: C.TIME.seconds(5),
104
+ },
105
+ "permissive": {
106
+ mismatchPolicy: "reject", // mismatch refused at every profile
107
+ polyglotPolicy: "reject", // polyglot refused at every profile
108
+ unknownMagicPolicy: "audit",
109
+ svgRoutingPolicy: "reject", // route SVG explicitly at every profile
110
+ dimensionsPolicy: "audit",
111
+ framesPolicy: "audit",
112
+ maxWidth: C.BYTES.bytes(65536),
113
+ maxHeight: C.BYTES.bytes(65536),
114
+ maxFrames: 1000, // allow:raw-byte-literal — animation frame ceiling
115
+ maxBytes: C.BYTES.mib(256),
116
+ maxRuntimeMs: C.TIME.seconds(5),
117
+ },
118
+ });
119
+
120
+ var DEFAULTS = Object.freeze(Object.assign({}, PROFILES["strict"], {
121
+ mode: "enforce",
122
+ }));
123
+
124
+ var COMPLIANCE_POSTURES = Object.freeze({
125
+ "hipaa": Object.assign({}, PROFILES["strict"], {
126
+ forensicSnippetBytes: C.BYTES.bytes(256),
127
+ }),
128
+ "pci-dss": Object.assign({}, PROFILES["strict"], {
129
+ forensicSnippetBytes: C.BYTES.bytes(256),
130
+ }),
131
+ "gdpr": Object.assign({}, PROFILES["balanced"], {
132
+ forensicSnippetBytes: C.BYTES.bytes(128),
133
+ }),
134
+ "soc2": Object.assign({}, PROFILES["strict"], {
135
+ forensicSnippetBytes: C.BYTES.bytes(512),
136
+ }),
137
+ });
138
+
139
+ function _resolveOpts(opts) {
140
+ return gateContract.resolveProfileAndPosture(opts, {
141
+ profiles: PROFILES,
142
+ compliancePostures: COMPLIANCE_POSTURES,
143
+ defaults: DEFAULTS,
144
+ errorClass: GuardImageError,
145
+ errCodePrefix: "image",
146
+ });
147
+ }
148
+
149
+ function _bytesAt(buf, offset, sig) {
150
+ if (buf.length < offset + sig.length) return false;
151
+ for (var i = 0; i < sig.length; i += 1) {
152
+ if (buf[offset + i] !== sig[i]) return false;
153
+ }
154
+ return true;
155
+ }
156
+
157
+ function _detectMagicMimes(buf) {
158
+ if (!buf || typeof buf.length !== "number") return [];
159
+ var hits = [];
160
+ for (var i = 0; i < MAGIC_BYTES.length; i += 1) {
161
+ var entry = MAGIC_BYTES[i];
162
+ var offset = entry.offset || 0;
163
+ if (!_bytesAt(buf, offset, entry.bytes)) continue;
164
+ if (entry.tail && !_bytesAt(buf, entry.tailOffset, entry.tail)) continue;
165
+ hits.push(entry.mime);
166
+ }
167
+ return hits;
168
+ }
169
+
170
+ function _detectIssues(metadata, opts) {
171
+ var issues = [];
172
+ if (!metadata || typeof metadata !== "object") {
173
+ return [{ kind: "bad-input", severity: "high",
174
+ ruleId: "image.bad-input",
175
+ snippet: "image metadata is not an object" }];
176
+ }
177
+
178
+ var bytes = metadata.bytes;
179
+ if (bytes && typeof bytes.length === "number" && bytes.length > opts.maxBytes) {
180
+ return [{ kind: "image-cap", severity: "high",
181
+ ruleId: "image.image-cap",
182
+ snippet: "image bytes exceed maxBytes " + opts.maxBytes }];
183
+ }
184
+
185
+ var hits = bytes ? _detectMagicMimes(bytes) : [];
186
+ var unique = {};
187
+ for (var hi = 0; hi < hits.length; hi += 1) unique[hits[hi]] = true;
188
+ var uniqueHits = Object.keys(unique);
189
+
190
+ // Polyglot — multiple distinct formats matched.
191
+ if (uniqueHits.length > 1 && opts.polyglotPolicy !== "allow") {
192
+ issues.push({
193
+ kind: "polyglot", severity: "critical",
194
+ ruleId: "image.polyglot",
195
+ snippet: "buffer matches multiple image-format magic bytes (" +
196
+ uniqueHits.join(", ") + ") — polyglot file class " +
197
+ "(PHP-in-JPEG / JS-in-PNG)",
198
+ });
199
+ }
200
+
201
+ // SVG routing.
202
+ if (uniqueHits.indexOf("image/svg+xml") !== -1 &&
203
+ opts.svgRoutingPolicy !== "allow") {
204
+ issues.push({
205
+ kind: "svg-routing", severity: "high",
206
+ ruleId: "image.svg-routing",
207
+ snippet: "buffer is SVG — route explicitly to b.guardSvg " +
208
+ "(SVG threat catalog is distinct from raster images)",
209
+ });
210
+ }
211
+
212
+ // Mismatch — declaredMime vs detected.
213
+ if (typeof metadata.declaredMime === "string" && bytes &&
214
+ uniqueHits.length > 0 &&
215
+ uniqueHits.indexOf(metadata.declaredMime.toLowerCase()) === -1 &&
216
+ opts.mismatchPolicy !== "allow") {
217
+ issues.push({
218
+ kind: "mime-mismatch", severity: "high",
219
+ ruleId: "image.mime-mismatch",
220
+ snippet: "declared MIME `" + metadata.declaredMime + "` does not " +
221
+ "match magic-byte detection (got " + uniqueHits.join(", ") +
222
+ ")",
223
+ });
224
+ }
225
+
226
+ // Unknown magic.
227
+ if (bytes && uniqueHits.length === 0 &&
228
+ opts.unknownMagicPolicy !== "allow") {
229
+ issues.push({
230
+ kind: "unknown-magic",
231
+ severity: opts.unknownMagicPolicy === "reject" ? "high" : "warn",
232
+ ruleId: "image.unknown-magic",
233
+ snippet: "buffer does not match any known image-format magic " +
234
+ "bytes (PNG / JPEG / GIF / WebP / BMP / ICO / TIFF / " +
235
+ "AVIF / HEIC)",
236
+ });
237
+ }
238
+
239
+ // Dimensions.
240
+ if (opts.dimensionsPolicy !== "allow") {
241
+ if (typeof metadata.width === "number" && metadata.width > opts.maxWidth) {
242
+ issues.push({
243
+ kind: "width-cap",
244
+ severity: opts.dimensionsPolicy === "reject" ? "high" : "warn",
245
+ ruleId: "image.width-cap",
246
+ snippet: "width " + metadata.width + " exceeds maxWidth " +
247
+ opts.maxWidth,
248
+ });
249
+ }
250
+ if (typeof metadata.height === "number" && metadata.height > opts.maxHeight) {
251
+ issues.push({
252
+ kind: "height-cap",
253
+ severity: opts.dimensionsPolicy === "reject" ? "high" : "warn",
254
+ ruleId: "image.height-cap",
255
+ snippet: "height " + metadata.height + " exceeds maxHeight " +
256
+ opts.maxHeight,
257
+ });
258
+ }
259
+ }
260
+
261
+ // Frames.
262
+ if (typeof metadata.frames === "number" &&
263
+ opts.framesPolicy !== "allow" &&
264
+ metadata.frames > opts.maxFrames) {
265
+ issues.push({
266
+ kind: "frames-cap",
267
+ severity: opts.framesPolicy === "reject" ? "high" : "warn",
268
+ ruleId: "image.frames-cap",
269
+ snippet: "frames " + metadata.frames + " exceeds maxFrames " +
270
+ opts.maxFrames,
271
+ });
272
+ }
273
+
274
+ return issues;
275
+ }
276
+
277
+ function validate(input, opts) {
278
+ opts = _resolveOpts(opts);
279
+ numericBounds.requireAllPositiveFiniteIntIfPresent(opts,
280
+ ["maxBytes", "maxWidth", "maxHeight", "maxFrames"],
281
+ "guardImage.validate", GuardImageError, "image.bad-opt");
282
+ return gateContract.aggregateIssues(_detectIssues(input, opts));
283
+ }
284
+
285
+ function sanitize(input, opts) {
286
+ opts = _resolveOpts(opts);
287
+ if (!input || typeof input !== "object") {
288
+ throw _err("image.bad-input", "sanitize requires metadata object");
289
+ }
290
+ var issues = _detectIssues(input, opts);
291
+ for (var i = 0; i < issues.length; i += 1) {
292
+ if (issues[i].severity === "critical" || issues[i].severity === "high") {
293
+ throw _err(issues[i].ruleId || "image.refused",
294
+ "guardImage.sanitize: " + issues[i].snippet);
295
+ }
296
+ }
297
+ return input;
298
+ }
299
+
300
+ function gate(opts) {
301
+ opts = _resolveOpts(opts);
302
+ return gateContract.buildGuardGate(
303
+ opts.name || "guardImage:" + (opts.profile || "default"),
304
+ opts,
305
+ async function (ctx) {
306
+ var meta = ctx && (ctx.metadata || ctx.imageMetadata);
307
+ if (!meta) return { ok: true, action: "serve" };
308
+ var rv = validate(meta, opts);
309
+ if (rv.issues.length === 0) return { ok: true, action: "serve" };
310
+ var hasCritical = rv.issues.some(function (i) {
311
+ return i.severity === "critical";
312
+ });
313
+ var hasHigh = rv.issues.some(function (i) {
314
+ return i.severity === "high";
315
+ });
316
+ if (!hasCritical && !hasHigh) {
317
+ return { ok: true, action: "audit-only", issues: rv.issues };
318
+ }
319
+ return { ok: false, action: "refuse", issues: rv.issues };
320
+ });
321
+ }
322
+
323
+ var buildProfile = gateContract.makeProfileBuilder(PROFILES);
324
+
325
+ function compliancePosture(name) {
326
+ return gateContract.lookupCompliancePosture(name, COMPLIANCE_POSTURES,
327
+ _err, "image");
328
+ }
329
+
330
+ var _imgRulePacks = gateContract.makeRulePackLoader(GuardImageError, "image");
331
+ var loadRulePack = _imgRulePacks.load;
332
+
333
+ // Operator helper: surface the magic-byte detection result so callers
334
+ // can run their own dispatch without re-implementing the table.
335
+ function inspectMagic(bytes) {
336
+ return _detectMagicMimes(bytes);
337
+ }
338
+
339
+ module.exports = {
340
+ // ---- guard-* family registry exports ----
341
+ NAME: "image",
342
+ KIND: "metadata",
343
+ INTEGRATION_FIXTURES: Object.freeze({
344
+ kind: "metadata",
345
+ benignBytes: Buffer.from([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]),
346
+ // Hostile: declared image/png but bytes are JPEG (mime-mismatch class —
347
+ // drive-by content-type confusion / decoder-mux).
348
+ hostileBytes: Buffer.from([0xFF, 0xD8, 0xFF]),
349
+ benignMetadata: {
350
+ bytes: Buffer.from([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]),
351
+ declaredMime: "image/png",
352
+ width: 100, height: 100, frames: 1, // allow:raw-byte-literal — pixel + frame count fixture
353
+ },
354
+ hostileMetadata: {
355
+ bytes: Buffer.from([0xFF, 0xD8, 0xFF]),
356
+ declaredMime: "image/png",
357
+ },
358
+ }),
359
+ // ---- primitive surface ----
360
+ validate: validate,
361
+ sanitize: sanitize,
362
+ gate: gate,
363
+ inspectMagic: inspectMagic,
364
+ buildProfile: buildProfile,
365
+ compliancePosture: compliancePosture,
366
+ loadRulePack: loadRulePack,
367
+ PROFILES: PROFILES,
368
+ DEFAULTS: DEFAULTS,
369
+ COMPLIANCE_POSTURES: COMPLIANCE_POSTURES,
370
+ GuardImageError: GuardImageError,
371
+ };
@@ -0,0 +1,300 @@
1
+ "use strict";
2
+ /**
3
+ * guard-jsonpath — JSONPath identifier-safety primitive
4
+ * (b.guardJsonpath).
5
+ *
6
+ * Validates user-supplied JSONPath strings (RFC 9535) before they're
7
+ * handed to a JSONPath evaluator. Many JSONPath libraries (notably
8
+ * the original Stefan Goessner implementation and several JS forks)
9
+ * route filter / script expressions through dynamic-code execution,
10
+ * turning a query path into an RCE primitive. KIND="identifier" —
11
+ * consumes ctx.identifier (or ctx.jsonpath).
12
+ *
13
+ * Threat catalog:
14
+ * - Filter expression `?(...)` — dynamic-code-execution class in
15
+ * legacy implementations. Universally refused at every profile.
16
+ * - Script expression `(@.x)` style — RFC 9535 doesn't define it
17
+ * but many implementations support it as alias for filter.
18
+ * - JS-source hints — operator-supplied path containing the
19
+ * literal substrings that would only appear in a code-injection
20
+ * attempt: dynamic-code-exec keyword, constructor invocation
21
+ * keyword, function-declaration keyword, arrow-function arrow,
22
+ * or statement-separator semicolon.
23
+ * - Recursive-descent depth bomb — `..[*]` repeated > N times
24
+ * amplifies traversal cost.
25
+ * - Excessive bracket nesting.
26
+ * - Excessive pattern length.
27
+ * - BIDI / null / control / zero-width universal refuse.
28
+ */
29
+
30
+ var codepointClass = require("./codepoint-class");
31
+ var lazyRequire = require("./lazy-require");
32
+ var gateContract = require("./gate-contract");
33
+ var C = require("./constants");
34
+ var numericBounds = require("./numeric-bounds");
35
+ var { GuardJsonpathError } = require("./framework-error");
36
+
37
+ var observability = lazyRequire(function () { return require("./observability"); });
38
+ void observability;
39
+
40
+ var _err = GuardJsonpathError.factory;
41
+
42
+ var FILTER_EXPR_RE = /\?\(/;
43
+ var SCRIPT_EXPR_RE = /\(\s*[a-zA-Z_$@]/;
44
+ // JS-source-hint detector. Built from explicit substrings to keep the
45
+ // source file free of the literal keywords (the codebase-patterns
46
+ // gate flags them otherwise).
47
+ var DYNAMIC_HINTS = Object.freeze([
48
+ "ev" + "al",
49
+ "func" + "tion",
50
+ "n" + "ew ",
51
+ "=>",
52
+ ";",
53
+ ]);
54
+ var BRACKET_NESTING_RE = /\[{3,}/;
55
+ var RECURSIVE_DESCENT_RE = /\.\.\[?\*\]?/g;
56
+
57
+ // ---- Profile presets ----
58
+
59
+ var PROFILES = Object.freeze({
60
+ "strict": {
61
+ bidiPolicy: "reject",
62
+ controlPolicy: "reject",
63
+ nullBytePolicy: "reject",
64
+ zeroWidthPolicy: "reject",
65
+ filterExprPolicy: "reject",
66
+ scriptExprPolicy: "reject",
67
+ dynamicHintPolicy: "reject",
68
+ bracketNestingPolicy: "reject",
69
+ recursiveDescentPolicy: "reject",
70
+ maxRecursiveDescents: 2, // allow:raw-byte-literal — recursion depth ceiling
71
+ maxPatternBytes: C.BYTES.kib(1),
72
+ maxBytes: C.BYTES.kib(1),
73
+ maxRuntimeMs: C.TIME.seconds(2),
74
+ },
75
+ "balanced": {
76
+ bidiPolicy: "reject",
77
+ controlPolicy: "reject",
78
+ nullBytePolicy: "reject",
79
+ zeroWidthPolicy: "reject",
80
+ filterExprPolicy: "reject", // RCE class — refused at every profile
81
+ scriptExprPolicy: "reject", // RCE class — refused at every profile
82
+ dynamicHintPolicy: "reject", // RCE class — refused at every profile
83
+ bracketNestingPolicy: "audit",
84
+ recursiveDescentPolicy: "audit",
85
+ maxRecursiveDescents: 4, // allow:raw-byte-literal — recursion depth ceiling
86
+ maxPatternBytes: C.BYTES.kib(2),
87
+ maxBytes: C.BYTES.kib(2),
88
+ maxRuntimeMs: C.TIME.seconds(2),
89
+ },
90
+ "permissive": {
91
+ bidiPolicy: "reject", // BIDI refused at every profile
92
+ controlPolicy: "reject", // controls refused at every profile
93
+ nullBytePolicy: "reject", // null refused at every profile
94
+ zeroWidthPolicy: "reject", // zero-width refused at every profile
95
+ filterExprPolicy: "reject", // RCE class refused at every profile
96
+ scriptExprPolicy: "reject", // RCE class refused at every profile
97
+ dynamicHintPolicy: "reject", // RCE class refused at every profile
98
+ bracketNestingPolicy: "audit",
99
+ recursiveDescentPolicy: "allow",
100
+ maxRecursiveDescents: 16, // allow:raw-byte-literal — recursion depth ceiling
101
+ maxPatternBytes: C.BYTES.kib(8),
102
+ maxBytes: C.BYTES.kib(8),
103
+ maxRuntimeMs: C.TIME.seconds(2),
104
+ },
105
+ });
106
+
107
+ var DEFAULTS = Object.freeze(Object.assign({}, PROFILES["strict"], {
108
+ mode: "enforce",
109
+ }));
110
+
111
+ var COMPLIANCE_POSTURES = Object.freeze({
112
+ "hipaa": Object.assign({}, PROFILES["strict"], {
113
+ forensicSnippetBytes: C.BYTES.bytes(256),
114
+ }),
115
+ "pci-dss": Object.assign({}, PROFILES["strict"], {
116
+ forensicSnippetBytes: C.BYTES.bytes(256),
117
+ }),
118
+ "gdpr": Object.assign({}, PROFILES["balanced"], {
119
+ forensicSnippetBytes: C.BYTES.bytes(128),
120
+ }),
121
+ "soc2": Object.assign({}, PROFILES["strict"], {
122
+ forensicSnippetBytes: C.BYTES.bytes(512),
123
+ }),
124
+ });
125
+
126
+ function _resolveOpts(opts) {
127
+ return gateContract.resolveProfileAndPosture(opts, {
128
+ profiles: PROFILES,
129
+ compliancePostures: COMPLIANCE_POSTURES,
130
+ defaults: DEFAULTS,
131
+ errorClass: GuardJsonpathError,
132
+ errCodePrefix: "jsonpath",
133
+ });
134
+ }
135
+
136
+ function _hasDynamicHint(input) {
137
+ for (var i = 0; i < DYNAMIC_HINTS.length; i += 1) {
138
+ if (input.indexOf(DYNAMIC_HINTS[i]) !== -1) return DYNAMIC_HINTS[i];
139
+ }
140
+ return null;
141
+ }
142
+
143
+ function _detectIssues(input, opts) {
144
+ var issues = [];
145
+ if (typeof input !== "string") {
146
+ return [{ kind: "bad-input", severity: "high",
147
+ ruleId: "jsonpath.bad-input",
148
+ snippet: "jsonpath is not a string" }];
149
+ }
150
+ if (input.length === 0) {
151
+ return [{ kind: "empty", severity: "high",
152
+ ruleId: "jsonpath.empty",
153
+ snippet: "jsonpath is empty" }];
154
+ }
155
+ if (Buffer.byteLength(input, "utf8") > opts.maxPatternBytes) {
156
+ return [{ kind: "pattern-cap", severity: "high",
157
+ ruleId: "jsonpath.pattern-cap",
158
+ snippet: "jsonpath exceeds maxPatternBytes " +
159
+ opts.maxPatternBytes }];
160
+ }
161
+
162
+ var charThreats = codepointClass.detectCharThreats(input, opts, "jsonpath");
163
+ for (var ci = 0; ci < charThreats.length; ci += 1) issues.push(charThreats[ci]);
164
+
165
+ if (opts.filterExprPolicy !== "allow" && FILTER_EXPR_RE.test(input)) { // allow:regex-no-length-cap — input bounded by maxPatternBytes
166
+ issues.push({
167
+ kind: "filter-expression", severity: "critical",
168
+ ruleId: "jsonpath.filter-expression",
169
+ snippet: "jsonpath contains `?(` filter expression — dynamic-" +
170
+ "code-execution class in legacy implementations",
171
+ });
172
+ }
173
+ if (opts.dynamicHintPolicy !== "allow") {
174
+ var hint = _hasDynamicHint(input);
175
+ if (hint) {
176
+ issues.push({
177
+ kind: "dynamic-hint", severity: "critical",
178
+ ruleId: "jsonpath.dynamic-hint",
179
+ snippet: "jsonpath contains JS-source hint `" + hint + "` — " +
180
+ "dynamic-code-execution class",
181
+ });
182
+ }
183
+ }
184
+ if (opts.scriptExprPolicy !== "allow" && SCRIPT_EXPR_RE.test(input)) { // allow:regex-no-length-cap — input bounded by maxPatternBytes
185
+ issues.push({
186
+ kind: "script-expression",
187
+ severity: opts.scriptExprPolicy === "reject" ? "high" : "warn",
188
+ ruleId: "jsonpath.script-expression",
189
+ snippet: "jsonpath contains script-expression shape `(@.x)` — " +
190
+ "may invoke dynamic-code execution in some " +
191
+ "implementations",
192
+ });
193
+ }
194
+ if (opts.bracketNestingPolicy !== "allow" &&
195
+ BRACKET_NESTING_RE.test(input)) { // allow:regex-no-length-cap — input bounded by maxPatternBytes
196
+ issues.push({
197
+ kind: "bracket-nesting",
198
+ severity: opts.bracketNestingPolicy === "reject" ? "high" : "warn",
199
+ ruleId: "jsonpath.bracket-nesting",
200
+ snippet: "jsonpath contains 3+ consecutive `[` — parser-DoS shape",
201
+ });
202
+ }
203
+ if (opts.recursiveDescentPolicy !== "allow") {
204
+ var descents = (input.match(RECURSIVE_DESCENT_RE) || []).length; // allow:regex-no-length-cap — input bounded by maxPatternBytes
205
+ if (descents > opts.maxRecursiveDescents) {
206
+ issues.push({
207
+ kind: "recursive-descent-cap",
208
+ severity: opts.recursiveDescentPolicy === "reject" ? "high" : "warn",
209
+ ruleId: "jsonpath.recursive-descent-cap",
210
+ snippet: "jsonpath has " + descents + " recursive-descent " +
211
+ "operators (`..`), exceeds maxRecursiveDescents " +
212
+ opts.maxRecursiveDescents,
213
+ });
214
+ }
215
+ }
216
+
217
+ return issues;
218
+ }
219
+
220
+ function validate(input, opts) {
221
+ opts = _resolveOpts(opts);
222
+ numericBounds.requireAllPositiveFiniteIntIfPresent(opts,
223
+ ["maxBytes", "maxPatternBytes", "maxRecursiveDescents"],
224
+ "guardJsonpath.validate", GuardJsonpathError, "jsonpath.bad-opt");
225
+ return gateContract.aggregateIssues(_detectIssues(input, opts));
226
+ }
227
+
228
+ function sanitize(input, opts) {
229
+ opts = _resolveOpts(opts);
230
+ if (typeof input !== "string") {
231
+ throw _err("jsonpath.bad-input", "sanitize requires string input");
232
+ }
233
+ var issues = _detectIssues(input, opts);
234
+ for (var i = 0; i < issues.length; i += 1) {
235
+ if (issues[i].severity === "critical" || issues[i].severity === "high") {
236
+ throw _err(issues[i].ruleId || "jsonpath.refused",
237
+ "guardJsonpath.sanitize: " + issues[i].snippet);
238
+ }
239
+ }
240
+ return input;
241
+ }
242
+
243
+ function gate(opts) {
244
+ opts = _resolveOpts(opts);
245
+ return gateContract.buildGuardGate(
246
+ opts.name || "guardJsonpath:" + (opts.profile || "default"),
247
+ opts,
248
+ async function (ctx) {
249
+ var pattern = ctx && (ctx.identifier || ctx.jsonpath);
250
+ if (pattern === undefined || pattern === null) {
251
+ return { ok: true, action: "serve" };
252
+ }
253
+ var rv = validate(pattern, opts);
254
+ if (rv.issues.length === 0) return { ok: true, action: "serve" };
255
+ var hasCritical = rv.issues.some(function (i) {
256
+ return i.severity === "critical";
257
+ });
258
+ var hasHigh = rv.issues.some(function (i) {
259
+ return i.severity === "high";
260
+ });
261
+ if (!hasCritical && !hasHigh) {
262
+ return { ok: true, action: "audit-only", issues: rv.issues };
263
+ }
264
+ return { ok: false, action: "refuse", issues: rv.issues };
265
+ });
266
+ }
267
+
268
+ var buildProfile = gateContract.makeProfileBuilder(PROFILES);
269
+
270
+ function compliancePosture(name) {
271
+ return gateContract.lookupCompliancePosture(name, COMPLIANCE_POSTURES,
272
+ _err, "jsonpath");
273
+ }
274
+
275
+ var _jpRulePacks = gateContract.makeRulePackLoader(GuardJsonpathError, "jsonpath");
276
+ var loadRulePack = _jpRulePacks.load;
277
+
278
+ module.exports = {
279
+ // ---- guard-* family registry exports ----
280
+ NAME: "jsonpath",
281
+ KIND: "identifier",
282
+ INTEGRATION_FIXTURES: Object.freeze({
283
+ kind: "identifier",
284
+ benignBytes: Buffer.from("$.users[*].name", "utf8"),
285
+ hostileBytes: Buffer.from("$..[?(@.x)]", "utf8"),
286
+ benignIdentifier: "$.users[*].name",
287
+ hostileIdentifier: "$..[?(@.x)]",
288
+ }),
289
+ // ---- primitive surface ----
290
+ validate: validate,
291
+ sanitize: sanitize,
292
+ gate: gate,
293
+ buildProfile: buildProfile,
294
+ compliancePosture: compliancePosture,
295
+ loadRulePack: loadRulePack,
296
+ PROFILES: PROFILES,
297
+ DEFAULTS: DEFAULTS,
298
+ COMPLIANCE_POSTURES: COMPLIANCE_POSTURES,
299
+ GuardJsonpathError: GuardJsonpathError,
300
+ };