@blamejs/core 0.8.42 → 0.8.49

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 (222) hide show
  1. package/CHANGELOG.md +93 -0
  2. package/README.md +10 -10
  3. package/index.js +52 -0
  4. package/lib/a2a.js +159 -34
  5. package/lib/acme.js +762 -0
  6. package/lib/ai-pref.js +166 -43
  7. package/lib/api-key.js +108 -47
  8. package/lib/api-snapshot.js +157 -40
  9. package/lib/app-shutdown.js +113 -77
  10. package/lib/archive.js +337 -40
  11. package/lib/arg-parser.js +697 -0
  12. package/lib/asyncapi.js +99 -55
  13. package/lib/atomic-file.js +465 -104
  14. package/lib/audit-chain.js +123 -34
  15. package/lib/audit-daily-review.js +389 -0
  16. package/lib/audit-sign.js +302 -56
  17. package/lib/audit-tools.js +412 -63
  18. package/lib/audit.js +656 -35
  19. package/lib/auth/jwt-external.js +17 -0
  20. package/lib/auth/oauth.js +7 -0
  21. package/lib/auth-bot-challenge.js +505 -0
  22. package/lib/auth-header.js +92 -25
  23. package/lib/backup/bundle.js +26 -0
  24. package/lib/backup/index.js +512 -89
  25. package/lib/backup/manifest.js +168 -7
  26. package/lib/break-glass.js +415 -39
  27. package/lib/budr.js +103 -30
  28. package/lib/bundler.js +86 -66
  29. package/lib/cache.js +192 -72
  30. package/lib/chain-writer.js +65 -40
  31. package/lib/circuit-breaker.js +56 -33
  32. package/lib/cli-helpers.js +106 -75
  33. package/lib/cli.js +6 -30
  34. package/lib/cloud-events.js +99 -32
  35. package/lib/cluster-storage.js +162 -37
  36. package/lib/cluster.js +340 -49
  37. package/lib/codepoint-class.js +66 -0
  38. package/lib/compliance.js +424 -24
  39. package/lib/config-drift.js +111 -46
  40. package/lib/config.js +94 -40
  41. package/lib/consent.js +165 -18
  42. package/lib/constants.js +1 -0
  43. package/lib/content-credentials.js +153 -48
  44. package/lib/cookies.js +154 -62
  45. package/lib/credential-hash.js +133 -61
  46. package/lib/crypto-field.js +702 -18
  47. package/lib/crypto-hpke.js +256 -0
  48. package/lib/crypto.js +744 -22
  49. package/lib/csv.js +178 -35
  50. package/lib/daemon.js +456 -0
  51. package/lib/dark-patterns.js +186 -55
  52. package/lib/db-query.js +79 -2
  53. package/lib/db.js +1431 -60
  54. package/lib/ddl-change-control.js +523 -0
  55. package/lib/deprecate.js +195 -40
  56. package/lib/dev.js +82 -39
  57. package/lib/dora.js +67 -48
  58. package/lib/dr-runbook.js +368 -0
  59. package/lib/dsr.js +142 -11
  60. package/lib/dual-control.js +91 -56
  61. package/lib/events.js +120 -41
  62. package/lib/external-db-migrate.js +192 -2
  63. package/lib/external-db.js +795 -50
  64. package/lib/fapi2.js +122 -1
  65. package/lib/fda-21cfr11.js +395 -0
  66. package/lib/fdx.js +132 -2
  67. package/lib/file-type.js +87 -0
  68. package/lib/file-upload.js +93 -0
  69. package/lib/flag.js +82 -20
  70. package/lib/forms.js +132 -29
  71. package/lib/framework-error.js +169 -0
  72. package/lib/framework-schema.js +163 -35
  73. package/lib/gate-contract.js +849 -175
  74. package/lib/graphql-federation.js +68 -7
  75. package/lib/guard-all.js +172 -55
  76. package/lib/guard-archive.js +286 -124
  77. package/lib/guard-auth.js +194 -21
  78. package/lib/guard-cidr.js +190 -28
  79. package/lib/guard-csv.js +397 -51
  80. package/lib/guard-domain.js +213 -91
  81. package/lib/guard-email.js +236 -29
  82. package/lib/guard-filename.js +307 -75
  83. package/lib/guard-graphql.js +263 -30
  84. package/lib/guard-html.js +310 -116
  85. package/lib/guard-image.js +243 -30
  86. package/lib/guard-json.js +260 -54
  87. package/lib/guard-jsonpath.js +235 -23
  88. package/lib/guard-jwt.js +284 -30
  89. package/lib/guard-markdown.js +204 -22
  90. package/lib/guard-mime.js +190 -26
  91. package/lib/guard-oauth.js +277 -28
  92. package/lib/guard-pdf.js +251 -27
  93. package/lib/guard-regex.js +226 -18
  94. package/lib/guard-shell.js +229 -26
  95. package/lib/guard-svg.js +177 -10
  96. package/lib/guard-template.js +232 -21
  97. package/lib/guard-time.js +195 -29
  98. package/lib/guard-uuid.js +189 -30
  99. package/lib/guard-xml.js +259 -36
  100. package/lib/guard-yaml.js +241 -44
  101. package/lib/honeytoken.js +63 -27
  102. package/lib/html-balance.js +83 -0
  103. package/lib/http-client.js +486 -59
  104. package/lib/http-message-signature.js +582 -0
  105. package/lib/i18n.js +102 -49
  106. package/lib/iab-mspa.js +112 -32
  107. package/lib/iab-tcf.js +107 -2
  108. package/lib/inbox.js +90 -52
  109. package/lib/keychain.js +865 -0
  110. package/lib/legal-hold.js +374 -0
  111. package/lib/local-db-thin.js +320 -0
  112. package/lib/log-stream.js +281 -51
  113. package/lib/log.js +184 -86
  114. package/lib/mail-bounce.js +107 -62
  115. package/lib/mail.js +295 -58
  116. package/lib/mcp.js +108 -27
  117. package/lib/metrics.js +98 -89
  118. package/lib/middleware/age-gate.js +36 -0
  119. package/lib/middleware/ai-act-disclosure.js +37 -0
  120. package/lib/middleware/api-encrypt.js +45 -0
  121. package/lib/middleware/assetlinks.js +40 -0
  122. package/lib/middleware/asyncapi-serve.js +35 -0
  123. package/lib/middleware/attach-user.js +40 -0
  124. package/lib/middleware/bearer-auth.js +40 -0
  125. package/lib/middleware/body-parser.js +230 -0
  126. package/lib/middleware/bot-disclose.js +34 -0
  127. package/lib/middleware/bot-guard.js +39 -0
  128. package/lib/middleware/compression.js +37 -0
  129. package/lib/middleware/cookies.js +32 -0
  130. package/lib/middleware/cors.js +40 -0
  131. package/lib/middleware/csp-nonce.js +40 -0
  132. package/lib/middleware/csp-report.js +34 -0
  133. package/lib/middleware/csrf-protect.js +43 -0
  134. package/lib/middleware/daily-byte-quota.js +53 -85
  135. package/lib/middleware/db-role-for.js +40 -0
  136. package/lib/middleware/dpop.js +40 -0
  137. package/lib/middleware/error-handler.js +37 -14
  138. package/lib/middleware/fetch-metadata.js +39 -0
  139. package/lib/middleware/flag-context.js +34 -0
  140. package/lib/middleware/gpc.js +33 -0
  141. package/lib/middleware/headers.js +35 -0
  142. package/lib/middleware/health.js +46 -0
  143. package/lib/middleware/host-allowlist.js +30 -0
  144. package/lib/middleware/network-allowlist.js +38 -0
  145. package/lib/middleware/openapi-serve.js +34 -0
  146. package/lib/middleware/rate-limit.js +160 -18
  147. package/lib/middleware/request-id.js +36 -18
  148. package/lib/middleware/request-log.js +37 -0
  149. package/lib/middleware/require-aal.js +29 -0
  150. package/lib/middleware/require-auth.js +32 -0
  151. package/lib/middleware/require-bound-key.js +41 -0
  152. package/lib/middleware/require-content-type.js +32 -0
  153. package/lib/middleware/require-methods.js +27 -0
  154. package/lib/middleware/require-mtls.js +33 -0
  155. package/lib/middleware/require-step-up.js +37 -0
  156. package/lib/middleware/security-headers.js +44 -0
  157. package/lib/middleware/security-txt.js +38 -0
  158. package/lib/middleware/span-http-server.js +37 -0
  159. package/lib/middleware/sse.js +36 -0
  160. package/lib/middleware/trace-log-correlation.js +33 -0
  161. package/lib/middleware/trace-propagate.js +32 -0
  162. package/lib/middleware/tus-upload.js +90 -0
  163. package/lib/middleware/web-app-manifest.js +53 -0
  164. package/lib/mtls-ca.js +100 -70
  165. package/lib/network-byte-quota.js +308 -0
  166. package/lib/network-heartbeat.js +135 -0
  167. package/lib/network-tls.js +534 -4
  168. package/lib/network.js +103 -0
  169. package/lib/notify.js +114 -43
  170. package/lib/ntp-check.js +192 -51
  171. package/lib/observability.js +145 -47
  172. package/lib/openapi.js +90 -44
  173. package/lib/outbox.js +99 -1
  174. package/lib/pagination.js +168 -86
  175. package/lib/parsers/index.js +16 -5
  176. package/lib/permissions.js +93 -40
  177. package/lib/pqc-agent.js +84 -8
  178. package/lib/pqc-software.js +94 -60
  179. package/lib/process-spawn.js +95 -21
  180. package/lib/pubsub.js +96 -66
  181. package/lib/queue.js +375 -54
  182. package/lib/redact.js +793 -21
  183. package/lib/render.js +139 -47
  184. package/lib/request-helpers.js +485 -121
  185. package/lib/restore-bundle.js +142 -39
  186. package/lib/restore-rollback.js +136 -45
  187. package/lib/retention.js +178 -50
  188. package/lib/retry.js +116 -33
  189. package/lib/router.js +475 -23
  190. package/lib/safe-async.js +543 -94
  191. package/lib/safe-buffer.js +337 -41
  192. package/lib/safe-json.js +467 -62
  193. package/lib/safe-jsonpath.js +285 -0
  194. package/lib/safe-schema.js +631 -87
  195. package/lib/safe-sql.js +221 -59
  196. package/lib/safe-url.js +278 -46
  197. package/lib/sandbox-worker.js +135 -0
  198. package/lib/sandbox.js +358 -0
  199. package/lib/scheduler.js +135 -70
  200. package/lib/self-update.js +647 -0
  201. package/lib/session-device-binding.js +431 -0
  202. package/lib/session.js +259 -49
  203. package/lib/slug.js +138 -26
  204. package/lib/ssrf-guard.js +316 -56
  205. package/lib/storage.js +433 -70
  206. package/lib/subject.js +405 -23
  207. package/lib/template.js +148 -8
  208. package/lib/tenant-quota.js +545 -0
  209. package/lib/testing.js +440 -53
  210. package/lib/time.js +291 -23
  211. package/lib/tls-exporter.js +239 -0
  212. package/lib/tracing.js +90 -74
  213. package/lib/uuid.js +97 -22
  214. package/lib/vault/index.js +284 -22
  215. package/lib/vault/seal-pem-file.js +66 -0
  216. package/lib/watcher.js +368 -0
  217. package/lib/webhook.js +196 -63
  218. package/lib/websocket.js +393 -68
  219. package/lib/wiki-concepts.js +338 -0
  220. package/lib/worker-pool.js +464 -0
  221. package/package.json +3 -3
  222. package/sbom.cyclonedx.json +7 -7
