@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.
- package/CHANGELOG.md +93 -0
- package/README.md +10 -10
- package/index.js +52 -0
- package/lib/a2a.js +159 -34
- package/lib/acme.js +762 -0
- package/lib/ai-pref.js +166 -43
- package/lib/api-key.js +108 -47
- package/lib/api-snapshot.js +157 -40
- package/lib/app-shutdown.js +113 -77
- package/lib/archive.js +337 -40
- package/lib/arg-parser.js +697 -0
- package/lib/asyncapi.js +99 -55
- package/lib/atomic-file.js +465 -104
- package/lib/audit-chain.js +123 -34
- package/lib/audit-daily-review.js +389 -0
- package/lib/audit-sign.js +302 -56
- package/lib/audit-tools.js +412 -63
- package/lib/audit.js +656 -35
- package/lib/auth/jwt-external.js +17 -0
- package/lib/auth/oauth.js +7 -0
- package/lib/auth-bot-challenge.js +505 -0
- package/lib/auth-header.js +92 -25
- package/lib/backup/bundle.js +26 -0
- package/lib/backup/index.js +512 -89
- package/lib/backup/manifest.js +168 -7
- package/lib/break-glass.js +415 -39
- package/lib/budr.js +103 -30
- package/lib/bundler.js +86 -66
- package/lib/cache.js +192 -72
- package/lib/chain-writer.js +65 -40
- package/lib/circuit-breaker.js +56 -33
- package/lib/cli-helpers.js +106 -75
- package/lib/cli.js +6 -30
- package/lib/cloud-events.js +99 -32
- package/lib/cluster-storage.js +162 -37
- package/lib/cluster.js +340 -49
- package/lib/codepoint-class.js +66 -0
- package/lib/compliance.js +424 -24
- package/lib/config-drift.js +111 -46
- package/lib/config.js +94 -40
- package/lib/consent.js +165 -18
- package/lib/constants.js +1 -0
- package/lib/content-credentials.js +153 -48
- package/lib/cookies.js +154 -62
- package/lib/credential-hash.js +133 -61
- package/lib/crypto-field.js +702 -18
- package/lib/crypto-hpke.js +256 -0
- package/lib/crypto.js +744 -22
- package/lib/csv.js +178 -35
- package/lib/daemon.js +456 -0
- package/lib/dark-patterns.js +186 -55
- package/lib/db-query.js +79 -2
- package/lib/db.js +1431 -60
- package/lib/ddl-change-control.js +523 -0
- package/lib/deprecate.js +195 -40
- package/lib/dev.js +82 -39
- package/lib/dora.js +67 -48
- package/lib/dr-runbook.js +368 -0
- package/lib/dsr.js +142 -11
- package/lib/dual-control.js +91 -56
- package/lib/events.js +120 -41
- package/lib/external-db-migrate.js +192 -2
- package/lib/external-db.js +795 -50
- package/lib/fapi2.js +122 -1
- package/lib/fda-21cfr11.js +395 -0
- package/lib/fdx.js +132 -2
- package/lib/file-type.js +87 -0
- package/lib/file-upload.js +93 -0
- package/lib/flag.js +82 -20
- package/lib/forms.js +132 -29
- package/lib/framework-error.js +169 -0
- package/lib/framework-schema.js +163 -35
- package/lib/gate-contract.js +849 -175
- package/lib/graphql-federation.js +68 -7
- package/lib/guard-all.js +172 -55
- package/lib/guard-archive.js +286 -124
- package/lib/guard-auth.js +194 -21
- package/lib/guard-cidr.js +190 -28
- package/lib/guard-csv.js +397 -51
- package/lib/guard-domain.js +213 -91
- package/lib/guard-email.js +236 -29
- package/lib/guard-filename.js +307 -75
- package/lib/guard-graphql.js +263 -30
- package/lib/guard-html.js +310 -116
- package/lib/guard-image.js +243 -30
- package/lib/guard-json.js +260 -54
- package/lib/guard-jsonpath.js +235 -23
- package/lib/guard-jwt.js +284 -30
- package/lib/guard-markdown.js +204 -22
- package/lib/guard-mime.js +190 -26
- package/lib/guard-oauth.js +277 -28
- package/lib/guard-pdf.js +251 -27
- package/lib/guard-regex.js +226 -18
- package/lib/guard-shell.js +229 -26
- package/lib/guard-svg.js +177 -10
- package/lib/guard-template.js +232 -21
- package/lib/guard-time.js +195 -29
- package/lib/guard-uuid.js +189 -30
- package/lib/guard-xml.js +259 -36
- package/lib/guard-yaml.js +241 -44
- package/lib/honeytoken.js +63 -27
- package/lib/html-balance.js +83 -0
- package/lib/http-client.js +486 -59
- package/lib/http-message-signature.js +582 -0
- package/lib/i18n.js +102 -49
- package/lib/iab-mspa.js +112 -32
- package/lib/iab-tcf.js +107 -2
- package/lib/inbox.js +90 -52
- package/lib/keychain.js +865 -0
- package/lib/legal-hold.js +374 -0
- package/lib/local-db-thin.js +320 -0
- package/lib/log-stream.js +281 -51
- package/lib/log.js +184 -86
- package/lib/mail-bounce.js +107 -62
- package/lib/mail.js +295 -58
- package/lib/mcp.js +108 -27
- package/lib/metrics.js +98 -89
- package/lib/middleware/age-gate.js +36 -0
- package/lib/middleware/ai-act-disclosure.js +37 -0
- package/lib/middleware/api-encrypt.js +45 -0
- package/lib/middleware/assetlinks.js +40 -0
- package/lib/middleware/asyncapi-serve.js +35 -0
- package/lib/middleware/attach-user.js +40 -0
- package/lib/middleware/bearer-auth.js +40 -0
- package/lib/middleware/body-parser.js +230 -0
- package/lib/middleware/bot-disclose.js +34 -0
- package/lib/middleware/bot-guard.js +39 -0
- package/lib/middleware/compression.js +37 -0
- package/lib/middleware/cookies.js +32 -0
- package/lib/middleware/cors.js +40 -0
- package/lib/middleware/csp-nonce.js +40 -0
- package/lib/middleware/csp-report.js +34 -0
- package/lib/middleware/csrf-protect.js +43 -0
- package/lib/middleware/daily-byte-quota.js +53 -85
- package/lib/middleware/db-role-for.js +40 -0
- package/lib/middleware/dpop.js +40 -0
- package/lib/middleware/error-handler.js +37 -14
- package/lib/middleware/fetch-metadata.js +39 -0
- package/lib/middleware/flag-context.js +34 -0
- package/lib/middleware/gpc.js +33 -0
- package/lib/middleware/headers.js +35 -0
- package/lib/middleware/health.js +46 -0
- package/lib/middleware/host-allowlist.js +30 -0
- package/lib/middleware/network-allowlist.js +38 -0
- package/lib/middleware/openapi-serve.js +34 -0
- package/lib/middleware/rate-limit.js +160 -18
- package/lib/middleware/request-id.js +36 -18
- package/lib/middleware/request-log.js +37 -0
- package/lib/middleware/require-aal.js +29 -0
- package/lib/middleware/require-auth.js +32 -0
- package/lib/middleware/require-bound-key.js +41 -0
- package/lib/middleware/require-content-type.js +32 -0
- package/lib/middleware/require-methods.js +27 -0
- package/lib/middleware/require-mtls.js +33 -0
- package/lib/middleware/require-step-up.js +37 -0
- package/lib/middleware/security-headers.js +44 -0
- package/lib/middleware/security-txt.js +38 -0
- package/lib/middleware/span-http-server.js +37 -0
- package/lib/middleware/sse.js +36 -0
- package/lib/middleware/trace-log-correlation.js +33 -0
- package/lib/middleware/trace-propagate.js +32 -0
- package/lib/middleware/tus-upload.js +90 -0
- package/lib/middleware/web-app-manifest.js +53 -0
- package/lib/mtls-ca.js +100 -70
- package/lib/network-byte-quota.js +308 -0
- package/lib/network-heartbeat.js +135 -0
- package/lib/network-tls.js +534 -4
- package/lib/network.js +103 -0
- package/lib/notify.js +114 -43
- package/lib/ntp-check.js +192 -51
- package/lib/observability.js +145 -47
- package/lib/openapi.js +90 -44
- package/lib/outbox.js +99 -1
- package/lib/pagination.js +168 -86
- package/lib/parsers/index.js +16 -5
- package/lib/permissions.js +93 -40
- package/lib/pqc-agent.js +84 -8
- package/lib/pqc-software.js +94 -60
- package/lib/process-spawn.js +95 -21
- package/lib/pubsub.js +96 -66
- package/lib/queue.js +375 -54
- package/lib/redact.js +793 -21
- package/lib/render.js +139 -47
- package/lib/request-helpers.js +485 -121
- package/lib/restore-bundle.js +142 -39
- package/lib/restore-rollback.js +136 -45
- package/lib/retention.js +178 -50
- package/lib/retry.js +116 -33
- package/lib/router.js +475 -23
- package/lib/safe-async.js +543 -94
- package/lib/safe-buffer.js +337 -41
- package/lib/safe-json.js +467 -62
- package/lib/safe-jsonpath.js +285 -0
- package/lib/safe-schema.js +631 -87
- package/lib/safe-sql.js +221 -59
- package/lib/safe-url.js +278 -46
- package/lib/sandbox-worker.js +135 -0
- package/lib/sandbox.js +358 -0
- package/lib/scheduler.js +135 -70
- package/lib/self-update.js +647 -0
- package/lib/session-device-binding.js +431 -0
- package/lib/session.js +259 -49
- package/lib/slug.js +138 -26
- package/lib/ssrf-guard.js +316 -56
- package/lib/storage.js +433 -70
- package/lib/subject.js +405 -23
- package/lib/template.js +148 -8
- package/lib/tenant-quota.js +545 -0
- package/lib/testing.js +440 -53
- package/lib/time.js +291 -23
- package/lib/tls-exporter.js +239 -0
- package/lib/tracing.js +90 -74
- package/lib/uuid.js +97 -22
- package/lib/vault/index.js +284 -22
- package/lib/vault/seal-pem-file.js +66 -0
- package/lib/watcher.js +368 -0
- package/lib/webhook.js +196 -63
- package/lib/websocket.js +393 -68
- package/lib/wiki-concepts.js +338 -0
- package/lib/worker-pool.js +464 -0
- package/package.json +3 -3
- package/sbom.cyclonedx.json +7 -7
package/lib/watcher.js
ADDED
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* b.watcher — recursive filesystem-watch primitive with cross-platform
|
|
4
|
+
* event normalization.
|
|
5
|
+
*
|
|
6
|
+
* Wraps `fs.watch(root, { recursive: true })` and turns the per-platform
|
|
7
|
+
* event soup (Linux inotify "rename" + "change", macOS FSEvents
|
|
8
|
+
* coalesced "rename", Windows ReadDirectoryChangesW pure "rename" /
|
|
9
|
+
* "change") into a single shape:
|
|
10
|
+
*
|
|
11
|
+
* onChange({ type, relativePath, fullPath, size, mtime })
|
|
12
|
+
* onDelete({ type, relativePath, fullPath })
|
|
13
|
+
* onError(err)
|
|
14
|
+
*
|
|
15
|
+
* `type` is one of "file" or "dir". The watcher is build-tool-shaped:
|
|
16
|
+
* use it to drive incremental rebuilds, hot-reload-on-change,
|
|
17
|
+
* config-file watching, or content-store cache busts. It is NOT a
|
|
18
|
+
* security primitive — fs.watch is best-effort across kernels and the
|
|
19
|
+
* caller must not rely on it for audit-grade change detection.
|
|
20
|
+
*
|
|
21
|
+
* Cross-platform notes baked in:
|
|
22
|
+
* - macOS FSEvents fires "rename" for create / delete / move; the
|
|
23
|
+
* watcher disambiguates by stat-ing the path post-event.
|
|
24
|
+
* - Linux inotify can emit "change" before the file is fully written;
|
|
25
|
+
* debounce coalesces a burst of writes into one onChange.
|
|
26
|
+
* - Windows ReadDirectoryChangesW emits both "rename" and "change"
|
|
27
|
+
* for a single create — debounce + stat dedup the duplicate.
|
|
28
|
+
* - Symlinks are skipped on the post-event stat (lstat) to avoid
|
|
29
|
+
* following an attacker-controlled link out of `root`.
|
|
30
|
+
* - `recursive: true` on Linux (kernel 6.0+, since Node 20+ uses
|
|
31
|
+
* inotify_add_watch with IN_MASK_CREATE for nested dirs) — older
|
|
32
|
+
* kernels degrade to a single-directory watch and emit a warning
|
|
33
|
+
* event through `onError` once at startup.
|
|
34
|
+
*
|
|
35
|
+
* Audit emits:
|
|
36
|
+
* watcher.started — { root }
|
|
37
|
+
* watcher.stopped — { root, eventCount }
|
|
38
|
+
* watcher.error — { root, code } (drop-silent fallback)
|
|
39
|
+
*
|
|
40
|
+
* Public surface:
|
|
41
|
+
* watcher.create({ root, ignore?, debounceMs?, onChange?, onDelete?,
|
|
42
|
+
* onError?, audit? })
|
|
43
|
+
* → { stop, root, _flushForTest }
|
|
44
|
+
*
|
|
45
|
+
* watcher.WatcherError
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
var fs = require("fs");
|
|
49
|
+
var path = require("path");
|
|
50
|
+
var lazyRequire = require("./lazy-require");
|
|
51
|
+
var validateOpts = require("./validate-opts");
|
|
52
|
+
var { WatcherError } = require("./framework-error");
|
|
53
|
+
|
|
54
|
+
var audit = lazyRequire(function () { return require("./audit"); });
|
|
55
|
+
var observability = lazyRequire(function () { return require("./observability"); });
|
|
56
|
+
|
|
57
|
+
var DEFAULT_DEBOUNCE_MS = 100;
|
|
58
|
+
// Per-watcher event count cap before we self-terminate as a safety net
|
|
59
|
+
// against runaway directories that emit millions of events per minute.
|
|
60
|
+
// Operators with legitimate high-churn directories raise this via opts.
|
|
61
|
+
var DEFAULT_MAX_PENDING = 10000; // allow:raw-byte-literal — pending-event queue cap
|
|
62
|
+
|
|
63
|
+
// ---- glob-style matcher ----
|
|
64
|
+
//
|
|
65
|
+
// Supports three shapes per entry:
|
|
66
|
+
// "*.ext" — basename glob (extension or wildcard pattern)
|
|
67
|
+
// "dir/**" — prefix match against the relative path
|
|
68
|
+
// "exact/path" — exact relative-path match (case-sensitive)
|
|
69
|
+
//
|
|
70
|
+
// Anything else throws at create-time so a typo surfaces at boot.
|
|
71
|
+
//
|
|
72
|
+
// Implementation note: every pattern is parsed at create-time into a
|
|
73
|
+
// fixed shape (literal segments + `*` placeholders) and matched with a
|
|
74
|
+
// linear two-pointer walk over the basename. No dynamic RegExp — the
|
|
75
|
+
// linear walker has no catastrophic backtracking surface even when the
|
|
76
|
+
// pattern contains many `*`. Pattern length + `*` count are also
|
|
77
|
+
// bounded at parse-time as defense in depth.
|
|
78
|
+
var MAX_IGNORE_PATTERN_LEN = 256;
|
|
79
|
+
var MAX_IGNORE_STAR_COUNT = 16;
|
|
80
|
+
|
|
81
|
+
function _parseGlobBasename(pattern) {
|
|
82
|
+
// Split on `*` — the alternating literal pieces are matched in order
|
|
83
|
+
// against the basename with `*` consuming any non-separator run.
|
|
84
|
+
var parts = pattern.split("*");
|
|
85
|
+
for (var i = 0; i < parts.length; i += 1) {
|
|
86
|
+
if (parts[i].indexOf("/") !== -1 || parts[i].indexOf("\\") !== -1) {
|
|
87
|
+
throw new WatcherError("watcher/bad-ignore",
|
|
88
|
+
"watcher.create: glob pattern '" + pattern +
|
|
89
|
+
"' contains a path separator outside the dir/** prefix form");
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return parts;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function _matchGlobBasename(parts, base) {
|
|
96
|
+
// Two-pointer walk. parts[0] must prefix the basename; parts[last]
|
|
97
|
+
// must suffix; intermediate parts must appear in order. `*` matches
|
|
98
|
+
// any run of non-separator chars (basename has no separators).
|
|
99
|
+
if (parts.length === 0) return false;
|
|
100
|
+
if (parts[0].length > 0) {
|
|
101
|
+
if (base.indexOf(parts[0]) !== 0) return false;
|
|
102
|
+
}
|
|
103
|
+
var pos = parts[0].length;
|
|
104
|
+
if (parts.length === 1) return base.length === pos;
|
|
105
|
+
// Match the trailing literal first to anchor.
|
|
106
|
+
var tail = parts[parts.length - 1];
|
|
107
|
+
if (tail.length > 0) {
|
|
108
|
+
if (base.length - tail.length < pos) return false;
|
|
109
|
+
if (base.lastIndexOf(tail) !== base.length - tail.length) return false;
|
|
110
|
+
}
|
|
111
|
+
var endLimit = base.length - tail.length;
|
|
112
|
+
for (var k = 1; k < parts.length - 1; k += 1) {
|
|
113
|
+
var seg = parts[k];
|
|
114
|
+
if (seg.length === 0) continue;
|
|
115
|
+
var found = base.indexOf(seg, pos);
|
|
116
|
+
if (found === -1 || found + seg.length > endLimit) return false;
|
|
117
|
+
pos = found + seg.length;
|
|
118
|
+
}
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function _compileIgnore(patterns) {
|
|
123
|
+
if (!Array.isArray(patterns) || patterns.length === 0) {
|
|
124
|
+
return function () { return false; };
|
|
125
|
+
}
|
|
126
|
+
var compiled = [];
|
|
127
|
+
for (var i = 0; i < patterns.length; i += 1) {
|
|
128
|
+
var p = patterns[i];
|
|
129
|
+
if (typeof p !== "string" || p.length === 0) {
|
|
130
|
+
throw new WatcherError("watcher/bad-ignore",
|
|
131
|
+
"watcher.create: ignore[" + i + "] must be a non-empty string");
|
|
132
|
+
}
|
|
133
|
+
if (p.length > MAX_IGNORE_PATTERN_LEN) {
|
|
134
|
+
throw new WatcherError("watcher/bad-ignore",
|
|
135
|
+
"watcher.create: ignore[" + i + "] exceeds " + MAX_IGNORE_PATTERN_LEN + "-byte cap");
|
|
136
|
+
}
|
|
137
|
+
var starCount = 0;
|
|
138
|
+
for (var s = 0; s < p.length; s += 1) if (p.charCodeAt(s) === 42 /* * */) starCount += 1;
|
|
139
|
+
if (starCount > MAX_IGNORE_STAR_COUNT) {
|
|
140
|
+
throw new WatcherError("watcher/bad-ignore",
|
|
141
|
+
"watcher.create: ignore[" + i + "] exceeds " + MAX_IGNORE_STAR_COUNT + "-wildcard cap");
|
|
142
|
+
}
|
|
143
|
+
if (p.indexOf("**") !== -1) {
|
|
144
|
+
// dir/** prefix-match — strip the trailing **; reject `**` mid-pattern.
|
|
145
|
+
if (!/^[^*]*\/?\*\*$/.test(p)) {
|
|
146
|
+
throw new WatcherError("watcher/bad-ignore",
|
|
147
|
+
"watcher.create: ignore[" + i + "] '**' is only supported as a trailing dir/** prefix form");
|
|
148
|
+
}
|
|
149
|
+
var prefix = p.replace(/\/?\*\*$/, "");
|
|
150
|
+
compiled.push({ kind: "prefix", value: prefix });
|
|
151
|
+
} else if (starCount > 0) {
|
|
152
|
+
compiled.push({ kind: "glob", value: _parseGlobBasename(p) });
|
|
153
|
+
} else {
|
|
154
|
+
compiled.push({ kind: "exact", value: p });
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return function (relPath) {
|
|
158
|
+
var base = path.basename(relPath);
|
|
159
|
+
var normalized = relPath.split(path.sep).join("/");
|
|
160
|
+
for (var j = 0; j < compiled.length; j += 1) {
|
|
161
|
+
var c = compiled[j];
|
|
162
|
+
if (c.kind === "exact" && (c.value === relPath || c.value === normalized)) return true;
|
|
163
|
+
if (c.kind === "prefix" && (normalized === c.value || normalized.indexOf(c.value + "/") === 0)) return true;
|
|
164
|
+
if (c.kind === "glob" && _matchGlobBasename(c.value, base)) return true;
|
|
165
|
+
}
|
|
166
|
+
return false;
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function _validateOpts(opts) {
|
|
171
|
+
validateOpts.requireObject(opts, "watcher.create", WatcherError, "watcher/bad-opts");
|
|
172
|
+
validateOpts.requireNonEmptyString(opts.root, "root", WatcherError, "watcher/bad-root");
|
|
173
|
+
validateOpts.optionalFiniteNonNegative(opts.debounceMs, "debounceMs", WatcherError, "watcher/bad-debounce-ms");
|
|
174
|
+
if (opts.maxPending !== undefined &&
|
|
175
|
+
(typeof opts.maxPending !== "number" || !isFinite(opts.maxPending) || opts.maxPending < 1)) {
|
|
176
|
+
throw new WatcherError("watcher/bad-max-pending",
|
|
177
|
+
"watcher.create: maxPending must be a positive finite number");
|
|
178
|
+
}
|
|
179
|
+
validateOpts.optionalFunction(opts.onChange, "onChange", WatcherError, "watcher/bad-hook");
|
|
180
|
+
validateOpts.optionalFunction(opts.onDelete, "onDelete", WatcherError, "watcher/bad-hook");
|
|
181
|
+
validateOpts.optionalFunction(opts.onError, "onError", WatcherError, "watcher/bad-hook");
|
|
182
|
+
if (opts.ignore !== undefined && !Array.isArray(opts.ignore)) {
|
|
183
|
+
throw new WatcherError("watcher/bad-ignore",
|
|
184
|
+
"watcher.create: ignore must be an array of glob patterns");
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function create(opts) {
|
|
189
|
+
_validateOpts(opts);
|
|
190
|
+
|
|
191
|
+
var root = path.resolve(opts.root);
|
|
192
|
+
var debounceMs = (opts.debounceMs !== undefined) ? opts.debounceMs : DEFAULT_DEBOUNCE_MS;
|
|
193
|
+
var maxPending = (opts.maxPending !== undefined) ? opts.maxPending : DEFAULT_MAX_PENDING;
|
|
194
|
+
var onChange = opts.onChange || function () {};
|
|
195
|
+
var onDelete = opts.onDelete || function () {};
|
|
196
|
+
var onError = opts.onError || function () {};
|
|
197
|
+
var isIgnored = _compileIgnore(opts.ignore);
|
|
198
|
+
var auditOn = opts.audit !== false;
|
|
199
|
+
|
|
200
|
+
// Pre-flight: root must exist and be a directory.
|
|
201
|
+
var rootStat;
|
|
202
|
+
try { rootStat = fs.statSync(root); }
|
|
203
|
+
catch (e) {
|
|
204
|
+
throw new WatcherError("watcher/root-missing",
|
|
205
|
+
"watcher.create: root '" + root + "' is not accessible: " + ((e && e.message) || String(e)));
|
|
206
|
+
}
|
|
207
|
+
if (!rootStat.isDirectory()) {
|
|
208
|
+
throw new WatcherError("watcher/root-not-dir",
|
|
209
|
+
"watcher.create: root '" + root + "' is not a directory");
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Pending-event coalescer: per-relative-path debounce timer +
|
|
213
|
+
// last-known shape ("change" | "delete"). The most recent observation
|
|
214
|
+
// wins when the timer fires. Uses Map for ordered iteration on
|
|
215
|
+
// self-terminate paths.
|
|
216
|
+
var pending = new Map();
|
|
217
|
+
var stopped = false;
|
|
218
|
+
var eventCount = 0;
|
|
219
|
+
var watcherHandle = null;
|
|
220
|
+
|
|
221
|
+
function _safeEmitAudit(action, metadata) {
|
|
222
|
+
if (!auditOn) return;
|
|
223
|
+
try { audit().safeEmit({ action: action, outcome: "success", metadata: metadata || {} }); }
|
|
224
|
+
catch (_e) { /* drop-silent — audit best-effort */ }
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function _safeError(err) {
|
|
228
|
+
try { observability().safeEvent("watcher.error", 1, { code: (err && err.code) || "unknown" }); }
|
|
229
|
+
catch (_e) { /* drop-silent */ }
|
|
230
|
+
try { onError(err); } catch (_e) { /* operator error handler must not crash the watcher */ }
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function _normalizeAndDispatch(relPath) {
|
|
234
|
+
if (stopped) return;
|
|
235
|
+
if (isIgnored(relPath)) return;
|
|
236
|
+
var fullPath = path.join(root, relPath);
|
|
237
|
+
// lstat (NOT stat) — refuses to follow symlinks out of root.
|
|
238
|
+
var lst;
|
|
239
|
+
try { lst = fs.lstatSync(fullPath); }
|
|
240
|
+
catch (e) {
|
|
241
|
+
if (e && e.code === "ENOENT") {
|
|
242
|
+
// Path is gone — delete event. Type unknown by the time we
|
|
243
|
+
// observe; emit "file" as the conservative default. Operators
|
|
244
|
+
// tracking dir vs file deletes must keep their own shadow tree.
|
|
245
|
+
try {
|
|
246
|
+
onDelete({ type: "file", relativePath: relPath, fullPath: fullPath });
|
|
247
|
+
} catch (_eh) { /* operator hook must not crash dispatch */ }
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
_safeError(e);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
if (lst.isSymbolicLink()) {
|
|
254
|
+
// Skip — never follow a symlink. The watcher ignores the event.
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
var type = lst.isDirectory() ? "dir" : "file";
|
|
258
|
+
try {
|
|
259
|
+
onChange({
|
|
260
|
+
type: type,
|
|
261
|
+
relativePath: relPath,
|
|
262
|
+
fullPath: fullPath,
|
|
263
|
+
size: lst.size,
|
|
264
|
+
mtime: lst.mtime,
|
|
265
|
+
});
|
|
266
|
+
} catch (_eh) { /* operator hook must not crash dispatch */ }
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function _enqueue(relPath) {
|
|
270
|
+
if (stopped) return;
|
|
271
|
+
eventCount += 1;
|
|
272
|
+
if (pending.size >= maxPending) {
|
|
273
|
+
// Safety net — operator's directory is producing more events
|
|
274
|
+
// than the watcher can keep up with. Emit one error and stop;
|
|
275
|
+
// operators raise maxPending or fix the source.
|
|
276
|
+
var overflow = new WatcherError("watcher/overflow",
|
|
277
|
+
"watcher: pending event queue exceeded maxPending=" + maxPending);
|
|
278
|
+
_safeError(overflow);
|
|
279
|
+
stop();
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
var existing = pending.get(relPath);
|
|
283
|
+
if (existing && existing.timer) clearTimeout(existing.timer);
|
|
284
|
+
var entry = { timer: null };
|
|
285
|
+
entry.timer = setTimeout(function () {
|
|
286
|
+
pending.delete(relPath);
|
|
287
|
+
_normalizeAndDispatch(relPath);
|
|
288
|
+
}, debounceMs);
|
|
289
|
+
// Keep timers from blocking process exit — the operator's stop()
|
|
290
|
+
// call (or appShutdown) clears them explicitly.
|
|
291
|
+
if (entry.timer && typeof entry.timer.unref === "function") entry.timer.unref();
|
|
292
|
+
pending.set(relPath, entry);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// ---- start the underlying watch ----
|
|
296
|
+
try {
|
|
297
|
+
watcherHandle = fs.watch(root, { recursive: true, persistent: true }, function (eventType, filename) {
|
|
298
|
+
if (stopped) return;
|
|
299
|
+
// filename can be null on some platforms when the buffer
|
|
300
|
+
// overflows. Drop — there is nothing actionable.
|
|
301
|
+
if (!filename) return;
|
|
302
|
+
// node returns OS-native paths; normalize to root-relative.
|
|
303
|
+
var rel = filename;
|
|
304
|
+
// fs.watch passes a relative path already, but on macOS it can
|
|
305
|
+
// be an absolute path under /private/var/... when the root is a
|
|
306
|
+
// tmpdir symlink. Strip the root prefix defensively.
|
|
307
|
+
if (path.isAbsolute(rel) && rel.indexOf(root) === 0) {
|
|
308
|
+
rel = path.relative(root, rel);
|
|
309
|
+
}
|
|
310
|
+
// Both inotify and ReadDirectoryChangesW occasionally fire with
|
|
311
|
+
// an empty filename for the root directory itself — ignore.
|
|
312
|
+
if (rel === "" || rel === ".") return;
|
|
313
|
+
_enqueue(rel);
|
|
314
|
+
});
|
|
315
|
+
watcherHandle.on("error", function (err) { _safeError(err); });
|
|
316
|
+
} catch (e) {
|
|
317
|
+
// Older kernels without recursive inotify return ERR_FEATURE_UNAVAILABLE.
|
|
318
|
+
// Surface as an operator-actionable error rather than a silent
|
|
319
|
+
// single-directory degradation.
|
|
320
|
+
if (e && (e.code === "ERR_FEATURE_UNAVAILABLE_ON_PLATFORM" || e.code === "ENOSYS")) {
|
|
321
|
+
throw new WatcherError("watcher/recursive-unsupported",
|
|
322
|
+
"watcher.create: recursive watch not supported on this platform/kernel: " +
|
|
323
|
+
((e && e.message) || String(e)));
|
|
324
|
+
}
|
|
325
|
+
throw new WatcherError("watcher/start-failed",
|
|
326
|
+
"watcher.create: fs.watch failed: " + ((e && e.message) || String(e)));
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
_safeEmitAudit("watcher.started", { root: root });
|
|
330
|
+
|
|
331
|
+
function stop() {
|
|
332
|
+
if (stopped) return;
|
|
333
|
+
stopped = true;
|
|
334
|
+
// Clear any pending debounces so process exit isn't held up.
|
|
335
|
+
pending.forEach(function (entry) {
|
|
336
|
+
if (entry && entry.timer) clearTimeout(entry.timer);
|
|
337
|
+
});
|
|
338
|
+
pending.clear();
|
|
339
|
+
if (watcherHandle) {
|
|
340
|
+
try { watcherHandle.close(); } catch (_e) { /* best-effort */ }
|
|
341
|
+
watcherHandle = null;
|
|
342
|
+
}
|
|
343
|
+
_safeEmitAudit("watcher.stopped", { root: root, eventCount: eventCount });
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Test seam — flushes all pending debounce timers immediately so
|
|
347
|
+
// tests don't have to await debounceMs. Not part of the operator
|
|
348
|
+
// contract.
|
|
349
|
+
function _flushForTest() {
|
|
350
|
+
var snapshot = Array.from(pending.entries());
|
|
351
|
+
pending.clear();
|
|
352
|
+
for (var i = 0; i < snapshot.length; i += 1) {
|
|
353
|
+
if (snapshot[i][1] && snapshot[i][1].timer) clearTimeout(snapshot[i][1].timer);
|
|
354
|
+
_normalizeAndDispatch(snapshot[i][0]);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
return {
|
|
359
|
+
stop: stop,
|
|
360
|
+
root: root,
|
|
361
|
+
_flushForTest: _flushForTest,
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
module.exports = {
|
|
366
|
+
create: create,
|
|
367
|
+
WatcherError: WatcherError,
|
|
368
|
+
};
|
package/lib/webhook.js
CHANGED
|
@@ -1,71 +1,53 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* b.webhook
|
|
3
|
+
* @module b.webhook
|
|
4
|
+
* @featured true
|
|
5
|
+
* @nav Communication
|
|
6
|
+
* @title Webhook
|
|
4
7
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* var verifier = b.webhook.verifier({
|
|
16
|
-
* algo: "hmac-sha3-512",
|
|
17
|
-
* keys: { v1: secret, v0: oldSecret }, // multi-key for rotation
|
|
18
|
-
* toleranceMs: b.constants.TIME.minutes(5),
|
|
19
|
-
* nonceStore: b.nonceStore.create({ ... }), // optional replay defense
|
|
20
|
-
* });
|
|
21
|
-
*
|
|
22
|
-
* router.use(b.middleware.bodyParser({ keepRawBody: true })); // REQUIRED
|
|
23
|
-
* router.post("/inbound-webhook", verifier.middleware(), function (req, res) {
|
|
24
|
-
* // req.webhook = { algo, kid, timestamp, id }
|
|
25
|
-
* });
|
|
26
|
-
*
|
|
27
|
-
* Algorithms:
|
|
28
|
-
* "hmac-sha3-512" — symmetric. keys: { kid → Buffer/string secret }
|
|
29
|
-
* "pqc-pem" — asymmetric. keys map for signer:
|
|
30
|
-
* { kid → { privateKey, publicKey } } (PEM)
|
|
31
|
-
* keys map for verifier:
|
|
32
|
-
* { kid → publicKey } (PEM)
|
|
33
|
-
* Algorithm (SLH-DSA-SHAKE-256f / ML-DSA-87) is
|
|
34
|
-
* auto-detected by Node from the PEM. No classical
|
|
35
|
-
* (Ed25519, RSA, ECDSA) signature scheme is exposed.
|
|
8
|
+
* @intro
|
|
9
|
+
* Outbound webhook delivery with cryptographic signing in a single
|
|
10
|
+
* `Webhook-Signature` header, retry + dead-letter via `b.retry`, and
|
|
11
|
+
* idempotency keys baked into the signed string so a captured
|
|
12
|
+
* signature cannot be replayed with a fresh id. Inbound verification
|
|
13
|
+
* is the symmetric primitive: `verifier()` returns a middleware that
|
|
14
|
+
* parses the header, enforces the timestamp window, finds a matching
|
|
15
|
+
* kid, runs constant-time signature compare, and (when configured)
|
|
16
|
+
* consults a nonce store for replay defense.
|
|
36
17
|
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
18
|
+
* Algorithms: `hmac-sha3-512` (symmetric, kid → Buffer/string secret)
|
|
19
|
+
* or `pqc-pem` (asymmetric — SLH-DSA-SHAKE-256f / ML-DSA-87 / ML-DSA-65,
|
|
20
|
+
* auto-detected by Node from the PEM). No classical (Ed25519 / RSA /
|
|
21
|
+
* ECDSA) signature scheme is exposed.
|
|
39
22
|
*
|
|
40
|
-
*
|
|
23
|
+
* Signed string is prefix-bound to defend against algorithm- and
|
|
24
|
+
* key-substitution attacks: `<algo>.<kid>.<timestamp>.<id>.<body>`.
|
|
25
|
+
* Header is the Stripe-shape `t=<seconds>,id=<uuid>,<kid>=<sig>`;
|
|
26
|
+
* `t` and `id` are reserved segment names, every other pair is a
|
|
27
|
+
* kid → signature mapping. The signer emits exactly one kid; the
|
|
28
|
+
* verifier accepts any number so operators rotating keys point the
|
|
29
|
+
* verifier at both old + new keys and migrate signers progressively.
|
|
41
30
|
*
|
|
42
|
-
*
|
|
31
|
+
* PQC signatures are emitted as base64url (~40 KB for SLH-DSA-SHAKE-
|
|
32
|
+
* 256f, vs ~59 KB hex) to fit common front-end header caps; the
|
|
33
|
+
* verifier accepts EITHER encoding for transition windows.
|
|
43
34
|
*
|
|
44
|
-
*
|
|
35
|
+
* Replay defense: passing a `nonceStore` (any object exposing
|
|
36
|
+
* `checkAndInsert(nonce, expireAt) → bool/Promise<bool>`) records
|
|
37
|
+
* seen ids; a second delivery with the same id rejects with REPLAY.
|
|
38
|
+
* `b.nonceStore` is the reference implementation; operators plug in
|
|
39
|
+
* Redis / SQL by satisfying the same shape.
|
|
45
40
|
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
* signers progressively.
|
|
41
|
+
* Audit defaults are ON for both success and failure on both sides
|
|
42
|
+
* — the inbound verify IS the auditable boundary event, not a
|
|
43
|
+
* precursor to one. Operators with extreme volume opt out via
|
|
44
|
+
* `auditSuccess: false`; failures remain on regardless.
|
|
51
45
|
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
* cannot be reused with a fresh id.
|
|
55
|
-
* - Optional `nonceStore` records seen ids; second delivery with the
|
|
56
|
-
* same id rejects with REPLAY. The framework's b.nonceStore is the
|
|
57
|
-
* reference impl; operators plug in Redis/SQL by passing any object
|
|
58
|
-
* with `checkAndInsert(nonce, expireAt) → bool/Promise<bool>`.
|
|
59
|
-
*
|
|
60
|
-
* Validation policy:
|
|
61
|
-
*
|
|
62
|
-
* - signer/verifier creation opts → throw at config time
|
|
63
|
-
* - signer.sign body type → throw at call site
|
|
64
|
-
* - signer.send url shape → throw at call site (via safeUrl)
|
|
65
|
-
* - verifier.verify input shape → throw WebhookError at call site
|
|
66
|
-
* - nonceStore.checkAndInsert err → propagates (fail-closed)
|
|
46
|
+
* @card
|
|
47
|
+
* Outbound webhook delivery with cryptographic signing in a single `Webhook-Signature` header, retry + dead-letter via `b.retry`, and idempotency keys baked into the signed string so a captured signature cannot be replayed with a fresh id.
|
|
67
48
|
*/
|
|
68
49
|
|
|
50
|
+
var nodeCrypto = require("crypto");
|
|
69
51
|
var crypto = require("./crypto");
|
|
70
52
|
var httpClient = require("./http-client");
|
|
71
53
|
var safeBuffer = require("./safe-buffer");
|
|
@@ -91,6 +73,16 @@ var ALGOS = Object.freeze({
|
|
|
91
73
|
PQC_PEM: "pqc-pem",
|
|
92
74
|
});
|
|
93
75
|
|
|
76
|
+
// PQC signature algorithms accepted under the "pqc-pem" algo. Node
|
|
77
|
+
// auto-detects the active algorithm from the PEM (asymmetricKeyType ===
|
|
78
|
+
// "ml-dsa-65" | "ml-dsa-87" | "slh-dsa-shake-256f"). When the operator
|
|
79
|
+
// pins `pqcAlgorithm` at signer/verifier construction the framework
|
|
80
|
+
// asserts the PEM matches at config time so a key-rotation that
|
|
81
|
+
// accidentally swapped algorithms surfaces at boot, not at first
|
|
82
|
+
// signature failure. Permitted values match the audit-signing primitive
|
|
83
|
+
// (lib/audit-sign.js SUPPORTED_SIGNING_ALGS).
|
|
84
|
+
var PQC_ALGORITHMS = Object.freeze(["slh-dsa-shake-256f", "ml-dsa-87", "ml-dsa-65"]);
|
|
85
|
+
|
|
94
86
|
var HEADER = Object.freeze({
|
|
95
87
|
SIGNATURE: "Webhook-Signature",
|
|
96
88
|
});
|
|
@@ -170,6 +162,45 @@ function _validateKeysShape(name, algo, keys, side) {
|
|
|
170
162
|
}
|
|
171
163
|
}
|
|
172
164
|
|
|
165
|
+
// _detectPqcAlgorithmFromPem — read asymmetricKeyType from a PEM key.
|
|
166
|
+
// Used to assert the operator-pinned pqcAlgorithm matches the PEM at
|
|
167
|
+
// config time. Returns null on un-parseable input (caller already
|
|
168
|
+
// validated key shape, so this only fires for malformed PEM).
|
|
169
|
+
function _detectPqcAlgorithmFromPem(pem) {
|
|
170
|
+
try {
|
|
171
|
+
var k = typeof pem === "string"
|
|
172
|
+
? nodeCrypto.createPrivateKey(pem)
|
|
173
|
+
: nodeCrypto.createPrivateKey({ key: pem, format: "pem" });
|
|
174
|
+
return k.asymmetricKeyType;
|
|
175
|
+
} catch (_e1) {
|
|
176
|
+
try {
|
|
177
|
+
var pubk = typeof pem === "string"
|
|
178
|
+
? nodeCrypto.createPublicKey(pem)
|
|
179
|
+
: nodeCrypto.createPublicKey({ key: pem, format: "pem" });
|
|
180
|
+
return pubk.asymmetricKeyType;
|
|
181
|
+
} catch (_e2) { return null; }
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function _assertPqcAlgorithmMatches(name, pqcAlgorithm, keys, side) {
|
|
186
|
+
if (typeof pqcAlgorithm !== "string") return;
|
|
187
|
+
if (PQC_ALGORITHMS.indexOf(pqcAlgorithm) === -1) {
|
|
188
|
+
throw _err("BAD_OPT", name + ": pqcAlgorithm must be one of " +
|
|
189
|
+
PQC_ALGORITHMS.join(", ") + ", got " + JSON.stringify(pqcAlgorithm));
|
|
190
|
+
}
|
|
191
|
+
var kids = Object.keys(keys);
|
|
192
|
+
for (var i = 0; i < kids.length; i++) {
|
|
193
|
+
var k = keys[kids[i]];
|
|
194
|
+
var pem = side === "signer" ? (k.privateKey || k.publicKey) : k;
|
|
195
|
+
var detected = _detectPqcAlgorithmFromPem(pem);
|
|
196
|
+
if (detected && detected !== pqcAlgorithm) {
|
|
197
|
+
throw _err("BAD_OPT", name + ": pqcAlgorithm '" + pqcAlgorithm +
|
|
198
|
+
"' does not match PEM (kid '" + kids[i] + "' has asymmetricKeyType=" +
|
|
199
|
+
JSON.stringify(detected) + ")");
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
173
204
|
function _validateBody(body) {
|
|
174
205
|
if (typeof body !== "string" && !Buffer.isBuffer(body)) {
|
|
175
206
|
throw _err("BAD_BODY", "webhook: body must be a string or Buffer, got " + typeof body);
|
|
@@ -275,8 +306,56 @@ function _validateSignerOpts(opts) {
|
|
|
275
306
|
validateOpts.optionalFunction(opts.idGenerator, "webhook.signer: idGenerator", WebhookError);
|
|
276
307
|
validateOpts.optionalFunction(opts.now, "webhook.signer: now", WebhookError);
|
|
277
308
|
validateOpts.auditShape(opts.audit, "webhook.signer", WebhookError);
|
|
309
|
+
if (opts.pqcAlgorithm !== undefined) {
|
|
310
|
+
if (opts.algo !== ALGOS.PQC_PEM) {
|
|
311
|
+
throw _err("BAD_OPT", "webhook.signer: pqcAlgorithm only meaningful with algo='pqc-pem'");
|
|
312
|
+
}
|
|
313
|
+
_assertPqcAlgorithmMatches("webhook.signer", opts.pqcAlgorithm, opts.keys, "signer");
|
|
314
|
+
}
|
|
278
315
|
}
|
|
279
316
|
|
|
317
|
+
/**
|
|
318
|
+
* @primitive b.webhook.signer
|
|
319
|
+
* @signature b.webhook.signer(opts)
|
|
320
|
+
* @since 0.1.0
|
|
321
|
+
* @status stable
|
|
322
|
+
* @compliance soc2, pci-dss
|
|
323
|
+
* @related b.webhook.verifier
|
|
324
|
+
*
|
|
325
|
+
* Build an outbound signer. Returns `{ sign, headers, send }`: `sign`
|
|
326
|
+
* computes the signature header pair for a body without doing I/O;
|
|
327
|
+
* `headers` returns just the headers map; `send` performs the POST via
|
|
328
|
+
* `b.httpClient.request` wrapped in `b.retry.withRetry`. Each call
|
|
329
|
+
* generates a fresh idempotency `id` (ULID-shaped via `b.crypto.
|
|
330
|
+
* generateToken` by default; operators override with `idGenerator`)
|
|
331
|
+
* that's bound into the signed string so captured signatures cannot
|
|
332
|
+
* replay with a different id.
|
|
333
|
+
*
|
|
334
|
+
* @opts
|
|
335
|
+
* algo: "hmac-sha3-512" | "pqc-pem",
|
|
336
|
+
* keys: { [kid]: Buffer | string } // hmac
|
|
337
|
+
* | { [kid]: { privateKey, publicKey } } // pqc-pem
|
|
338
|
+
* defaultKid: string, // required when keys has >1 kid
|
|
339
|
+
* pqcAlgorithm: "slh-dsa-shake-256f" | "ml-dsa-87" | "ml-dsa-65",
|
|
340
|
+
* signatureHeader: string, // default "Webhook-Signature"
|
|
341
|
+
* idGenerator: function () => string,
|
|
342
|
+
* now: function () => number, // ms
|
|
343
|
+
* retry: object, // b.retry.withRetry opts
|
|
344
|
+
* http: object, // b.httpClient.request opts
|
|
345
|
+
* audit: object, // b.audit handle
|
|
346
|
+
* auditFailures: boolean, // default true
|
|
347
|
+
* auditSuccess: boolean, // default true
|
|
348
|
+
*
|
|
349
|
+
* @example
|
|
350
|
+
* var b = require("@blamejs/core");
|
|
351
|
+
* var signer = b.webhook.signer({
|
|
352
|
+
* algo: "hmac-sha3-512",
|
|
353
|
+
* keys: { v1: Buffer.from("0123456789abcdef0123456789abcdef") },
|
|
354
|
+
* defaultKid: "v1",
|
|
355
|
+
* });
|
|
356
|
+
* var headers = signer.headers('{"event":"user.created"}');
|
|
357
|
+
* // → { "Webhook-Signature": "t=1714500000,id=...,v1=<hex>" }
|
|
358
|
+
*/
|
|
280
359
|
function signer(opts) {
|
|
281
360
|
_validateSignerOpts(opts);
|
|
282
361
|
var algo = opts.algo;
|
|
@@ -435,8 +514,61 @@ function _validateVerifierOpts(opts) {
|
|
|
435
514
|
validateOpts.auditShape(opts.audit, "webhook.verifier", WebhookError);
|
|
436
515
|
validateOpts.optionalBoolean(opts.auditFailures, "webhook.verifier: auditFailures", WebhookError);
|
|
437
516
|
validateOpts.optionalBoolean(opts.auditSuccess, "webhook.verifier: auditSuccess", WebhookError);
|
|
517
|
+
if (opts.pqcAlgorithm !== undefined) {
|
|
518
|
+
if (opts.algo !== ALGOS.PQC_PEM) {
|
|
519
|
+
throw _err("BAD_OPT", "webhook.verifier: pqcAlgorithm only meaningful with algo='pqc-pem'");
|
|
520
|
+
}
|
|
521
|
+
_assertPqcAlgorithmMatches("webhook.verifier", opts.pqcAlgorithm, opts.keys, "verifier");
|
|
522
|
+
}
|
|
438
523
|
}
|
|
439
524
|
|
|
525
|
+
/**
|
|
526
|
+
* @primitive b.webhook.verifier
|
|
527
|
+
* @signature b.webhook.verifier(opts)
|
|
528
|
+
* @since 0.1.0
|
|
529
|
+
* @status stable
|
|
530
|
+
* @compliance soc2, pci-dss
|
|
531
|
+
* @related b.webhook.signer
|
|
532
|
+
*
|
|
533
|
+
* Build an inbound verifier. Returns `{ verify, middleware }`: `verify`
|
|
534
|
+
* checks an explicit `{ body, headers }` pair and resolves to
|
|
535
|
+
* `{ algo, kid, timestamp, id }` on success; `middleware` is an
|
|
536
|
+
* Express-style middleware that pulls `req.bodyRaw` (requires
|
|
537
|
+
* `b.middleware.bodyParser({ keepRawBody: true })`), verifies, and
|
|
538
|
+
* stashes the result on `req.webhook`. Failures throw `WebhookError`
|
|
539
|
+
* with a stable `code` (`MISSING_HEADER` / `BAD_HEADER_FORMAT` /
|
|
540
|
+
* `EXPIRED` / `FUTURE` / `UNKNOWN_KID` / `BAD_SIGNATURE` / `REPLAY` /
|
|
541
|
+
* ...) and the middleware translates them to HTTP 401 / 500.
|
|
542
|
+
*
|
|
543
|
+
* @opts
|
|
544
|
+
* algo: "hmac-sha3-512" | "pqc-pem",
|
|
545
|
+
* keys: { [kid]: Buffer | string } // hmac
|
|
546
|
+
* | { [kid]: string | Buffer }, // pqc-pem (PEM public key)
|
|
547
|
+
* pqcAlgorithm: "slh-dsa-shake-256f" | "ml-dsa-87" | "ml-dsa-65",
|
|
548
|
+
* toleranceMs: number, // default 5 minutes
|
|
549
|
+
* clockSkewMs: number, // default 1 minute
|
|
550
|
+
* signatureHeader: string, // default "Webhook-Signature"
|
|
551
|
+
* nonceStore: { checkAndInsert(nonce, expireAt) },
|
|
552
|
+
* now: function () => number,
|
|
553
|
+
* audit: object,
|
|
554
|
+
* auditFailures: boolean, // default true
|
|
555
|
+
* auditSuccess: boolean, // default true
|
|
556
|
+
*
|
|
557
|
+
* @example
|
|
558
|
+
* var b = require("@blamejs/core");
|
|
559
|
+
* var verifier = b.webhook.verifier({
|
|
560
|
+
* algo: "hmac-sha3-512",
|
|
561
|
+
* keys: { v1: Buffer.from("0123456789abcdef0123456789abcdef") },
|
|
562
|
+
* toleranceMs: b.constants.TIME.minutes(5),
|
|
563
|
+
* });
|
|
564
|
+
* // wire into a router:
|
|
565
|
+
* // router.use(b.middleware.bodyParser({ keepRawBody: true }));
|
|
566
|
+
* // router.post("/inbound", verifier.middleware(), function (req, res) {
|
|
567
|
+
* // // req.webhook = { algo, kid, timestamp, id }
|
|
568
|
+
* // });
|
|
569
|
+
* var mw = verifier.middleware();
|
|
570
|
+
* // → function (req, res, next) { ... }
|
|
571
|
+
*/
|
|
440
572
|
function verifier(opts) {
|
|
441
573
|
_validateVerifierOpts(opts);
|
|
442
574
|
var cfg = validateOpts.applyDefaults(opts, DEFAULTS);
|
|
@@ -606,10 +738,11 @@ function _writeError(res, status, code, message) {
|
|
|
606
738
|
// ---- Public surface ----
|
|
607
739
|
|
|
608
740
|
module.exports = {
|
|
609
|
-
signer:
|
|
610
|
-
verifier:
|
|
611
|
-
ALGOS:
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
741
|
+
signer: signer,
|
|
742
|
+
verifier: verifier,
|
|
743
|
+
ALGOS: ALGOS,
|
|
744
|
+
PQC_ALGORITHMS: PQC_ALGORITHMS,
|
|
745
|
+
HEADER: HEADER,
|
|
746
|
+
DEFAULTS: DEFAULTS,
|
|
747
|
+
WebhookError: WebhookError,
|
|
615
748
|
};
|