@blamejs/core 0.8.43 → 0.8.50

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
package/lib/ssrf-guard.js CHANGED
@@ -1,43 +1,38 @@
1
1
  "use strict";
2
2
  /**
3
- * ssrf-guard — outbound URL gate against private / loopback / link-local /
4
- * cloud-metadata / reserved IP ranges.
5
- *
6
- * Wired as default-on in b.httpClient.request. Operators with internal
7
- * mesh calls opt out per call:
8
- *
9
- * b.httpClient.request({ url: "http://internal.svc", allowInternal: true });
10
- * b.httpClient.request({ url: "http://10.0.5.1", allowInternal: ["10.0.0.0/8"] });
11
- *
12
- * Standalone use:
13
- *
14
- * var ssrf = b.ssrfGuard;
15
- * await ssrf.checkUrl("https://example.com"); // throws SsrfError on hit
16
- * ssrf.classify("169.254.169.254"); // "cloud-metadata"
17
- * ssrf.cidrContains("10.0.0.0/8", "10.1.2.3"); // → true
18
- *
19
- * What's blocked by default:
20
- * - IPv4 private (RFC 1918): 10/8, 172.16/12, 192.168/16
21
- * - IPv4 loopback: 127/8
22
- * - IPv4 link-local: 169.254/16
23
- * - IPv4 reserved/broadcast: 0/8, 100.64/10 (CGNAT), 224/4 (multicast),
24
- * 240/4, 255.255.255.255
25
- * - IPv4 documentation/test: 192.0.2/24, 198.51.100/24, 203.0.113/24, 198.18/15
26
- * - IPv6 loopback: ::1
27
- * - IPv6 ULA (private): fc00::/7
28
- * - IPv6 link-local: fe80::/10
29
- * - Cloud metadata IPs: 169.254.169.254 (AWS/GCP/Azure),
30
- * 169.254.170.2 (AWS ECS task role),
31
- * fd00:ec2::254
32
- *
33
- * Hostnames are resolved via dns.lookup before classification, so a
34
- * malicious hostname pointing at a private IP fails the guard.
35
- *
36
- * The validated IPs are returned in the result and `b.httpClient` pins
37
- * the actual TCP connect to those exact addresses (via a custom
38
- * `lookup` callback passed to https / http2 connect). A hostile DNS
39
- * server cannot rebind between guard-check and connect to redirect
40
- * traffic at a private / metadata address.
3
+ * @module b.ssrfGuard
4
+ * @nav HTTP
5
+ * @title SSRF Guard
6
+ *
7
+ * @intro
8
+ * Outbound-URL Server-Side-Request-Forgery defense. Every URL the
9
+ * framework dials on behalf of an operator (b.httpClient, webhook
10
+ * delivery, OAuth discovery, OIDC JWKS fetch, image-by-URL upload)
11
+ * routes through the gate. The gate refuses private (RFC 1918 +
12
+ * RFC 4193 ULA), loopback (127/8 + ::1), link-local
13
+ * (169.254/16 + fe80::/10), reserved / documentation / CGNAT,
14
+ * IPv4-mapped / 6to4 / NAT64 / discard-prefix wrappers, and the
15
+ * cloud-metadata IPs (169.254.169.254 AWS/GCP/Azure/OpenStack/DO,
16
+ * 169.254.170.2 AWS ECS task role, fd00:ec2::254 IPv6 IMDS).
17
+ *
18
+ * DNS rebinding is closed by resolving the hostname once during
19
+ * classification AND returning the validated IP set in the result.
20
+ * b.httpClient pins the actual TCP connect to those exact addresses
21
+ * via a custom `lookup` callback — a hostile DNS server cannot flip
22
+ * the answer between guard-check and connect. Redirect chains are
23
+ * re-validated end-to-end by b.httpClient (each Location header
24
+ * passes through `checkUrl` before the next hop is dialed), and
25
+ * `createAllowlist` builds operator-specific egress allowlists that
26
+ * compose on top of the framework's hard-coded ban list.
27
+ *
28
+ * Cloud-metadata IPs are blocked unconditionally — `allowInternal`
29
+ * does NOT override this class because metadata endpoints leak
30
+ * instance credentials. Operators with a legitimate need for the
31
+ * metadata service do it through their cloud SDK with explicit IAM,
32
+ * never through the framework's outbound HTTP.
33
+ *
34
+ * @card
35
+ * Outbound-URL Server-Side-Request-Forgery defense.
41
36
  */
