@blamejs/core 0.8.43 → 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 +92 -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,124 +1,69 @@
1
1
  "use strict";
2
2
  /**
3
- * guard-archive — archive content-safety primitive (b.guardArchive).
4
- *
5
- * Threat catalog grounded in current research (multiple 2025-2026 CVEs):
6
- * - CVE-2025-3445 mholt/archiver Zip Slip
7
- * - CVE-2025-32779 EDDI Zip Slip
8
- * - CVE-2025-62156 Argo Workflows Zip Slip
9
- * - CVE-2025-66945 Zdir Pro Path Traversal
10
- * - CVE-2025-45582 GNU Tar Path Traversal (two-step symlink bypass)
11
- * - CVE-2025-11001 / 11002 7-Zip symlink + directory traversal RCE
12
- * - CVE-2025-4138 Python tarfile extraction-filter symlink bypass
13
- * - CVE-2025-4517 Python tarfile path traversal
14
- * - CVE-2025-10854 txtai Framework path traversal
15
- * - CVE-2025-12060 Keras path traversal
16
- * - CVE-2026-26960 node-tar hardlink-via-symlink-chain escape
17
- *
18
- * var rv = b.guardArchive.validateEntries(entries, { profile: "strict" });
19
- * var fmt = b.guardArchive.inspectMagic(buffer);
20
- * var g = b.guardArchive.gate({ profile: "strict" });
21
- *
22
- * **Scope.** This primitive validates archive METADATA (entry list +
23
- * sizes + flags + types) before extraction. It does NOT include a
24
- * pure-JS unzip / untar implementation — the framework's no-deps rule
25
- * argues against shipping a parser for every archive format. Operators
26
- * use their archive library (built-in zlib for gzip/deflate, OS tar /
27
- * unzip CLI, or vendored libraries) to enumerate entries, then validate
28
- * the list before extracting. The gate's job is to refuse hostile
29
- * metadata BEFORE files touch the filesystem.
30
- *
31
- * var entries = parseZipCentralDirectory(uploadedBuffer);
32
- * var rv = b.guardArchive.validateEntries(entries, { profile: "strict" });
33
- * if (!rv.ok) throw new Error("hostile archive: " + rv.issues[0].snippet);
34
- * await extractEachEntry(entries, extractionRoot);
35
- *
36
- * Entry shape (operator passes one of these per archive entry):
37
- *
38
- * {
39
- * name: string, // entry filename / path WITHIN archive
40
- * size: number, // uncompressed size in bytes
41
- * compressedSize: number, // compressed size (optional; enables ratio check)
42
- * isSymlink: boolean, // true if entry creates a symbolic link
43
- * isHardlink: boolean, // true if entry creates a hardlink
44
- * linkTarget: string, // when isSymlink/isHardlink: where it points
45
- * isDirectory: boolean, // directory entry (no extraction needed)
46
- * isEncrypted: boolean, // entry is encrypted
47
- * attrs: object, // optional: extra format-specific metadata
48
- * }
49
- *
50
- * Threat catalog covered:
51
- *
52
- * 1. Zip slip / path traversal entry name with `../`, `..\\`, or
53
- * absolute path (leading `/` or `\\` or drive letter). Composes
54
- * `b.guardFilename` for per-entry-name validation; archive-level
55
- * adds the absolute-path check that filename-leaf doesn't.
56
- *
57
- * 2. Symlink escape entry creates a symbolic link whose `linkTarget`
58
- * contains `..` or absolute path that resolves outside the
59
- * extraction root. Refused or audited per profile.
60
- *
61
- * 3. Hardlink escape same as symlink but via the hardlink mechanism
62
- * (CVE-2026-26960 node-tar class). The extraction step typically
63
- * resolves hardlink targets relative to extraction root; entries
64
- * with `..` in linkTarget escape.
65
- *
66
- * 4. Symlink-chained traversaloperator pre-extracts a symlink, then
67
- * a later entry writes through the symlink's target. We refuse any
68
- * entry whose extraction path passes THROUGH a symlink already in
69
- * the entry list (when the operator passes pre-sorted entries).
70
- *
71
- * 5. Decompression-ratio bombs — per-entry compressedSize/size ratio
72
- * cap (default: 100:1 strict, 1000:1 permissive). Aggregate ratio
73
- * across all entries also capped.
74
- *
75
- * 6. Total-size cap — sum of uncompressed sizes (anti-DoS).
76
- *
77
- * 7. File-count cap — number of entries.
78
- *
79
- * 8. Nested-archive depth — refuses entries that are themselves
80
- * archives unless `maxNestedDepth > 0`. Entry name suffixes are
81
- * checked against an archive-extension catalog (.zip / .tar /
82
- * .tar.gz / .tgz / .gz / .bz2 / .xz / .7z / .rar / .ar / .cpio /
83
- * .lzma / .zst).
84
- *
85
- * 9. Per-entry-name validation via b.guardFilename — applies the full
86
- * filename-safety catalog (path traversal / null-byte / Windows
87
- * reserved names / NTFS ADS / RTLO bidi / overlong UTF-8 / shell-
88
- * exec extensions / double-extension) to every entry's name.
89
- *
90
- * 10. Duplicate entry names — second entry with the same name silently
91
- * overwrites the first on extraction. Refused.
92
- *
93
- * 11. Mixed-case duplicate names — case-insensitive collision on Windows
94
- * / macOS HFS+ / APFS-non-case-sensitive volumes. Audited.
95
- *
96
- * 12. Encryption-claim mismatch — operator opts in to either "all
97
- * entries encrypted" or "no entries encrypted"; mixing flagged.
98
- *
99
- * 13. Format-claim mismatch — `inspectMagic(buffer)` reads the first
100
- * bytes and returns the detected format. Operator can compare
101
- * against the declared content-type / extension; mismatch flagged.
102
- *
103
- * 14. Sparse archive (tar) — sparse entries can claim large
104
- * uncompressed size with zero data; refused unless explicitly
105
- * allowed.
106
- *
107
- * 15. Anti-DoS caps — total entry count, per-entry size, total size,
108
- * compression ratio, recursion depth.
109
- *
110
- * Profiles:
111
- * strict — every threat refused; no symlinks; no hardlinks;
112
- * no nested archives; 100 entry max; 100 MiB total;
113
- * 100:1 ratio cap; case-insensitive collision refused.
114
- * balanced — symlinks within extraction-root allowed; no hardlinks;
115
- * nested-depth 2; 10000 entries; 1 GiB total; 100:1
116
- * per-entry / 1000:1 aggregate; case-collision audited.
117
- * permissive — symlinks + hardlinks within root allowed; nested-depth
118
- * 4; 100000 entries; 10 GiB total; 1000:1 ratio.
119
- *
120
- * Compliance postures: hipaa / pci-dss / gdpr / soc2 — strict
121
- * overlay + forensic snapshots.
3
+ * @module b.guardArchive
4
+ * @nav Guards
5
+ * @title Guard Archive
6
+ *
7
+ * @intro
8
+ * Archive content-safety guard refuses hostile archive metadata
9
+ * BEFORE files touch the filesystem. Validates an operator-supplied
10
+ * entry list (the framework ships no pure-JS unzip / untar parser per
11
+ * the no-deps rule) plus an optional magic-byte inspection on raw
12
+ * bytes. Operators enumerate entries via their archive library
13
+ * (built-in zlib, OS tar / unzip CLI, vendored mupdf-of-archives) and
14
+ * pass `[{ name, size, compressedSize, isSymlink, isHardlink,
15
+ * linkTarget, isDirectory, isEncrypted, attrs }, ...]` to
16
+ * `validateEntries`.
17
+ *
18
+ * Zip-slip / path-traversal: entry names containing `..` segments,
19
+ * leading `/` or `\\`, or Windows drive-letter prefixes (`C:\\`) are
20
+ * refused under every profile. Composes `b.guardFilename` for the
21
+ * full leaf-safety catalog (null-byte, Windows reserved names, NTFS
22
+ * ADS, RTLO bidi, overlong UTF-8, shell-exec extensions, double-
23
+ * extension). Tracks the 2025-2026 CVE class: CVE-2025-3445
24
+ * (mholt/archiver), CVE-2025-32779 (EDDI), CVE-2025-62156 (Argo
25
+ * Workflows), CVE-2025-66945 (Zdir Pro), CVE-2025-45582 (GNU Tar
26
+ * two-step symlink bypass), CVE-2025-11001 / 11002 (7-Zip RCE),
27
+ * CVE-2025-4138 / 4517 (Python tarfile), CVE-2025-10854 (txtai),
28
+ * CVE-2025-12060 (Keras), CVE-2026-26960 (node-tar hardlink-via-
29
+ * symlink chain).
30
+ *
31
+ * Symlink / hardlink escape: entries whose `linkTarget` contains `..`
32
+ * or is absolute are refused. `strict` rejects symlinks AND hardlinks
33
+ * outright; `balanced` permits in-root symlinks and rejects hardlinks
34
+ * (CVE-2026-26960 class); `permissive` audits both.
35
+ *
36
+ * Decompression amplification: per-entry `compressedSize`/`size` ratio
37
+ * cap defaults 100:1 (strict) / 100:1 (balanced) / 1000:1 (permissive).
38
+ * Aggregate ratio across all entries also capped (`maxAggregateRatio`).
39
+ * Entry-count cap (`maxEntries`), per-entry size cap (`maxEntryBytes`),
40
+ * total uncompressed cap (`maxTotalBytes`).
41
+ *
42
+ * NTFS ADS, overlong UTF-8, leaf-bidi: routed through `b.guardFilename`
43
+ * on every entry name with `pathSeparatorsPolicy: "allow"` (archive
44
+ * entries legitimately use `/` as separator).
45
+ *
46
+ * Nested archives: entries with archive extensions (`.zip`, `.tar.gz`,
47
+ * `.7z`, `.rar`, `.zst`, ...) refused under `strict` (`maxNestedDepth:
48
+ * 0`); audited under `balanced` (depth 2) / `permissive` (depth 4) so
49
+ * the operator can recurse.
50
+ *
51
+ * Duplicate-name + case-insensitive collision detection — the second
52
+ * entry with the same name silently overwrites on extraction (refused);
53
+ * case-insensitive collisions on Windows / HFS+ / APFS-non-case-
54
+ * sensitive volumes (audited / refused per profile).
55
+ *
56
+ * `inspectMagic(buffer)` returns `{ format, magic }` for ZIP / GZIP /
57
+ * BZIP2 / XZ / 7Z / RAR4 / RAR5 / LZMA / ZSTD / TAR (the latter via
58
+ * the "ustar" magic at offset 257). `checkExtractionPath(name, root)`
59
+ * provides a single-entry boolean for callers that already enumerate.
60
+ *
61
+ * Profiles `strict` / `balanced` / `permissive` and compliance
62
+ * postures `hipaa` / `pci-dss` / `gdpr` / `soc2` overlay on the
63
+ * profile baseline.
64
+ *
65
+ * @card
66
+ * Archive content-safety guardrefuses hostile archive metadata BEFORE files touch the filesystem.
122
67
  */
