@blamejs/core 0.16.33 → 0.16.35
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 +4 -0
- package/lib/agent-saga.js +36 -8
- package/lib/middleware/dpop.js +10 -5
- package/lib/webhook-dispatcher.js +24 -2
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,10 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.16.x
|
|
10
10
|
|
|
11
|
+
- v0.16.35 (2026-07-16) — **A resumed saga that later fails now compensates the steps it completed before the crash, and a webhook delivery retries a transient DNS failure during its safety re-check instead of dead-lettering.** Two durability fixes in the agent orchestration and webhook delivery primitives. b.agent.saga rebuilt its completed-step list from empty when a saga resumed from a persisted checkpoint, so a failure after the resume compensated only the steps that ran in that resumed pass -- the steps completed before the crash were never rolled back, defeating the saga's whole purpose (a charge committed before the crash would never be refunded when a later step failed). Resume now seeds the completed-step list with the steps that finished before the crash, so a failure compensates the full set in reverse order. Separately, b.webhook.dispatcher re-checks a delivery's destination for an SSRF rebind just before each attempt; that check resolves the destination host, and a transient resolver failure (a temporary DNS error) during it was treated as a permanent failure and dead-lettered the delivery on the first attempt. The re-check now dead-letters only a genuine SSRF refusal or malformed URL and treats a transient resolver fault as retryable, like every other transport error. **Added:** *b.webhook.dispatcher accepts a dnsLookup override for the destination SSRF check* — b.webhook.dispatcher now accepts an optional dnsLookup(host) resolver, forwarded to the SSRF destination check, so an operator can point destination resolution at a specific resolver (and a test can drive the transient-versus-permanent classification offline). It defaults to the framework's DNS-over-TLS resolver, unchanged. **Fixed:** *A resumed saga compensates the steps it completed before the crash, not only those in the resumed run* — b.agent.saga runs a sequence of steps and, on a step failure, compensates the completed steps in reverse order. When a saga resumed from a persisted checkpoint, it started its completed-step list empty, so a failure after the resume compensated only the steps that ran in the resumed pass -- the steps completed before the crash were left uncompensated. For a distributed transaction that is the exact failure the pattern exists to prevent: work committed before the crash (a charge, a reservation, an external call) would never be rolled back when a later step failed. Resume now seeds the completed-step list with the steps that finished before the crash, reconstructed from the saga definition, so a subsequent failure compensates the full completed set in reverse; compensation runs against the resumed state, which already reflects those steps' effects, and the failing step itself is not compensated. To keep that reseeding safe against replay, resume also refuses a saga the state store marks terminal (failed-and-compensated or completed) rather than re-running its compensators, and the state-store interface documents that compensators must be idempotent -- a crash mid-compensation can replay a compensation on the next resume, so compensating twice must be safe. · *Webhook delivery retries a transient DNS failure during its SSRF re-check instead of dead-lettering* — b.webhook.dispatcher re-validates a delivery's destination against SSRF (private / loopback / metadata IPs, or a rebind since registration) just before each attempt, which resolves the destination host. A transient resolver failure during that resolution -- a lookup timeout or system/resolve failure -- was caught and marked a permanent failure, dead-lettering the delivery on its first attempt rather than retrying. The re-check now classifies the failure: a genuine SSRF refusal or malformed URL dead-letters, a resolver failure honors the framework DNS resolver's own terminal-versus-transient verdict (a permanent failure such as a host with no addresses or a removed record dead-letters immediately, a transient one is retried on the backoff curve, capped at maxAttempts), matching how the dispatcher already treats a transient DNS error during the delivery POST itself. So a webhook is no longer lost to a momentary DNS blip, a genuine rebind to an internal address is still dead-lettered immediately, and a permanently unresolvable destination dead-letters without burning every retry attempt.
|
|
12
|
+
|
|
13
|
+
- v0.16.34 (2026-07-16) — **The DPoP middleware returns the correct multiple-proof rejection when a request carries a repeated DPoP header, instead of mislabeling it as a missing proof.** RFC 9449 §4.1 permits only one DPoP header value per request. b.middleware.dpop rejected a request that carried the header as an array -- repeated DPoP: lines a custom server or proxy did not collapse -- but its array-shape check sat after the missing-header guard, and an array is not a string, so the missing-header guard always ran first. A duplicated DPoP proof was therefore rejected as a missing proof (and, when a DPoP nonce was required, answered with use_dpop_nonce, prompting the client into a pointless nonce-retry loop) rather than with the invalid_dpop_proof / multiple-DPoP-headers rejection the specification calls for. Both paths already refused the request, so this was never a fail-open -- only an incorrect diagnostic and a wasted round trip. The array-shape check now runs before the missing-header guard, so a repeated DPoP header is rejected with the correct error. **Fixed:** *DPoP middleware rejects a repeated DPoP header with the correct multiple-proof error* — b.middleware.dpop enforces the RFC 9449 §4.1 single-value rule, but its Array.isArray check for a repeated header (when a server or proxy delivered the DPoP header as an array rather than a comma-joined string) sat after the non-string / empty guard. Because an array fails the non-string check first, the dedicated multiple-DPoP-headers branch never ran: a duplicated proof was reported as a missing DPoP header, and under a required-nonce policy it returned use_dpop_nonce, driving the client into a fruitless nonce-retry loop. The array-shape check now runs first, so a repeated DPoP header is rejected with invalid_dpop_proof and a multiple-DPoP-headers message. This changes only the error code and message for that malformed-request case; both orderings already refused the request, so no valid request is affected.
|
|
14
|
+
|
|
11
15
|
- v0.16.33 (2026-07-16) — **Agent-snapshot restore now authenticates a sealed snapshot's tenant and capture-time against its signature, closing a cross-tenant restore path, alongside fail-closed and never-throw hardening across the DKIM/ARC, crypto-envelope, and image/PDF verifiers.** A sealed agent snapshot's authenticated envelope binds its table, snapshot id, and schema version, but the decorative wrapper fields a hostile or compromised storage backend can rewrite -- the tenant id that loadLatest filters on and the capture time it sorts on -- were trusted without being checked against the signed body. b.agent.snapshot restore now cross-checks both wrapper fields against the signature-covered values and refuses a mismatch, so a relabelled row can no longer surface one tenant's authentic snapshot to another (cross-tenant restore) or misrepresent when the restored state was captured. The release also hardens several verifiers that a valid-but-unusual or hostile input could push off their documented contract: the DKIM, inbound-authentication, and ARC verifiers now accept a bare-LF (Unix line-ending) message and return an authentication verdict instead of throwing; the crypto envelope and packed-secret decoders reject a truncated ciphertext with their typed error instead of leaking a raw cipher exception; the image guard routes a byte-order-mark-prefixed SVG to refusal at every profile instead of serving it as unknown content; and the image and PDF guards no longer throw on a hostile metadata bag whose bytes field is an array-like object, honoring their never-throw inspection contract. **Fixed:** *DKIM, inbound-authentication, and ARC verifiers accept bare-LF messages instead of throwing* — b.mail.dkim.verify, b.mail.inbound.verify, and b.mail.arc.verify canonicalize over CRLF and split the header block on a CRLF-CRLF separator. A message read from a Unix file or mbox, or passed through operator tooling that stripped carriage returns, arrives with bare-LF line endings and previously raised an uncaught error out of the verifier rather than returning an authentication verdict. The header/body split now normalizes bare-LF to canonical CRLF before locating the separator (a no-op on a proper CRLF message), so a bare-LF message produces a verdict -- and a message signed on the CRLF wire but transported bare-LF now verifies correctly rather than failing. inbound.verify and arc.verify additionally treat a message with no separator at all as headers-only, returning a verdict in keeping with their always-return-a-verdict contract. · *Crypto envelope and packed-secret decoders reject truncated ciphertext with a typed error* — b.crypto.decryptEnvelope and b.crypto.decryptPacked verify that each declared component of an untrusted ciphertext -- the length-prefixed KEM ciphertext and hybrid ephemeral public key, and the trailing nonce and authentication tag -- fits within the envelope before handing it to the cipher or the key-agreement step. A ciphertext truncated inside any of those components previously reached Node crypto as an under-length value and surfaced as a raw exception (a cipher RangeError on the nonce, or a Failed to perform decapsulation / key-parse error on the KEM ciphertext or ephemeral key), escaping the documented Invalid envelope error contract and leaking implementation detail. Both decoders now reject a truncated input with their typed Invalid envelope / Invalid packed format error, while a truncation inside the ciphertext body still surfaces as the genuine authentication-tag failure; a well-formed input is never affected. · *Image guard routes a BOM-prefixed SVG to refusal at every profile* — b.guardImage detects SVG by its leading markup so it can route it to the SVG guard or refuse it. A UTF-8 byte-order-mark before the markup previously defeated the offset-anchored signature scan, so a BOM-prefixed SVG fell through as unknown content -- served rather than refused under the balanced and permissive profiles. The magic-byte scanner now skips a leading BOM when matching the SVG and XML signatures, so a BOM-prefixed SVG is detected and refused at every profile. The BOM skip applies only to those text-family signatures: a binary raster's magic must sit at its real offset, so a BOM-prefixed PNG or JPEG is still refused as unknown content rather than accepted as a valid raster. · *Image and PDF guards no longer throw on a hostile array-like metadata bag* — b.guardImage.validate and b.guardPdf.validate document pure inspection that never throws on hostile metadata. Their byte-size cap measured any value carrying a numeric length, but the measurement primitive accepts only strings, Buffers, and Uint8Arrays and threw on a plain Array or array-like object -- crashing a direct validate or sanitize caller (the gate path already fails closed). The cap now measures only those byte-carrying types and passes an unmeasurable array-like through to magic detection, which reads only the leading bytes and refuses unrecognized content, so validate returns a refusal instead of throwing. **Security:** *Agent-snapshot restore binds the requested tenant and capture-time to the sealed snapshot's signature* — b.agent.snapshot seals each snapshot under an authenticated envelope whose AAD binds the table, snapshot id, and schema version. The metadata a backend stores alongside the sealed blob -- the tenant id that loadLatest({ tenantId }) filters on and the takenAt it sorts on to pick the latest -- is not covered by that AAD, and a hostile or compromised backend can return independently tampered list() and get() results. A backend could therefore relabel tenant A's list() entry as tenant B (leaving A's get() row honest) so that loadLatest({ tenantId: 'tenant-b' }) selected and returned tenant A's authentic snapshot -- a cross-tenant restore of in-flight sagas, streams, and idempotency state -- or inflate a row's list() age to serve an older snapshot as the latest. loadLatest now binds the requested selection criteria to the loaded snapshot's signature-covered values: the authenticated tenant id must equal the requested tenant id, and the list() sort key that selected a row must equal that row's authenticated capture time; a divergence is refused (agent-snapshot/tenant-id-mismatch, agent-snapshot/taken-at-mismatch), and the load path additionally cross-checks the get() wrapper against the signed body. The fix is at load time and does not change the seal format, so previously persisted snapshots remain restorable. A hostile backend can still withhold snapshots it never reveals, but every snapshot returned is authentic and bound to the requested tenant. **Detectors:** *A byte-size cap must vet its input type before measuring it* — A codebase-patterns gate refuses a guard that measures a metadata bag's byte length gated only on a numeric length property -- the shape that let an array-like bytes field crash the image and PDF guards. A byte-size cap over untrusted metadata must confirm the value is a string, Buffer, or Uint8Array before measuring it, so a future guard cannot reintroduce the never-throw-contract violation.
|
|
12
16
|
|
|
13
17
|
- v0.16.32 (2026-07-15) — **The vendored Public Suffix List is refreshed to the current upstream snapshot, and vendor-update.sh gains the --refresh-data mode its file headers and verifier messages have always pointed at.** The vendored Mozilla Public Suffix List is updated to the latest upstream build, so organizational-domain derivation for DMARC alignment, BIMI issuer scoping, cookie-scope confinement, and same-site policy reflects the current registry delegations; the data module is regenerated and re-signed (SHA-256 + SHA3-512 + SLH-DSA), the manifest hashes refreshed, and the NOTICE attribution date updated. The refresh itself now runs through scripts/vendor-update.sh --refresh-data — the maintenance command that every vendored data file's header and lib/vendor-data.js's tamper-error messages reference — which fetches the upstream where one exists, sanity-checks the body before anything reaches the signer, re-appends the in-payload integrity canary, regenerates and re-signs the .data.js carrier, updates the manifest and NOTICE dates, and verifies all four integrity layers. A codebase-patterns gate now refuses any script flag referenced from error messages, file headers, or operator docs that the target script does not implement. **Added:** *scripts/vendor-update.sh --refresh-data — fetch, canary, re-sign, and verify the vendored data files in one command* — The mode that vendored data-file headers and lib/vendor-data.js tamper-error messages direct operators to now exists. `vendor-update.sh --refresh-data [entry]` refreshes the Public Suffix List and the SecLists common-password corpus from their upstreams (refusing a truncated or error body before it can reach the signer), re-appends each file's in-payload integrity canary, regenerates and re-signs the .data.js carrier with the operator-local SLH-DSA key whenever the raw file changed OR the carrier fails four-layer verification (so a corrupted signature block or a stripped provenance header is repairable through the documented path), updates the MANIFEST bundledAt and NOTICE attribution dates, refreshes the manifest hashes, and finishes by running the four-layer verifier (SHA-256 + SHA3-512 + SLH-DSA signature + canary) over every vendored data file. The operator-managed BIMI trust-anchor bundle is never fetched — it is re-signed only when the local .pem was edited per its file-header procedure. A Public Suffix List fetch whose VERSION timestamp is older than the vendored one is refused rather than signed (the list is CDN-served, and a lagging edge can return an older snapshot than the one already vendored). Entries whose upstream is unchanged are left byte-identical, so a no-op refresh produces a clean working tree; in-flight downloads live in gitignored *.refresh-tmp files that are removed on exit. **Changed:** *Vendored Public Suffix List refreshed to the current upstream snapshot* — The vendored Mozilla Public Suffix List is updated to the latest upstream build (2026-07-15), so organizational-domain derivation for DMARC alignment, BIMI issuer scoping, cookie-scope confinement, and same-site policy reflects the current registry delegations. The data module is regenerated and re-signed (SHA-256 + SHA3-512 + SLH-DSA), the manifest hashes refreshed, and the NOTICE attribution date updated. **Detectors:** *Script flags referenced from error messages, file headers, and operator docs must exist in the target script* — A codebase-patterns gate resolves every `<script>.sh --flag` / `<script>.js --flag` reference in lib/, scripts/, and the operator docs against the scripts/ directory and refuses any flag with no whole-token occurrence outside comment lines in the target script — a usage-header comment alone does not count as an implementation. A maintenance command recommended by a verification-failure message or a stale-data gate must work when the operator reaches for it.
|
package/lib/agent-saga.js
CHANGED
|
@@ -115,10 +115,15 @@ function create(config) {
|
|
|
115
115
|
// Interface: { saveStep, loadResumePoint, markCompleted, markFailed }.
|
|
116
116
|
// saveStep({sagaId, stepIndex, stepName, state, status}) commits
|
|
117
117
|
// after each step.run; loadResumePoint(sagaId) returns the resume
|
|
118
|
-
// shape `{ stepIndex, state }` on restart
|
|
119
|
-
//
|
|
120
|
-
//
|
|
121
|
-
//
|
|
118
|
+
// shape `{ stepIndex, state }` on restart — and, for a saga that already
|
|
119
|
+
// reached a terminal state, SHOULD set `terminal: true` (or return null)
|
|
120
|
+
// after markFailed / markCompleted so a resume of a failed-and-compensated
|
|
121
|
+
// (or completed) saga is refused rather than replayed. Compensators MUST be
|
|
122
|
+
// idempotent: a crash mid-compensation-cascade (before markFailed persists)
|
|
123
|
+
// can replay a compensation on the next resume, so compensating twice must be
|
|
124
|
+
// safe. Without a stateStore, the saga still runs end-to-end in-memory but a
|
|
125
|
+
// mid-saga crash loses progress (operator-acknowledged dev mode; the audit
|
|
126
|
+
// emit `agent.saga.no_state_store` surfaces the posture per call).
|
|
122
127
|
var stateStore = config.stateStore || null;
|
|
123
128
|
if (stateStore !== null) {
|
|
124
129
|
validateOpts.requireMethods(stateStore, ["saveStep", "loadResumePoint"],
|
|
@@ -147,6 +152,21 @@ async function _resume(config, auditImpl, stateStore, sagaId, ctx, opts) {
|
|
|
147
152
|
throw new AgentSagaError("agent-saga/not-found",
|
|
148
153
|
"resume: no resume point for saga '" + sagaId + "'");
|
|
149
154
|
}
|
|
155
|
+
// A saga that already reached a terminal state — failed-and-compensated, or
|
|
156
|
+
// completed — must NOT be resumed. Replaying it re-runs the remaining steps
|
|
157
|
+
// AND, on a failure, re-invokes the completed steps' compensators (including
|
|
158
|
+
// the pre-crash steps this run seeds); compensators are not required to be
|
|
159
|
+
// idempotent, so a second compensation could corrupt external state (a double
|
|
160
|
+
// refund). A well-behaved stateStore marks the saga terminal via markFailed /
|
|
161
|
+
// markCompleted and reflects it here (resumePoint.terminal); refuse the resume
|
|
162
|
+
// when it does. The residual window — a crash after a compensation but before
|
|
163
|
+
// markFailed — is inherent at-least-once compensation and is why compensators
|
|
164
|
+
// MUST be idempotent (see the interface note on create()).
|
|
165
|
+
if (resumePoint.terminal === true) {
|
|
166
|
+
throw new AgentSagaError("agent-saga/not-resumable",
|
|
167
|
+
"resume: saga '" + sagaId + "' is terminal and cannot be resumed — " +
|
|
168
|
+
"replaying a failed/completed saga would re-invoke its compensators");
|
|
169
|
+
}
|
|
150
170
|
return _runFrom(config, auditImpl, stateStore, ctx,
|
|
151
171
|
resumePoint.state || {}, opts, sagaId, resumePoint.stepIndex);
|
|
152
172
|
}
|
|
@@ -158,11 +178,19 @@ async function _run(config, auditImpl, stateStore, ctx, initialState, opts) {
|
|
|
158
178
|
}
|
|
159
179
|
|
|
160
180
|
async function _runFrom(config, auditImpl, stateStore, ctx, state, opts, sagaId, startIndex) {
|
|
161
|
-
// completedSteps captures index + step reference
|
|
162
|
-
//
|
|
163
|
-
//
|
|
164
|
-
//
|
|
181
|
+
// completedSteps captures index + step reference for the reverse-order
|
|
182
|
+
// compensation cascade. On resume (startIndex > 0) the steps before
|
|
183
|
+
// startIndex were completed and committed BEFORE the crash; a failure in
|
|
184
|
+
// this run must still compensate them, or a saga that resumes and then
|
|
185
|
+
// fails leaves its pre-crash work uncompensated (a broken distributed-
|
|
186
|
+
// transaction guarantee — e.g. a charge committed pre-crash is never
|
|
187
|
+
// refunded when a later step fails). Seed those prior steps so the cascade
|
|
188
|
+
// unwinds the FULL completed set, not only this run's steps. Compensation
|
|
189
|
+
// runs against the resumed state, which already reflects their effects.
|
|
165
190
|
var completedSteps = [];
|
|
191
|
+
for (var pre = 0; pre < startIndex; pre += 1) {
|
|
192
|
+
completedSteps.push({ step: config.steps[pre], index: pre });
|
|
193
|
+
}
|
|
166
194
|
|
|
167
195
|
if (startIndex === 0) {
|
|
168
196
|
agentAudit.safeAudit(auditImpl, "agent.saga.started", opts.actor, {
|
package/lib/middleware/dpop.js
CHANGED
|
@@ -311,16 +311,21 @@ function create(opts) {
|
|
|
311
311
|
|
|
312
312
|
var middleware = async function dpopMiddleware(req, res, next) {
|
|
313
313
|
var proofHeader = req.headers && req.headers.dpop;
|
|
314
|
+
// RFC 9449 §4.1 — only ONE DPoP header value per request. Check the
|
|
315
|
+
// array shape (repeated `DPoP:` header lines a custom server/proxy did
|
|
316
|
+
// not collapse) BEFORE the non-string guard: an array is not a string,
|
|
317
|
+
// so the guard below would otherwise shadow this branch and mislabel a
|
|
318
|
+
// duplicated proof as a missing one, letting the multiple-proof
|
|
319
|
+
// rejection never run.
|
|
320
|
+
if (Array.isArray(proofHeader)) {
|
|
321
|
+
return _writeUnauthorized(req, res, "invalid_dpop_proof",
|
|
322
|
+
"multiple DPoP headers are not allowed", null, onDeny, problemMode);
|
|
323
|
+
}
|
|
314
324
|
if (typeof proofHeader !== "string" || proofHeader.length === 0) {
|
|
315
325
|
return _writeUnauthorized(req, res,
|
|
316
326
|
nonceMgr ? "use_dpop_nonce" : "invalid_dpop_proof",
|
|
317
327
|
"DPoP header required", _freshNonce(), onDeny, problemMode);
|
|
318
328
|
}
|
|
319
|
-
// RFC 9449 §4.1 — only ONE DPoP header value per request.
|
|
320
|
-
if (Array.isArray(proofHeader)) {
|
|
321
|
-
return _writeUnauthorized(req, res, "invalid_dpop_proof",
|
|
322
|
-
"multiple DPoP headers are not allowed", null, onDeny, problemMode);
|
|
323
|
-
}
|
|
324
329
|
// RFC 9449 §4.1 single-value invariant. node:http
|
|
325
330
|
// collapses repeated headers into a comma-joined string when the
|
|
326
331
|
// client ships `DPoP: proof1, DPoP: proof2`; the Array.isArray
|
|
@@ -154,6 +154,7 @@ function _coercePayloadString(payload) {
|
|
|
154
154
|
* allowInternalDestinations: boolean, // default false — refuse SSRF (private/loopback/metadata)
|
|
155
155
|
* httpRequest: function, // (url, body, headers) → { status } — inject for tests
|
|
156
156
|
* now: function, // clock injection → ms epoch
|
|
157
|
+
* dnsLookup: function, // (host) → [{ address, family }] — override the SSRF destination resolver
|
|
157
158
|
*
|
|
158
159
|
* @example
|
|
159
160
|
* var wd = b.webhook.dispatcher({ externalDb: b.externalDb });
|
|
@@ -186,6 +187,7 @@ function dispatcher(opts) {
|
|
|
186
187
|
allowInternalDestinations: "optional-boolean",
|
|
187
188
|
httpRequest: "optional-function",
|
|
188
189
|
now: "optional-function",
|
|
190
|
+
dnsLookup: "optional-function",
|
|
189
191
|
}, "webhook.dispatcher", WebhookDispatcherError, "webhook-dispatcher/bad-opts");
|
|
190
192
|
var externalDb = opts.externalDb;
|
|
191
193
|
var endpointsTable = _validateTableName(
|
|
@@ -216,12 +218,20 @@ function dispatcher(opts) {
|
|
|
216
218
|
// because the IP check resolves the host. Throws a dispatcher-coded error.
|
|
217
219
|
async function _assertSafeDestination(url, where) {
|
|
218
220
|
safeUrl.parse(url, { allowedProtocols: allowedProtocols, errorClass: WebhookDispatcherError });
|
|
221
|
+
var checkOpts = { allowInternal: allowInternal };
|
|
222
|
+
if (typeof opts.dnsLookup === "function") checkOpts.dnsLookup = opts.dnsLookup;
|
|
219
223
|
try {
|
|
220
|
-
await ssrfGuard().checkUrl(url,
|
|
224
|
+
await ssrfGuard().checkUrl(url, checkOpts);
|
|
221
225
|
} catch (e) {
|
|
222
226
|
if (e && e.isSsrfError) {
|
|
227
|
+
// A genuine SSRF refusal (destination resolved to a private / loopback /
|
|
228
|
+
// metadata IP, or a rebind since registration) is PERMANENT — dead-letter.
|
|
223
229
|
throw _err("webhook-dispatcher/ssrf-refused", where + ": " + e.message);
|
|
224
230
|
}
|
|
231
|
+
// A non-SsrfError from checkUrl is a raw resolver / network fault during
|
|
232
|
+
// host resolution (EAI_AGAIN, ETIMEDOUT, SERVFAIL, ...). Re-throw it as-is
|
|
233
|
+
// so the caller classifies it as TRANSIENT (retry), not permanent — a
|
|
234
|
+
// transient DNS blip must not dead-letter a delivery.
|
|
225
235
|
throw e;
|
|
226
236
|
}
|
|
227
237
|
}
|
|
@@ -483,8 +493,20 @@ function dispatcher(opts) {
|
|
|
483
493
|
try {
|
|
484
494
|
await _assertSafeDestination(row.url, "deliver");
|
|
485
495
|
} catch (err) {
|
|
496
|
+
// Permanence by CLASS: a genuine SSRF refusal or malformed URL surfaces
|
|
497
|
+
// as a WebhookDispatcherError (won't fix on retry) -> dead-letter. A
|
|
498
|
+
// resolver fault from the host-resolution step carries the framework's
|
|
499
|
+
// own terminal-vs-transient verdict on err.permanent — honor it so a
|
|
500
|
+
// PERMANENT lookup failure (no addresses / a removed record: the
|
|
501
|
+
// dns/no-result DnsError) dead-letters, while a transient one (a lookup
|
|
502
|
+
// timeout, a system/resolve failure) is retried on the backoff curve
|
|
503
|
+
// (capped at maxAttempts) like every other transport error below. A raw
|
|
504
|
+
// resolver error without that verdict (EAI_AGAIN from the native
|
|
505
|
+
// fallback, an injected resolver) is treated as transient — a DNS blip
|
|
506
|
+
// must not dead-letter a delivery.
|
|
507
|
+
var permanent = (err instanceof WebhookDispatcherError) || (err && err.permanent === true);
|
|
486
508
|
return await _onFailure(deliveryId, attemptNo,
|
|
487
|
-
(err && err.message) || String(err),
|
|
509
|
+
(err && err.message) || String(err), permanent);
|
|
488
510
|
}
|
|
489
511
|
// Sign + POST. Transport errors (network, TLS, timeout, DNS) and any
|
|
490
512
|
// non-2xx HTTP status are TRANSIENT — back off and retry (capped at
|
package/package.json
CHANGED
package/sbom.cdx.json
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
|
|
3
3
|
"bomFormat": "CycloneDX",
|
|
4
4
|
"specVersion": "1.5",
|
|
5
|
-
"serialNumber": "urn:uuid:
|
|
5
|
+
"serialNumber": "urn:uuid:19f33cb5-0c9f-4b8e-81a7-381d0d4b9eaf",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-07-
|
|
8
|
+
"timestamp": "2026-07-16T15:13:48.546Z",
|
|
9
9
|
"lifecycles": [
|
|
10
10
|
{
|
|
11
11
|
"phase": "build"
|
|
@@ -19,14 +19,14 @@
|
|
|
19
19
|
}
|
|
20
20
|
],
|
|
21
21
|
"component": {
|
|
22
|
-
"bom-ref": "@blamejs/core@0.16.
|
|
22
|
+
"bom-ref": "@blamejs/core@0.16.35",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.16.
|
|
25
|
+
"version": "0.16.35",
|
|
26
26
|
"scope": "required",
|
|
27
27
|
"author": "blamejs contributors",
|
|
28
28
|
"description": "The Node framework that owns its stack.",
|
|
29
|
-
"purl": "pkg:npm/%40blamejs/core@0.16.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/core@0.16.35",
|
|
30
30
|
"properties": [],
|
|
31
31
|
"externalReferences": [
|
|
32
32
|
{
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"components": [],
|
|
55
55
|
"dependencies": [
|
|
56
56
|
{
|
|
57
|
-
"ref": "@blamejs/core@0.16.
|
|
57
|
+
"ref": "@blamejs/core@0.16.35",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|