42
37
 
43
38
  var dns = require("node:dns").promises;
@@ -64,6 +59,30 @@ var IPV6_BYTES = C.BYTES.bytes(16); // 128 bits / 8 bits-per-byte
64
59
  var IPV6_GROUPS = C.BYTES.bytes(8); // hex groups
65
60
  var HEX_RADIX = C.BYTES.bytes(16); // parseInt / toString radix
66
61
 
62
+ /**
63
+ * @primitive b.ssrfGuard.SsrfError
64
+ * @signature b.ssrfGuard.SsrfError
65
+ * @since 0.7.0
66
+ * @status stable
67
+ * @related b.ssrfGuard.checkUrl, b.ssrfGuard.createAllowlist
68
+ *
69
+ * Error class thrown by every `b.ssrfGuard` primitive on a refused
70
+ * URL or refused address. Extends `FrameworkError`. Carries a stable
71
+ * `.code` (e.g. `ssrf-guard/blocked-cloud-metadata`,
72
+ * `ssrf-guard/blocked-private`, `ssrf-guard/not-on-allowlist`) plus
73
+ * the offending `.url` / `.ip` / `.category` for the audit log. Is
74
+ * marked `.permanent = true` so retry layers do not loop on it.
75
+ *
76
+ * @example
77
+ * var b = require("blamejs");
78
+ * try {
79
+ * await b.ssrfGuard.checkUrl("http://169.254.169.254/latest/meta-data/");
80
+ * } catch (e) {
81
+ * e instanceof b.ssrfGuard.SsrfError; // → true
82
+ * e.code; // → "ssrf-guard/blocked-cloud-metadata"
83
+ * e.category; // → "cloud-metadata"
84
+ * }
85
+ */
67
86
  class SsrfError extends FrameworkError {
68
87
  constructor(message, code, ctx) {
69
88
  super(message, code);
@@ -261,6 +280,30 @@ function _ipv6PrefixMatch(prefixBytes, prefixLen, ipBytes) {
261
280
 
262
281
  // ---- Public classification API ----
263
282
 
283
+ /**
284
+ * @primitive b.ssrfGuard.classify
285
+ * @signature b.ssrfGuard.classify(ip)
286
+ * @since 0.7.0
287
+ * @status stable
288
+ * @related b.ssrfGuard.checkUrl, b.ssrfGuard.cidrContains
289
+ *
290
+ * Synchronous IP-string classifier. Returns one of `"loopback"`,
291
+ * `"link-local"`, `"private"`, `"reserved"`, `"cloud-metadata"`, or
292
+ * `null` when the address is a routable public IP (or not a valid IP
293
+ * at all — non-string / malformed input returns `null` rather than
294
+ * throwing). Recognizes IPv4-mapped (`::ffff:a.b.c.d`), 6to4
295
+ * (`2002::/16`), and NAT64 (`64:ff9b::/96`) v6 wrappers and reclassifies
296
+ * the embedded v4 address — a 6to4-wrapped private IP returns
297
+ * `"private"`, never `null`.
298
+ *
299
+ * @example
300
+ * var b = require("blamejs");
301
+ * b.ssrfGuard.classify("169.254.169.254"); // → "cloud-metadata"
302
+ * b.ssrfGuard.classify("10.0.0.1"); // → "private"
303
+ * b.ssrfGuard.classify("127.0.0.1"); // → "loopback"
304
+ * b.ssrfGuard.classify("8.8.8.8"); // → null
305
+ * b.ssrfGuard.classify("::ffff:10.0.0.1"); // → "private"
306
+ */
264
307
  function classify(ip) {
265
308
  if (typeof ip !== "string") return null;
266
309
  var family = net.isIP(ip);
@@ -313,6 +356,27 @@ function _bufEqual(a, b) {
313
356
  return Buffer.compare(a, b) === 0;
314
357
  }
315
358
 
359
+ /**
360
+ * @primitive b.ssrfGuard.cidrContains
361
+ * @signature b.ssrfGuard.cidrContains(cidr, ip)
362
+ * @since 0.7.0
363
+ * @status stable
364
+ * @related b.ssrfGuard.classify, b.ssrfGuard.createAllowlist
365
+ *
366
+ * Returns `true` if `ip` falls inside the CIDR block `cidr`, else
367
+ * `false`. Both arguments must be the same address family (v4-in-v4
368
+ * or v6-in-v6 — mixed families return `false`). Used internally by
369
+ * `checkUrl` to evaluate the operator's `allowInternal` exception
370
+ * list and exposed publicly so operator code can drive the same
371
+ * range arithmetic for routing / allowlist UI.
372
+ *
373
+ * @example
374
+ * var b = require("blamejs");
375
+ * b.ssrfGuard.cidrContains("10.0.0.0/8", "10.1.2.3"); // → true
376
+ * b.ssrfGuard.cidrContains("10.0.0.0/8", "11.0.0.1"); // → false
377
+ * b.ssrfGuard.cidrContains("fd00::/8", "fd12:3456::1"); // → true
378
+ * b.ssrfGuard.cidrContains("10.0.0.0/8", "::1"); // → false (mixed family)
379
+ */
316
380
  function cidrContains(cidr, ip) {
317
381
  if (typeof cidr !== "string" || typeof ip !== "string") return false;
318
382
  var slash = cidr.indexOf("/");
@@ -326,6 +390,63 @@ function cidrContains(cidr, ip) {
326
390
 
327
391
  // ---- URL check (DNS-resolving) ----
328
392
 
393
+ /**
394
+ * @primitive b.ssrfGuard.checkUrl
395
+ * @signature b.ssrfGuard.checkUrl(url, opts?)
396
+ * @since 0.7.0
397
+ * @status stable
398
+ * @related b.ssrfGuard.classify, b.ssrfGuard.createAllowlist, b.httpClient.request
399
+ *
400
+ * Async DNS-resolving URL gate — the canonical pre-flight before
401
+ * any outbound fetch / webhook delivery / OAuth discovery. Resolves
402
+ * the hostname (via `b.network.dns` when available so DoH overrides
403
+ * apply, else native `dns.lookup`), classifies every returned address,
404
+ * and throws `SsrfError` on the first refused class. Cloud-metadata
405
+ * IPs throw unconditionally; other classes can be overridden by
406
+ * `allowInternal: true` (allow every private class) or
407
+ * `allowInternal: ["10.0.0.0/8", ...]` (allow specific CIDRs only).
408
+ *
409
+ * Returns `{ url, ips }` on success — `ips` is the resolved address
410
+ * list, suitable for passing to `https.request({ lookup })` so the
411
+ * subsequent TCP connect pins to the validated set and a hostile DNS
412
+ * server cannot rebind between guard-check and connect.
413
+ *
414
+ * @opts
415
+ * allowInternal: boolean | string[], // override private-range refusal
416
+ * // (cloud-metadata is NEVER overridable)
417
+ * errorClass: Function, // subclass of SsrfError to throw
418
+ * dnsLookup: Function, // override DNS resolver (testing / fixtures)
419
+ *
420
+ * @example
421
+ * // assertSafe before fetch — refuse private / metadata / loopback
422
+ * var b = require("blamejs");
423
+ * var result = await b.ssrfGuard.checkUrl("https://api.partner.example.com/v1/x");
424
+ * result.ips[0].address; // → "203.0.113.42"
425
+ *
426
+ * // Pin TCP connect to the validated IP set (defeats DNS rebinding):
427
+ * var validatedIps = result.ips;
428
+ * var lookup = function (host, opts, cb) { cb(null, validatedIps[0].address, validatedIps[0].family); };
429
+ *
430
+ * @example
431
+ * // Allow an internal mesh CIDR for one specific call:
432
+ * var b = require("blamejs");
433
+ * await b.ssrfGuard.checkUrl("http://10.0.5.42:8080/health", {
434
+ * allowInternal: ["10.0.0.0/8"],
435
+ * });
436
+ * // → { url: parsedUrl, ips: [{ address: "10.0.5.42", family: 4 }] }
437
+ *
438
+ * @example
439
+ * // Cloud-metadata IPs are blocked unconditionally (allowInternal does NOT override):
440
+ * var b = require("blamejs");
441
+ * try {
442
+ * await b.ssrfGuard.checkUrl("http://169.254.169.254/latest/meta-data/iam/", {
443
+ * allowInternal: true,
444
+ * });
445
+ * } catch (e) {
446
+ * e.code; // → "ssrf-guard/blocked-cloud-metadata"
447
+ * e.category; // → "cloud-metadata"
448
+ * }
449
+ */
329
450
  async function checkUrl(url, opts) {
330
451
  opts = opts || {};
331
452
  validateOpts(opts, ["allowInternal", "errorClass", "dnsLookup"], "ssrfGuard.checkUrl");
@@ -403,19 +524,54 @@ async function checkUrl(url, opts) {
403
524
  return { url: parsed, ips: ips };
404
525
  }
405
526
 
406
- // b.network.allowlist — contextual per-call egress allowlist composing
407
- // on ssrfGuard. Operators describe an allowed CIDR set + denylist;
408
- // the resulting `assert(url)` either resolves to the validated IP set
409
- // or throws SsrfError. Distinct from `ssrfGuard.checkUrl` (which uses
410
- // the framework's hard-coded private/cloud-metadata ban list) — this
411
- // is for cases where the operator's deployment has SPECIFIC outbound
412
- // targets and everything else should be refused.
413
- //
414
- // var egress = b.network.allowlist.create({
415
- // allow: ["api.partner.example.com", "192.0.2.0/24"],
416
- // deny: ["api.partner.example.com/admin"],
417
- // });
418
- // await egress.assert("https://api.partner.example.com/v1/x");
527
+ /**
528
+ * @primitive b.ssrfGuard.createAllowlist
529
+ * @signature b.ssrfGuard.createAllowlist(opts)
530
+ * @since 0.7.0
531
+ * @status stable
532
+ * @related b.ssrfGuard.checkUrl, b.ssrfGuard.cidrContains
533
+ *
534
+ * Build a contextual per-call egress allowlist composing on top of
535
+ * `ssrfGuard`. Operators describe an allowed host / CIDR set plus an
536
+ * optional denylist; the returned `{ assert(url) }` either resolves
537
+ * to the validated IP set (delegating to `checkUrl` with
538
+ * `allowInternal: true` because the explicit allowlist supersedes
539
+ * the private-range refusal) or throws `SsrfError`. Distinct from
540
+ * `checkUrl`'s hard-coded ban list — use `createAllowlist` when the
541
+ * deployment has SPECIFIC outbound targets and everything else
542
+ * should be refused.
543
+ *
544
+ * Throws at construction time if `allow` is empty (an empty
545
+ * allowlist would refuse every URL — almost certainly a config typo).
546
+ *
547
+ * @opts
548
+ * allow: string[], // required; entries are exact hostnames OR CIDR blocks
549
+ * deny: string[], // optional; checked AFTER allow — denylist wins
550
+ *
551
+ * @example
552
+ * // Allow-list a single partner domain — refuse everything else:
553
+ * var b = require("blamejs");
554
+ * var egress = b.ssrfGuard.createAllowlist({
555
+ * allow: ["api.partner.example.com", "203.0.113.0/24"],
556
+ * deny: ["evil.partner.example.com"],
557
+ * });
558
+ * await egress.assert("https://api.partner.example.com/v1/x");
559
+ * // → { url: parsedUrl, ips: [{ address: "203.0.113.10", family: 4 }] }
560
+ *
561
+ * @example
562
+ * // Custom blocklist for cloud-metadata IPs at the allowlist layer
563
+ * // (defense-in-depth; checkUrl already refuses these unconditionally):
564
+ * var b = require("blamejs");
565
+ * var egress = b.ssrfGuard.createAllowlist({
566
+ * allow: ["10.0.0.0/8"],
567
+ * deny: ["169.254.169.254", "169.254.170.2"],
568
+ * });
569
+ * try {
570
+ * await egress.assert("http://169.254.169.254/latest/");
571
+ * } catch (e) {
572
+ * e.code; // → "ssrf-guard/blocked-cloud-metadata"
573
+ * }
574
+ */
419
575
  function createAllowlist(opts) {
420
576
  opts = opts || {};
421
577
  var allowList = Array.isArray(opts.allow) ? opts.allow.slice() : [];
@@ -457,15 +613,119 @@ function createAllowlist(opts) {
457
613
  return { assert: assertUrl };
458
614
  }
459
615
 
616
+ /**
617
+ * @primitive b.ssrfGuard.isPrivate
618
+ * @signature b.ssrfGuard.isPrivate(ip)
619
+ * @since 0.7.0
620
+ * @status stable
621
+ * @related b.ssrfGuard.classify
622
+ *
623
+ * Returns `true` if `ip` is in an RFC 1918 IPv4 private range
624
+ * (10/8, 172.16/12, 192.168/16) or RFC 4193 IPv6 ULA (fc00::/7).
625
+ * Convenience wrapper over `classify(ip) === "private"`.
626
+ *
627
+ * @example
628
+ * var b = require("blamejs");
629
+ * b.ssrfGuard.isPrivate("10.0.0.1"); // → true
630
+ * b.ssrfGuard.isPrivate("8.8.8.8"); // → false
631
+ * b.ssrfGuard.isPrivate("fd12:3456::1"); // → true
632
+ */
633
+ function isPrivate(ip) { return classify(ip) === "private"; }
634
+
635
+ /**
636
+ * @primitive b.ssrfGuard.isLoopback
637
+ * @signature b.ssrfGuard.isLoopback(ip)
638
+ * @since 0.7.0
639
+ * @status stable
640
+ * @related b.ssrfGuard.classify
641
+ *
642
+ * Returns `true` if `ip` is in 127/8 (IPv4 loopback) or `::1`
643
+ * (IPv6 loopback). Convenience wrapper over
644
+ * `classify(ip) === "loopback"`.
645
+ *
646
+ * @example
647
+ * var b = require("blamejs");
648
+ * b.ssrfGuard.isLoopback("127.0.0.1"); // → true
649
+ * b.ssrfGuard.isLoopback("::1"); // → true
650
+ * b.ssrfGuard.isLoopback("8.8.8.8"); // → false
651
+ */
652
+ function isLoopback(ip) { return classify(ip) === "loopback"; }
653
+
654
+ /**
655
+ * @primitive b.ssrfGuard.isLinkLocal
656
+ * @signature b.ssrfGuard.isLinkLocal(ip)
657
+ * @since 0.7.0
658
+ * @status stable
659
+ * @related b.ssrfGuard.classify, b.ssrfGuard.isCloudMetadata
660
+ *
661
+ * Returns `true` if `ip` is in 169.254/16 (IPv4 link-local) or
662
+ * fe80::/10 (IPv6 link-local). Note that the cloud-metadata IPs
663
+ * (169.254.169.254 / 169.254.170.2) classify as `"cloud-metadata"`,
664
+ * NOT `"link-local"` — use `isCloudMetadata` if that distinction
665
+ * matters.
666
+ *
667
+ * @example
668
+ * var b = require("blamejs");
669
+ * b.ssrfGuard.isLinkLocal("169.254.0.1"); // → true
670
+ * b.ssrfGuard.isLinkLocal("169.254.169.254"); // → false (it's cloud-metadata)
671
+ * b.ssrfGuard.isLinkLocal("fe80::1"); // → true
672
+ */
673
+ function isLinkLocal(ip) { return classify(ip) === "link-local"; }
674
+
675
+ /**
676
+ * @primitive b.ssrfGuard.isCloudMetadata
677
+ * @signature b.ssrfGuard.isCloudMetadata(ip)
678
+ * @since 0.7.0
679
+ * @status stable
680
+ * @related b.ssrfGuard.classify, b.ssrfGuard.checkUrl
681
+ *
682
+ * Returns `true` if `ip` is one of the cloud-metadata service
683
+ * addresses (169.254.169.254 AWS/GCP/Azure/OpenStack/DO,
684
+ * 169.254.170.2 AWS ECS task role, fd00:ec2::254 IPv6 IMDS).
685
+ * These IPs leak instance credentials and `checkUrl` refuses them
686
+ * unconditionally — `allowInternal` does NOT override.
687
+ *
688
+ * @example
689
+ * var b = require("blamejs");
690
+ * b.ssrfGuard.isCloudMetadata("169.254.169.254"); // → true
691
+ * b.ssrfGuard.isCloudMetadata("169.254.170.2"); // → true
692
+ * b.ssrfGuard.isCloudMetadata("fd00:ec2::254"); // → true
693
+ * b.ssrfGuard.isCloudMetadata("169.254.0.1"); // → false (link-local but not metadata)
694
+ */
695
+ function isCloudMetadata(ip) { return classify(ip) === "cloud-metadata"; }
696
+
697
+ /**
698
+ * @primitive b.ssrfGuard.isReserved
699
+ * @signature b.ssrfGuard.isReserved(ip)
700
+ * @since 0.7.0
701
+ * @status stable
702
+ * @related b.ssrfGuard.classify
703
+ *
704
+ * Returns `true` if `ip` is in an IETF-reserved range — 0/8 ("this
705
+ * network"), 100.64/10 (CGNAT, RFC 6598), 192.0.0/24 (IETF protocol
706
+ * assignments), TEST-NET-1/2/3 (192.0.2/24, 198.51.100/24, 203.0.113/24),
707
+ * 198.18/15 (network benchmark), 224/4 (multicast), 240/4 (reserved +
708
+ * 255.255.255.255), 2001:db8::/32 (IPv6 documentation), ff00::/8 (IPv6
709
+ * multicast), or 100::/64 (IPv6 discard prefix).
710
+ *
711
+ * @example
712
+ * var b = require("blamejs");
713
+ * b.ssrfGuard.isReserved("192.0.2.1"); // → true (TEST-NET-1)
714
+ * b.ssrfGuard.isReserved("100.64.0.1"); // → true (CGNAT)
715
+ * b.ssrfGuard.isReserved("224.0.0.1"); // → true (multicast)
716
+ * b.ssrfGuard.isReserved("8.8.8.8"); // → false
717
+ */
718
+ function isReserved(ip) { return classify(ip) === "reserved"; }
719
+
460
720
  module.exports = {
461
721
  classify: classify,
462
722
  cidrContains: cidrContains,
463
723
  checkUrl: checkUrl,
464
724
  createAllowlist: createAllowlist,
465
- isPrivate: function (ip) { return classify(ip) === "private"; },
466
- isLoopback: function (ip) { return classify(ip) === "loopback"; },
467
- isLinkLocal: function (ip) { return classify(ip) === "link-local"; },
468
- isCloudMetadata: function (ip) { return classify(ip) === "cloud-metadata"; },
469
- isReserved: function (ip) { return classify(ip) === "reserved"; },
725
+ isPrivate: isPrivate,
726
+ isLoopback: isLoopback,
727
+ isLinkLocal: isLinkLocal,
728
+ isCloudMetadata: isCloudMetadata,
729
+ isReserved: isReserved,
470
730
  SsrfError: SsrfError,
471
731
  };