@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
package/lib/safe-url.js CHANGED
@@ -1,64 +1,186 @@
1
1
  "use strict";
2
2
  /**
3
- * URL-safe — validate URL scheme + shape against an allowlist.
4
- *
5
- * Per the framework's modernity stance: outbound network calls
6
- * REQUIRE TLS by default. Operators with internal cleartext
7
- * endpoints (development, behind-VPN services, internal mesh) opt in
8
- * explicitly via opts.allowedProtocols. The framework refuses to
9
- * silently drop bytes on the wire as cleartext.
10
- *
11
- * Public API:
12
- *
13
- * safeUrl.parse(url, opts?) URL
14
- * Returns a parsed URL object. Throws if the URL is malformed
15
- * or its protocol is not in the allowlist.
16
- *
17
- * opts:
18
- * allowedProtocols — array of accepted protocol strings
19
- * (e.g. ["https:"] or safeUrl.ALLOW_HTTP_TLS).
20
- * Default: ALLOW_HTTP_TLS.
21
- * errorClass — FrameworkError subclass for the thrown
22
- * error. Lets callers (object-store,
23
- * log-stream, http-client) surface their
24
- * own decorated error class. Default:
25
- * SafeUrlError.
26
- * allowUserinfo — accept URLs that carry user:pass@ credentials
27
- * in the authority. Default: false. Userinfo in
28
- * outbound URLs leaks into request logs, error
29
- * messages, metric labels, and trace spans;
30
- * credential placement belongs in headers /
31
- * cookies / a credential store, not the URL.
32
- * Operators with a legacy endpoint that
33
- * REQUIRES userinfo opt in explicitly per call.
34
- *
35
- * Constants pre-baked allowlists for the common caller cases:
36
- *
37
- * ALLOW_HTTP_TLS ["https:"] (the secure HTTP default)
38
- * ALLOW_HTTP_ALL ["http:", "https:"] (HTTP + cleartext opt-in)
39
- * ALLOW_WS_TLS ["wss:"] (the secure WS default)
40
- * ALLOW_WS_ALL ["ws:", "wss:"] (WS + cleartext opt-in)
41
- * ALLOW_ANY ["http:", "https:", "ws:", "wss:"]
42
- *
43
- * Why per-call constants instead of one global "secure" list:
44
- * The http-client only speaks HTTP, so wss:// is a category error
45
- * (operator passed a WebSocket URL to a non-WebSocket client). Each
46
- * caller declares its own narrow allowlist; an off-protocol URL
47
- * fails with a clear "protocol not allowed here" error rather than
48
- * trying and failing weirdly later.
3
+ * @module b.safeUrl
4
+ * @nav Validation
5
+ * @title Safe Url
6
+ *
7
+ * @intro
8
+ * Defensive URL parsing with a protocol allowlist (HTTPS-only by
9
+ * default), authority validation, IDN-homograph defense, and a
10
+ * length cap that runs BEFORE Node's WHATWG URL parser sees the
11
+ * input. The framework's stance on outbound URLs: TLS-required by
12
+ * default; cleartext (`http:` / `ws:`) is opt-in per call via
13
+ * `opts.allowedProtocols`. `user:pass@` userinfo refuses by
14
+ * default credentials belong in headers / a credential store,
15
+ * not in URL strings that leak into request logs, error messages,
16
+ * metric labels, and trace spans. Mixed-script host labels
17
+ * (Cyrillic 'о' inside an otherwise-Latin label, etc. — UTS #39 §5
18
+ * homograph shape) refuse by default and emit
19
+ * `safeurl.idn_homograph.refused` to the audit chain so a forensic
20
+ * review can reconstruct every accepted host.
21
+ *
22
+ * Pre-baked protocol allowlists are exposed as frozen arrays so
23
+ * each caller can declare a NARROW per-call allowlist (the
24
+ * http-client speaks HTTP, not WebSocket; a `wss://` URL handed to
25
+ * it is a category error that should fail loudly here, not later
26
+ * inside a transport):
27
+ *
28
+ * ALLOW_HTTP_TLS ["https:"] (secure HTTP default)
29
+ * ALLOW_HTTP_ALL ["http:", "https:"] (HTTP + cleartext opt-in)
30
+ * ALLOW_WS_TLS ["wss:"] (secure WS default)
31
+ * ALLOW_WS_ALL ["ws:", "wss:"] (WS + cleartext opt-in)
32
+ * ALLOW_ANY ["http:", "https:", "ws:", "wss:"]
33
+ *
34
+ * `parse` throws `SafeUrlError` (or a caller-supplied error class
35
+ * via `opts.errorClass`, used by `b.objectStore` / `b.logStream` /
36
+ * `b.httpClient` to surface their own decorated error type) with a
37
+ * stable `.code`: `safe-url/missing` / `safe-url/too-long` /
38
+ * `safe-url/malformed` / `safe-url/protocol-disallowed` /
39
+ * `safe-url/userinfo-disallowed` / `safe-url/idn-homograph` /
40
+ * `safe-url/bad-opt`. Operator code that wants a boolean
41
+ * parse-without-throw shape wraps the throw in a try / catch.
42
+ *
43
+ * @card
44
+ * Defensive URL parsing with a protocol allowlist (HTTPS-only by default), authority validation, IDN-homograph defense, and a length cap that runs BEFORE Node's WHATWG URL parser sees the input.
49
45
  */
