@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/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: create,
194
- CraReportError: CraReportError,
296
+ create: create,
297
+ conformityAssessment: conformityAssessment,
298
+ CraReportError: CraReportError,
195
299
  };
@@ -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: serverGuard,
699
- parseRequest: parseRequest,
700
- refuse: refuse,
701
- toolResult: { sanitize: _toolResultSanitize },
702
- capability: { create: _capabilityCreate },
703
- validateToolInput: _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
  };
@@ -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: {