@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/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: {
@@ -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
- middleware.close = function () { return backend.close && backend.close(); };
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,