50
46
 
51
47
  var C = require("./constants");
48
+ var codepointClass = require("./codepoint-class");
49
+ var lazyRequire = require("./lazy-require");
52
50
  var numericBounds = require("./numeric-bounds");
53
51
  var { FrameworkError } = require("./framework-error");
52
+ var nodeUrl = require("url");
54
53
  var { URL } = require("url");
55
54
 
55
+ var audit = lazyRequire(function () { return require("./audit"); });
56
+
57
+ /**
58
+ * @primitive b.safeUrl.ALLOW_HTTP_TLS
59
+ * @signature b.safeUrl.ALLOW_HTTP_TLS
60
+ * @since 0.1.0
61
+ * @status stable
62
+ * @related b.safeUrl.parse, b.safeUrl.ALLOW_HTTP_ALL
63
+ *
64
+ * Frozen protocol allowlist for HTTPS-only HTTP traffic — `["https:"]`.
65
+ * The framework default for any outbound URL parsed without an
66
+ * explicit `opts.allowedProtocols`. Operators with a legitimate
67
+ * cleartext use case opt in per call via `ALLOW_HTTP_ALL`.
68
+ *
69
+ * @example
70
+ * var b = require("blamejs");
71
+ * b.safeUrl.ALLOW_HTTP_TLS;
72
+ * // → ["https:"]
73
+ */
56
74
  var ALLOW_HTTP_TLS = Object.freeze(["https:"]);
75
+
76
+ /**
77
+ * @primitive b.safeUrl.ALLOW_HTTP_ALL
78
+ * @signature b.safeUrl.ALLOW_HTTP_ALL
79
+ * @since 0.1.0
80
+ * @status stable
81
+ * @related b.safeUrl.parse, b.safeUrl.ALLOW_HTTP_TLS
82
+ *
83
+ * Frozen protocol allowlist accepting both HTTP and HTTPS —
84
+ * `["http:", "https:"]`. Pass to `parse` when the call site
85
+ * legitimately speaks cleartext (loopback admin endpoints, on-prem
86
+ * service mesh terminating TLS at a sidecar, legacy partner APIs).
87
+ * Never the framework default — TLS-required is.
88
+ *
89
+ * @example
90
+ * var b = require("blamejs");
91
+ * var u = b.safeUrl.parse("http://127.0.0.1:8080/health", {
92
+ * allowedProtocols: b.safeUrl.ALLOW_HTTP_ALL,
93
+ * });
94
+ * u.protocol;
95
+ * // → "http:"
96
+ */
57
97
  var ALLOW_HTTP_ALL = Object.freeze(["http:", "https:"]);
98
+
99
+ /**
100
+ * @primitive b.safeUrl.ALLOW_WS_TLS
101
+ * @signature b.safeUrl.ALLOW_WS_TLS
102
+ * @since 0.1.0
103
+ * @status stable
104
+ * @related b.safeUrl.parse, b.safeUrl.ALLOW_WS_ALL
105
+ *
106
+ * Frozen protocol allowlist for secure WebSocket traffic — `["wss:"]`.
107
+ * The framework default for any WebSocket URL parsed without an
108
+ * explicit `opts.allowedProtocols`.
109
+ *
110
+ * @example
111
+ * var b = require("blamejs");
112
+ * b.safeUrl.ALLOW_WS_TLS;
113
+ * // → ["wss:"]
114
+ */
58
115
  var ALLOW_WS_TLS = Object.freeze(["wss:"]);