123
68
 
124
69
  var lazyRequire = require("./lazy-require");
@@ -296,8 +241,33 @@ function _bufferStartsWith(buf, sig) {
296
241
  return true;
297
242
  }
298
243
 
299
- // inspectMagic — reads the first bytes of a buffer and returns the
300
- // detected archive format, or null if not recognized.
244
+ /**
245
+ * @primitive b.guardArchive.inspectMagic
246
+ * @signature b.guardArchive.inspectMagic(buffer)
247
+ * @since 0.7.8
248
+ * @status stable
249
+ * @related b.guardArchive.validateEntries, b.guardArchive.gate
250
+ *
251
+ * Read the first bytes of `buffer` and return
252
+ * `{ format, magic }` when the buffer matches a known archive-format
253
+ * signature (`zip` / `gzip` / `bzip2` / `xz` / `7z` / `rar4` / `rar5` /
254
+ * `lzma` / `zstd` / `tar`). TAR is detected via the `"ustar"` magic
255
+ * at offset 257 within the first 512-byte header block. Returns
256
+ * `null` on unrecognized input or non-Buffer / empty input. Pure
257
+ * inspection — never mutates the buffer or throws.
258
+ *
259
+ * Operators compare the detected format against the declared
260
+ * Content-Type / extension to surface format-claim mismatches before
261
+ * routing the bytes to a parser.
262
+ *
263
+ * @example
264
+ * var zipBytes = Buffer.from([0x50, 0x4B, 0x03, 0x04, 0x14, 0x00]);
265
+ * var hit = b.guardArchive.inspectMagic(zipBytes);
266
+ * hit.format; // → "zip"
267
+ *
268
+ * var noise = Buffer.from([0x00, 0x01, 0x02, 0x03]);
269
+ * b.guardArchive.inspectMagic(noise); // → null
270
+ */
301
271
  function inspectMagic(buffer) {
302
272
  if (!Buffer.isBuffer(buffer) || buffer.length === 0) return null;
303
273
  for (var i = 0; i < MAGIC_SIGNATURES.length; i += 1) {
@@ -318,9 +288,35 @@ function inspectMagic(buffer) {
318
288
  return null;
319
289
  }
320
290
 
321
- // checkExtractionPath — single-entry helper. Returns { ok, reason } for
322
- // a candidate (entryName, extractionRoot) pair. Anchored, for callers
323
- // that already enumerate entries and want a per-call boolean.
291
+ /**
292
+ * @primitive b.guardArchive.checkExtractionPath
293
+ * @signature b.guardArchive.checkExtractionPath(entryName, extractionRoot)
294
+ * @since 0.7.8
295
+ * @status stable
296
+ * @related b.guardArchive.validateEntries, b.guardArchive.gate
297
+ *
298
+ * Single-entry boolean check: returns `{ ok, reason }` for a candidate
299
+ * `(entryName, extractionRoot)` pair. Refuses entries whose name
300
+ * contains a `..` component (zip slip — CVE-2025-3445 class), is an
301
+ * absolute path (leading `/`, `\\`, or `C:\\` drive-letter prefix),
302
+ * carries a null byte, or is empty. The framework cannot resolve
303
+ * `path.resolve(extractionRoot, entryName)` without a `node:path`
304
+ * coupling that the gate keeps portable; the operator's extraction
305
+ * code is expected to additionally call `path.resolve` and confirm
306
+ * the result starts with `path.resolve(extractionRoot)`.
307
+ *
308
+ * Use when the operator already enumerates archive entries and wants
309
+ * a per-call boolean rather than running the full
310
+ * `validateEntries` issue list.
311
+ *
312
+ * @example
313
+ * b.guardArchive.checkExtractionPath("docs/readme.txt", "/var/extract").ok;
314
+ * // → true
315
+ *
316
+ * var bad = b.guardArchive.checkExtractionPath("../etc/passwd", "/var/extract");
317
+ * bad.ok; // → false
318
+ * bad.reason; // → "entry name contains .. component (zip slip)"
319
+ */
324
320
  function checkExtractionPath(entryName, extractionRoot) {
325
321
  if (typeof entryName !== "string" || entryName.length === 0) {
326
322
  return { ok: false, reason: "empty entry name" };
@@ -630,6 +626,64 @@ function _detectIssues(entries, opts) {
630
626
 
631
627
  // ---- Public surface ----
632
628
 
629
+ /**
630
+ * @primitive b.guardArchive.validateEntries
631
+ * @signature b.guardArchive.validateEntries(entries, opts)
632
+ * @since 0.7.8
633
+ * @status stable
634
+ * @compliance hipaa, pci-dss, gdpr, soc2
635
+ * @related b.guardArchive.gate, b.guardArchive.inspectMagic, b.guardFilename.validate
636
+ *
637
+ * Inspect an operator-supplied `entries` array (one entry per archive
638
+ * member: `{ name, size, compressedSize, isSymlink, isHardlink,
639
+ * linkTarget, isDirectory, isEncrypted, attrs }`) and return
640
+ * `{ ok, issues }`. Issues carry `{ kind, severity, ruleId, location,
641
+ * snippet }` with severity `"warn"` / `"high"` / `"critical"`.
642
+ * Detected: zip-slip, absolute path, symlink / hardlink escape,
643
+ * compression-ratio bombs (per-entry + aggregate), per-entry size
644
+ * cap, total-size cap, entry-count cap, nested-archive entries,
645
+ * duplicate names, case-insensitive collisions, encryption-claim
646
+ * mismatch, sparse-tar entries, plus the full `b.guardFilename`
647
+ * leaf-safety catalog re-attached with archive-context locations.
648
+ * Pure inspection — never mutates input or throws on hostile entries.
649
+ *
650
+ * @opts
651
+ * profile: "strict"|"balanced"|"permissive",
652
+ * compliance: "hipaa"|"pci-dss"|"gdpr"|"soc2",
653
+ * traversalPolicy: "reject"|"audit"|"allow",
654
+ * absolutePathPolicy: "reject"|"audit"|"allow",
655
+ * symlinkPolicy: "reject"|"audit"|"allow",
656
+ * hardlinkPolicy: "reject"|"audit"|"allow",
657
+ * nestedArchivePolicy: "reject"|"audit"|"allow",
658
+ * duplicateNamePolicy: "reject"|"audit"|"allow",
659
+ * caseInsensitiveCollisionPolicy: "reject"|"audit"|"allow",
660
+ * encryptionPolicy: "reject"|"audit"|"allow",
661
+ * sparseEntryPolicy: "reject"|"audit"|"allow",
662
+ * filenameProfile: "balanced"|"strict"|"permissive",
663
+ * maxEntries: number, // strict 100, balanced 10000, permissive 100000
664
+ * maxTotalBytes: number, // strict 100 MiB, balanced 1 GiB, permissive 10 GiB
665
+ * maxEntryBytes: number, // strict 50 MiB, balanced 500 MiB, permissive 2 GiB
666
+ * maxCompressionRatio: number, // strict / balanced 100, permissive 1000
667
+ * maxAggregateRatio: number, // strict 200, balanced 1000, permissive 10000
668
+ * maxNestedDepth: number, // strict 0, balanced 2, permissive 4
669
+ *
670
+ * @example
671
+ * var rv = b.guardArchive.validateEntries([
672
+ * { name: "docs/readme.txt", size: 1000, compressedSize: 500 },
673
+ * { name: "../etc/passwd", size: 100, compressedSize: 50 },
674
+ * ], { profile: "strict" });
675
+ * rv.ok; // → false
676
+ * rv.issues[0].kind; // → "zip-slip"
677
+ * rv.issues[0].severity; // → "critical"
678
+ *
679
+ * // Compression-ratio bomb — 50 MiB uncompressed from 50 KiB compressed
680
+ * // is 1000:1, far above the 100:1 strict cap.
681
+ * var bomb = b.guardArchive.validateEntries([
682
+ * { name: "bomb.bin", size: 52428800, compressedSize: 51200 },
683
+ * ], { profile: "strict" });
684
+ * bomb.issues.some(function (i) { return i.kind === "compression-ratio-bomb"; });
685
+ * // → true
686
+ */
633
687
  function validateEntries(entries, opts) {
634
688
  opts = _resolveOpts(opts);
635
689
  numericBounds.requireAllPositiveFiniteIntIfPresent(opts,
@@ -646,6 +700,49 @@ function validateEntries(entries, opts) {
646
700
  return gateContract.aggregateIssues(_detectIssues(entries, opts));
647
701
  }
648
702
 
703
+ /**
704
+ * @primitive b.guardArchive.gate
705
+ * @signature b.guardArchive.gate(opts)
706
+ * @since 0.7.8
707
+ * @status stable
708
+ * @compliance hipaa, pci-dss, gdpr, soc2
709
+ * @related b.guardArchive.validateEntries, b.guardArchive.inspectMagic, b.fileUpload, b.staticServe
710
+ *
711
+ * Build a `b.gateContract` gate suitable for `b.fileUpload({ contentSafety:
712
+ * { "application/zip": gate } })` or `b.staticServe`. Operators pass
713
+ * `ctx.entries` (the enumerated entry list from their archive library)
714
+ * — when only `ctx.bytes` is supplied, the gate runs `inspectMagic` to
715
+ * confirm the format and refuses with a `"no-entry-list"` issue
716
+ * directing the operator to enumerate entries explicitly (the
717
+ * framework ships no parser for any archive format).
718
+ *
719
+ * Action chain: `serve` (no issues) → `audit-only` (warn-only) →
720
+ * `refuse` (any critical/high). Archive content has no safe
721
+ * sanitization — there is no `sanitize` action in the chain.
722
+ *
723
+ * @opts
724
+ * profile: "strict"|"balanced"|"permissive",
725
+ * compliance: "hipaa"|"pci-dss"|"gdpr"|"soc2",
726
+ * name: string,
727
+ * ...: any validateEntries opt
728
+ *
729
+ * @example
730
+ * var archiveGate = b.guardArchive.gate({ profile: "strict" });
731
+ *
732
+ * var verdict = await archiveGate.check({
733
+ * entries: [
734
+ * { name: "docs/readme.txt", size: 1000, compressedSize: 500 },
735
+ * { name: "../etc/passwd", size: 100, compressedSize: 50 },
736
+ * ],
737
+ * });
738
+ * verdict.action; // → "refuse"
739
+ *
740
+ * // Bytes-only call without an entry list — operator must enumerate.
741
+ * var zipBytes = Buffer.from([0x50, 0x4B, 0x03, 0x04, 0x14, 0x00]);
742
+ * var v2 = await archiveGate.check({ bytes: zipBytes });
743
+ * v2.action; // → "refuse"
744
+ * v2.issues[0].kind; // → "no-entry-list"
745
+ */
649
746
  function gate(opts) {
650
747
  opts = _resolveOpts(opts);
651
748
  return gateContract.buildGuardGate(
@@ -689,13 +786,78 @@ function gate(opts) {
689
786
  });
690
787
  }
691
788
 
789
+ /**
790
+ * @primitive b.guardArchive.buildProfile
791
+ * @signature b.guardArchive.buildProfile(opts)
792
+ * @since 0.7.8
793
+ * @status stable
794
+ * @related b.guardArchive.compliancePosture, b.guardArchive.gate
795
+ *
796
+ * Resolve a named profile against the guard's PROFILES catalog and
797
+ * return the merged options bag. Operators introspecting the active
798
+ * caps (without calling `validateEntries` / `gate`) use this. Throws
799
+ * `GuardArchiveError("archive.bad-profile")` on unknown name.
800
+ *
801
+ * @opts
802
+ * profile: "strict"|"balanced"|"permissive",
803
+ *
804
+ * @example
805
+ * var resolved = b.guardArchive.buildProfile({ profile: "strict" });
806
+ * resolved.maxEntries; // → 100
807
+ * resolved.symlinkPolicy; // → "reject"
808
+ * resolved.maxCompressionRatio; // → 100
809
+ */
692
810
  var buildProfile = gateContract.makeProfileBuilder(PROFILES);
693
811
 
812
+ /**
813
+ * @primitive b.guardArchive.compliancePosture
814
+ * @signature b.guardArchive.compliancePosture(name)
815
+ * @since 0.7.8
816
+ * @status stable
817
+ * @compliance hipaa, pci-dss, gdpr, soc2
818
+ * @related b.guardArchive.gate, b.guardArchive.buildProfile
819
+ *
820
+ * Return the option overlay for a named compliance posture
821
+ * (`"hipaa"` / `"pci-dss"` / `"gdpr"` / `"soc2"`). Composes over a
822
+ * base profile to harden defaults per regulatory regime. Throws
823
+ * `GuardArchiveError("archive.bad-posture")` on unknown name.
824
+ *
825
+ * @example
826
+ * var posture = b.guardArchive.compliancePosture("hipaa");
827
+ * posture.symlinkPolicy; // → "reject"
828
+ * posture.hardlinkPolicy; // → "reject"
829
+ * posture.forensicSnippetBytes; // → 256
830
+ */
694
831
  function compliancePosture(name) {
695
832
  return gateContract.lookupCompliancePosture(name, COMPLIANCE_POSTURES, _err, "archive");
696
833
  }
697
834
 
698
835
  var _archiveRulePacks = gateContract.makeRulePackLoader(GuardArchiveError, "archive");
836
+ /**
837
+ * @primitive b.guardArchive.loadRulePack
838
+ * @signature b.guardArchive.loadRulePack(pack)
839
+ * @since 0.7.8
840
+ * @status stable
841
+ * @related b.guardArchive.gate
842
+ *
843
+ * Register an operator-supplied rule pack with the guard-archive
844
+ * registry. The pack is identified by `pack.id` (non-empty string)
845
+ * and stored for later inspection / dispatch by gates that opt in
846
+ * via `opts.rulePackId`. Returns the pack object unchanged on
847
+ * success; throws `GuardArchiveError("archive.bad-opt")` when
848
+ * `pack` is missing or `pack.id` is not a non-empty string.
849
+ *
850
+ * @example
851
+ * var pack = b.guardArchive.loadRulePack({
852
+ * id: "kb-2026-archive",
853
+ * extraReservedNames: ["system32"],
854
+ * rules: [
855
+ * { id: "no-windows-system", severity: "critical",
856
+ * reason: "entry name targets Windows system directory" },
857
+ * ],
858
+ * });
859
+ * pack.id; // → "kb-2026-archive"
860
+ */
699
861
  var loadRulePack = _archiveRulePacks.load;
700
862
 
701
863
  module.exports = {