@blamejs/core 0.8.72 → 0.8.77
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 +5 -0
- package/index.js +2 -0
- package/lib/acme.js +200 -1
- package/lib/auth/oauth.js +329 -0
- package/lib/compliance-ai-act.js +161 -3
- package/lib/compliance.js +48 -0
- package/lib/config.js +90 -13
- package/lib/content-credentials.js +227 -0
- package/lib/cra-report.js +106 -2
- package/lib/crypto-field.js +5 -0
- package/lib/dsr.js +96 -0
- package/lib/mcp.js +239 -6
- package/lib/middleware/index.js +4 -0
- package/lib/middleware/protected-resource-metadata.js +165 -0
- package/lib/middleware/rate-limit.js +59 -3
- package/lib/middleware/scim-server.js +375 -0
- package/lib/middleware/security-headers.js +12 -0
- package/lib/nist-crosswalk.js +293 -0
- package/package.json +3 -3
- package/{sbom.cyclonedx.json → sbom.cdx.json} +6 -6
package/lib/cra-report.js
CHANGED
|
@@ -189,7 +189,111 @@ function create(opts) {
|
|
|
189
189
|
};
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
+
/**
|
|
193
|
+
* @primitive b.cra.conformityAssessment
|
|
194
|
+
* @signature b.cra.conformityAssessment(opts)
|
|
195
|
+
* @since 0.8.77
|
|
196
|
+
*
|
|
197
|
+
* EU Cyber Resilience Act (Regulation 2024/2847) — Annex VIII
|
|
198
|
+
* conformity-assessment dossier scaffold. Returns the structured
|
|
199
|
+
* JSON document operators submit to the notified body (Module B/C/D/H
|
|
200
|
+
* route per Annex VII) or self-attest under Annex VI (default for
|
|
201
|
+
* non-critical products). The framework auto-fills sections it can
|
|
202
|
+
* derive from the runtime — SBOM (`sbom.cdx.json` + `sbom.vendored.cdx.json`),
|
|
203
|
+
* vulnerability-handling process (CVD per RFC 9116 + SECURITY.md),
|
|
204
|
+
* security-by-design defaults (cite SECURITY.md threat-model
|
|
205
|
+
* section), end-of-life schedule (operator-supplied) — and leaves
|
|
206
|
+
* Annex I Part II essential-cybersecurity-requirements mapping for
|
|
207
|
+
* the operator to fill (it's product-specific).
|
|
208
|
+
*
|
|
209
|
+
* Enforcement: products placed on the EU market on/after 2027-12-11
|
|
210
|
+
* require a CE marking that depends on this dossier. Notified-body
|
|
211
|
+
* review takes 60-90 days for self-certifying products. Run this
|
|
212
|
+
* primitive at release time + commit the output under `compliance/cra/`.
|
|
213
|
+
*
|
|
214
|
+
* @opts
|
|
215
|
+
* {
|
|
216
|
+
* manufacturer: { name, address, contact },
|
|
217
|
+
* product: { name, identifier, version, description },
|
|
218
|
+
* classification: "default" | "important-class-I" | "important-class-II" | "critical",
|
|
219
|
+
* sbomPaths: string[], // paths to attached SBOMs
|
|
220
|
+
* supportEnd: string, // ISO date — manufacturer support cessation
|
|
221
|
+
* vulnDisclosurePolicy?: string, // URL to /.well-known/security.txt or VDP
|
|
222
|
+
* essentialReqMapping?: object, // operator-supplied Annex I Part II mapping
|
|
223
|
+
* }
|
|
224
|
+
*
|
|
225
|
+
* @example
|
|
226
|
+
* var dossier = b.cra.conformityAssessment({
|
|
227
|
+
* manufacturer: { name: "Acme Inc.", address: "1 St", contact: "ce@acme.example" },
|
|
228
|
+
* product: { name: "Widget Pro", identifier: "WID-001", version: "1.0", description: "..." },
|
|
229
|
+
* classification: "default",
|
|
230
|
+
* supportEnd: "2032-12-31",
|
|
231
|
+
* });
|
|
232
|
+
*/
|
|
233
|
+
function conformityAssessment(opts) {
|
|
234
|
+
if (!opts || typeof opts !== "object") {
|
|
235
|
+
throw new CraReportError("cra-report/bad-conformity-opts",
|
|
236
|
+
"conformityAssessment: opts required");
|
|
237
|
+
}
|
|
238
|
+
if (!opts.manufacturer || typeof opts.manufacturer.name !== "string") {
|
|
239
|
+
throw new CraReportError("cra-report/no-manufacturer",
|
|
240
|
+
"conformityAssessment: opts.manufacturer.name required");
|
|
241
|
+
}
|
|
242
|
+
if (!opts.product || typeof opts.product.name !== "string") {
|
|
243
|
+
throw new CraReportError("cra-report/no-product",
|
|
244
|
+
"conformityAssessment: opts.product.name required");
|
|
245
|
+
}
|
|
246
|
+
var classification = opts.classification || "default";
|
|
247
|
+
var validClasses = ["default", "important-class-I", "important-class-II", "critical"];
|
|
248
|
+
if (validClasses.indexOf(classification) === -1) {
|
|
249
|
+
throw new CraReportError("cra-report/bad-classification",
|
|
250
|
+
"conformityAssessment: classification must be one of " + validClasses.join(", "));
|
|
251
|
+
}
|
|
252
|
+
return {
|
|
253
|
+
"$schema": "https://blamejs.com/schema/cra-conformity-assessment-v1.json",
|
|
254
|
+
regulation: "EU 2024/2847 (Cyber Resilience Act)",
|
|
255
|
+
annex: "Annex VIII (technical documentation)",
|
|
256
|
+
generatedAt: new Date().toISOString(),
|
|
257
|
+
manufacturer: opts.manufacturer,
|
|
258
|
+
product: opts.product,
|
|
259
|
+
classification: classification,
|
|
260
|
+
assessmentRoute:
|
|
261
|
+
classification === "default" ? "Module A (Annex VI — internal control)" :
|
|
262
|
+
classification === "important-class-I" ? "Module B+C (Annex VII — EU-type examination)" :
|
|
263
|
+
classification === "important-class-II" ? "Module H (Annex VII — full quality assurance)" :
|
|
264
|
+
"Module H + notified-body for critical (Annex VII)",
|
|
265
|
+
sections: {
|
|
266
|
+
annexI_part1_essentialRequirements: {
|
|
267
|
+
status: "operator-supplied",
|
|
268
|
+
mapping: opts.essentialReqMapping || null,
|
|
269
|
+
note: "Annex I Part I essential cybersecurity requirements — operator supplies the mapping",
|
|
270
|
+
},
|
|
271
|
+
annexI_part2_vulnerabilityHandling: {
|
|
272
|
+
status: "framework-derived",
|
|
273
|
+
sbomAttached: Array.isArray(opts.sbomPaths) ? opts.sbomPaths : ["sbom.cdx.json", "sbom.vendored.cdx.json"],
|
|
274
|
+
vulnDisclosurePolicy: opts.vulnDisclosurePolicy || "https://blamejs.com/.well-known/security.txt",
|
|
275
|
+
cvdProcess: "Coordinated Vulnerability Disclosure per ISO/IEC 29147 + 30111",
|
|
276
|
+
incidentReporter: "b.cra (24h early warning + 14d intermediate + 1m final per Art 14)",
|
|
277
|
+
},
|
|
278
|
+
annexII_userInformation: {
|
|
279
|
+
status: "operator-supplied",
|
|
280
|
+
note: "Operator emits per-product handover docs",
|
|
281
|
+
},
|
|
282
|
+
supportPeriod: {
|
|
283
|
+
end: opts.supportEnd || null,
|
|
284
|
+
note: "Manufacturer support-cessation date triggers end-of-life obligations per Art 13(8)",
|
|
285
|
+
},
|
|
286
|
+
},
|
|
287
|
+
declarations: {
|
|
288
|
+
ceMarking: classification === "critical" ? "requires notified body" : "self-attest eligible",
|
|
289
|
+
eolNotification: "Manufacturer commits to 60-day pre-EOL notification per Art 13(8)",
|
|
290
|
+
vulnReporting: "Active exploitation reported within 24h to ENISA per Art 14(2)",
|
|
291
|
+
},
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
192
295
|
module.exports = {
|
|
193
|
-
create:
|
|
194
|
-
|
|
296
|
+
create: create,
|
|
297
|
+
conformityAssessment: conformityAssessment,
|
|
298
|
+
CraReportError: CraReportError,
|
|
195
299
|
};
|
package/lib/crypto-field.js
CHANGED
|
@@ -895,6 +895,11 @@ module.exports = {
|
|
|
895
895
|
getSealedFields: getSealedFields,
|
|
896
896
|
sealRow: sealRow,
|
|
897
897
|
unsealRow: unsealRow,
|
|
898
|
+
// Doc-shaped aliases — operators / tests preparing a JS document
|
|
899
|
+
// object (vs. a SQL row) reach for sealDoc / unsealDoc naming. Same
|
|
900
|
+
// function, identical shape, returns a new object (input untouched).
|
|
901
|
+
sealDoc: sealRow,
|
|
902
|
+
unsealDoc: unsealRow,
|
|
898
903
|
eraseRow: eraseRow,
|
|
899
904
|
applyPosture: applyPosture,
|
|
900
905
|
getActivePosture: getActivePosture,
|
package/lib/dsr.js
CHANGED
|
@@ -1071,6 +1071,100 @@ function dbTicketStore(opts) {
|
|
|
1071
1071
|
};
|
|
1072
1072
|
}
|
|
1073
1073
|
|
|
1074
|
+
// ---- v0.8.77 — US state-law DSR drift registry -------------------
|
|
1075
|
+
//
|
|
1076
|
+
// Each US state consumer-privacy law expresses the same DSR core
|
|
1077
|
+
// (access / deletion / correction / portability) but with per-state
|
|
1078
|
+
// drift on three knobs: cure-period (days between operator-receipt
|
|
1079
|
+
// and statutory-deadline-to-respond), profiling-opt-out
|
|
1080
|
+
// (right-to-limit-automated-decision-making variants), and minor-
|
|
1081
|
+
// consent (age threshold + opt-in vs. opt-out vs. parental-VPC).
|
|
1082
|
+
//
|
|
1083
|
+
// `b.dsr.stateRules(state)` returns the metadata; operators feed it
|
|
1084
|
+
// into their own DSR ticket-routing layer to surface "this VA
|
|
1085
|
+
// resident's correction request must be acknowledged within 45 days
|
|
1086
|
+
// with one 45-day extension".
|
|
1087
|
+
|
|
1088
|
+
// State DSR rule table — `responseDays` / `extensionDays` / `cureDays`
|
|
1089
|
+
// are integer day-counts from per-state statutes (not durations in
|
|
1090
|
+
// seconds/ms). allow:raw-time-literal — statute-defined day counts.
|
|
1091
|
+
var STATE_RULES = Object.freeze({
|
|
1092
|
+
"vcdpa": { posture: "vcdpa", state: "VA", responseDays: 45, extensionDays: 45, cureDays: 30, profilingOptOut: true, minorOptIn: 13, notes: "Cure right sunset 2025-01-01" }, // allow:raw-time-literal
|
|
1093
|
+
"co-cpa": { posture: "co-cpa", state: "CO", responseDays: 45, extensionDays: 45, cureDays: 60, profilingOptOut: true, minorOptIn: 13, notes: "Cure right sunset 2025-01-01; UOOM (GPC) mandatory" }, // allow:raw-time-literal
|
|
1094
|
+
"ctdpa": { posture: "ctdpa", state: "CT", responseDays: 45, extensionDays: 45, cureDays: 60, profilingOptOut: true, minorOptIn: 13, notes: "Cure right sunset 2025-01-01; GPC mandatory" }, // allow:raw-time-literal
|
|
1095
|
+
"ucpa": { posture: "ucpa", state: "UT", responseDays: 45, extensionDays: 45, cureDays: 30, profilingOptOut: false, minorOptIn: 13, notes: "Narrowest scope; no cure-period sunset" }, // allow:raw-time-literal
|
|
1096
|
+
"tdpsa": { posture: "tdpsa", state: "TX", responseDays: 45, extensionDays: 45, cureDays: 30, profilingOptOut: true, minorOptIn: 13, notes: "Small-business carve-out applies" }, // allow:raw-time-literal
|
|
1097
|
+
"or-cpa": { posture: "or-cpa", state: "OR", responseDays: 45, extensionDays: 45, cureDays: 60, profilingOptOut: true, minorOptIn: 13, notes: "Specific-third-party-name DSR enhancement" }, // allow:raw-time-literal
|
|
1098
|
+
"mt-cdpa": { posture: "mt-cdpa", state: "MT", responseDays: 45, extensionDays: 45, cureDays: 60, profilingOptOut: true, minorOptIn: 13, notes: "Cure period sunsets 2026-04-01" }, // allow:raw-time-literal
|
|
1099
|
+
"ia-icdpa": { posture: "ia-icdpa", state: "IA", responseDays: 90, extensionDays: 45, cureDays: 90, profilingOptOut: false, minorOptIn: null, notes: "Weakest framework — longest response, no profiling opt-out" }, // allow:raw-time-literal
|
|
1100
|
+
"in-indpa": { posture: "in-indpa", state: "IN", responseDays: 45, extensionDays: 45, cureDays: 30, profilingOptOut: true, minorOptIn: 13, notes: "Effective 2026-01-01" }, // allow:raw-time-literal
|
|
1101
|
+
"de-dpdpa": { posture: "de-dpdpa", state: "DE", responseDays: 45, extensionDays: 45, cureDays: 60, profilingOptOut: true, minorOptIn: 13, notes: "Effective 2026-01-01" }, // allow:raw-time-literal
|
|
1102
|
+
"nh-nhpa": { posture: "nh-nhpa", state: "NH", responseDays: 45, extensionDays: 45, cureDays: 60, profilingOptOut: true, minorOptIn: 13, notes: "Effective 2026-01-01; cure right sunset 2026-01-01" }, // allow:raw-time-literal
|
|
1103
|
+
"nj-njdpa": { posture: "nj-njdpa", state: "NJ", responseDays: 45, extensionDays: 45, cureDays: 30, profilingOptOut: true, minorOptIn: 17, notes: "Under-17 opt-in default" }, // allow:raw-time-literal
|
|
1104
|
+
"ky-kcdpa": { posture: "ky-kcdpa", state: "KY", responseDays: 45, extensionDays: 45, cureDays: 30, profilingOptOut: true, minorOptIn: 13, notes: "Effective 2026-01-01" }, // allow:raw-time-literal
|
|
1105
|
+
"tn-tipa": { posture: "tn-tipa", state: "TN", responseDays: 45, extensionDays: 45, cureDays: 60, profilingOptOut: true, minorOptIn: 13, notes: "NIST CSF safe-harbor available" }, // allow:raw-time-literal
|
|
1106
|
+
"mn-mncdpa": { posture: "mn-mncdpa", state: "MN", responseDays: 45, extensionDays: 45, cureDays: 30, profilingOptOut: true, minorOptIn: 13, notes: "Effective 2026-07-31; profiling opt-out for consequential decisions" }, // allow:raw-time-literal
|
|
1107
|
+
"ri-ricpa": { posture: "ri-ricpa", state: "RI", responseDays: 45, extensionDays: 45, cureDays: 0, profilingOptOut: true, minorOptIn: 13, notes: "Effective 2026-01-01; no cure period" }, // allow:raw-time-literal
|
|
1108
|
+
"ne-dpa": { posture: "ne-dpa", state: "NE", responseDays: 45, extensionDays: 45, cureDays: 30, profilingOptOut: true, minorOptIn: 13, notes: "Effective 2025-01-01" }, // allow:raw-time-literal
|
|
1109
|
+
"nv-sb370": { posture: "nv-sb370", state: "NV", responseDays: 60, extensionDays: 30, cureDays: 0, profilingOptOut: false, minorOptIn: null, notes: "Consumer-health data only" }, // allow:raw-time-literal
|
|
1110
|
+
"ca-aadc": { posture: "ca-aadc", state: "CA", responseDays: 0, extensionDays: 0, cureDays: 90, profilingOptOut: true, minorOptIn: 18, notes: "Under-18 default-high-privacy; partial preliminary injunction NetChoice v. Bonta" }, // allow:raw-time-literal
|
|
1111
|
+
"ct-sb3": { posture: "ct-sb3", state: "CT", responseDays: 45, extensionDays: 45, cureDays: 60, profilingOptOut: false, minorOptIn: null, notes: "Consumer-health data only" }, // allow:raw-time-literal
|
|
1112
|
+
"tx-cubi": { posture: "tx-cubi", state: "TX", responseDays: 0, extensionDays: 0, cureDays: 0, profilingOptOut: false, minorOptIn: null, notes: "Biometric-only; private-right-of-action absent" }, // allow:raw-time-literal
|
|
1113
|
+
"modpa": { posture: "modpa", state: "MD", responseDays: 45, extensionDays: 45, cureDays: 60, profilingOptOut: true, minorOptIn: 13, notes: "Strict data-minimization; effective 2026-10-01" }, // allow:raw-time-literal
|
|
1114
|
+
"quebec-25": { posture: "quebec-25", state: "QC", responseDays: 30, extensionDays: 30, cureDays: 0, profilingOptOut: true, minorOptIn: 14, notes: "DPIA + automated-decision opt-out; FR-language obligations" }, // allow:raw-time-literal
|
|
1115
|
+
});
|
|
1116
|
+
|
|
1117
|
+
/**
|
|
1118
|
+
* @primitive b.dsr.stateRules
|
|
1119
|
+
* @signature b.dsr.stateRules(state)
|
|
1120
|
+
* @since 0.8.77
|
|
1121
|
+
* @related b.compliance.describe
|
|
1122
|
+
*
|
|
1123
|
+
* Returns per-state DSR rules: response window, extension period,
|
|
1124
|
+
* cure period (statutory grace before enforcement attaches),
|
|
1125
|
+
* profiling-opt-out availability, and minor-consent age threshold.
|
|
1126
|
+
* `state` accepts either the posture name (`"vcdpa"`) or the
|
|
1127
|
+
* 2-letter state abbreviation (`"VA"`). Returns null when unknown.
|
|
1128
|
+
*
|
|
1129
|
+
* @example
|
|
1130
|
+
* var rules = b.dsr.stateRules("vcdpa");
|
|
1131
|
+
* // rules.responseDays → 45
|
|
1132
|
+
* // rules.cureDays → 30
|
|
1133
|
+
* // rules.profilingOptOut → true
|
|
1134
|
+
*/
|
|
1135
|
+
function stateRules(state) {
|
|
1136
|
+
if (typeof state !== "string" || state.length === 0) return null;
|
|
1137
|
+
// Direct posture-name lookup first
|
|
1138
|
+
if (STATE_RULES[state]) return Object.assign({}, STATE_RULES[state]);
|
|
1139
|
+
// 2-letter state abbreviation lookup (case-insensitive)
|
|
1140
|
+
var u = state.toUpperCase();
|
|
1141
|
+
var keys = Object.keys(STATE_RULES);
|
|
1142
|
+
for (var i = 0; i < keys.length; i++) {
|
|
1143
|
+
if (STATE_RULES[keys[i]].state === u) {
|
|
1144
|
+
return Object.assign({}, STATE_RULES[keys[i]]);
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
return null;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
/**
|
|
1151
|
+
* @primitive b.dsr.listStateRules
|
|
1152
|
+
* @signature b.dsr.listStateRules()
|
|
1153
|
+
* @since 0.8.77
|
|
1154
|
+
*
|
|
1155
|
+
* Returns every state-rule entry as an array (useful for admin UI
|
|
1156
|
+
* cure-period dashboards / operator-facing matrices).
|
|
1157
|
+
*
|
|
1158
|
+
* @example
|
|
1159
|
+
* var all = b.dsr.listStateRules();
|
|
1160
|
+
* // → [{ posture: "vcdpa", state: "VA", responseDays: 45, ... }, ...]
|
|
1161
|
+
*/
|
|
1162
|
+
function listStateRules() {
|
|
1163
|
+
return Object.keys(STATE_RULES).map(function (k) {
|
|
1164
|
+
return Object.assign({}, STATE_RULES[k]);
|
|
1165
|
+
});
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1074
1168
|
module.exports = {
|
|
1075
1169
|
create: create,
|
|
1076
1170
|
memoryTicketStore: memoryTicketStore,
|
|
@@ -1080,5 +1174,7 @@ module.exports = {
|
|
|
1080
1174
|
VALID_VERIFICATION_LEVELS: VALID_VERIFICATION_LEVELS,
|
|
1081
1175
|
TYPE_MIN_VERIFICATION: TYPE_MIN_VERIFICATION,
|
|
1082
1176
|
POSTURE_DEADLINE_MS: POSTURE_DEADLINE_MS,
|
|
1177
|
+
stateRules: stateRules,
|
|
1178
|
+
listStateRules: listStateRules,
|
|
1083
1179
|
DsrError: DsrError,
|
|
1084
1180
|
};
|
package/lib/mcp.js
CHANGED
|
@@ -694,11 +694,244 @@ function _validateToolInput(toolName, input, schema) {
|
|
|
694
694
|
return input;
|
|
695
695
|
}
|
|
696
696
|
|
|
697
|
+
// ---- MCP 2025-11-25 spec — sampling / elicitation / protocol version ----
|
|
698
|
+
|
|
699
|
+
var MCP_PROTOCOL_VERSIONS_ACCEPTED = ["2024-11-05", "2025-03-26", "2025-06-18", "2025-11-25"];
|
|
700
|
+
|
|
701
|
+
/**
|
|
702
|
+
* @primitive b.mcp.assertProtocolVersion
|
|
703
|
+
* @signature b.mcp.assertProtocolVersion(req, opts?)
|
|
704
|
+
* @since 0.8.77
|
|
705
|
+
* @related b.mcp.serverGuard
|
|
706
|
+
*
|
|
707
|
+
* MCP 2025-11-25 spec §4.1 — every HTTP request after `initialize`
|
|
708
|
+
* MUST carry an `MCP-Protocol-Version` header naming a version the
|
|
709
|
+
* server supports. Returns the resolved version on success; throws
|
|
710
|
+
* with a tagged refusal when the header is missing OR names an
|
|
711
|
+
* unsupported version. Clients pre-negotiation (before `initialize`)
|
|
712
|
+
* may omit the header — the resolved value is `null` in that case.
|
|
713
|
+
*
|
|
714
|
+
* @opts
|
|
715
|
+
* {
|
|
716
|
+
* accepted?: string[], // override the default acceptance set
|
|
717
|
+
* allowMissing?: boolean, // true → return null when header absent
|
|
718
|
+
* }
|
|
719
|
+
*
|
|
720
|
+
* @example
|
|
721
|
+
* var version = b.mcp.assertProtocolVersion(req, { allowMissing: false });
|
|
722
|
+
* // throws if missing/unsupported; returns e.g. "2025-11-25" on success.
|
|
723
|
+
*/
|
|
724
|
+
function _assertProtocolVersion(req, opts) {
|
|
725
|
+
opts = opts || {};
|
|
726
|
+
var accepted = Array.isArray(opts.accepted) && opts.accepted.length > 0
|
|
727
|
+
? opts.accepted : MCP_PROTOCOL_VERSIONS_ACCEPTED;
|
|
728
|
+
var hdr = req && req.headers && req.headers["mcp-protocol-version"];
|
|
729
|
+
if (typeof hdr !== "string" || hdr.length === 0) {
|
|
730
|
+
if (opts.allowMissing === true) return null;
|
|
731
|
+
throw new McpError("mcp/missing-protocol-version",
|
|
732
|
+
"assertProtocolVersion: request missing MCP-Protocol-Version header " +
|
|
733
|
+
"(MCP 2025-11-25 §4.1 requires it on every post-initialize request)");
|
|
734
|
+
}
|
|
735
|
+
if (accepted.indexOf(hdr) === -1) {
|
|
736
|
+
throw new McpError("mcp/unsupported-protocol-version",
|
|
737
|
+
"assertProtocolVersion: '" + hdr + "' not in accepted set: " +
|
|
738
|
+
accepted.join(", "));
|
|
739
|
+
}
|
|
740
|
+
return hdr;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
var SAMPLING_DEFAULTS = {
|
|
744
|
+
maxRequestsPerSession: 10,
|
|
745
|
+
maxMessagesPerRequest: 20,
|
|
746
|
+
maxTokensPerRequest: 4096, // allow:raw-byte-literal — LLM token count, not bytes
|
|
747
|
+
allowedModelHint: null, // null = allow all
|
|
748
|
+
refuseStopSequences: false,
|
|
749
|
+
};
|
|
750
|
+
|
|
751
|
+
/**
|
|
752
|
+
* @primitive b.mcp.sampling.guard
|
|
753
|
+
* @signature b.mcp.sampling.guard(opts?)
|
|
754
|
+
* @since 0.8.77
|
|
755
|
+
* @related b.mcp.toolResult.sanitize
|
|
756
|
+
*
|
|
757
|
+
* MCP server-initiated `sampling/createMessage` gate — the highest-
|
|
758
|
+
* risk surface in the protocol. A compromised tool can issue
|
|
759
|
+
* `sampling/createMessage` to make the host model emit attacker-
|
|
760
|
+
* chosen text. This primitive returns a guard function the operator
|
|
761
|
+
* wraps around the sampling endpoint that refuses requests violating
|
|
762
|
+
* size caps, allow-listed models, or budget-per-session.
|
|
763
|
+
*
|
|
764
|
+
* Returns `{ enforce(samplingRequest, sessionId), reset(sessionId) }`.
|
|
765
|
+
* `enforce` throws on violation; the operator wraps the actual model
|
|
766
|
+
* call only after `enforce` returns.
|
|
767
|
+
*
|
|
768
|
+
* @opts
|
|
769
|
+
* {
|
|
770
|
+
* maxRequestsPerSession?: number, // default 10
|
|
771
|
+
* maxMessagesPerRequest?: number, // default 20
|
|
772
|
+
* maxTokensPerRequest?: number, // default 4096
|
|
773
|
+
* allowedModelHints?: string[], // null → allow all
|
|
774
|
+
* refuseStopSequences?: boolean, // refuse client-supplied stop sequences
|
|
775
|
+
* }
|
|
776
|
+
*
|
|
777
|
+
* @example
|
|
778
|
+
* var guard = b.mcp.sampling.guard({ maxRequestsPerSession: 5 });
|
|
779
|
+
* server.on("sampling/createMessage", function (req, sid) {
|
|
780
|
+
* guard.enforce(req, sid); // throws on violation
|
|
781
|
+
* return invokeModel(req);
|
|
782
|
+
* });
|
|
783
|
+
*/
|
|
784
|
+
function _samplingGuard(opts) {
|
|
785
|
+
opts = opts || {};
|
|
786
|
+
var maxReq = opts.maxRequestsPerSession || SAMPLING_DEFAULTS.maxRequestsPerSession;
|
|
787
|
+
var maxMsg = opts.maxMessagesPerRequest || SAMPLING_DEFAULTS.maxMessagesPerRequest;
|
|
788
|
+
var maxTokens = opts.maxTokensPerRequest || SAMPLING_DEFAULTS.maxTokensPerRequest;
|
|
789
|
+
var allowedModels = Array.isArray(opts.allowedModelHints) ? opts.allowedModelHints.slice() : null;
|
|
790
|
+
var refuseStop = opts.refuseStopSequences === true;
|
|
791
|
+
var sessionCounts = new Map();
|
|
792
|
+
|
|
793
|
+
function enforce(samplingRequest, sessionId) {
|
|
794
|
+
if (!samplingRequest || typeof samplingRequest !== "object") {
|
|
795
|
+
throw new McpError("mcp/sampling-bad-request",
|
|
796
|
+
"sampling.guard: request must be an object");
|
|
797
|
+
}
|
|
798
|
+
var sid = sessionId || "_anonymous";
|
|
799
|
+
var n = (sessionCounts.get(sid) || 0) + 1;
|
|
800
|
+
if (n > maxReq) {
|
|
801
|
+
throw new McpError("mcp/sampling-session-budget-exceeded",
|
|
802
|
+
"sampling.guard: session '" + sid + "' exceeded " + maxReq + " sampling requests");
|
|
803
|
+
}
|
|
804
|
+
sessionCounts.set(sid, n);
|
|
805
|
+
var messages = samplingRequest.messages;
|
|
806
|
+
if (!Array.isArray(messages) || messages.length === 0) {
|
|
807
|
+
throw new McpError("mcp/sampling-no-messages",
|
|
808
|
+
"sampling.guard: request.messages must be a non-empty array");
|
|
809
|
+
}
|
|
810
|
+
if (messages.length > maxMsg) {
|
|
811
|
+
throw new McpError("mcp/sampling-too-many-messages",
|
|
812
|
+
"sampling.guard: " + messages.length + " messages > maxMessagesPerRequest=" + maxMsg);
|
|
813
|
+
}
|
|
814
|
+
if (typeof samplingRequest.maxTokens === "number" && samplingRequest.maxTokens > maxTokens) {
|
|
815
|
+
throw new McpError("mcp/sampling-too-many-tokens",
|
|
816
|
+
"sampling.guard: requested maxTokens " + samplingRequest.maxTokens +
|
|
817
|
+
" > cap " + maxTokens);
|
|
818
|
+
}
|
|
819
|
+
if (refuseStop && samplingRequest.stopSequences) {
|
|
820
|
+
throw new McpError("mcp/sampling-stop-sequences-refused",
|
|
821
|
+
"sampling.guard: client-supplied stopSequences refused by policy");
|
|
822
|
+
}
|
|
823
|
+
if (allowedModels && samplingRequest.modelPreferences &&
|
|
824
|
+
samplingRequest.modelPreferences.hints) {
|
|
825
|
+
var hints = samplingRequest.modelPreferences.hints;
|
|
826
|
+
if (Array.isArray(hints)) {
|
|
827
|
+
hints.forEach(function (h, i) {
|
|
828
|
+
if (h && typeof h.name === "string" && allowedModels.indexOf(h.name) === -1) {
|
|
829
|
+
throw new McpError("mcp/sampling-model-not-allowed",
|
|
830
|
+
"sampling.guard: modelPreferences.hints[" + i + "].name='" + h.name +
|
|
831
|
+
"' not in allowedModelHints: " + allowedModels.join(", "));
|
|
832
|
+
}
|
|
833
|
+
});
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
function reset(sessionId) {
|
|
839
|
+
if (sessionId) sessionCounts.delete(sessionId);
|
|
840
|
+
else sessionCounts.clear();
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
return { enforce: enforce, reset: reset };
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
/**
|
|
847
|
+
* @primitive b.mcp.elicitation.guard
|
|
848
|
+
* @signature b.mcp.elicitation.guard(opts?)
|
|
849
|
+
* @since 0.8.77
|
|
850
|
+
* @related b.mcp.sampling.guard
|
|
851
|
+
*
|
|
852
|
+
* MCP 2025-11-25 `elicitation/create` gate — server-initiated user
|
|
853
|
+
* prompt requests. Refuses prompts whose `message` contains
|
|
854
|
+
* prompt-injection markers OR `requestedSchema` shape is missing.
|
|
855
|
+
* The risk class is symmetric to `sampling`: a compromised tool can
|
|
856
|
+
* elicit credentials / approval-text from the user. This guard
|
|
857
|
+
* applies the same prompt-injection scan `toolResult.sanitize` does,
|
|
858
|
+
* plus an allow-listed `requestedSchema.type` set.
|
|
859
|
+
*
|
|
860
|
+
* @opts
|
|
861
|
+
* {
|
|
862
|
+
* maxMessageBytes?: number, // default 8 KiB
|
|
863
|
+
* allowedSchemaTypes?: string[], // default ["object"]
|
|
864
|
+
* posture?: "refuse" | "sanitize" | "audit-only",
|
|
865
|
+
* }
|
|
866
|
+
*
|
|
867
|
+
* @example
|
|
868
|
+
* var guard = b.mcp.elicitation.guard({ posture: "refuse" });
|
|
869
|
+
* guard.enforce({
|
|
870
|
+
* message: "What's your name?",
|
|
871
|
+
* requestedSchema: { type: "object", properties: { name: { type: "string" } } },
|
|
872
|
+
* });
|
|
873
|
+
*/
|
|
874
|
+
function _elicitationGuard(opts) {
|
|
875
|
+
opts = opts || {};
|
|
876
|
+
var maxBytes = opts.maxMessageBytes || (8 * 1024); // allow:raw-byte-literal — 8 KiB elicitation message cap
|
|
877
|
+
var allowedSchemaTypes = Array.isArray(opts.allowedSchemaTypes) && opts.allowedSchemaTypes.length > 0
|
|
878
|
+
? opts.allowedSchemaTypes : ["object"];
|
|
879
|
+
var posture = opts.posture || "refuse";
|
|
880
|
+
|
|
881
|
+
function enforce(elicitRequest) {
|
|
882
|
+
if (!elicitRequest || typeof elicitRequest !== "object") {
|
|
883
|
+
throw new McpError("mcp/elicitation-bad-request",
|
|
884
|
+
"elicitation.guard: request must be an object");
|
|
885
|
+
}
|
|
886
|
+
var message = elicitRequest.message;
|
|
887
|
+
if (typeof message !== "string" || message.length === 0) {
|
|
888
|
+
throw new McpError("mcp/elicitation-no-message",
|
|
889
|
+
"elicitation.guard: request.message must be a non-empty string");
|
|
890
|
+
}
|
|
891
|
+
if (Buffer.byteLength(message, "utf8") > maxBytes) {
|
|
892
|
+
throw new McpError("mcp/elicitation-message-too-large",
|
|
893
|
+
"elicitation.guard: message exceeds " + maxBytes + " bytes");
|
|
894
|
+
}
|
|
895
|
+
var schema = elicitRequest.requestedSchema;
|
|
896
|
+
if (!schema || typeof schema !== "object") {
|
|
897
|
+
throw new McpError("mcp/elicitation-no-schema",
|
|
898
|
+
"elicitation.guard: request.requestedSchema must be an object");
|
|
899
|
+
}
|
|
900
|
+
if (allowedSchemaTypes.indexOf(schema.type) === -1) {
|
|
901
|
+
throw new McpError("mcp/elicitation-bad-schema-type",
|
|
902
|
+
"elicitation.guard: requestedSchema.type '" + schema.type +
|
|
903
|
+
"' not in allowed: " + allowedSchemaTypes.join(", "));
|
|
904
|
+
}
|
|
905
|
+
// Prompt-injection scan over the prompt-to-user message.
|
|
906
|
+
var regexInput = Buffer.byteLength(message, "utf8") > maxBytes
|
|
907
|
+
? Buffer.from(message, "utf8").subarray(0, maxBytes).toString("utf8")
|
|
908
|
+
: message;
|
|
909
|
+
if (INJECTION_RE.test(regexInput)) { // allow:regex-no-length-cap regexInput byteLength bounded above
|
|
910
|
+
if (posture === "refuse") {
|
|
911
|
+
throw new McpError("mcp/elicitation-injection-refused",
|
|
912
|
+
"elicitation.guard: message contains prompt-injection markers");
|
|
913
|
+
}
|
|
914
|
+
if (posture === "sanitize") {
|
|
915
|
+
return Object.assign({}, elicitRequest, {
|
|
916
|
+
message: message.replace(INJECTION_RE, "[REDACTED]"),
|
|
917
|
+
});
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
return elicitRequest;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
return { enforce: enforce };
|
|
924
|
+
}
|
|
925
|
+
|
|
697
926
|
module.exports = {
|
|
698
|
-
serverGuard:
|
|
699
|
-
parseRequest:
|
|
700
|
-
refuse:
|
|
701
|
-
toolResult:
|
|
702
|
-
capability:
|
|
703
|
-
validateToolInput:
|
|
927
|
+
serverGuard: serverGuard,
|
|
928
|
+
parseRequest: parseRequest,
|
|
929
|
+
refuse: refuse,
|
|
930
|
+
toolResult: { sanitize: _toolResultSanitize },
|
|
931
|
+
capability: { create: _capabilityCreate },
|
|
932
|
+
validateToolInput: _validateToolInput,
|
|
933
|
+
assertProtocolVersion: _assertProtocolVersion,
|
|
934
|
+
sampling: { guard: _samplingGuard },
|
|
935
|
+
elicitation: { guard: _elicitationGuard },
|
|
936
|
+
MCP_PROTOCOL_VERSIONS_ACCEPTED: MCP_PROTOCOL_VERSIONS_ACCEPTED,
|
|
704
937
|
};
|
package/lib/middleware/index.js
CHANGED
|
@@ -64,6 +64,8 @@ var traceLogCorrelation = require("./trace-log-correlation");
|
|
|
64
64
|
var tracePropagate = require("./trace-propagate");
|
|
65
65
|
var tusUpload = require("./tus-upload");
|
|
66
66
|
var webAppManifest = require("./web-app-manifest");
|
|
67
|
+
var protectedResourceMetadata = require("./protected-resource-metadata");
|
|
68
|
+
var scimServer = require("./scim-server");
|
|
67
69
|
|
|
68
70
|
module.exports = {
|
|
69
71
|
requestId: requestId.create,
|
|
@@ -114,6 +116,8 @@ module.exports = {
|
|
|
114
116
|
clearSiteData: clearSiteData.create,
|
|
115
117
|
nel: nel.create,
|
|
116
118
|
speculationRules: speculationRules.create,
|
|
119
|
+
protectedResourceMetadata: protectedResourceMetadata.create,
|
|
120
|
+
scimServer: scimServer.create,
|
|
117
121
|
|
|
118
122
|
// Module exports for advanced use (constants, raw factory access)
|
|
119
123
|
_modules: {
|