116
+
117
+ /**
118
+ * @primitive b.safeUrl.ALLOW_WS_ALL
119
+ * @signature b.safeUrl.ALLOW_WS_ALL
120
+ * @since 0.1.0
121
+ * @status stable
122
+ * @related b.safeUrl.parse, b.safeUrl.ALLOW_WS_TLS
123
+ *
124
+ * Frozen protocol allowlist accepting both `ws:` and `wss:` —
125
+ * `["ws:", "wss:"]`. Opt-in per call when cleartext WebSocket is
126
+ * acceptable (loopback dev, sidecar-terminated TLS).
127
+ *
128
+ * @example
129
+ * var b = require("blamejs");
130
+ * var u = b.safeUrl.parse("ws://127.0.0.1:9000/stream", {
131
+ * allowedProtocols: b.safeUrl.ALLOW_WS_ALL,
132
+ * });
133
+ * u.protocol;
134
+ * // → "ws:"
135
+ */
59
136
  var ALLOW_WS_ALL = Object.freeze(["ws:", "wss:"]);
137
+
138
+ /**
139
+ * @primitive b.safeUrl.ALLOW_ANY
140
+ * @signature b.safeUrl.ALLOW_ANY
141
+ * @since 0.1.0
142
+ * @status stable
143
+ * @related b.safeUrl.parse, b.safeUrl.ALLOW_HTTP_TLS
144
+ *
145
+ * Frozen allowlist accepting every framework-supported scheme —
146
+ * `["http:", "https:", "ws:", "wss:"]`. Suited to a generic
147
+ * URL-validation surface where the caller already enforces the
148
+ * protocol downstream; narrower allowlists are preferred wherever
149
+ * possible.
150
+ *
151
+ * @example
152
+ * var b = require("blamejs");
153
+ * b.safeUrl.ALLOW_ANY.length;
154
+ * // → 4
155
+ */
60
156
  var ALLOW_ANY = Object.freeze(["http:", "https:", "ws:", "wss:"]);
61
157
 
