@blamejs/core 0.8.76 → 0.8.78
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 +2 -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 +129 -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 +1 -1
- package/sbom.cdx.json +6 -6
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: {
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @module b.middleware.protectedResourceMetadata
|
|
4
|
+
* @nav Identity & access
|
|
5
|
+
* @title Protected Resource Metadata
|
|
6
|
+
* @order 210
|
|
7
|
+
* @slug protected-resource-metadata
|
|
8
|
+
*
|
|
9
|
+
* @intro
|
|
10
|
+
* draft-ietf-oauth-resource-metadata: serves the
|
|
11
|
+
* `/.well-known/oauth-protected-resource` document so RFC 9728
|
|
12
|
+
* clients can auto-discover which authorization servers issue
|
|
13
|
+
* tokens for this resource, what scopes the resource accepts,
|
|
14
|
+
* what dpop algorithms the resource verifies, and which bearer-
|
|
15
|
+
* method binding (DPoP / mTLS cnf claim) is required. Pairs with
|
|
16
|
+
* b.middleware.bearerAuth so a 401 from the protected resource
|
|
17
|
+
* includes `WWW-Authenticate: Bearer resource_metadata=<url>` and
|
|
18
|
+
* the client can self-rediscover.
|
|
19
|
+
*
|
|
20
|
+
* @card
|
|
21
|
+
* `.well-known/oauth-protected-resource` discovery endpoint per
|
|
22
|
+
* draft-ietf-oauth-resource-metadata. Closes the gap that previously
|
|
23
|
+
* forced operators to hand-write the JSON.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
var framework_error = require("../framework-error");
|
|
27
|
+
var validateOpts = require("../validate-opts");
|
|
28
|
+
var requestHelpers = require("../request-helpers");
|
|
29
|
+
|
|
30
|
+
var H = requestHelpers.HTTP_STATUS;
|
|
31
|
+
|
|
32
|
+
var ProtectedResourceMetadataError = framework_error.defineClass(
|
|
33
|
+
"ProtectedResourceMetadataError",
|
|
34
|
+
"middleware/protected-resource-metadata"
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
var ALLOWED_BEARER_METHODS = ["header", "body", "query"];
|
|
38
|
+
var ALLOWED_DPOP_ALGS = ["ES256", "ES384", "RS256", "PS256", "EdDSA", "ML-DSA-65", "ML-DSA-87"];
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @primitive b.middleware.protectedResourceMetadata
|
|
42
|
+
* @signature b.middleware.protectedResourceMetadata(opts)
|
|
43
|
+
* @since 0.8.77
|
|
44
|
+
* @related b.auth.oauth.introspectToken, b.middleware.bearerAuth
|
|
45
|
+
*
|
|
46
|
+
* Returns a request middleware that serves the protected-resource
|
|
47
|
+
* metadata JSON document at `/.well-known/oauth-protected-resource`
|
|
48
|
+
* (or operator-overridden path).
|
|
49
|
+
*
|
|
50
|
+
* @opts
|
|
51
|
+
* {
|
|
52
|
+
* resource: string, // canonical resource URI (required)
|
|
53
|
+
* authorizationServers: string[], // issuer URLs that mint tokens for this resource (required, ≥1)
|
|
54
|
+
* scopesSupported?: string[],
|
|
55
|
+
* bearerMethodsSupported?: ("header"|"body"|"query")[], // default ["header"]
|
|
56
|
+
* resourceSigningAlgValuesSupported?: string[], // for signed introspection / jwt-secured responses
|
|
57
|
+
* resourceDocumentation?: string, // URL to operator docs
|
|
58
|
+
* resourcePolicyUri?: string,
|
|
59
|
+
* resourceTosUri?: string,
|
|
60
|
+
* dpopSigningAlgValuesSupported?: string[],
|
|
61
|
+
* dpopBoundAccessTokensRequired?: boolean,
|
|
62
|
+
* mtlsBoundAccessTokensRequired?: boolean,
|
|
63
|
+
* path?: string, // default "/.well-known/oauth-protected-resource"
|
|
64
|
+
* }
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* var mw = b.middleware.protectedResourceMetadata({
|
|
68
|
+
* resource: "https://api.example.com",
|
|
69
|
+
* authorizationServers: ["https://idp.example.com"],
|
|
70
|
+
* scopesSupported: ["read", "write"],
|
|
71
|
+
* dpopBoundAccessTokensRequired: true,
|
|
72
|
+
* });
|
|
73
|
+
* app.use(mw);
|
|
74
|
+
*/
|
|
75
|
+
function create(opts) {
|
|
76
|
+
validateOpts.requireObject(opts, "middleware.protectedResourceMetadata",
|
|
77
|
+
ProtectedResourceMetadataError, "middleware/protected-resource-metadata/bad-opts");
|
|
78
|
+
validateOpts.requireNonEmptyString(opts.resource, "resource",
|
|
79
|
+
ProtectedResourceMetadataError, "middleware/protected-resource-metadata/no-resource");
|
|
80
|
+
|
|
81
|
+
if (!Array.isArray(opts.authorizationServers) || opts.authorizationServers.length === 0) {
|
|
82
|
+
throw new ProtectedResourceMetadataError(
|
|
83
|
+
"middleware/protected-resource-metadata/no-as",
|
|
84
|
+
"authorizationServers must be a non-empty array of issuer URLs");
|
|
85
|
+
}
|
|
86
|
+
opts.authorizationServers.forEach(function (u, i) {
|
|
87
|
+
if (typeof u !== "string" || u.length === 0) {
|
|
88
|
+
throw new ProtectedResourceMetadataError(
|
|
89
|
+
"middleware/protected-resource-metadata/bad-as",
|
|
90
|
+
"authorizationServers[" + i + "] must be a non-empty string");
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
var bearerMethods = opts.bearerMethodsSupported || ["header"];
|
|
95
|
+
bearerMethods.forEach(function (m, i) {
|
|
96
|
+
if (ALLOWED_BEARER_METHODS.indexOf(m) === -1) {
|
|
97
|
+
throw new ProtectedResourceMetadataError(
|
|
98
|
+
"middleware/protected-resource-metadata/bad-bearer-method",
|
|
99
|
+
"bearerMethodsSupported[" + i + "] must be one of: " + ALLOWED_BEARER_METHODS.join(", "));
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
if (opts.dpopSigningAlgValuesSupported) {
|
|
104
|
+
opts.dpopSigningAlgValuesSupported.forEach(function (a, i) {
|
|
105
|
+
if (ALLOWED_DPOP_ALGS.indexOf(a) === -1) {
|
|
106
|
+
throw new ProtectedResourceMetadataError(
|
|
107
|
+
"middleware/protected-resource-metadata/bad-dpop-alg",
|
|
108
|
+
"dpopSigningAlgValuesSupported[" + i + "] = '" + a +
|
|
109
|
+
"' not in allowlist: " + ALLOWED_DPOP_ALGS.join(", "));
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
var path = opts.path || "/.well-known/oauth-protected-resource";
|
|
115
|
+
|
|
116
|
+
var doc = {
|
|
117
|
+
resource: opts.resource,
|
|
118
|
+
authorization_servers: opts.authorizationServers,
|
|
119
|
+
bearer_methods_supported: bearerMethods,
|
|
120
|
+
};
|
|
121
|
+
if (opts.scopesSupported) doc.scopes_supported = opts.scopesSupported;
|
|
122
|
+
if (opts.resourceSigningAlgValuesSupported) doc.resource_signing_alg_values_supported = opts.resourceSigningAlgValuesSupported;
|
|
123
|
+
if (opts.resourceDocumentation) doc.resource_documentation = opts.resourceDocumentation;
|
|
124
|
+
if (opts.resourcePolicyUri) doc.resource_policy_uri = opts.resourcePolicyUri;
|
|
125
|
+
if (opts.resourceTosUri) doc.resource_tos_uri = opts.resourceTosUri;
|
|
126
|
+
if (opts.dpopSigningAlgValuesSupported) doc.dpop_signing_alg_values_supported = opts.dpopSigningAlgValuesSupported;
|
|
127
|
+
if (opts.dpopBoundAccessTokensRequired === true) doc.dpop_bound_access_tokens_required = true;
|
|
128
|
+
if (opts.mtlsBoundAccessTokensRequired === true) doc.tls_client_certificate_bound_access_tokens = true;
|
|
129
|
+
|
|
130
|
+
var bodyText = JSON.stringify(doc);
|
|
131
|
+
var bodyBytes = Buffer.byteLength(bodyText, "utf8");
|
|
132
|
+
|
|
133
|
+
function middleware(req, res, next) {
|
|
134
|
+
if (req.url !== path && req.url.split("?")[0] !== path) {
|
|
135
|
+
next();
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
139
|
+
res.writeHead(H.METHOD_NOT_ALLOWED, {
|
|
140
|
+
"Allow": "GET, HEAD",
|
|
141
|
+
"Cache-Control": "no-store",
|
|
142
|
+
});
|
|
143
|
+
res.end();
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
res.writeHead(H.OK, {
|
|
147
|
+
"Content-Type": "application/json",
|
|
148
|
+
"Content-Length": String(bodyBytes),
|
|
149
|
+
"Cache-Control": "public, max-age=3600",
|
|
150
|
+
});
|
|
151
|
+
if (req.method === "HEAD") { res.end(); return; }
|
|
152
|
+
res.end(bodyText);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
middleware.document = doc;
|
|
156
|
+
middleware.path = path;
|
|
157
|
+
return middleware;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
module.exports = {
|
|
161
|
+
create: create,
|
|
162
|
+
ProtectedResourceMetadataError: ProtectedResourceMetadataError,
|
|
163
|
+
ALLOWED_BEARER_METHODS: ALLOWED_BEARER_METHODS,
|
|
164
|
+
ALLOWED_DPOP_ALGS: ALLOWED_DPOP_ALGS,
|
|
165
|
+
};
|
|
@@ -159,12 +159,16 @@ function _memoryTokenBucketBackend(opts) {
|
|
|
159
159
|
buckets.delete(key);
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
+
function resetAll() {
|
|
163
|
+
buckets.clear();
|
|
164
|
+
}
|
|
165
|
+
|
|
162
166
|
function close() {
|
|
163
167
|
try { gcInterval.stop(); } catch (_e) { /* timer already stopped */ }
|
|
164
168
|
buckets.clear();
|
|
165
169
|
}
|
|
166
170
|
|
|
167
|
-
return { take: take, reset: reset, close: close };
|
|
171
|
+
return { take: take, reset: reset, resetAll: resetAll, close: close };
|
|
168
172
|
}
|
|
169
173
|
|
|
170
174
|
// Fixed-window in-memory algorithm — per-key counter that resets at the
|
|
@@ -224,12 +228,14 @@ function _memoryFixedWindowBackend(opts) {
|
|
|
224
228
|
|
|
225
229
|
function reset(key) { counters.delete(key); }
|
|
226
230
|
|
|
231
|
+
function resetAll() { counters.clear(); }
|
|
232
|
+
|
|
227
233
|
function close() {
|
|
228
234
|
try { gcInterval.stop(); } catch (_e) { /* timer already stopped */ }
|
|
229
235
|
counters.clear();
|
|
230
236
|
}
|
|
231
237
|
|
|
232
|
-
return { take: take, reset: reset, close: close };
|
|
238
|
+
return { take: take, reset: reset, resetAll: resetAll, close: close };
|
|
233
239
|
}
|
|
234
240
|
|
|
235
241
|
// ---- Cluster backend (fixed-window counter, SQL-backed) ----
|
|
@@ -485,13 +491,63 @@ function create(opts) {
|
|
|
485
491
|
|
|
486
492
|
// Expose a couple of operator hooks on the middleware function.
|
|
487
493
|
middleware.reset = function (key) { return backend.reset(key); };
|
|
488
|
-
|
|
494
|
+
// Global drop-all for the in-memory backend. Used by incident-
|
|
495
|
+
// response workflows ("operator confirmed false-positive lockout
|
|
496
|
+
// wave, drop the whole table") + by test suites that need a clean
|
|
497
|
+
// slate between cases without re-creating the middleware. For the
|
|
498
|
+
// cluster backend this is a no-op (cluster backends are
|
|
499
|
+
// multi-process and require operator-side coordination — flushing
|
|
500
|
+
// a shared row table from one replica races every other replica's
|
|
501
|
+
// in-flight take() calls).
|
|
502
|
+
middleware.resetAll = function () {
|
|
503
|
+
if (typeof backend.resetAll === "function") return backend.resetAll();
|
|
504
|
+
return null;
|
|
505
|
+
};
|
|
506
|
+
middleware.close = function () {
|
|
507
|
+
_instances.delete(middleware);
|
|
508
|
+
return backend.close && backend.close();
|
|
509
|
+
};
|
|
489
510
|
|
|
511
|
+
_instances.add(middleware);
|
|
490
512
|
return middleware;
|
|
491
513
|
}
|
|
492
514
|
|
|
515
|
+
// Module-level registry of every rate-limit middleware in the running
|
|
516
|
+
// process. Operators reach for this during incident response: when a
|
|
517
|
+
// false-positive lockout wave hits, an oncall script can iterate
|
|
518
|
+
// `instances()` and call `.resetAll()` on each, without having to
|
|
519
|
+
// thread a reference to every rate-limit middleware through wherever
|
|
520
|
+
// the response code runs. Tests use it to assert a clean slate.
|
|
521
|
+
//
|
|
522
|
+
// Lifetime: a middleware joins on `create()` return and leaves on
|
|
523
|
+
// `middleware.close()`. Long-lived servers create rate-limiters once at
|
|
524
|
+
// boot; throwaway middlewares (tests, sandboxes) must close() to
|
|
525
|
+
// deregister. We deliberately don't use WeakRef here — operators want
|
|
526
|
+
// strong, observable membership ("did this rate-limiter actually get
|
|
527
|
+
// created?"), and the count is bounded by how many limiters an app
|
|
528
|
+
// configures, not how much traffic it sees.
|
|
529
|
+
var _instances = new Set();
|
|
530
|
+
|
|
531
|
+
function instances() {
|
|
532
|
+
return Array.from(_instances);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// Global drop-all across every middleware in the process. Returns the
|
|
536
|
+
// number of instances that responded to resetAll (cluster-backed
|
|
537
|
+
// middlewares no-op their own resetAll but still count toward the
|
|
538
|
+
// total so operators see all instances were addressed).
|
|
539
|
+
function resetAll() {
|
|
540
|
+
var n = 0;
|
|
541
|
+
_instances.forEach(function (m) {
|
|
542
|
+
try { m.resetAll(); n += 1; } catch (_e) { /* best-effort */ }
|
|
543
|
+
});
|
|
544
|
+
return n;
|
|
545
|
+
}
|
|
546
|
+
|
|
493
547
|
module.exports = {
|
|
494
548
|
create: create,
|
|
549
|
+
instances: instances,
|
|
550
|
+
resetAll: resetAll,
|
|
495
551
|
// Backends exported for tests + advanced operator wiring.
|
|
496
552
|
_memoryBackend: _memoryBackend,
|
|
497
553
|
_memoryTokenBucketBackend: _memoryTokenBucketBackend,
|