@@ -1,88 +1,67 @@
1
1
  "use strict";
2
2
  /**
3
- * guard-filename — filename content-safety primitive (b.guardFilename).
3
+ * @module b.guardFilename
4
+ * @nav Guards
5
+ * @title Guard Filename
4
6
  *
5
- * Threat catalog grounded in current research: OWASP Path Traversal +
6
- * WSTG file-inclusion testing guides; CWE-22 / CWE-23 / CWE-35 / CWE-73
7
- * / CWE-78 / CWE-434 / CWE-36; PortSwigger File-path-traversal series
8
- * (null-byte bypass + extension validation); Memento-RTLO + RTL-Spiegel
9
- * file-name spoofing reports (CVE-2021-42574 in filename context); Kevin
10
- * Boone overlong UTF-8 sequence write-up.
7
+ * @intro
8
+ * Filename content-safety primitive (KIND="filename"). Validates
9
+ * user-supplied filenames before they reach disk, network paths,
10
+ * or Content-Disposition headers. Standalone primitive does NOT
11
+ * register into `b.guardAll`'s content-type-routed dispatch (no
12
+ * canonical mime / ext); operators wire it directly via
13
+ * `b.fileUpload({ filenameSafety: gate })` and similar host opts.
11
14
  *
12
- * var rv = b.guardFilename.validate("../etc/passwd", { profile: "strict" });
13
- * var safe = b.guardFilename.sanitize("My File‮.txt", { profile: "balanced" });
14
- * var g = b.guardFilename.gate({ profile: "strict" });
15
- *
16
- * Threat catalog covered:
17
- *
18
- * 1. Path traversal `..`, `../`, `..\\`, percent-encoded `%2e%2e`,
19
- * double-encoded `%252e%252e`, UTF-8 overlong `0xC0 0xAE` for `.`
20
- * and `0xC0 0xAF` for `/`. Refused regardless of profile.
21
- *
22
- * 2. Null-byte truncation — `file.txt\x00.exe` — string ends at null
23
- * in C-shaped APIs while validation sees `.txt`. Refused.
24
- *
25
- * 3. Windows reserved device names — CON / PRN / AUX / NUL / COM1-9 /
26
- * LPT1-9 / CLOCK$ — even with extensions (CON.txt is reserved).
27
- * Case-insensitive match. Refused.
28
- *
29
- * 4. NTFS alternate data streams — `file.txt:hidden.exe`. Colon is
30
- * the ADS separator on NTFS. Refused.
31
- *
32
- * 5. Leading/trailing whitespace + trailing dots — Windows strips
33
- * them silently, so `secret.txt ` and `secret.txt.` save as
34
- * `secret.txt`. Refused under strict, stripped under balanced.
35
- *
36
- * 6. Unicode bidi / RTLO — CVE-2021-42574 in filename context. The
37
- * Memento-RTLO toolkit weaponizes this: `Photo01By‮gpj.SCR`
38
- * displays as `Photo01ByRCS.jpg` while the OS opens `.SCR`.
39
- * Refused regardless of profile.
40
- *
41
- * 7. Zero-width / invisible-formatting chars — used to hide the real
42
- * extension between the visible name and what the OS sees.
43
- *
44
- * 8. Homoglyph chars — Cyrillic / Greek / fullwidth Latin mixed with
45
- * ASCII letters in a single name. Operator-decided severity per
46
- * profile.
15
+ * Path-traversal defense: `..` / `../` / `..\\`, percent-encoded
16
+ * `%2e%2e`, double-encoded `%252e%252e`, and UTF-8 overlong
17
+ * sequences `0xC0 0xAE` (for `.`) and `0xC0 0xAF` (for `/`) ALWAYS
18
+ * throw — no profile downgrades the refusal. Threat catalog
19
+ * grounded in OWASP Path Traversal + WSTG file-inclusion testing
20
+ * guides; CWE-22 / 23 / 35 / 73 / 78 / 434 / 36; PortSwigger
21
+ * File-path-traversal series (null-byte bypass + extension
22
+ * validation); Memento-RTLO + RTL-Spiegel filename-spoofing
23
+ * reports (CVE-2021-42574 in filename context); Kevin Boone
24
+ * overlong UTF-8 write-up.
47
25
  *
48
- * 9. Path separators inside a leaf-name — `file/with/slashes` and
49
- * `\\` variants. Reserved-character check.
26
+ * Universal-throw security floor: null-byte truncation
27
+ * (`file.txt\x00.exe`), NTFS alternate data streams
28
+ * (`file.txt:hidden.exe`), UNC paths (`\\server\share\file` and
29
+ * `//host/share/file`), and overlong UTF-8 byte sequences ALL
30
+ * throw `GuardFilenameError` regardless of profile — there is no
31
+ * sanitize-action that repairs these classes. Windows reserved
32
+ * device names (CON / PRN / AUX / NUL / COM1-9 / LPT1-9 / CLOCK$
33
+ * / CONFIG$) refuse under strict and balanced (even with
34
+ * extensions — `CON.txt` collides with the device).
50
35
  *
51
- * 10. Reserved characters — Windows: `< > : " / \ | ? *` plus the C0
52
- * controls and DEL. Refused under strict / balanced; permissive
53
- * strips and re-checks length.
36
+ * Unicode hygiene: BIDI / RTLO refuses at every profile (Memento-
37
+ * RTLO `Photo01By‮gpj.SCR` displays as `Photo01ByRCS.jpg` while
38
+ * the OS opens `.SCR`). Zero-width and invisible-formatting strip
39
+ * under balanced/permissive, refuse under strict. Homoglyph
40
+ * (Cyrillic / Greek / fullwidth Latin mixed with ASCII letters)
41
+ * refuses under strict, audits under balanced/permissive.
54
42
  *
55
- * 11. UNC paths — `\\server\share\file` syntax. Refused (network-path
56
- * resolution is outside the local-file scope).
43
+ * Extension policy: operator-supplied `extensionAllowlist`
44
+ * catches double-extension bypass (`file.jpg.exe` lands at the
45
+ * last `.exe` and refuses). Shell-shortcut / executable extensions
46
+ * (`.lnk` / `.url` / `.desktop` / `.scr` / `.bat` / `.cmd` /
47
+ * `.com` / `.pif` / `.vbs` / `.js` / `.jse` / `.wsf` / `.wsh` /
48
+ * `.ps1` / `.psm1` / `.app` / `.deb` / `.rpm` / `.msi` and the
49
+ * broader native-binary family) refuse under strict, audit under
50
+ * balanced/permissive.
57
51
  *
58
- * 12. Length caps Windows MAX_PATH 260, NTFS 32767, ext4 255 bytes
59
- * per component. Default leaf cap 255 bytes; total-path cap 260.
52
+ * Length caps: 64 bytes (strict), 255 bytes (balanced/permissive).
53
+ * Path separators in the leaf refuse under strict/balanced;
54
+ * permissive opts in to multi-component paths via
55
+ * `pathSeparatorsPolicy: "audit"` and `maxComponents > 1`.
60
56
  *
61
- * 13. Empty / dot components — `..//.//file` after normalization. The
62
- * validator surfaces these as path-traversal-shape issues.
57
+ * Profiles: `strict` / `balanced` / `permissive`. Compliance
58
+ * postures: `hipaa` / `pci-dss` / `gdpr` / `soc2`. Threat-detection
59
+ * regex literals composed programmatically from numeric codepoint
60
+ * range tables (`lib/codepoint-class`); source file never embeds
61
+ * attack characters.
63
62
  *
64
- * 14. Single-dot leaf — name === "." or ".." refused.
65
- *
66
- * 15. Allowlist mode — operators can pass `extensionAllowlist:
67
- * [".png", ".jpg", ".pdf"]` to require a single allowed extension.
68
- * The validator catches double-extension bypass: `file.jpg.exe`
69
- * lands at the last dot's extension `.exe` and is refused.
70
- *
71
- * 16. Shell-shortcut extensions — `.lnk`, `.url`, `.desktop`, `.scr`,
72
- * `.bat`, `.cmd`, `.com`, `.pif`, `.vbs`, `.js`, `.jse`, `.wsf`,
73
- * `.wsh`, `.ps1`, `.psm1`, `.app`, `.deb`, `.rpm`, `.msi`. Refused
74
- * under strict; balanced/permissive emit a warn-level audit issue.
75
- *
76
- * 17. UTF-8 overlong encoding — bytes that decode to ASCII separators
77
- * via non-shortest-form encoding (Unicode standard prohibits, but
78
- * legacy decoders accept them).
79
- *
80
- * 18. Anti-DoS caps — total filename byte length, total component
81
- * count when the operator allows path-shape (default: leaf only).
82
- *
83
- * Threat-detection regex literals composed programmatically from numeric
84
- * codepoint range tables (lib/codepoint-class). Source file never
85
- * embeds attack characters.
63
+ * @card
64
+ * Filename content-safety primitive (KIND="filename").
86
65
  */
