@blamejs/core 0.8.82 → 0.8.86
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/README.md +1 -1
- package/index.js +6 -0
- package/lib/a2a-tasks.js +598 -0
- package/lib/a2a.js +10 -0
- package/lib/acme.js +189 -5
- package/lib/audit.js +1 -0
- package/lib/cache-status.js +288 -0
- package/lib/compliance.js +36 -0
- package/lib/framework-error.js +19 -0
- package/lib/mcp-tool-registry.js +473 -0
- package/lib/mcp.js +3 -0
- package/lib/middleware/idempotency-key.js +424 -0
- package/lib/middleware/index.js +10 -0
- package/lib/middleware/no-cache.js +106 -0
- package/lib/problem-details.js +439 -0
- package/lib/server-timing.js +174 -0
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/a2a.js
CHANGED
|
@@ -389,9 +389,19 @@ function verifyCard(envelope, publicKeyPem, opts) {
|
|
|
389
389
|
};
|
|
390
390
|
}
|
|
391
391
|
|
|
392
|
+
var tasks = require("./a2a-tasks");
|
|
393
|
+
|
|
392
394
|
module.exports = {
|
|
393
395
|
signCard: signCard,
|
|
394
396
|
verifyCard: verifyCard,
|
|
395
397
|
canonicalize: canonicalize,
|
|
396
398
|
createCard: createCard,
|
|
399
|
+
tasks: {
|
|
400
|
+
send: tasks.send,
|
|
401
|
+
get: tasks.get,
|
|
402
|
+
cancel: tasks.cancel,
|
|
403
|
+
ALLOWED_METHODS: tasks.ALLOWED_METHODS,
|
|
404
|
+
},
|
|
405
|
+
middleware: tasks.middleware,
|
|
406
|
+
A2aTasksError: tasks.A2aTasksError,
|
|
397
407
|
};
|
package/lib/acme.js
CHANGED
|
@@ -565,6 +565,23 @@ function create(opts) {
|
|
|
565
565
|
var payload = { identifiers: orderOpts.identifiers.slice() };
|
|
566
566
|
if (typeof orderOpts.notBefore === "string") payload.notBefore = orderOpts.notBefore;
|
|
567
567
|
if (typeof orderOpts.notAfter === "string") payload.notAfter = orderOpts.notAfter;
|
|
568
|
+
// draft-aaron-acme-profiles — operator-selected certificate profile.
|
|
569
|
+
// The CA advertises profile names + descriptions via
|
|
570
|
+
// `directory.meta.profiles`; operator passes the chosen name through
|
|
571
|
+
// newOrder. CAs honoring the draft return 400 when the name isn't
|
|
572
|
+
// in the advertised set; ones that haven't adopted the draft ignore
|
|
573
|
+
// the field. v1-defensible scope: refuse non-string + cap length so
|
|
574
|
+
// attacker-supplied profile values can't bloat the JSON payload.
|
|
575
|
+
if (typeof orderOpts.profile === "string") {
|
|
576
|
+
if (orderOpts.profile.length === 0 || orderOpts.profile.length > C.BYTES.bytes(64)) {
|
|
577
|
+
throw _err("acme/bad-profile",
|
|
578
|
+
"newOrder: profile name must be a non-empty string <= 64 bytes", true);
|
|
579
|
+
}
|
|
580
|
+
payload.profile = orderOpts.profile;
|
|
581
|
+
} else if (orderOpts.profile !== undefined) {
|
|
582
|
+
throw _err("acme/bad-profile",
|
|
583
|
+
"newOrder: profile must be a string when provided", true);
|
|
584
|
+
}
|
|
568
585
|
var rsp = await _signedPost(state.directory.newOrder, payload);
|
|
569
586
|
if (rsp.statusCode !== 201) {
|
|
570
587
|
_emitAudit(audit, "acme.order.created", "failure",
|
|
@@ -710,7 +727,31 @@ function create(opts) {
|
|
|
710
727
|
async function renewIfDue(opts2) {
|
|
711
728
|
var ari = await fetchAri(opts2);
|
|
712
729
|
var nowMs = Date.now();
|
|
713
|
-
|
|
730
|
+
// RFC 9773 §4.2 — when called inside the suggested window, return
|
|
731
|
+
// a renewAt timestamp picked uniformly across the remaining window
|
|
732
|
+
// so a fleet of operators running on the same poll cadence don't
|
|
733
|
+
// cluster their renewal storms at the window-start instant. Operators
|
|
734
|
+
// opt in via `{ jitter: true }`; default behavior preserves the
|
|
735
|
+
// pre-0.8.83 "renew now" semantics.
|
|
736
|
+
var jitter = opts2 && opts2.jitter === true;
|
|
737
|
+
var beforeWindow = nowMs < ari.suggestedWindow.startMs;
|
|
738
|
+
var pastWindow = nowMs > ari.suggestedWindow.endMs;
|
|
739
|
+
var renewAtMs = null;
|
|
740
|
+
if (jitter) {
|
|
741
|
+
// Uniform random point in [max(now, start), end].
|
|
742
|
+
var jLo = beforeWindow ? ari.suggestedWindow.startMs : nowMs;
|
|
743
|
+
var jHi = ari.suggestedWindow.endMs;
|
|
744
|
+
if (jHi >= jLo) {
|
|
745
|
+
// Non-crypto: RFC 9773 §4.2 fleet-scheduling jitter inside the
|
|
746
|
+
// CA-suggested renewal window. Predictability is not a threat
|
|
747
|
+
// here; uniform distribution across the window is the goal.
|
|
748
|
+
renewAtMs = jLo + Math.floor(Math.random() * (jHi - jLo + 1)); // allow:math-random-noncrypto — RFC 9773 fleet jitter, predictability not a threat
|
|
749
|
+
} else {
|
|
750
|
+
// Past-window — renew immediately, no jitter.
|
|
751
|
+
renewAtMs = nowMs;
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
if (beforeWindow) {
|
|
714
755
|
_emitAudit(audit, "acme.cert.renew.skipped", "success", {
|
|
715
756
|
certId: ari.certId,
|
|
716
757
|
windowStart: ari.suggestedWindow.start,
|
|
@@ -718,24 +759,31 @@ function create(opts) {
|
|
|
718
759
|
nowIso: new Date(nowMs).toISOString(),
|
|
719
760
|
});
|
|
720
761
|
_emitObs("acme.cert.renew.skipped", { reason: "before-window" });
|
|
721
|
-
|
|
762
|
+
var ret = { shouldRenew: false, reason: "before-window", ari: ari };
|
|
763
|
+
if (jitter) ret.renewAt = new Date(renewAtMs).toISOString();
|
|
764
|
+
return ret;
|
|
722
765
|
}
|
|
723
|
-
if (
|
|
766
|
+
if (pastWindow) {
|
|
724
767
|
_emitAudit(audit, "acme.cert.renew.scheduled", "warning", {
|
|
725
768
|
certId: ari.certId,
|
|
726
769
|
reason: "past-window",
|
|
727
770
|
windowEnd: ari.suggestedWindow.end,
|
|
728
771
|
});
|
|
729
772
|
_emitObs("acme.cert.renew.scheduled", { reason: "past-window" });
|
|
730
|
-
|
|
773
|
+
var rp = { shouldRenew: true, reason: "past-window", ari: ari };
|
|
774
|
+
if (jitter) rp.renewAt = new Date(renewAtMs).toISOString();
|
|
775
|
+
return rp;
|
|
731
776
|
}
|
|
732
777
|
_emitAudit(audit, "acme.cert.renew.scheduled", "success", {
|
|
733
778
|
certId: ari.certId,
|
|
734
779
|
windowStart: ari.suggestedWindow.start,
|
|
735
780
|
windowEnd: ari.suggestedWindow.end,
|
|
781
|
+
renewAt: jitter ? new Date(renewAtMs).toISOString() : null,
|
|
736
782
|
});
|
|
737
783
|
_emitObs("acme.cert.renew.scheduled", { reason: "in-window" });
|
|
738
|
-
|
|
784
|
+
var ri = { shouldRenew: true, reason: "in-window", ari: ari };
|
|
785
|
+
if (jitter) ri.renewAt = new Date(renewAtMs).toISOString();
|
|
786
|
+
return ri;
|
|
739
787
|
}
|
|
740
788
|
|
|
741
789
|
/**
|
|
@@ -896,6 +944,118 @@ function create(opts) {
|
|
|
896
944
|
return crypto.createHash("sha256").update(keyAuth, "utf8").digest();
|
|
897
945
|
}
|
|
898
946
|
|
|
947
|
+
/**
|
|
948
|
+
* @primitive b.acme.create.listProfiles
|
|
949
|
+
* @signature b.acme.create.listProfiles()
|
|
950
|
+
* @since 0.8.83
|
|
951
|
+
* @status experimental
|
|
952
|
+
*
|
|
953
|
+
* Returns the CA-advertised certificate profile catalog as
|
|
954
|
+
* `{ name: description }` per draft-aaron-acme-profiles. Operators
|
|
955
|
+
* pass the chosen name through `newOrder({ profile: name })`; CAs
|
|
956
|
+
* use the profile to select certificate lifetime + key-usage +
|
|
957
|
+
* validation rigor. As CA/B Forum 47-day cert TTLs phase in (Mar
|
|
958
|
+
* 2026 ballot SC-081v3), profile-name vocabulary becomes the
|
|
959
|
+
* operator-facing handle for "long-lived" vs "47-day" vs "short-
|
|
960
|
+
* lived". Returns an empty object when the directory has no
|
|
961
|
+
* `meta.profiles` map (CA hasn't adopted the draft). Refreshes the
|
|
962
|
+
* directory cache when none has been fetched yet.
|
|
963
|
+
*
|
|
964
|
+
* @example
|
|
965
|
+
* await acme.fetchDirectory();
|
|
966
|
+
* var profiles = acme.listProfiles();
|
|
967
|
+
* // → { "default": "Standard 90-day certificate",
|
|
968
|
+
* // "shortlived": "47-day certificate (CA/B Forum SC-081v3)",
|
|
969
|
+
* // "tlsserver": "TLS server profile with Must-Staple" }
|
|
970
|
+
*
|
|
971
|
+
* await acme.newOrder({ identifiers: [{ type: "dns", value: "example.com" }],
|
|
972
|
+
* profile: "shortlived" });
|
|
973
|
+
*/
|
|
974
|
+
function listProfiles() {
|
|
975
|
+
if (!state.directory) return {};
|
|
976
|
+
var meta = state.directory.meta;
|
|
977
|
+
if (!meta || typeof meta !== "object") return {};
|
|
978
|
+
var profiles = meta.profiles;
|
|
979
|
+
if (!profiles || typeof profiles !== "object") return {};
|
|
980
|
+
var out = {};
|
|
981
|
+
var keys = Object.keys(profiles);
|
|
982
|
+
for (var i = 0; i < keys.length; i += 1) {
|
|
983
|
+
var k = keys[i];
|
|
984
|
+
var v = profiles[k];
|
|
985
|
+
out[k] = typeof v === "string" ? v : "";
|
|
986
|
+
}
|
|
987
|
+
return out;
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
/**
|
|
991
|
+
* @primitive b.acme.create.dnsAccount01ChallengeRecord
|
|
992
|
+
* @signature b.acme.create.dnsAccount01ChallengeRecord(token, opts?)
|
|
993
|
+
* @since 0.8.83
|
|
994
|
+
* @status experimental
|
|
995
|
+
* @related b.acme.create.tlsAlpn01KeyAuthorization
|
|
996
|
+
*
|
|
997
|
+
* Build the DNS TXT record an operator publishes to satisfy a
|
|
998
|
+
* `dns-account-01` challenge per draft-ietf-acme-dns-account-label.
|
|
999
|
+
* Unlike `dns-01` (record at `_acme-challenge.<host>`),
|
|
1000
|
+
* `dns-account-01` scopes the record by account so the same domain
|
|
1001
|
+
* can be validated from multiple ACME accounts without record-name
|
|
1002
|
+
* collisions; the record name becomes
|
|
1003
|
+
* `_<accountLabel>._acme-challenge.<identifier>` where
|
|
1004
|
+
* `accountLabel` is the SHA-256 truncated-base32 of the account URL.
|
|
1005
|
+
*
|
|
1006
|
+
* Returns `{ name, value, ttl }` where `name` is the FQDN to publish
|
|
1007
|
+
* the TXT record at (with operator-supplied `identifier` substituted
|
|
1008
|
+
* in) and `value` is the SHA-256 of the key authorization in
|
|
1009
|
+
* unpadded base64url (same as `dns-01`). Refuses when `newAccount`
|
|
1010
|
+
* has not run (no accountUrl yet); refuses non-string token /
|
|
1011
|
+
* identifier.
|
|
1012
|
+
*
|
|
1013
|
+
* @opts
|
|
1014
|
+
* identifier: string, // host being validated (required)
|
|
1015
|
+
* ttl: number, // suggested DNS TTL in seconds; default: 60
|
|
1016
|
+
*
|
|
1017
|
+
* @example
|
|
1018
|
+
* await acme.newAccount({ contact: ["mailto:ops@example.com"] });
|
|
1019
|
+
* var rec = acme.dnsAccount01ChallengeRecord("token123", {
|
|
1020
|
+
* identifier: "example.com",
|
|
1021
|
+
* });
|
|
1022
|
+
* // rec.name → "_<accountLabel>._acme-challenge.example.com"
|
|
1023
|
+
* // rec.value → "<base64url-of-sha256(token123.<thumbprint>)>"
|
|
1024
|
+
* // rec.ttl → 60
|
|
1025
|
+
*/
|
|
1026
|
+
function dnsAccount01ChallengeRecord(token, opts2) {
|
|
1027
|
+
if (typeof token !== "string" || token.length === 0) {
|
|
1028
|
+
throw _err("acme/bad-token", "dnsAccount01ChallengeRecord: token must be a non-empty string", true);
|
|
1029
|
+
}
|
|
1030
|
+
if (!opts2 || typeof opts2 !== "object" || typeof opts2.identifier !== "string" || opts2.identifier.length === 0) {
|
|
1031
|
+
throw _err("acme/bad-identifier", "dnsAccount01ChallengeRecord: opts.identifier (host) is required", true);
|
|
1032
|
+
}
|
|
1033
|
+
if (opts2.identifier.length > C.BYTES.bytes(255)) {
|
|
1034
|
+
throw _err("acme/bad-identifier", "dnsAccount01ChallengeRecord: identifier exceeds 255 bytes", true);
|
|
1035
|
+
}
|
|
1036
|
+
if (!state.accountUrl) {
|
|
1037
|
+
throw _err("acme/no-account",
|
|
1038
|
+
"dnsAccount01ChallengeRecord: newAccount() must run first (account URL is the label seed)", true);
|
|
1039
|
+
}
|
|
1040
|
+
if (opts2.ttl !== undefined && (typeof opts2.ttl !== "number" || !isFinite(opts2.ttl) || opts2.ttl < 1 || opts2.ttl > C.TIME.hours(24) / C.TIME.seconds(1))) {
|
|
1041
|
+
throw _err("acme/bad-ttl",
|
|
1042
|
+
"dnsAccount01ChallengeRecord: ttl must be a positive integer <= 86400 seconds", true);
|
|
1043
|
+
}
|
|
1044
|
+
var crypto = require("node:crypto");
|
|
1045
|
+
// Account label: lowercase base32 of first 10 bytes of SHA-256(accountUrl)
|
|
1046
|
+
// (per draft-ietf-acme-dns-account-label §3.1 — 80-bit truncated label).
|
|
1047
|
+
var hash = crypto.createHash("sha256").update(state.accountUrl, "utf8").digest();
|
|
1048
|
+
var label = _base32lc(hash.subarray(0, 10));
|
|
1049
|
+
// Record value: same key-authorization digest shape as dns-01.
|
|
1050
|
+
var keyAuth = token + "." + _jwkThumbprint(publicJwk);
|
|
1051
|
+
var digest = crypto.createHash("sha256").update(keyAuth, "utf8").digest();
|
|
1052
|
+
return {
|
|
1053
|
+
name: "_" + label + "._acme-challenge." + opts2.identifier,
|
|
1054
|
+
value: _b64u(digest),
|
|
1055
|
+
ttl: typeof opts2.ttl === "number" ? Math.floor(opts2.ttl) : (C.TIME.minutes(1) / C.TIME.seconds(1)),
|
|
1056
|
+
};
|
|
1057
|
+
}
|
|
1058
|
+
|
|
899
1059
|
return Object.freeze({
|
|
900
1060
|
fetchDirectory: fetchDirectory,
|
|
901
1061
|
newAccount: newAccount,
|
|
@@ -908,6 +1068,8 @@ function create(opts) {
|
|
|
908
1068
|
accountKeyRollover: accountKeyRollover,
|
|
909
1069
|
deactivateAccount: deactivateAccount,
|
|
910
1070
|
tlsAlpn01KeyAuthorization: tlsAlpn01KeyAuthorization,
|
|
1071
|
+
listProfiles: listProfiles,
|
|
1072
|
+
dnsAccount01ChallengeRecord: dnsAccount01ChallengeRecord,
|
|
911
1073
|
accountUrl: function () { return state.accountUrl; },
|
|
912
1074
|
directory: function () { return state.directory; },
|
|
913
1075
|
publicJwk: function () { return Object.assign({}, publicJwk); },
|
|
@@ -955,6 +1117,28 @@ function _sleep(ms) {
|
|
|
955
1117
|
});
|
|
956
1118
|
}
|
|
957
1119
|
|
|
1120
|
+
// RFC 4648 §6 base32 lowercase (no padding) — used by
|
|
1121
|
+
// draft-ietf-acme-dns-account-label to derive the 80-bit account label
|
|
1122
|
+
// from SHA-256(accountUrl). 5-bit groups MSB-first.
|
|
1123
|
+
function _base32lc(buf) {
|
|
1124
|
+
var alphabet = "abcdefghijklmnopqrstuvwxyz234567";
|
|
1125
|
+
var out = "";
|
|
1126
|
+
var bits = 0;
|
|
1127
|
+
var value = 0;
|
|
1128
|
+
for (var i = 0; i < buf.length; i += 1) {
|
|
1129
|
+
value = (value << 8) | buf[i]; // allow:raw-byte-literal — bit-shift count, byte boundary
|
|
1130
|
+
bits += 8; // allow:raw-byte-literal — bits-per-byte constant
|
|
1131
|
+
while (bits >= 5) {
|
|
1132
|
+
out += alphabet[(value >>> (bits - 5)) & 31];
|
|
1133
|
+
bits -= 5;
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
if (bits > 0) {
|
|
1137
|
+
out += alphabet[(value << (5 - bits)) & 31];
|
|
1138
|
+
}
|
|
1139
|
+
return out;
|
|
1140
|
+
}
|
|
1141
|
+
|
|
958
1142
|
module.exports = {
|
|
959
1143
|
create: create,
|
|
960
1144
|
AcmeError: AcmeError,
|
package/lib/audit.js
CHANGED
|
@@ -294,6 +294,7 @@ var FRAMEWORK_NAMESPACES = [
|
|
|
294
294
|
"mailbimi", // b.mail.bimi (mail.bimi.vmc.fetched / verified — RFC 9091 VMC chain validation)
|
|
295
295
|
"localdb", // b.localDb.thin (localdb.thin.opened / recovered / closed — desktop-daemon SQLite wrapper)
|
|
296
296
|
"dataact", // b.dataAct (EU Data Act 2023/2854 — product_declared / user_access / share_with_third_party / share_refused / switch_request)
|
|
297
|
+
"idempotency", // b.middleware.idempotencyKey (idempotency.missing_key / bad_key / replay / key_reuse_mismatch / cache_store / store_read_failed / store_write_failed / skip_5xx / body_too_large — draft-ietf-httpapi-idempotency-key)
|
|
297
298
|
];
|
|
298
299
|
var registeredNamespaces = new Set(FRAMEWORK_NAMESPACES);
|
|
299
300
|
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @module b.cacheStatus
|
|
4
|
+
* @nav HTTP
|
|
5
|
+
* @title RFC 9211 Cache-Status
|
|
6
|
+
* @order 310
|
|
7
|
+
*
|
|
8
|
+
* @intro
|
|
9
|
+
* RFC 9211 Cache-Status response header builder + parser. The
|
|
10
|
+
* `Cache-Status` header documents which intermediate cache (CDN,
|
|
11
|
+
* reverse proxy, application cache) handled a request — operators
|
|
12
|
+
* diagnosing why a request was slow / stale / not-cached read the
|
|
13
|
+
* header and see the entire cache-decision chain instead of
|
|
14
|
+
* guessing from elapsed-time metrics.
|
|
15
|
+
*
|
|
16
|
+
* Each cache in the response path appends a comma-separated entry:
|
|
17
|
+
*
|
|
18
|
+
* Cache-Status: ExampleCache; hit; fwd=stale; ttl=600
|
|
19
|
+
*
|
|
20
|
+
* Where:
|
|
21
|
+
* - The first token is the cache identifier (sf-string)
|
|
22
|
+
* - Parameters follow as `key` or `key=value` pairs
|
|
23
|
+
* - Standard parameters per RFC 9211 §2: `hit`, `fwd`, `fwd-status`,
|
|
24
|
+
* `ttl`, `stored`, `collapsed`, `key`, `detail`
|
|
25
|
+
*
|
|
26
|
+
* `b.cacheStatus.append(prevHeader, entry)` builds a single
|
|
27
|
+
* well-formed entry and appends to whatever previous caches in the
|
|
28
|
+
* chain wrote. `b.cacheStatus.parse(headerValue)` returns the
|
|
29
|
+
* parsed chain as an array of `{ cache, params }` records.
|
|
30
|
+
*
|
|
31
|
+
* @card
|
|
32
|
+
* RFC 9211 Cache-Status header — documents which intermediate caches handled a request with structured `hit` / `fwd` / `ttl` parameters so operators diagnose cache-decision chains.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
var validateOpts = require("./validate-opts");
|
|
36
|
+
var { defineClass } = require("./framework-error");
|
|
37
|
+
|
|
38
|
+
var CacheStatusError = defineClass("CacheStatusError", { alwaysPermanent: true });
|
|
39
|
+
|
|
40
|
+
// RFC 9211 §2 — cache identifier is a Structured-Fields Item: sf-token
|
|
41
|
+
// (RFC 8941 §3.3.4) OR sf-string. We accept sf-token shape bare; an
|
|
42
|
+
// operator wanting an identifier with sf-delimiter chars (comma /
|
|
43
|
+
// semicolon / quote / backslash / whitespace) can emit it quoted via
|
|
44
|
+
// the operator-side sf-string form themselves, but this builder
|
|
45
|
+
// refuses raw delimiters since they would split into multiple list
|
|
46
|
+
// members or break the parameter grammar downstream. Token grammar
|
|
47
|
+
// per RFC 8941: starts with ALPHA or "*", continues with tchar / ":"
|
|
48
|
+
// / "/". tchar excludes `, ; " \ space and all controls.
|
|
49
|
+
var CACHE_NAME_RE = /^[A-Za-z*][!#$%&'*+\-.^_`|~0-9A-Za-z:/]*$/; // allow:duplicate-regex — sf-token shape per RFC 8941 §3.3.4
|
|
50
|
+
var CACHE_NAME_MAX = 128; // allow:raw-byte-literal — cache-name length cap, not bytes
|
|
51
|
+
var FWD_VALUES = Object.freeze(["bypass", "method", "uri-miss", "vary-miss", "miss", "request", "stale", "partial"]);
|
|
52
|
+
var BOOLEAN_PARAMS = Object.freeze(["hit", "stored", "collapsed"]);
|
|
53
|
+
// Reserved parameter names per RFC 9211 §2 — the framework knows their
|
|
54
|
+
// semantics (hit/stored/collapsed are flags, fwd is enum, ttl is number,
|
|
55
|
+
// fwd-status is HTTP status, key + detail are sf-strings). Operators
|
|
56
|
+
// passing other keys get passed-through verbatim as token=value.
|
|
57
|
+
var KNOWN_PARAMS = Object.freeze(["hit", "fwd", "fwd-status", "ttl", "stored", "collapsed", "key", "detail"]);
|
|
58
|
+
|
|
59
|
+
function _sfStringQuote(s) {
|
|
60
|
+
// RFC 8941 sf-string — quoted-string with escaping for " and \.
|
|
61
|
+
// Operator-supplied detail/key strings get the full quote-escape.
|
|
62
|
+
return "\"" + String(s).replace(/\\/g, "\\\\").replace(/"/g, "\\\"") + "\"";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* @primitive b.cacheStatus.append
|
|
67
|
+
* @signature b.cacheStatus.append(prevHeader, entry)
|
|
68
|
+
* @since 0.8.86
|
|
69
|
+
* @status stable
|
|
70
|
+
* @related b.cacheStatus.parse, b.cacheStatus.entry
|
|
71
|
+
*
|
|
72
|
+
* Append a Cache-Status entry to an existing chain header. `prevHeader`
|
|
73
|
+
* is the inbound Cache-Status string (empty / undefined / null means
|
|
74
|
+
* "this is the first entry"). `entry` is an object describing the
|
|
75
|
+
* current cache's decision. Returns the combined header string.
|
|
76
|
+
*
|
|
77
|
+
* @opts
|
|
78
|
+
* cache: string, // required — cache identifier (e.g. "ExampleCDN")
|
|
79
|
+
* hit: boolean, // true if served from cache
|
|
80
|
+
* fwd: string, // one of: bypass | method | uri-miss | vary-miss
|
|
81
|
+
* // | miss | request | stale | partial
|
|
82
|
+
* fwdStatus: number, // HTTP status the upstream returned (when fwd)
|
|
83
|
+
* ttl: number, // remaining freshness lifetime in seconds
|
|
84
|
+
* stored: boolean, // true if the response was newly stored
|
|
85
|
+
* collapsed: boolean, // true if request-collapsing merged this with another
|
|
86
|
+
* key: string, // operator-defined cache-key shape
|
|
87
|
+
* detail: string, // free-form diagnostic note
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* res.setHeader("Cache-Status",
|
|
91
|
+
* b.cacheStatus.append(req.headers["cache-status"], {
|
|
92
|
+
* cache: "blamejs",
|
|
93
|
+
* hit: false,
|
|
94
|
+
* fwd: "miss",
|
|
95
|
+
* stored: true,
|
|
96
|
+
* ttl: 3600,
|
|
97
|
+
* }));
|
|
98
|
+
* // → "ExampleCDN; hit; ttl=300, blamejs; fwd=miss; stored; ttl=3600"
|
|
99
|
+
*/
|
|
100
|
+
function append(prevHeader, entry) {
|
|
101
|
+
var formatted = entryString(entry);
|
|
102
|
+
if (typeof prevHeader === "string" && prevHeader.length > 0) {
|
|
103
|
+
return prevHeader + ", " + formatted;
|
|
104
|
+
}
|
|
105
|
+
return formatted;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* @primitive b.cacheStatus.entry
|
|
110
|
+
* @signature b.cacheStatus.entry(entry)
|
|
111
|
+
* @since 0.8.86
|
|
112
|
+
* @status stable
|
|
113
|
+
* @related b.cacheStatus.append, b.cacheStatus.parse
|
|
114
|
+
*
|
|
115
|
+
* Format a single Cache-Status entry without combining with a prior
|
|
116
|
+
* chain. Useful when the operator wants to write the header without
|
|
117
|
+
* regard to upstream entries (e.g. an origin-only deployment).
|
|
118
|
+
*
|
|
119
|
+
* @example
|
|
120
|
+
* res.setHeader("Cache-Status", b.cacheStatus.entry({
|
|
121
|
+
* cache: "blamejs", hit: true, ttl: 600,
|
|
122
|
+
* }));
|
|
123
|
+
* // → "blamejs; hit; ttl=600"
|
|
124
|
+
*/
|
|
125
|
+
function entryString(entry) {
|
|
126
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
127
|
+
throw new CacheStatusError("cache-status/bad-entry",
|
|
128
|
+
"entry must be a non-null object", true);
|
|
129
|
+
}
|
|
130
|
+
validateOpts.requireNonEmptyString(
|
|
131
|
+
entry.cache, "entry.cache", CacheStatusError, "cache-status/bad-cache-name");
|
|
132
|
+
if (entry.cache.length > CACHE_NAME_MAX || !CACHE_NAME_RE.test(entry.cache)) {
|
|
133
|
+
throw new CacheStatusError("cache-status/bad-cache-name",
|
|
134
|
+
"entry.cache '" + entry.cache + "' must be a structured-fields token " +
|
|
135
|
+
"(RFC 8941 §3.3.4: starts with ALPHA or '*', uses tchar / ':' / '/' only — " +
|
|
136
|
+
"no comma / semicolon / quote / backslash / whitespace) and <= " +
|
|
137
|
+
CACHE_NAME_MAX + " chars. Quote-and-escape an operator-supplied label " +
|
|
138
|
+
"via b.cacheStatus.entry({ ..., key: '<label>' }) instead.");
|
|
139
|
+
}
|
|
140
|
+
var parts = [entry.cache];
|
|
141
|
+
|
|
142
|
+
// Booleans — emit as bare-token when truthy.
|
|
143
|
+
for (var i = 0; i < BOOLEAN_PARAMS.length; i += 1) {
|
|
144
|
+
if (entry[BOOLEAN_PARAMS[i]] === true) parts.push(BOOLEAN_PARAMS[i]);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (entry.fwd !== undefined && entry.fwd !== null) {
|
|
148
|
+
if (typeof entry.fwd !== "string" || FWD_VALUES.indexOf(entry.fwd) === -1) {
|
|
149
|
+
throw new CacheStatusError("cache-status/bad-fwd",
|
|
150
|
+
"entry.fwd must be one of " + FWD_VALUES.join(", "));
|
|
151
|
+
}
|
|
152
|
+
parts.push("fwd=" + entry.fwd);
|
|
153
|
+
}
|
|
154
|
+
if (entry.fwdStatus !== undefined && entry.fwdStatus !== null) {
|
|
155
|
+
if (typeof entry.fwdStatus !== "number" || !Number.isInteger(entry.fwdStatus) ||
|
|
156
|
+
entry.fwdStatus < 100 || entry.fwdStatus > 599) { // allow:raw-byte-literal — HTTP status range
|
|
157
|
+
throw new CacheStatusError("cache-status/bad-fwd-status",
|
|
158
|
+
"entry.fwdStatus must be an integer 100..599");
|
|
159
|
+
}
|
|
160
|
+
parts.push("fwd-status=" + entry.fwdStatus);
|
|
161
|
+
}
|
|
162
|
+
if (entry.ttl !== undefined && entry.ttl !== null) {
|
|
163
|
+
// RFC 9211 §2.2 — ttl is a signed Integer. Negative values are
|
|
164
|
+
// explicitly valid: a `hit` paired with `ttl=-30` reports the
|
|
165
|
+
// response was served stale by 30 seconds (typically with
|
|
166
|
+
// `fwd=stale`). Refusing negatives would block the very scenario
|
|
167
|
+
// `fwd=stale` exists to surface.
|
|
168
|
+
if (typeof entry.ttl !== "number" || !Number.isInteger(entry.ttl)) {
|
|
169
|
+
throw new CacheStatusError("cache-status/bad-ttl",
|
|
170
|
+
"entry.ttl must be an integer (negative permitted for stale-cache hits per RFC 9211 §2.2)");
|
|
171
|
+
}
|
|
172
|
+
parts.push("ttl=" + entry.ttl);
|
|
173
|
+
}
|
|
174
|
+
if (entry.key !== undefined && entry.key !== null) {
|
|
175
|
+
if (typeof entry.key !== "string") {
|
|
176
|
+
throw new CacheStatusError("cache-status/bad-key",
|
|
177
|
+
"entry.key must be a string when provided");
|
|
178
|
+
}
|
|
179
|
+
parts.push("key=" + _sfStringQuote(entry.key));
|
|
180
|
+
}
|
|
181
|
+
if (entry.detail !== undefined && entry.detail !== null) {
|
|
182
|
+
if (typeof entry.detail !== "string") {
|
|
183
|
+
throw new CacheStatusError("cache-status/bad-detail",
|
|
184
|
+
"entry.detail must be a string when provided");
|
|
185
|
+
}
|
|
186
|
+
parts.push("detail=" + _sfStringQuote(entry.detail));
|
|
187
|
+
}
|
|
188
|
+
return parts.join("; ");
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* @primitive b.cacheStatus.parse
|
|
193
|
+
* @signature b.cacheStatus.parse(headerValue)
|
|
194
|
+
* @since 0.8.86
|
|
195
|
+
* @status stable
|
|
196
|
+
* @related b.cacheStatus.append, b.cacheStatus.entry
|
|
197
|
+
*
|
|
198
|
+
* Parse a Cache-Status header into an array of `{ cache, params }`
|
|
199
|
+
* records, one per cache in the chain. The params object carries the
|
|
200
|
+
* RFC 9211 §2 standard parameters as proper types (`hit`/`stored`/
|
|
201
|
+
* `collapsed` as booleans, `ttl`/`fwdStatus` as numbers, `fwd` as the
|
|
202
|
+
* raw enum string, `key`/`detail` as unquoted strings). Unknown
|
|
203
|
+
* params survive as raw string values so operators inspecting custom
|
|
204
|
+
* cache implementations can read them.
|
|
205
|
+
*
|
|
206
|
+
* Empty / non-string / malformed inputs return `[]` — defensive
|
|
207
|
+
* request-shape reader returns sane defaults rather than throwing.
|
|
208
|
+
*
|
|
209
|
+
* @example
|
|
210
|
+
* var chain = b.cacheStatus.parse(
|
|
211
|
+
* 'ExampleCDN; hit; ttl=300, blamejs; fwd=miss; stored; ttl=3600');
|
|
212
|
+
* // chain[0] = { cache: "ExampleCDN", params: { hit: true, ttl: 300 } }
|
|
213
|
+
* // chain[1] = { cache: "blamejs", params: { fwd: "miss", stored: true, ttl: 3600 } }
|
|
214
|
+
*/
|
|
215
|
+
function parse(headerValue) {
|
|
216
|
+
if (typeof headerValue !== "string" || headerValue.length === 0) return [];
|
|
217
|
+
var out = [];
|
|
218
|
+
// Split entries on commas NOT inside quoted strings.
|
|
219
|
+
var entries = _splitTopLevel(headerValue, ",");
|
|
220
|
+
for (var i = 0; i < entries.length; i += 1) {
|
|
221
|
+
var raw = entries[i].trim();
|
|
222
|
+
if (raw.length === 0) continue;
|
|
223
|
+
var fields = _splitTopLevel(raw, ";").map(function (s) { return s.trim(); });
|
|
224
|
+
var cache = fields.shift();
|
|
225
|
+
if (!cache) continue;
|
|
226
|
+
var params = {};
|
|
227
|
+
for (var j = 0; j < fields.length; j += 1) {
|
|
228
|
+
var f = fields[j];
|
|
229
|
+
if (f.length === 0) continue;
|
|
230
|
+
var eq = f.indexOf("=");
|
|
231
|
+
if (eq === -1) {
|
|
232
|
+
// Bare token — boolean
|
|
233
|
+
params[f] = true;
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
var name = f.slice(0, eq).trim();
|
|
237
|
+
var val = f.slice(eq + 1).trim();
|
|
238
|
+
params[_normalizeParamName(name)] = _parseParamValue(name, val);
|
|
239
|
+
}
|
|
240
|
+
out.push({ cache: cache, params: params });
|
|
241
|
+
}
|
|
242
|
+
return out;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function _normalizeParamName(n) {
|
|
246
|
+
// RFC 9211 §2 uses fwd-status as the canonical name; surface as
|
|
247
|
+
// `fwdStatus` in the parsed object for JS-natural access.
|
|
248
|
+
if (n === "fwd-status") return "fwdStatus";
|
|
249
|
+
return n;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function _parseParamValue(name, raw) {
|
|
253
|
+
if (raw.length >= 2 && raw.charAt(0) === "\"" && raw.charAt(raw.length - 1) === "\"") {
|
|
254
|
+
// sf-string — unquote + unescape.
|
|
255
|
+
return raw.slice(1, -1).replace(/\\(.)/g, "$1");
|
|
256
|
+
}
|
|
257
|
+
if (name === "ttl" || name === "fwd-status" || name === "fwdStatus") {
|
|
258
|
+
var n = Number(raw);
|
|
259
|
+
return Number.isFinite(n) ? n : raw;
|
|
260
|
+
}
|
|
261
|
+
return raw;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function _splitTopLevel(s, sep) {
|
|
265
|
+
var out = [];
|
|
266
|
+
var buf = "";
|
|
267
|
+
var inQuotes = false;
|
|
268
|
+
var escaped = false;
|
|
269
|
+
for (var i = 0; i < s.length; i += 1) {
|
|
270
|
+
var c = s.charAt(i);
|
|
271
|
+
if (escaped) { buf += c; escaped = false; continue; }
|
|
272
|
+
if (c === "\\" && inQuotes) { buf += c; escaped = true; continue; }
|
|
273
|
+
if (c === "\"") { inQuotes = !inQuotes; buf += c; continue; }
|
|
274
|
+
if (c === sep && !inQuotes) { out.push(buf); buf = ""; continue; }
|
|
275
|
+
buf += c;
|
|
276
|
+
}
|
|
277
|
+
if (buf.length > 0) out.push(buf);
|
|
278
|
+
return out;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
module.exports = {
|
|
282
|
+
append: append,
|
|
283
|
+
entry: entryString,
|
|
284
|
+
parse: parse,
|
|
285
|
+
FWD_VALUES: FWD_VALUES,
|
|
286
|
+
KNOWN_PARAMS: KNOWN_PARAMS,
|
|
287
|
+
CacheStatusError: CacheStatusError,
|
|
288
|
+
};
|
package/lib/compliance.js
CHANGED
|
@@ -201,6 +201,17 @@ var KNOWN_POSTURES = Object.freeze([
|
|
|
201
201
|
"eu-cer", // EU Critical Entities Resilience Directive (2022/2557; transposition 2024-10-17)
|
|
202
202
|
"eu-cyber-sol", // EU Cyber Solidarity Act (Regulation 2025/38; effective 2025-02-04)
|
|
203
203
|
"eidas-2", // eIDAS 2 / EUDI Wallet (Regulation 2024/1183; rollout 2026-2027)
|
|
204
|
+
// ---- v0.8.86 expansion — sectoral + cybersecurity directives ----
|
|
205
|
+
"cmmc-2.0", // US DoD Cybersecurity Maturity Model Certification 2.0 (effective 2025-Q1)
|
|
206
|
+
"cjis-v6", // FBI Criminal Justice Information Services Security Policy v6.0 (Dec 2024)
|
|
207
|
+
"iso-27001-2022", // ISO/IEC 27001:2022 — Information Security Management System
|
|
208
|
+
"iso-27002-2022", // ISO/IEC 27002:2022 — Code of practice for information security controls
|
|
209
|
+
"iso-27017", // ISO/IEC 27017 — Cloud-services security controls
|
|
210
|
+
"iso-27018", // ISO/IEC 27018 — PII protection in public-cloud processors
|
|
211
|
+
"iso-27701", // ISO/IEC 27701 — Privacy Information Management System
|
|
212
|
+
"nist-800-66-r2", // NIST SP 800-66 Rev 2 — HIPAA Security Rule implementation guidance // allow:raw-byte-literal — NIST publication number, not bytes
|
|
213
|
+
"ehds", // EU European Health Data Space (Regulation 2025/327; phased 2027-2029)
|
|
214
|
+
"circia", // US Cyber Incident Reporting for Critical Infrastructure Act (final rule pending)
|
|
204
215
|
]);
|
|
205
216
|
|
|
206
217
|
var STATE = { posture: null, setAt: null };
|
|
@@ -665,6 +676,17 @@ var REGIME_MAP = Object.freeze({
|
|
|
665
676
|
"eu-cer": { name: "EU Critical Entities Resilience Directive", citation: "Directive (EU) 2022/2557 (transposition 2024-10-17)", jurisdiction: "EU", domain: "cybersecurity" },
|
|
666
677
|
"eu-cyber-sol": { name: "EU Cyber Solidarity Act", citation: "Regulation (EU) 2025/38 (effective 2025-02-04)", jurisdiction: "EU", domain: "cybersecurity" },
|
|
667
678
|
"eidas-2": { name: "eIDAS 2 / EUDI Wallet", citation: "Regulation (EU) 2024/1183 (rollout 2026-2027)", jurisdiction: "EU", domain: "identity" },
|
|
679
|
+
// ---- v0.8.86 — sectoral + cybersecurity directives ----
|
|
680
|
+
"cmmc-2.0": { name: "Cybersecurity Maturity Model Certification 2.0", citation: "32 CFR Part 170 (DFARS rule effective 2025-Q1)", jurisdiction: "US", domain: "cybersecurity" },
|
|
681
|
+
"cjis-v6": { name: "FBI CJIS Security Policy v6.0", citation: "CJIS Security Policy v6.0 (effective 2024-12)", jurisdiction: "US", domain: "law-enforcement" },
|
|
682
|
+
"iso-27001-2022": { name: "ISO/IEC 27001:2022 Information Security Management System", citation: "ISO/IEC 27001:2022", jurisdiction: "international", domain: "cybersecurity" },
|
|
683
|
+
"iso-27002-2022": { name: "ISO/IEC 27002:2022 Information Security Controls", citation: "ISO/IEC 27002:2022", jurisdiction: "international", domain: "cybersecurity" },
|
|
684
|
+
"iso-27017": { name: "ISO/IEC 27017 Cloud Services Security Controls", citation: "ISO/IEC 27017:2015", jurisdiction: "international", domain: "cybersecurity" },
|
|
685
|
+
"iso-27018": { name: "ISO/IEC 27018 PII Protection in Public Cloud", citation: "ISO/IEC 27018:2019", jurisdiction: "international", domain: "privacy" },
|
|
686
|
+
"iso-27701": { name: "ISO/IEC 27701 Privacy Information Management System", citation: "ISO/IEC 27701:2019", jurisdiction: "international", domain: "privacy" },
|
|
687
|
+
"nist-800-66-r2": { name: "NIST SP 800-66 Rev 2 — HIPAA Security Rule Guidance", citation: "NIST SP 800-66 Rev 2 (Feb 2024)", jurisdiction: "US", domain: "health" },
|
|
688
|
+
"ehds": { name: "European Health Data Space", citation: "Regulation (EU) 2025/327 (phased 2027-2029)", jurisdiction: "EU", domain: "health" },
|
|
689
|
+
"circia": { name: "Cyber Incident Reporting for Critical Infrastructure Act", citation: "6 U.S.C. §681 et seq. (final rule pending)", jurisdiction: "US", domain: "cybersecurity" },
|
|
668
690
|
});
|
|
669
691
|
|
|
670
692
|
/**
|
|
@@ -928,6 +950,20 @@ var POSTURE_DEFAULTS = Object.freeze({
|
|
|
928
950
|
"eu-cer": Object.freeze({ backupEncryptionRequired: true, auditChainSignedRequired: true, tlsMinVersion: "TLSv1.3", requireVacuumAfterErase: false }),
|
|
929
951
|
"eu-cyber-sol": Object.freeze({ backupEncryptionRequired: true, auditChainSignedRequired: true, tlsMinVersion: "TLSv1.3", requireVacuumAfterErase: false }),
|
|
930
952
|
"eidas-2": Object.freeze({ backupEncryptionRequired: true, auditChainSignedRequired: true, tlsMinVersion: "TLSv1.3", requireVacuumAfterErase: true }),
|
|
953
|
+
// v0.8.86 — sectoral + cybersecurity directives. DoD CMMC + FBI
|
|
954
|
+
// CJIS + healthcare regimes share an encrypted-at-rest + signed-
|
|
955
|
+
// audit-chain floor; ISO 27001/27002 + ISO 27017/27018/27701 are
|
|
956
|
+
// operator-adopted governance standards with the same baseline.
|
|
957
|
+
"cmmc-2.0": Object.freeze({ backupEncryptionRequired: true, auditChainSignedRequired: true, tlsMinVersion: "TLSv1.3", requireVacuumAfterErase: true }),
|
|
958
|
+
"cjis-v6": Object.freeze({ backupEncryptionRequired: true, auditChainSignedRequired: true, tlsMinVersion: "TLSv1.3", requireVacuumAfterErase: true }),
|
|
959
|
+
"iso-27001-2022": Object.freeze({ backupEncryptionRequired: true, auditChainSignedRequired: true, tlsMinVersion: "TLSv1.3", requireVacuumAfterErase: true }),
|
|
960
|
+
"iso-27002-2022": Object.freeze({ backupEncryptionRequired: false, auditChainSignedRequired: true, tlsMinVersion: "TLSv1.3", requireVacuumAfterErase: false }),
|
|
961
|
+
"iso-27017": Object.freeze({ backupEncryptionRequired: true, auditChainSignedRequired: true, tlsMinVersion: "TLSv1.3", requireVacuumAfterErase: true }),
|
|
962
|
+
"iso-27018": Object.freeze({ backupEncryptionRequired: true, auditChainSignedRequired: true, tlsMinVersion: "TLSv1.3", requireVacuumAfterErase: true }),
|
|
963
|
+
"iso-27701": Object.freeze({ backupEncryptionRequired: false, auditChainSignedRequired: true, tlsMinVersion: "TLSv1.3", requireVacuumAfterErase: true }),
|
|
964
|
+
"nist-800-66-r2": Object.freeze({ backupEncryptionRequired: true, auditChainSignedRequired: true, tlsMinVersion: "TLSv1.3", requireVacuumAfterErase: true }),
|
|
965
|
+
"ehds": Object.freeze({ backupEncryptionRequired: true, auditChainSignedRequired: true, tlsMinVersion: "TLSv1.3", requireVacuumAfterErase: true }),
|
|
966
|
+
"circia": Object.freeze({ backupEncryptionRequired: false, auditChainSignedRequired: true, tlsMinVersion: "TLSv1.3", requireVacuumAfterErase: false }),
|
|
931
967
|
});
|
|
932
968
|
|
|
933
969
|
/**
|
package/lib/framework-error.js
CHANGED
|
@@ -602,6 +602,23 @@ var PublicSuffixError = defineClass("PublicSuffixError", { alwaysPermane
|
|
|
602
602
|
// alwaysPermanent — every case is operator-shape or message-shape
|
|
603
603
|
// errors that retry will not recover.
|
|
604
604
|
var MailMdnError = defineClass("MailMdnError", { alwaysPermanent: true });
|
|
605
|
+
// ProblemDetailsError — b.problemDetails (lib/problem-details.js). RFC
|
|
606
|
+
// 9457 Problem Details for HTTP APIs builder + validator violations:
|
|
607
|
+
// bad opts at create/respond/validate, type/title/status/detail/
|
|
608
|
+
// instance shape mismatches, reserved-field collision in extensions,
|
|
609
|
+
// prototype-pollution-shaped extension keys, bad response object at
|
|
610
|
+
// respond(), bad inbound document shape. alwaysPermanent — every case
|
|
611
|
+
// is operator-shape or wire-shape errors that retry will not recover.
|
|
612
|
+
var ProblemDetailsError = defineClass("ProblemDetailsError", { alwaysPermanent: true });
|
|
613
|
+
// IdempotencyError — b.middleware.idempotencyKey (lib/middleware/
|
|
614
|
+
// idempotency-key.js). draft-ietf-httpapi-idempotency-key middleware
|
|
615
|
+
// violations: bad opts at create (missing store, bad ttl, bad methods
|
|
616
|
+
// list), bad idempotency key shape (non-string, too long, control
|
|
617
|
+
// chars), store-backend transport errors that exhausted retries.
|
|
618
|
+
// alwaysPermanent — every operator-facing failure is config-shape;
|
|
619
|
+
// transient store-backend failures route through audit signals so
|
|
620
|
+
// they don't escape as exceptions to the middleware caller.
|
|
621
|
+
var IdempotencyError = defineClass("IdempotencyError", { alwaysPermanent: true });
|
|
605
622
|
|
|
606
623
|
module.exports = {
|
|
607
624
|
FrameworkError: FrameworkError,
|
|
@@ -696,4 +713,6 @@ module.exports = {
|
|
|
696
713
|
FidoMds3Error: FidoMds3Error,
|
|
697
714
|
PublicSuffixError: PublicSuffixError,
|
|
698
715
|
MailMdnError: MailMdnError,
|
|
716
|
+
ProblemDetailsError: ProblemDetailsError,
|
|
717
|
+
IdempotencyError: IdempotencyError,
|
|
699
718
|
};
|