@blamejs/core 0.8.83 → 0.8.87
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/index.js +7 -0
- package/lib/a2a-tasks.js +598 -0
- package/lib/a2a.js +10 -0
- package/lib/audit.js +1 -0
- package/lib/auth/fal.js +210 -0
- package/lib/cache-status.js +288 -0
- package/lib/compliance.js +36 -0
- package/lib/framework-error.js +19 -0
- package/lib/mail.js +84 -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/network-dns.js +39 -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/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
|
|
package/lib/auth/fal.js
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @module b.auth.fal
|
|
4
|
+
* @nav Identity & Access
|
|
5
|
+
* @title NIST 800-63-4 FAL Classifier
|
|
6
|
+
* @order 120
|
|
7
|
+
*
|
|
8
|
+
* @intro
|
|
9
|
+
* NIST SP 800-63-4 Federation Assurance Levels — FAL1 / FAL2 /
|
|
10
|
+
* FAL3. While AAL describes the rigor of authentication (what the
|
|
11
|
+
* user did to prove they are who they say they are), FAL describes
|
|
12
|
+
* the rigor of the FEDERATION assertion that carried that
|
|
13
|
+
* authentication from the IdP to the RP.
|
|
14
|
+
*
|
|
15
|
+
* FAL bands per NIST 800-63C-4:
|
|
16
|
+
*
|
|
17
|
+
* FAL1: Bearer assertion delivered through the front channel
|
|
18
|
+
* (typical OIDC ID token over the browser redirect).
|
|
19
|
+
* Signed by the IdP; verified by the RP. No audience
|
|
20
|
+
* binding beyond the standard `aud` claim.
|
|
21
|
+
*
|
|
22
|
+
* FAL2: Bearer assertion delivered through the back channel
|
|
23
|
+
* OR front-channel assertion that is encrypted to the RP.
|
|
24
|
+
* Replay-protection nonce required. Typical OIDC
|
|
25
|
+
* Authorization Code Flow with mTLS or DPoP-bound token.
|
|
26
|
+
*
|
|
27
|
+
* FAL3: Holder-of-Key assertion. RP verifies the subject
|
|
28
|
+
* cryptographically holds a key bound to the assertion
|
|
29
|
+
* (mTLS client-cert pinned to the subject, DPoP-bound +
|
|
30
|
+
* audience-restricted, OR SAML HoK SubjectConfirmation).
|
|
31
|
+
* Defeats stolen-bearer-token replay.
|
|
32
|
+
*
|
|
33
|
+
* Operators classify the FAL of an incoming federation assertion
|
|
34
|
+
* via `fromAssertion(opts)` — pass the assertion's properties
|
|
35
|
+
* (channel, encrypted, hokBinding, etc.) and get back the band.
|
|
36
|
+
* Compose with `b.middleware.requireFal({ minimum: "FAL2" })` for
|
|
37
|
+
* the gate.
|
|
38
|
+
*
|
|
39
|
+
* @card
|
|
40
|
+
* NIST 800-63-4 Federation Assurance Level classifier — describes the rigor of the federation assertion (FAL1 bearer / FAL2 encrypted-or-back-channel / FAL3 Holder-of-Key) carried from IdP to RP.
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
var validateOpts = require("../validate-opts");
|
|
44
|
+
var { AuthError } = require("../framework-error");
|
|
45
|
+
|
|
46
|
+
var FAL1 = "FAL1";
|
|
47
|
+
var FAL2 = "FAL2";
|
|
48
|
+
var FAL3 = "FAL3";
|
|
49
|
+
|
|
50
|
+
var BANDS = Object.freeze([FAL1, FAL2, FAL3]);
|
|
51
|
+
|
|
52
|
+
function _bandRank(band) {
|
|
53
|
+
if (band === FAL1) return 1;
|
|
54
|
+
if (band === FAL2) return 2;
|
|
55
|
+
if (band === FAL3) return 3;
|
|
56
|
+
return 0;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @primitive b.auth.fal.isValidBand
|
|
61
|
+
* @signature b.auth.fal.isValidBand(band)
|
|
62
|
+
* @since 0.8.87
|
|
63
|
+
* @status stable
|
|
64
|
+
*
|
|
65
|
+
* Predicate returning `true` when `band` is one of the documented
|
|
66
|
+
* FAL band strings (`"FAL1"` / `"FAL2"` / `"FAL3"`).
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* b.auth.fal.isValidBand("FAL2"); // → true
|
|
70
|
+
* b.auth.fal.isValidBand("FALX"); // → false
|
|
71
|
+
*/
|
|
72
|
+
function isValidBand(band) {
|
|
73
|
+
return _bandRank(band) > 0;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* @primitive b.auth.fal.meets
|
|
78
|
+
* @signature b.auth.fal.meets(actualBand, requiredBand)
|
|
79
|
+
* @since 0.8.87
|
|
80
|
+
* @status stable
|
|
81
|
+
*
|
|
82
|
+
* Predicate returning `true` when `actualBand` satisfies the
|
|
83
|
+
* `requiredBand` floor (FAL3 ≥ FAL2 ≥ FAL1). Invalid band strings
|
|
84
|
+
* on either argument return `false`.
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* b.auth.fal.meets("FAL3", "FAL2"); // → true
|
|
88
|
+
* b.auth.fal.meets("FAL1", "FAL2"); // → false
|
|
89
|
+
*/
|
|
90
|
+
function meets(actualBand, requiredBand) {
|
|
91
|
+
return _bandRank(actualBand) >= _bandRank(requiredBand);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* @primitive b.auth.fal.fromAssertion
|
|
96
|
+
* @signature b.auth.fal.fromAssertion(opts)
|
|
97
|
+
* @since 0.8.87
|
|
98
|
+
* @status stable
|
|
99
|
+
*
|
|
100
|
+
* Classify an incoming federation assertion's FAL band per NIST
|
|
101
|
+
* 800-63C-4. Returns one of `"FAL1"` / `"FAL2"` / `"FAL3"`. Throws
|
|
102
|
+
* `auth/bad-fal-opts` on missing required fields.
|
|
103
|
+
*
|
|
104
|
+
* - HoK binding (mTLS client-cert pinned, DPoP-bound, SAML HoK) → FAL3
|
|
105
|
+
* - Back-channel delivery OR encrypted-to-RP front-channel +
|
|
106
|
+
* replay-protection nonce → FAL2
|
|
107
|
+
* - Anything else → FAL1
|
|
108
|
+
*
|
|
109
|
+
* The classifier is conservative: missing replay-protection on a
|
|
110
|
+
* back-channel assertion downgrades to FAL1 because §5.2 requires
|
|
111
|
+
* nonce / jti binding before back-channel can claim FAL2.
|
|
112
|
+
*
|
|
113
|
+
* @opts
|
|
114
|
+
* channel: "front" | "back", // REQUIRED
|
|
115
|
+
* encrypted: boolean, // assertion encrypted to RP
|
|
116
|
+
* replayProtected: boolean, // nonce / jti / iat binding present
|
|
117
|
+
* hokBinding: "mtls" | "dpop" | "saml-hok" | null,
|
|
118
|
+
* // proof-of-possession binding present
|
|
119
|
+
* bearerOnly: boolean, // alias for hokBinding === null
|
|
120
|
+
*
|
|
121
|
+
* @example
|
|
122
|
+
* var fal = b.auth.fal.fromAssertion({
|
|
123
|
+
* channel: "back",
|
|
124
|
+
* encrypted: false,
|
|
125
|
+
* replayProtected: true,
|
|
126
|
+
* hokBinding: null,
|
|
127
|
+
* });
|
|
128
|
+
* // → "FAL2"
|
|
129
|
+
*
|
|
130
|
+
* var fal3 = b.auth.fal.fromAssertion({
|
|
131
|
+
* channel: "back",
|
|
132
|
+
* hokBinding: "mtls",
|
|
133
|
+
* replayProtected: true,
|
|
134
|
+
* });
|
|
135
|
+
* // → "FAL3"
|
|
136
|
+
*/
|
|
137
|
+
function fromAssertion(opts) {
|
|
138
|
+
if (!opts || typeof opts !== "object") {
|
|
139
|
+
throw new AuthError("auth/bad-fal-opts",
|
|
140
|
+
"fal.fromAssertion: opts required (channel + replayProtected at minimum)");
|
|
141
|
+
}
|
|
142
|
+
if (opts.channel !== "front" && opts.channel !== "back") {
|
|
143
|
+
throw new AuthError("auth/bad-fal-opts",
|
|
144
|
+
"fal.fromAssertion: channel must be 'front' or 'back'");
|
|
145
|
+
}
|
|
146
|
+
var hokBinding = opts.hokBinding;
|
|
147
|
+
if (hokBinding !== undefined && hokBinding !== null) {
|
|
148
|
+
if (hokBinding !== "mtls" && hokBinding !== "dpop" && hokBinding !== "saml-hok") {
|
|
149
|
+
throw new AuthError("auth/bad-fal-opts",
|
|
150
|
+
"fal.fromAssertion: hokBinding must be 'mtls' | 'dpop' | 'saml-hok' | null");
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// FAL3 — Holder-of-Key with replay protection.
|
|
155
|
+
if (hokBinding && opts.replayProtected === true) {
|
|
156
|
+
return FAL3;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// FAL2 — back-channel OR encrypted front-channel, with replay protection.
|
|
160
|
+
var replaySafe = opts.replayProtected === true;
|
|
161
|
+
if (replaySafe && (opts.channel === "back" || opts.encrypted === true)) {
|
|
162
|
+
return FAL2;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Everything else — FAL1 (bearer front-channel).
|
|
166
|
+
return FAL1;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* @primitive b.auth.fal.requireFal
|
|
171
|
+
* @signature b.auth.fal.requireFal(minimumBand)
|
|
172
|
+
* @since 0.8.87
|
|
173
|
+
* @status stable
|
|
174
|
+
* @related b.auth.fal.fromAssertion
|
|
175
|
+
*
|
|
176
|
+
* Build a guard that throws `auth/fal-insufficient` when the
|
|
177
|
+
* supplied band is below the minimum. The middleware form
|
|
178
|
+
* (`b.middleware.requireFal`) wraps this guard at the request layer.
|
|
179
|
+
*
|
|
180
|
+
* @example
|
|
181
|
+
* var fal3Only = b.auth.fal.requireFal("FAL3");
|
|
182
|
+
* fal3Only(req.session.federationFal);
|
|
183
|
+
* // throws auth/fal-insufficient if not FAL3
|
|
184
|
+
*/
|
|
185
|
+
function requireFal(minimumBand) {
|
|
186
|
+
validateOpts.requireNonEmptyString(
|
|
187
|
+
minimumBand, "fal.requireFal.minimumBand", AuthError, "auth/bad-fal-band");
|
|
188
|
+
if (!isValidBand(minimumBand)) {
|
|
189
|
+
throw new AuthError("auth/bad-fal-band",
|
|
190
|
+
"fal.requireFal: minimumBand must be one of " + BANDS.join(", "));
|
|
191
|
+
}
|
|
192
|
+
return function falGuard(actualBand) {
|
|
193
|
+
if (!isValidBand(actualBand) || !meets(actualBand, minimumBand)) {
|
|
194
|
+
throw new AuthError("auth/fal-insufficient",
|
|
195
|
+
"fal.requireFal: actual band '" + actualBand + "' does not meet minimum '" + minimumBand + "'");
|
|
196
|
+
}
|
|
197
|
+
return actualBand;
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
module.exports = {
|
|
202
|
+
FAL1: FAL1,
|
|
203
|
+
FAL2: FAL2,
|
|
204
|
+
FAL3: FAL3,
|
|
205
|
+
BANDS: BANDS,
|
|
206
|
+
isValidBand: isValidBand,
|
|
207
|
+
meets: meets,
|
|
208
|
+
fromAssertion: fromAssertion,
|
|
209
|
+
requireFal: requireFal,
|
|
210
|
+
};
|
|
@@ -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
|
};
|