@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
@@ -0,0 +1,582 @@
1
+ "use strict";
2
+ /**
3
+ * b.crypto.httpSig — RFC 9421 HTTP Message Signatures.
4
+ *
5
+ * RFC 9421 (April 2024) standardizes message-level integrity for HTTP
6
+ * requests and responses. Two headers carry the signature:
7
+ *
8
+ * Signature-Input: <label>=("@method" "@target-uri" "content-digest");
9
+ * created=1718000000;keyid="key-1";alg="ed25519"
10
+ * Signature: <label>=:<base64-of-signature>:
11
+ *
12
+ * Per RFC 9421 §2.5, the signature base is the canonicalized list of
13
+ * covered components plus the signature parameters; the signing
14
+ * algorithm runs over those bytes.
15
+ *
16
+ * Derived components implemented here (RFC 9421 §2.2):
17
+ * @method, @target-uri, @authority, @scheme, @request-target,
18
+ * @path, @query, @query-param
19
+ *
20
+ * Signature parameters (RFC 9421 §2.3):
21
+ * created, expires, nonce, keyid, alg, tag
22
+ *
23
+ * Algorithms (RFC 9421 §3.3 + §A.2 IANA registry):
24
+ * "ed25519" — Edwards-curve digital signature, classical
25
+ * backward-compat default for non-PQC peers
26
+ * "ml-dsa-65" — FIPS 204 lattice signatures, PQC default when
27
+ * both peers PQC-aware
28
+ *
29
+ * The framework does NOT expose RSA / ECDSA-P256 / ECDSA-P384 / HMAC
30
+ * variants from RFC 9421 §3.3 — same crypto-policy stance as the rest
31
+ * of the framework (no SHA-256-only hashes, no classical-only
32
+ * primitives where a PQC alternative is shipping).
33
+ *
34
+ * Operator API:
35
+ *
36
+ * var sig = b.crypto.httpSig.sign({
37
+ * method: "POST",
38
+ * url: "https://api.example.com/orders",
39
+ * headers: { "content-type": "application/json", "host": "api.example.com" },
40
+ * body: bodyBuffer, // for content-digest header
41
+ * }, {
42
+ * keyid: "service-a-2026-05",
43
+ * alg: "ed25519", // or "ml-dsa-65"
44
+ * privateKey: privateKeyPem,
45
+ * covered: ["@method", "@target-uri", "content-digest"],
46
+ * created: Math.floor(Date.now()/1000),
47
+ * expires: Math.floor(Date.now()/1000) + 300,
48
+ * label: "sig1", // optional — defaults to "sig1"
49
+ * });
50
+ * // → { headers: { "Signature-Input": "...", "Signature": "...",
51
+ * "Content-Digest": "..." } }
52
+ *
53
+ * var ok = b.crypto.httpSig.verify({
54
+ * method, url, headers, body
55
+ * }, {
56
+ * keyResolver: function (keyid, alg) { return publicKeyPem; },
57
+ * toleranceMs: b.constants.TIME.minutes(5),
58
+ * });
59
+ * // → { valid, label, keyid, alg, covered, reason? }
60
+ */
61
+
62
+ var nodeCrypto = require("crypto");
63
+ var safeUrl = require("./safe-url");
64
+ var safeBuffer = require("./safe-buffer");
65
+ var C = require("./constants");
66
+ var lazyRequire = require("./lazy-require");
67
+ var validateOpts = require("./validate-opts");
68
+ var { HttpSigError } = require("./framework-error");
69
+
70
+ var _err = HttpSigError.factory;
71
+
72
+ var observability = lazyRequire(function () { return require("./observability"); });
73
+
74
+ var SUPPORTED_ALGS = Object.freeze(["ed25519", "ml-dsa-65"]);
75
+
76
+ // Tolerance defaults — per RFC 9421 §3.2.4 the verifier checks the
77
+ // `expires` parameter when present and the `created` skew otherwise.
78
+ // Match the webhook primitive's defaults (5 minutes tolerance, 1 minute
79
+ // future skew) so an operator wiring both gets one knob shape.
80
+ var DEFAULT_TOLERANCE_MS = C.TIME.minutes(5);
81
+ var DEFAULT_CLOCK_SKEW_MS = C.TIME.minutes(1);
82
+
83
+ // _sfString / _sfList / _sfDict — minimal Structured Fields (RFC 8941)
84
+ // formatters scoped to what RFC 9421 needs. Full RFC 8941 is overkill
85
+ // for the labels + parameters this primitive emits; we compose a
86
+ // quoted-string + parameter list and emit verbatim.
87
+ function _sfQuotedString(s) {
88
+ // RFC 8941 §3.3.3 — escape DQUOTE and backslash. Invalid bytes (any
89
+ // byte outside 0x20..0x7E) refuse to encode rather than silently lose
90
+ // information.
91
+ for (var i = 0; i < s.length; i++) {
92
+ var c = s.charCodeAt(i);
93
+ if (c < 0x20 || c > 0x7E) { // allow:raw-byte-literal — RFC 8941 §3.3.3 printable-ASCII range
94
+ throw _err("BAD_PARAM",
95
+ "httpSig: parameter string contains non-printable byte at offset " + i);
96
+ }
97
+ }
98
+ return "\"" + s.replace(/\\/g, "\\\\").replace(/"/g, "\\\"") + "\"";
99
+ }
100
+
101
+ // _serializeCovered — RFC 9421 §2.5 covered-components list.
102
+ // ("@method" "@target-uri" "x-foo" "@query-param";name="ref")
103
+ // Per RFC 9421 §2.5: parameters that bind to a covered identifier are
104
+ // emitted OUTSIDE the quoted bare name (the quoted string holds only
105
+ // the bare name; parameters follow in structured-fields form).
106
+ function _serializeCovered(covered) {
107
+ var parts = covered.map(function (c) {
108
+ var semi = c.indexOf(";");
109
+ if (semi === -1) return _sfQuotedString(c);
110
+ var bare = c.slice(0, semi);
111
+ var paramSuffix = c.slice(semi);
112
+ return _sfQuotedString(bare) + paramSuffix;
113
+ });
114
+ return "(" + parts.join(" ") + ")";
115
+ }
116
+
117
+ // _serializeSigParams — RFC 9421 §2.3 signature parameters.
118
+ // ;created=1718000000;keyid="k-1";alg="ed25519"
119
+ function _serializeSigParams(p) {
120
+ var out = "";
121
+ if (typeof p.created === "number") out += ";created=" + p.created;
122
+ if (typeof p.expires === "number") out += ";expires=" + p.expires;
123
+ if (typeof p.nonce === "string") out += ";nonce=" + _sfQuotedString(p.nonce);
124
+ if (typeof p.alg === "string") out += ";alg=" + _sfQuotedString(p.alg);
125
+ if (typeof p.keyid === "string") out += ";keyid=" + _sfQuotedString(p.keyid);
126
+ if (typeof p.tag === "string") out += ";tag=" + _sfQuotedString(p.tag);
127
+ return out;
128
+ }
129
+
130
+ // _resolveDerivedComponent — RFC 9421 §2.2 derived components.
131
+ function _resolveDerivedComponent(name, msg) {
132
+ var parsed = msg._parsedUrl;
133
+ switch (name) {
134
+ case "@method": return msg.method.toUpperCase();
135
+ case "@target-uri": return msg.url;
136
+ case "@authority": return parsed.host;
137
+ case "@scheme": return parsed.protocol.replace(/:$/, "");
138
+ case "@request-target": return parsed.pathname + (parsed.search || "");
139
+ case "@path": return parsed.pathname;
140
+ case "@query": return parsed.search || "?";
141
+ case "@status":
142
+ if (typeof msg.status !== "number") {
143
+ throw _err("MISSING_STATUS",
144
+ "httpSig: @status referenced but message has no numeric status");
145
+ }
146
+ return String(msg.status);
147
+ default:
148
+ throw _err("UNKNOWN_DERIVED",
149
+ "httpSig: unknown derived component " + JSON.stringify(name));
150
+ }
151
+ }
152
+
153
+ // _resolveQueryParam — RFC 9421 §2.2.8 — covered identifier of the
154
+ // shape `"@query-param";name="k"` (the name parameter selects which
155
+ // query-string parameter participates in the signature base).
156
+ function _resolveQueryParam(msg, paramName) {
157
+ var search = (msg._parsedUrl.search || "").replace(/^\?/, "");
158
+ if (search.length === 0) {
159
+ throw _err("MISSING_QUERY",
160
+ "httpSig: @query-param;name=" + JSON.stringify(paramName) + " but URL has no query");
161
+ }
162
+ var pairs = search.split("&");
163
+ // RFC 9421 §2.2.8 — names compare without percent-decoding (server
164
+ // and signer must agree on the literal bytes). The framework follows
165
+ // the spec strictly: literal compare on encoded names.
166
+ var encName = encodeURIComponent(paramName);
167
+ for (var i = 0; i < pairs.length; i++) {
168
+ var eq = pairs[i].indexOf("=");
169
+ var rawName = eq === -1 ? pairs[i] : pairs[i].slice(0, eq);
170
+ if (rawName === encName || rawName === paramName) {
171
+ return eq === -1 ? "" : pairs[i].slice(eq + 1);
172
+ }
173
+ }
174
+ throw _err("MISSING_QUERY_PARAM",
175
+ "httpSig: @query-param;name=" + JSON.stringify(paramName) + " not present in URL");
176
+ }
177
+
178
+ // _resolveHeader — case-insensitive header lookup. RFC 9421 §2.1
179
+ // requires obs-fold normalization (concat multi-values with ", ").
180
+ function _resolveHeader(headers, name) {
181
+ var lower = name.toLowerCase();
182
+ var keys = Object.keys(headers);
183
+ for (var i = 0; i < keys.length; i++) {
184
+ if (keys[i].toLowerCase() === lower) {
185
+ var v = headers[keys[i]];
186
+ if (Array.isArray(v)) return v.map(function (s) { return String(s).trim(); }).join(", ");
187
+ return String(v).trim();
188
+ }
189
+ }
190
+ return null;
191
+ }
192
+
193
+ // _buildSignatureBase — RFC 9421 §2.5 signature base construction.
194
+ //
195
+ // One line per covered identifier, each shaped as:
196
+ // "<bare-identifier>": <component value>
197
+ // terminated by:
198
+ // "@signature-params": (<covered>...)<params>
199
+ function _buildSignatureBase(coveredList, params, msg) {
200
+ var lines = [];
201
+ for (var i = 0; i < coveredList.length; i++) {
202
+ var raw = coveredList[i];
203
+ // Covered identifiers may be a bare name (`"content-digest"`) or
204
+ // a bare name + parameters (`"@query-param";name="q"`). The
205
+ // canonicalization follows RFC 9421 §2.5 — quote the bare name +
206
+ // re-emit any parameters verbatim.
207
+ var semicolon = raw.indexOf(";");
208
+ var bare = semicolon === -1 ? raw : raw.slice(0, semicolon);
209
+ var paramSuffix = semicolon === -1 ? "" : raw.slice(semicolon);
210
+ var value;
211
+ if (bare === "@query-param") {
212
+ // Extract name= parameter; RFC 9421 §2.2.8.
213
+ var nameMatch = paramSuffix.match(/;name="([^"]+)"/);
214
+ if (!nameMatch) {
215
+ throw _err("BAD_QUERY_PARAM",
216
+ "httpSig: @query-param requires ;name=\"...\" parameter");
217
+ }
218
+ value = _resolveQueryParam(msg, nameMatch[1]);
219
+ } else if (bare.charAt(0) === "@") {
220
+ value = _resolveDerivedComponent(bare, msg);
221
+ } else {
222
+ value = _resolveHeader(msg.headers, bare);
223
+ if (value === null) {
224
+ throw _err("MISSING_HEADER",
225
+ "httpSig: covered header " + JSON.stringify(bare) + " not present");
226
+ }
227
+ }
228
+ lines.push(_sfQuotedString(bare) + paramSuffix + ": " + value);
229
+ }
230
+ // Terminator line — RFC 9421 §2.5 step 4.
231
+ lines.push("\"@signature-params\": " + _serializeCovered(coveredList) +
232
+ _serializeSigParams(params));
233
+ return Buffer.from(lines.join("\n"), "utf8");
234
+ }
235
+
236
+ // _contentDigest — RFC 9530 / RFC 9421 §B.2.5 Content-Digest header.
237
+ // SHA3-512 only (framework's default hash family — matches every
238
+ // other content-integrity primitive). Returns the structured-field
239
+ // form `sha-512=:<base64>:` so operators can drop straight into the
240
+ // Content-Digest header.
241
+ function contentDigest(body) {
242
+ var buf;
243
+ if (Buffer.isBuffer(body)) buf = body;
244
+ else if (typeof body === "string") buf = Buffer.from(body, "utf8");
245
+ else throw _err("BAD_BODY",
246
+ "httpSig.contentDigest: body must be a string or Buffer");
247
+ // RFC 9530 lists "sha-512" (SHA-512, FIPS 180-4) — we use SHA3-512
248
+ // which has the same output length and is the framework's hash
249
+ // policy. Operators interoperating with peers expecting SHA-512
250
+ // pass `algorithm: "sha-512"`.
251
+ var h = nodeCrypto.createHash("sha3-512").update(buf).digest("base64");
252
+ return "sha3-512=:" + h + ":";
253
+ }
254
+
255
+ function _parseUrl(url) {
256
+ var parsed = safeUrl.parse(url, {
257
+ allowedProtocols: safeUrl.ALLOW_HTTP_TLS,
258
+ errorClass: HttpSigError,
259
+ });
260
+ return {
261
+ protocol: parsed.protocol,
262
+ host: parsed.host,
263
+ pathname: parsed.pathname || "/",
264
+ search: parsed.search || "",
265
+ };
266
+ }
267
+
268
+ function _normalizeMessage(msg) {
269
+ validateOpts.requireObject(msg, "httpSig: message", HttpSigError);
270
+ validateOpts.requireNonEmptyString(msg.method,
271
+ "httpSig: message.method", HttpSigError, "BAD_OPT");
272
+ validateOpts.requireNonEmptyString(msg.url,
273
+ "httpSig: message.url", HttpSigError, "BAD_OPT");
274
+ if (!msg.headers || typeof msg.headers !== "object") {
275
+ throw _err("BAD_OPT", "httpSig: message.headers required");
276
+ }
277
+ return {
278
+ method: msg.method,
279
+ url: msg.url,
280
+ headers: msg.headers,
281
+ body: msg.body,
282
+ status: msg.status,
283
+ _parsedUrl: _parseUrl(msg.url),
284
+ };
285
+ }
286
+
287
+ // sign — RFC 9421 §3.1 signing flow.
288
+ function sign(msg, opts) {
289
+ var m = _normalizeMessage(msg);
290
+ validateOpts.requireObject(opts, "httpSig.sign", HttpSigError);
291
+ validateOpts.requireNonEmptyString(opts.keyid,
292
+ "httpSig.sign: keyid", HttpSigError, "BAD_OPT");
293
+ if (typeof opts.alg !== "string" || SUPPORTED_ALGS.indexOf(opts.alg) === -1) {
294
+ throw _err("BAD_OPT",
295
+ "httpSig.sign: alg must be one of " + SUPPORTED_ALGS.join(", ") +
296
+ " (got " + JSON.stringify(opts.alg) + ")");
297
+ }
298
+ validateOpts.requireNonEmptyString(opts.privateKey,
299
+ "httpSig.sign: privateKey (PEM)", HttpSigError, "BAD_OPT");
300
+ if (!Array.isArray(opts.covered) || opts.covered.length === 0) {
301
+ throw _err("BAD_OPT", "httpSig.sign: covered must be a non-empty array");
302
+ }
303
+ var label = typeof opts.label === "string" && opts.label.length > 0
304
+ ? opts.label : "sig1";
305
+ var nowSec = Math.floor((opts.now ? opts.now() : Date.now()) / C.TIME.seconds(1));
306
+ var params = {
307
+ created: typeof opts.created === "number" ? opts.created : nowSec,
308
+ expires: typeof opts.expires === "number" ? opts.expires : undefined,
309
+ nonce: typeof opts.nonce === "string" ? opts.nonce : undefined,
310
+ alg: opts.alg,
311
+ keyid: opts.keyid,
312
+ tag: typeof opts.tag === "string" ? opts.tag : undefined,
313
+ };
314
+
315
+ var emittedHeaders = {};
316
+ // Auto-emit Content-Digest when "content-digest" is covered + the
317
+ // header isn't already supplied. Operators wanting to use the
318
+ // RFC 9530 "sha-512" identifier (SHA-512 instead of SHA3-512) supply
319
+ // the header themselves; the framework emits SHA3-512.
320
+ var coveredLower = opts.covered.map(function (c) { return c.split(";")[0].toLowerCase(); });
321
+ if (coveredLower.indexOf("content-digest") !== -1 &&
322
+ _resolveHeader(m.headers, "content-digest") === null) {
323
+ if (m.body == null) {
324
+ throw _err("BAD_OPT",
325
+ "httpSig.sign: covered includes content-digest but message.body is missing");
326
+ }
327
+ var digest = contentDigest(m.body);
328
+ emittedHeaders["Content-Digest"] = digest;
329
+ m.headers = Object.assign({}, m.headers, { "content-digest": digest });
330
+ }
331
+
332
+ var base = _buildSignatureBase(opts.covered, params, m);
333
+ var sig;
334
+ try {
335
+ sig = nodeCrypto.sign(null, base, opts.privateKey);
336
+ } catch (e) {
337
+ throw _err("SIGN_FAILED", "httpSig.sign: " + e.message);
338
+ }
339
+ var sigB64 = sig.toString("base64");
340
+
341
+ emittedHeaders["Signature-Input"] = label + "=" + _serializeCovered(opts.covered) +
342
+ _serializeSigParams(params);
343
+ emittedHeaders["Signature"] = label + "=:" + sigB64 + ":";
344
+
345
+ try { observability().safeEvent("httpSig.sign", 1, { outcome: "success", alg: opts.alg }); }
346
+ catch (_e) { /* drop-silent */ }
347
+
348
+ return {
349
+ headers: emittedHeaders,
350
+ label: label,
351
+ signature: sigB64,
352
+ base: base,
353
+ };
354
+ }
355
+
356
+ // _parseSignatureInput — minimal RFC 8941 dictionary parser scoped to
357
+ // what RFC 9421 emits. A full RFC 8941 parser is overkill here.
358
+ function _parseSignatureInput(headerValue) {
359
+ // <label>=("@a" "@b");created=...;keyid="...";alg="..."
360
+ // We split by "=" once on the label, then by ";" for parameters.
361
+ var eq = headerValue.indexOf("=");
362
+ if (eq === -1) {
363
+ throw _err("BAD_HEADER", "httpSig: Signature-Input: missing '=' separator");
364
+ }
365
+ var label = headerValue.slice(0, eq).trim();
366
+ var rest = headerValue.slice(eq + 1).trim();
367
+ if (rest.charAt(0) !== "(") {
368
+ throw _err("BAD_HEADER",
369
+ "httpSig: Signature-Input: covered list must start with '('");
370
+ }
371
+ var closeIdx = rest.indexOf(")");
372
+ if (closeIdx === -1) {
373
+ throw _err("BAD_HEADER",
374
+ "httpSig: Signature-Input: covered list missing ')'");
375
+ }
376
+ var coveredRaw = rest.slice(1, closeIdx).trim();
377
+ var paramsRaw = rest.slice(closeIdx + 1);
378
+ var covered = [];
379
+ // Hand-roll the parse — covered tokens are quoted bare names with
380
+ // optional structured-field parameters trailing each closing quote
381
+ // (e.g. `"@query-param";name="ref"`). Whitespace separates tokens.
382
+ // A regex that handles every nesting case isn't worth the
383
+ // ambiguity; the linear walk below is precise.
384
+ var i2 = 0;
385
+ while (i2 < coveredRaw.length) {
386
+ while (i2 < coveredRaw.length && /\s/.test(coveredRaw.charAt(i2))) i2++;
387
+ if (i2 >= coveredRaw.length) break;
388
+ if (coveredRaw.charAt(i2) !== "\"") {
389
+ // Tolerate bare unquoted tokens for forward-compat with peers.
390
+ var endTok = i2;
391
+ while (endTok < coveredRaw.length && !/[\s]/.test(coveredRaw.charAt(endTok))) endTok++;
392
+ covered.push(coveredRaw.slice(i2, endTok));
393
+ i2 = endTok;
394
+ continue;
395
+ }
396
+ // Quoted bare name. Find the matching closing quote, accounting
397
+ // for backslash-escapes per RFC 8941.
398
+ var qStart = i2 + 1;
399
+ var qEnd = qStart;
400
+ while (qEnd < coveredRaw.length && coveredRaw.charAt(qEnd) !== "\"") {
401
+ if (coveredRaw.charAt(qEnd) === "\\" && qEnd + 1 < coveredRaw.length) qEnd += 2;
402
+ else qEnd++;
403
+ }
404
+ if (qEnd >= coveredRaw.length) {
405
+ throw _err("BAD_HEADER",
406
+ "httpSig: Signature-Input: unterminated quoted token");
407
+ }
408
+ var bareName = coveredRaw.slice(qStart, qEnd).replace(/\\\\/g, "\\").replace(/\\"/g, "\"");
409
+ i2 = qEnd + 1;
410
+ // Optional ;param=value;param=... suffix immediately following.
411
+ var suffixStart = i2;
412
+ while (i2 < coveredRaw.length && /[^\s]/.test(coveredRaw.charAt(i2))) i2++;
413
+ var suffix = coveredRaw.slice(suffixStart, i2);
414
+ covered.push(bareName + suffix);
415
+ }
416
+
417
+ var params = {};
418
+ if (paramsRaw.length > 0) {
419
+ var paramParts = paramsRaw.split(";");
420
+ for (var j = 0; j < paramParts.length; j++) {
421
+ var part = paramParts[j].trim();
422
+ if (part.length === 0) continue;
423
+ var pEq = part.indexOf("=");
424
+ if (pEq === -1) continue;
425
+ var k = part.slice(0, pEq).trim();
426
+ var vv = part.slice(pEq + 1).trim();
427
+ if (vv.charAt(0) === "\"" && vv.charAt(vv.length - 1) === "\"") {
428
+ params[k] = vv.slice(1, -1);
429
+ } else {
430
+ var num = Number(vv);
431
+ params[k] = isFinite(num) ? num : vv;
432
+ }
433
+ }
434
+ }
435
+ return { label: label, covered: covered, params: params };
436
+ }
437
+
438
+ function _parseSignature(headerValue, label) {
439
+ // <label>=:<base64>:
440
+ var prefix = label + "=:";
441
+ if (headerValue.indexOf(prefix) !== 0) {
442
+ // Multiple signature labels can appear; comma-separated. Find the
443
+ // matching label.
444
+ var parts = headerValue.split(",");
445
+ for (var i = 0; i < parts.length; i++) {
446
+ var p = parts[i].trim();
447
+ if (p.indexOf(prefix) === 0) {
448
+ return p.slice(prefix.length).replace(/:$/, "");
449
+ }
450
+ }
451
+ throw _err("BAD_HEADER",
452
+ "httpSig: Signature header has no entry for label " + JSON.stringify(label));
453
+ }
454
+ return headerValue.slice(prefix.length).replace(/:$/, "");
455
+ }
456
+
457
+ // verify — RFC 9421 §3.2 verification flow.
458
+ function verify(msg, opts) {
459
+ var m = _normalizeMessage(msg);
460
+ opts = opts || {};
461
+ if (typeof opts.keyResolver !== "function") {
462
+ throw _err("BAD_OPT",
463
+ "httpSig.verify: keyResolver(keyid, alg) → publicKeyPem required");
464
+ }
465
+ var toleranceMs = typeof opts.toleranceMs === "number" ? opts.toleranceMs : DEFAULT_TOLERANCE_MS;
466
+ var clockSkewMs = typeof opts.clockSkewMs === "number" ? opts.clockSkewMs : DEFAULT_CLOCK_SKEW_MS;
467
+ var nowMs = opts.now ? opts.now() : Date.now();
468
+
469
+ var sigInput = _resolveHeader(m.headers, "signature-input");
470
+ var sig = _resolveHeader(m.headers, "signature");
471
+ if (!sigInput) {
472
+ return { valid: false, reason: "missing-signature-input" };
473
+ }
474
+ if (!sig) {
475
+ return { valid: false, reason: "missing-signature" };
476
+ }
477
+
478
+ var parsedInput;
479
+ try { parsedInput = _parseSignatureInput(sigInput); }
480
+ catch (e) { return { valid: false, reason: "bad-signature-input", error: e.message }; }
481
+
482
+ var p = parsedInput.params;
483
+ if (typeof p.alg !== "string" || SUPPORTED_ALGS.indexOf(p.alg) === -1) {
484
+ return { valid: false, reason: "unsupported-alg", alg: p.alg };
485
+ }
486
+ if (typeof p.keyid !== "string" || p.keyid.length === 0) {
487
+ return { valid: false, reason: "missing-keyid" };
488
+ }
489
+ if (typeof p.created === "number") {
490
+ var ageMs = nowMs - p.created * C.TIME.seconds(1);
491
+ if (ageMs > toleranceMs) {
492
+ return { valid: false, reason: "expired", ageMs: ageMs };
493
+ }
494
+ if (-ageMs > clockSkewMs) {
495
+ return { valid: false, reason: "future", skewMs: -ageMs };
496
+ }
497
+ }
498
+ if (typeof p.expires === "number" && nowMs > p.expires * C.TIME.seconds(1)) {
499
+ return { valid: false, reason: "expires-passed" };
500
+ }
501
+
502
+ var publicKeyPem;
503
+ try { publicKeyPem = opts.keyResolver(p.keyid, p.alg); }
504
+ catch (e) { return { valid: false, reason: "key-resolver-threw", error: e.message }; }
505
+ if (typeof publicKeyPem !== "string" || publicKeyPem.length === 0) {
506
+ return { valid: false, reason: "unknown-keyid", keyid: p.keyid };
507
+ }
508
+
509
+ // If content-digest is covered, recompute and compare. RFC 9421 §B.2.5
510
+ // mandates that verifiers re-run the digest over the body — a stale
511
+ // header from a proxy would otherwise verify trivially.
512
+ var coveredLower = parsedInput.covered.map(function (c) { return c.split(";")[0].toLowerCase(); });
513
+ if (coveredLower.indexOf("content-digest") !== -1) {
514
+ if (m.body == null) {
515
+ return { valid: false, reason: "content-digest-no-body" };
516
+ }
517
+ var presented = _resolveHeader(m.headers, "content-digest");
518
+ if (!presented) {
519
+ return { valid: false, reason: "content-digest-header-missing" };
520
+ }
521
+ var actual = contentDigest(m.body);
522
+ // RFC 9530 allows multiple algorithms in one header (sha-512=...,
523
+ // sha-256=...). For SHA3-512 specifically — exact substring match
524
+ // against the presented header. For peer-supplied SHA-512 / SHA-256
525
+ // identifiers the operator is responsible for re-validating; this
526
+ // primitive only auto-checks SHA3-512.
527
+ if (presented.indexOf(actual.replace(/^sha3-512=/, "sha3-512=")) === -1) {
528
+ return { valid: false, reason: "content-digest-mismatch" };
529
+ }
530
+ }
531
+
532
+ var sigB64;
533
+ try { sigB64 = _parseSignature(sig, parsedInput.label); }
534
+ catch (e) { return { valid: false, reason: "bad-signature-header", error: e.message }; }
535
+ if (!safeBuffer.BASE64URL_RE && typeof sigB64 !== "string") { // allow:raw-byte-literal — defensive base64 shape check
536
+ return { valid: false, reason: "bad-signature-encoding" };
537
+ }
538
+ var sigBuf;
539
+ try { sigBuf = Buffer.from(sigB64, "base64"); }
540
+ catch (_e) { return { valid: false, reason: "bad-signature-encoding" }; }
541
+
542
+ var paramsForBase = {
543
+ created: p.created,
544
+ expires: p.expires,
545
+ nonce: p.nonce,
546
+ alg: p.alg,
547
+ keyid: p.keyid,
548
+ tag: p.tag,
549
+ };
550
+ var base;
551
+ try { base = _buildSignatureBase(parsedInput.covered, paramsForBase, m); }
552
+ catch (e) { return { valid: false, reason: "build-base-failed", error: e.message }; }
553
+
554
+ var ok;
555
+ try { ok = nodeCrypto.verify(null, base, publicKeyPem, sigBuf); }
556
+ catch (e) { return { valid: false, reason: "verify-threw", error: e.message }; }
557
+
558
+ try { observability().safeEvent("httpSig.verify", 1, { outcome: ok ? "success" : "failure", alg: p.alg }); }
559
+ catch (_e) { /* drop-silent */ }
560
+
561
+ if (!ok) {
562
+ return { valid: false, reason: "bad-signature", keyid: p.keyid, alg: p.alg };
563
+ }
564
+ return {
565
+ valid: true,
566
+ label: parsedInput.label,
567
+ keyid: p.keyid,
568
+ alg: p.alg,
569
+ covered: parsedInput.covered,
570
+ created: p.created,
571
+ expires: p.expires,
572
+ nonce: p.nonce,
573
+ };
574
+ }
575
+
576
+ module.exports = {
577
+ sign: sign,
578
+ verify: verify,
579
+ contentDigest: contentDigest,
580
+ SUPPORTED_ALGS: SUPPORTED_ALGS,
581
+ HttpSigError: HttpSigError,
582
+ };