158
+ /**
159
+ * @primitive b.safeUrl.SafeUrlError
160
+ * @signature b.safeUrl.SafeUrlError
161
+ * @since 0.1.0
162
+ * @status stable
163
+ * @related b.safeUrl.parse
164
+ *
165
+ * Error class thrown by `parse` (or by the caller-supplied
166
+ * `opts.errorClass`, used by `b.objectStore` / `b.logStream` /
167
+ * `b.httpClient` to surface a decorated operational error type).
168
+ * Extends `FrameworkError`. Carries a stable `.code`:
169
+ * `safe-url/missing` / `safe-url/too-long` / `safe-url/malformed` /
170
+ * `safe-url/protocol-disallowed` / `safe-url/userinfo-disallowed` /
171
+ * `safe-url/idn-homograph` / `safe-url/bad-opt`. HTTP middleware
172
+ * inspects `.code` to translate the throw into a 400 without
173
+ * leaking parser internals.
174
+ *
175
+ * @example
176
+ * var b = require("blamejs");
177
+ * try {
178
+ * b.safeUrl.parse("ftp://example.com/file.txt");
179
+ * } catch (e) {
180
+ * e instanceof b.safeUrl.SafeUrlError; // → true
181
+ * e.code; // → "safe-url/protocol-disallowed"
182
+ * }
183
+ */
62
184
  class SafeUrlError extends FrameworkError {
63
185
  constructor(code, message) {
64
186
  super(message, code);
@@ -83,6 +205,71 @@ function _makeError(errorClass, code, message) {
83
205
  // payloads) override via opts.maxUrlLength.
84
206
  var DEFAULT_MAX_URL_LENGTH = C.BYTES.kib(8);
85
207
 
208
+ /**
209
+ * @primitive b.safeUrl.parse
210
+ * @signature b.safeUrl.parse(url, opts?)
211
+ * @since 0.1.0
212
+ * @status stable
213
+ * @related b.safeUrl.SafeUrlError, b.safeUrl.ALLOW_HTTP_TLS, b.safeUrl.ALLOW_HTTP_ALL
214
+ *
215
+ * Parse a URL string (or an existing `URL` instance) through the
216
+ * framework's defensive gates: length cap BEFORE Node's WHATWG parser
217
+ * sees the input (RFC 7230 §3.1.1 — 8 KiB default), protocol
218
+ * allowlist (`https:` only by default), `user:pass@` userinfo refusal
219
+ * (credentials leak into request logs / error messages / metric
220
+ * labels / trace spans), and per-label IDN-homograph defense
221
+ * (UTS #39 §5 mixed-script — Cyrillic 'о' inside an otherwise-Latin
222
+ * label). Returns the parsed `URL` instance on success.
223
+ *
224
+ * Throws `SafeUrlError` (or the caller-supplied `opts.errorClass`)
225
+ * with one of the documented `.code` strings: `safe-url/missing` /
226
+ * `safe-url/too-long` / `safe-url/malformed` /
227
+ * `safe-url/protocol-disallowed` / `safe-url/userinfo-disallowed` /
228
+ * `safe-url/idn-homograph` / `safe-url/bad-opt`. Operator code that
229
+ * wants a boolean parse-without-throw shape wraps the call in a
230
+ * `try` / `catch`.
231
+ *
232
+ * @opts
233
+ * allowedProtocols: string[], // default ALLOW_HTTP_TLS (["https:"])
234
+ * maxUrlLength: number, // default 8192 (RFC 7230 §3.1.1)
235
+ * allowUserinfo: boolean, // default false; opt-in to user:pass@
236
+ * allowMixedScript: boolean, // default false; opt-in to mixed-script labels
237
+ * allowedScripts: string[], // narrow mixed-script allowlist (e.g. ["latin","cyrillic"])
238
+ * errorClass: Function, // throw this instead of SafeUrlError (used by b.httpClient / b.objectStore)
239
+ *
240
+ * @example
241
+ * var b = require("blamejs");
242
+ *
243
+ * // Default: HTTPS-only, length cap, userinfo refused, IDN-homograph defended.
244
+ * var u = b.safeUrl.parse("https://example.com/path?q=1");
245
+ * u.hostname;
246
+ * // → "example.com"
247
+ *
248
+ * // Cleartext is opt-in per call via the ALLOW_HTTP_ALL preset.
249
+ * var http = b.safeUrl.parse("http://127.0.0.1:8080/health", {
250
+ * allowedProtocols: b.safeUrl.ALLOW_HTTP_ALL,
251
+ * });
252
+ * http.protocol;
253
+ * // → "http:"
254
+ *
255
+ * // Disallowed protocol throws SafeUrlError.
256
+ * try { b.safeUrl.parse("javascript:alert(1)"); }
257
+ * catch (e) { e.code; }
258
+ * // → "safe-url/protocol-disallowed"
259
+ *
260
+ * // Userinfo refused by default — credentials belong in headers.
261
+ * try { b.safeUrl.parse("https://alice:s3cr3t@example.com/"); }
262
+ * catch (e) { e.code; }
263
+ * // → "safe-url/userinfo-disallowed"
264
+ *
265
+ * // Boolean parse-without-throw shape via try/catch wrapper.
266
+ * function isValid(s) {
267
+ * try { b.safeUrl.parse(s); return true; }
268
+ * catch (_e) { return false; }
269
+ * }
270
+ * isValid("https://example.com/"); // → true
271
+ * isValid("ftp://example.com/"); // → false
272
+ */
86
273
  function parse(url, opts) {
87
274
  opts = opts || {};
88
275
  var allowed = Array.isArray(opts.allowedProtocols) && opts.allowedProtocols.length > 0
@@ -145,6 +332,51 @@ function parse(url, opts) {
145
332
  "reads at call time), or pass opts.allowUserinfo: true to opt this URL in.");
146
333
  }
147
334
 
335
+ // IDN homograph defense — each host label MUST be single-script
336
+ // (UTS #39 §5). A label that mixes Cyrillic + Latin (e.g. `gооgle.com`
337
+ // with Cyrillic 'о' inside the otherwise-Latin label) presents
338
+ // visually as a trusted host while resolving via DNS to attacker-
339
+ // controlled infrastructure. Defaults to refuse; operators with
340
+ // legitimate non-Latin host labels opt in via `allowMixedScript: true`
341
+ // and the opt-in audits with the host so a forensic review can
342
+ // reconstruct which call sites accept mixed-script hosts. Per-label
343
+ // detection (not whole-host) so a legitimate `eu.shop.example.org`
344
+ // mixing Latin + Cyrillic across labels still refuses. Node's URL
345
+ // parser normalizes IDN hosts to Punycode (`xn--`), so we decode each
346
+ // label to Unicode first via nodeUrl.domainToUnicode and run the
347
+ // mixed-script catalog on the decoded codepoints.
348
+ if (opts.allowMixedScript !== true && parsed.hostname) {
349
+ var unicodeHost;
350
+ try { unicodeHost = nodeUrl.domainToUnicode(parsed.hostname); }
351
+ catch (_e) { unicodeHost = parsed.hostname; }
352
+ var labels = (unicodeHost || parsed.hostname).split(".");
353
+ var allowedScripts = Array.isArray(opts.allowedScripts) ? opts.allowedScripts : null;
354
+ for (var li = 0; li < labels.length; li += 1) {
355
+ var label = labels[li];
356
+ if (label.length === 0) continue;
357
+ var mixed = codepointClass.detectMixedScripts(label, allowedScripts);
358
+ if (mixed) {
359
+ try {
360
+ audit().safeEmit({
361
+ action: "safeurl.idn_homograph.refused",
362
+ outcome: "denied",
363
+ metadata: {
364
+ host: parsed.hostname,
365
+ label: label,
366
+ scripts: mixed,
367
+ },
368
+ });
369
+ } catch (_e) { /* audit best-effort */ }
370
+ throw _makeError(errClass, "safe-url/idn-homograph",
371
+ "URL host label '" + label + "' mixes scripts (" + mixed.join(", ") +
372
+ ") — IDN homograph attack shape (UTS #39 §5). Refuses by default; " +
373
+ "operators with a legitimate mixed-script host pass " +
374
+ "opts.allowMixedScript: true (with an audited reason) or " +
375
+ "opts.allowedScripts: ['latin','cyrillic'] to allowlist specific scripts.");
376
+ }
377
+ }
378
+ }
379
+
148
380
  return parsed;
149
381
  }
150
382
 
@@ -0,0 +1,135 @@
1
+ "use strict";
2
+ /**
3
+ * sandbox-worker — bootstrap module loaded inside the worker_threads
4
+ * Worker spawned by lib/sandbox.js. Runs UNTRUSTED operator-supplied
5
+ * source against a pre-stripped global scope.
6
+ *
7
+ * NOT operator-facing — operators interact via b.sandbox.run().
8
+ *
9
+ * Wire format:
10
+ * workerData: {
11
+ * source: string, // operator-supplied JS — function body
12
+ * input: any, // pass-through input
13
+ * allowedGlobals: string[], // intersected with KNOWN_SAFE_BUILTINS
14
+ * maxResultBytes: number, // hard-cap on JSON.stringify(result)
15
+ * }
16
+ *
17
+ * Posts back via parentPort:
18
+ * { ok: true, resultJson, runtimeMs, peakBytes }
19
+ * { ok: false, code, message, runtimeMs, peakBytes }
20
+ *
21
+ * Containment summary:
22
+ * - require / process / Buffer / setTimeout / setInterval / setImmediate /
23
+ * queueMicrotask / global are deleted off globalThis before the
24
+ * operator code is compiled.
25
+ * - The operator source is compiled via the JS language's
26
+ * string-to-callable primitive — the compiled function's outer
27
+ * scope is GLOBAL (stripped) and CANNOT see the bootstrap's
28
+ * own locals (require, workerThreads, parentPort).
29
+ * - Resource limits (maxOldGenerationSizeMb / maxYoungGenerationSizeMb /
30
+ * codeRangeSizeMb / stackSizeMb) are set by the host on Worker
31
+ * construction; v8 kills the worker on heap overflow.
32
+ * - Output is JSON-serialized; the worker refuses any result whose
33
+ * stringified form exceeds maxResultBytes.
34
+ */
35
+
36
+ var workerThreads = require("node:worker_threads");
37
+
38
+ (function () {
39
+ var data = workerThreads.workerData || {};
40
+ var allowed = Array.isArray(data.allowedGlobals) ? data.allowedGlobals : [];
41
+ var maxResultBytes = (typeof data.maxResultBytes === "number") ? data.maxResultBytes : null;
42
+
43
+ var ALWAYS_AVAILABLE = [
44
+ "Object", "Array", "String", "Number", "Boolean", "Symbol",
45
+ "Promise", "Error", "TypeError", "RangeError", "RegExp",
46
+ ];
47
+
48
+ var keep = Object.create(null);
49
+ for (var i = 0; i < ALWAYS_AVAILABLE.length; i += 1) keep[ALWAYS_AVAILABLE[i]] = true;
50
+ for (var j = 0; j < allowed.length; j += 1) keep[allowed[j]] = true;
51
+
52
+ var NODE_BUILTINS = [
53
+ "process", "Buffer",
54
+ "setImmediate", "clearImmediate",
55
+ "setTimeout", "clearTimeout",
56
+ "setInterval", "clearInterval",
57
+ "queueMicrotask",
58
+ "global",
59
+ ];
60
+ for (var k = 0; k < NODE_BUILTINS.length; k += 1) {
61
+ var nm = NODE_BUILTINS[k];
62
+ if (!keep[nm]) {
63
+ try { delete globalThis[nm]; }
64
+ catch (_e1) { try { globalThis[nm] = undefined; } catch (_e2) { /* best-effort */ } }
65
+ }
66
+ }
67
+
68
+ try { delete globalThis.require; } catch (_e) { /* best-effort */ }
69
+
70
+ var startedAt = Date.now();
71
+ var peakBytes = 0;
72
+
73
+ function snapshotPeak() {
74
+ try {
75
+ var proc = (typeof process !== "undefined") ? process : null;
76
+ if (proc && typeof proc.memoryUsage === "function") {
77
+ var u = proc.memoryUsage();
78
+ if (u && typeof u.heapUsed === "number" && u.heapUsed > peakBytes) peakBytes = u.heapUsed;
79
+ }
80
+ } catch (_e) { /* process gone or stripped */ }
81
+ }
82
+
83
+ snapshotPeak();
84
+
85
+ // Compile operator source via the JS language's string-to-callable
86
+ // primitive. The compiled function's outer scope is GLOBAL (already
87
+ // stripped above); it cannot see this bootstrap's own locals.
88
+ var Compiler = (function () { return Function; }());
89
+
90
+ var fn;
91
+ try {
92
+ fn = new Compiler("input", data.source);
93
+ } catch (eParse) {
94
+ workerThreads.parentPort.postMessage({
95
+ ok: false, code: "sandbox/parse-error",
96
+ message: "sandbox source did not parse: " + (eParse && eParse.message),
97
+ runtimeMs: Date.now() - startedAt, peakBytes: peakBytes,
98
+ });
99
+ return;
100
+ }
101
+
102
+ try {
103
+ var result = fn(data.input);
104
+ snapshotPeak();
105
+ var runtimeMs = Date.now() - startedAt;
106
+ var serialized;
107
+ try { serialized = (result === undefined) ? undefined : JSON.stringify(result); }
108
+ catch (eSer) {
109
+ workerThreads.parentPort.postMessage({
110
+ ok: false, code: "sandbox/result-not-serializable",
111
+ message: "sandbox result is not JSON-serializable: " + (eSer && eSer.message),
112
+ runtimeMs: runtimeMs, peakBytes: peakBytes,
113
+ });
114
+ return;
115
+ }
116
+ if (maxResultBytes !== null && serialized && serialized.length > maxResultBytes) {
117
+ workerThreads.parentPort.postMessage({
118
+ ok: false, code: "sandbox/oversized-result",
119
+ message: "sandbox result exceeded maxResultBytes (" + serialized.length + " > " + maxResultBytes + ")",
120
+ runtimeMs: runtimeMs, peakBytes: peakBytes,
121
+ });
122
+ return;
123
+ }
124
+ workerThreads.parentPort.postMessage({
125
+ ok: true, resultJson: serialized, runtimeMs: runtimeMs, peakBytes: peakBytes,
126
+ });
127
+ } catch (eRun) {
128
+ snapshotPeak();
129
+ workerThreads.parentPort.postMessage({
130
+ ok: false, code: "sandbox/runtime-error",
131
+ message: "sandbox transform threw: " + (eRun && eRun.message ? eRun.message : String(eRun)),
132
+ runtimeMs: Date.now() - startedAt, peakBytes: peakBytes,
133
+ });
134
+ }
135
+ }());