87
66
 
88
67
  var codepointClass = require("./codepoint-class");
@@ -623,6 +602,56 @@ function _sanitize(input, opts) {
623
602
 
624
603
  // ---- Public surface ----
625
604
 
605
+ /**
606
+ * @primitive b.guardFilename.validate
607
+ * @signature b.guardFilename.validate(input, opts?)
608
+ * @since 0.7.5
609
+ * @status stable
610
+ * @compliance hipaa, pci-dss, gdpr, soc2
611
+ * @related b.guardFilename.sanitize, b.guardFilename.gate
612
+ *
613
+ * Inspect a filename (string or Buffer) and return
614
+ * `{ ok, issues, summary }`. Each issue carries
615
+ * `{ kind, severity, ruleId, location, snippet }` with severity in
616
+ * `"warn"|"high"|"critical"`. Detected: path-traversal raw and
617
+ * percent-encoded, null-byte truncation, NTFS ADS, UNC path,
618
+ * overlong UTF-8, Windows reserved-name, reserved character,
619
+ * leading/trailing whitespace + trailing dot, BIDI / control /
620
+ * zero-width / homoglyph, non-ASCII (when `requireAscii`), length
621
+ * cap, multi-dot violation, extension allowlist miss, double-
622
+ * extension with executable last segment, shell-shortcut extension.
623
+ * Pure inspection — never throws.
624
+ *
625
+ * @opts
626
+ * profile: "strict"|"balanced"|"permissive",
627
+ * compliance: "hipaa"|"pci-dss"|"gdpr"|"soc2",
628
+ * bidiPolicy: "reject"|"strip"|"allow",
629
+ * controlPolicy: "reject"|"strip"|"allow",
630
+ * nullBytePolicy: "reject", // always reject
631
+ * zeroWidthPolicy: "reject"|"strip"|"allow",
632
+ * homoglyphPolicy: "reject"|"audit"|"allow",
633
+ * traversalPolicy: "reject", // always reject
634
+ * reservedCharPolicy: "reject"|"strip"|"allow",
635
+ * reservedNamePolicy: "reject"|"audit"|"allow",
636
+ * adsPolicy: "reject", // always reject
637
+ * leadingTrailingPolicy: "reject"|"strip"|"allow",
638
+ * shellExecExtPolicy: "reject"|"audit"|"allow",
639
+ * pathSeparatorsPolicy: "reject"|"audit"|"allow",
640
+ * unicodeNormalization: "NFC"|null,
641
+ * requireAscii: boolean,
642
+ * extensionAllowlist: string[]|null,
643
+ * requireSingleDot: boolean,
644
+ * maxBytes: number, // leaf-name byte cap
645
+ * maxComponents: number, // path-component count
646
+ *
647
+ * @example
648
+ * var rv = b.guardFilename.validate("../etc/passwd", { profile: "strict" });
649
+ * rv.ok; // → false
650
+ * rv.issues.some(function (i) { return i.kind === "path-traversal"; }); // → true
651
+ *
652
+ * var ok = b.guardFilename.validate("report-2026-Q1.txt", { profile: "strict" });
653
+ * ok.ok; // → true
654
+ */
626
655
  function validate(input, opts) {
627
656
  opts = _resolveOpts(opts);
628
657
  numericBounds.requireAllPositiveFiniteIntIfPresent(opts,
@@ -634,14 +663,151 @@ function validate(input, opts) {
634
663
  return gateContract.aggregateIssues(_detectIssues(input, opts));
635
664
  }
636
665
 
666
+ function _sanitizeStripMode(input, opts) {
667
+ // mode: "strip" — operator-friendly Content-Disposition path. C0 control
668
+ // chars (CR / LF / etc., excluding NUL — null-byte truncation is never
669
+ // sanitizable) and bidi-override codepoints are replaced with "_". The
670
+ // security floor still applies: path traversal, null-byte, NTFS ADS,
671
+ // UNC, and overlong UTF-8 throw at every profile level.
672
+ if (Buffer.isBuffer(input) && _hasOverlongUtf8(input)) {
673
+ throw _err("filename.overlong-utf8", "filename has overlong UTF-8 sequence — cannot sanitize");
674
+ }
675
+ var name = typeof input === "string"
676
+ ? input
677
+ : (Buffer.isBuffer(input) ? input.toString("utf8") : "");
678
+ if (name.length === 0) {
679
+ throw _err("filename.empty", "sanitize requires non-empty filename");
680
+ }
681
+ if (name.indexOf("\0") !== -1) {
682
+ throw _err("filename.null-byte", "filename contains null byte — null-byte truncation is never sanitizable");
683
+ }
684
+ // Replace control chars + bidi codepoints with "_". Zero-width is
685
+ // also stripped (visible-name spoofing has no value if the bytes
686
+ // round-trip silently). CR (U+0D) / LF (U+0A) are NOT in the shared
687
+ // C0 table (they're dialect-shaped chars elsewhere) but they're the
688
+ // exact chars that enable Content-Disposition response splitting,
689
+ // which is the primary use case for strip mode — replace explicitly.
690
+ // allow:dynamic-regex — replace-character class composed at construction
691
+ name = name.replace(/[\r\n\t\v\f]/g, "_");
692
+ name = name.replace(codepointClass.C0_CTRL_RE_G, "_");
693
+ name = name.replace(codepointClass.BIDI_RE_G, "_");
694
+ name = name.replace(codepointClass.ZW_RE_G, "_");
695
+ if (opts.unicodeNormalization === "NFC") name = _normalizeNFC(name);
696
+
697
+ // Security floor — never sanitizable.
698
+ // allow:regex-no-length-cap — operator-supplied filename; length validated below
699
+ if (PATH_TRAVERSAL_RE.test(name) || PERCENT_ENCODED_TRAVERSAL_RE.test(name) ||
700
+ name === "." || name === "..") {
701
+ throw _err("filename.traversal", "filename contains path-traversal sequence");
702
+ }
703
+ if (/^\\\\|^\/\//.test(name)) {
704
+ throw _err("filename.unc", "UNC path syntax");
705
+ }
706
+ if (/:[^:\\/]+$/.test(name) && name.charAt(0) !== "/") {
707
+ throw _err("filename.ntfs-ads", "filename contains NTFS alternate data stream syntax");
708
+ }
709
+ if (Buffer.byteLength(name, "utf8") > opts.maxBytes) {
710
+ throw _err("filename.length", "filename exceeds maxBytes " + opts.maxBytes);
711
+ }
712
+ if (name.length === 0) {
713
+ throw _err("filename.empty", "sanitize produced empty filename");
714
+ }
715
+ return name;
716
+ }
717
+
718
+ /**
719
+ * @primitive b.guardFilename.sanitize
720
+ * @signature b.guardFilename.sanitize(input, opts?)
721
+ * @since 0.7.5
722
+ * @status stable
723
+ * @related b.guardFilename.validate, b.guardFilename.gate
724
+ *
725
+ * Best-effort cleanup of a filename. Two modes: `"enforce"` (default;
726
+ * applies the profile's strip/reject policies and throws on
727
+ * unsanitizable refusals) and `"strip"` (operator-friendly
728
+ * Content-Disposition path — replaces control / bidi / zero-width
729
+ * codepoints with `_` and applies a security floor).
730
+ *
731
+ * The security floor ALWAYS throws regardless of mode/profile:
732
+ * path-traversal raw and percent-encoded, null-byte, NTFS alternate
733
+ * data streams, UNC paths, overlong UTF-8 sequences, and post-strip
734
+ * length-cap violation. These classes are unrepairable — silently
735
+ * fixing them would mask the attack signal an audit log needs.
736
+ *
737
+ * @opts
738
+ * profile: "strict"|"balanced"|"permissive",
739
+ * compliance: "hipaa"|"pci-dss"|"gdpr"|"soc2",
740
+ * mode: "enforce"|"strip",
741
+ * audit: { safeEmit: function }, // optional sink for strip mode
742
+ * unicodeNormalization: "NFC"|null,
743
+ * maxBytes: number,
744
+ *
745
+ * @example
746
+ * var safe = b.guardFilename.sanitize("My File.txt", { profile: "balanced" });
747
+ * safe; // → "My File.txt"
748
+ *
749
+ * // Path traversal ALWAYS throws — never sanitizable.
750
+ * try {
751
+ * b.guardFilename.sanitize("../etc/passwd", { profile: "permissive" });
752
+ * } catch (e) {
753
+ * e.code; // → "filename.traversal"
754
+ * }
755
+ */
637
756
  function sanitize(input, opts) {
757
+ var rawMode = opts && opts.mode;
638
758
  opts = _resolveOpts(opts);
639
759
  if (typeof input !== "string" && !Buffer.isBuffer(input)) {
640
760
  throw _err("filename.bad-input", "sanitize requires string or Buffer input");
641
761
  }
762
+ if (rawMode === "strip") {
763
+ var stripped = _sanitizeStripMode(input, opts);
764
+ if (opts.audit && typeof opts.audit.safeEmit === "function") {
765
+ try {
766
+ opts.audit.safeEmit({
767
+ action: "guardfilename.sanitize.stripped",
768
+ outcome: "success",
769
+ metadata: {
770
+ originalLength: Buffer.byteLength(
771
+ typeof input === "string" ? input : input.toString("utf8"), "utf8"),
772
+ sanitizedLength: Buffer.byteLength(stripped, "utf8"),
773
+ },
774
+ });
775
+ } catch (_e) { /* drop-silent — audit sinks must never crash the producer */ }
776
+ }
777
+ return stripped;
778
+ }
642
779
  return _sanitize(input, opts);
643
780
  }
644
781
 
782
+ /**
783
+ * @primitive b.guardFilename.gate
784
+ * @signature b.guardFilename.gate(opts?)
785
+ * @since 0.7.5
786
+ * @status stable
787
+ * @compliance hipaa, pci-dss, gdpr, soc2
788
+ * @related b.guardFilename.validate, b.guardFilename.sanitize, b.fileUpload.create
789
+ *
790
+ * Build a `b.gateContract` gate that consumes `ctx.filename` (or
791
+ * `ctx.name`). Action chain: `serve` (no filename or clean) →
792
+ * `audit-only` (warn-only issues) → `sanitize` (critical/high but
793
+ * every reject-policy off — strip-eligible classes only) → `refuse`
794
+ * (any reject-policy active or sanitize fails). Path-traversal /
795
+ * null-byte / NTFS-ADS / UNC / overlong-UTF-8 always cause `refuse`
796
+ * — there is no `sanitize` action for those classes.
797
+ *
798
+ * @opts
799
+ * profile: "strict"|"balanced"|"permissive",
800
+ * compliance: "hipaa"|"pci-dss"|"gdpr"|"soc2",
801
+ * name: string, // gate identity for audit / observability
802
+ *
803
+ * @example
804
+ * var fnGate = b.guardFilename.gate({ profile: "strict" });
805
+ * var verdict = await fnGate.check({ filename: "../etc/passwd" });
806
+ * verdict.action; // → "refuse"
807
+ *
808
+ * var ok = await fnGate.check({ filename: "report.txt" });
809
+ * ok.action; // → "serve"
810
+ */
645
811
  function gate(opts) {
646
812
  opts = _resolveOpts(opts);
647
813
  return gateContract.buildGuardGate(
@@ -682,13 +848,79 @@ function gate(opts) {
682
848
  });
683
849
  }
684
850
 
851
+ /**
852
+ * @primitive b.guardFilename.buildProfile
853
+ * @signature b.guardFilename.buildProfile(opts)
854
+ * @since 0.7.5
855
+ * @status stable
856
+ * @related b.guardFilename.gate, b.guardFilename.compliancePosture
857
+ *
858
+ * Compose a derived profile from one or more named bases plus inline
859
+ * overrides. `opts.extends` is a profile name (`"strict"` /
860
+ * `"balanced"` / `"permissive"`) or an array of names; later entries
861
+ * shadow earlier ones. Inline `opts` keys win last.
862
+ *
863
+ * @opts
864
+ * extends: string|string[], // base profile name(s) to compose
865
+ *
866
+ * @example
867
+ * var custom = b.guardFilename.buildProfile({
868
+ * extends: "balanced",
869
+ * extensionAllowlist: [".pdf", ".png", ".jpg"],
870
+ * });
871
+ * custom.extensionAllowlist.length; // → 3
872
+ * custom.traversalPolicy; // → "reject"
873
+ */
685
874
  var buildProfile = gateContract.makeProfileBuilder(PROFILES);
686
875
 
876
+ /**
877
+ * @primitive b.guardFilename.compliancePosture
878
+ * @signature b.guardFilename.compliancePosture(name)
879
+ * @since 0.7.5
880
+ * @status stable
881
+ * @compliance hipaa, pci-dss, gdpr, soc2
882
+ * @related b.guardFilename.gate, b.guardFilename.buildProfile
883
+ *
884
+ * Look up a compliance-posture overlay by name (`"hipaa"` /
885
+ * `"pci-dss"` / `"gdpr"` / `"soc2"`). Returns the posture object —
886
+ * the caller may mutate freely. Throws
887
+ * `GuardFilenameError("filename.bad-posture")` on unknown name.
888
+ *
889
+ * @example
890
+ * var posture = b.guardFilename.compliancePosture("hipaa");
891
+ * posture.requireAscii; // → true
892
+ * posture.shellExecExtPolicy; // → "reject"
893
+ */
687
894
  function compliancePosture(name) {
688
895
  return gateContract.lookupCompliancePosture(name, COMPLIANCE_POSTURES, _err, "filename");
689
896
  }
690
897
 
691
898
  var _filenameRulePacks = gateContract.makeRulePackLoader(GuardFilenameError, "filename");
899
+ /**
900
+ * @primitive b.guardFilename.loadRulePack
901
+ * @signature b.guardFilename.loadRulePack(pack)
902
+ * @since 0.7.5
903
+ * @status stable
904
+ * @related b.guardFilename.gate
905
+ *
906
+ * Register an operator-supplied rule pack with the guard-filename
907
+ * registry. The pack is identified by `pack.id` (non-empty string)
908
+ * and stored for later inspection / dispatch by gates that opt in
909
+ * via `opts.rulePackId`. Returns the pack object unchanged on
910
+ * success; throws `GuardFilenameError("filename.bad-opt")` when
911
+ * `pack` is missing or `pack.id` is not a non-empty string.
912
+ *
913
+ * @example
914
+ * var pack = b.guardFilename.loadRulePack({
915
+ * id: "tenant-uploads-policy",
916
+ * rules: [
917
+ * { id: "tenant-prefix", severity: "high",
918
+ * detect: function (n) { return n.indexOf("tenant_") !== 0; },
919
+ * reason: "tenant policy: filenames must be tenant_-prefixed" },
920
+ * ],
921
+ * });
922
+ * pack.id; // → "tenant-uploads-policy"
923
+ */
692
924
  var loadRulePack = _filenameRulePacks.load;
693
925
 
694
926
  module.exports = {