@orkestrel/mcp 0.0.26 → 0.0.27

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.
@@ -4,10 +4,10 @@ let _src_core = require("../core/index.cjs");
4
4
  let _orkestrel_contract = require("@orkestrel/contract");
5
5
  let _orkestrel_server = require("@orkestrel/server");
6
6
  let _orkestrel_emitter = require("@orkestrel/emitter");
7
+ let _orkestrel_websocket = require("@orkestrel/websocket");
7
8
  let node_crypto = require("node:crypto");
8
9
  let node_http = require("node:http");
9
10
  let node_https = require("node:https");
10
- let _orkestrel_websocket = require("@orkestrel/websocket");
11
11
  let _orkestrel_process_server = require("@orkestrel/process/server");
12
12
  let _orkestrel_process = require("@orkestrel/process");
13
13
  let node_stream = require("node:stream");
@@ -527,13 +527,51 @@ function bridgeMessageTransport(transport) {
527
527
  //#endregion
528
528
  //#region src/server/inferers.ts
529
529
  /**
530
+ * Infers the target one modern request's `Mcp-Name` header must carry.
531
+ *
532
+ * @remarks
533
+ * The protocol scopes the header to the methods whose body carries a name-shaped field, and
534
+ * names the field per method: `tools/call` and `prompts/get` carry `params.name`, and
535
+ * `resources/read` carries `params.uri`. Every other method — `server/discover`, `tools/list`,
536
+ * `resources/list`, `prompts/list` — has nothing to derive a target from, so the header is not
537
+ * required there and a peer that sent one anyway is not held to it.
538
+ *
539
+ * A method within the scope whose named member is absent or is not a string reads as no
540
+ * target. There is nothing for a header to match, and refusing the request over a body member
541
+ * the header rule does not own would report a parameter fault as a header fault. Total.
542
+ *
543
+ * @param request - The parsed modern invocation to read the target from
544
+ * @returns The target the header must carry, or `undefined` when the method carries none
545
+ *
546
+ * @example
547
+ * ```ts
548
+ * inferHeaderTarget({ jsonrpc: '2.0', id: 1, method: 'resources/read', params: { uri: 'file:///a' } })
549
+ * // → 'file:///a'
550
+ * ```
551
+ */
552
+ function inferHeaderTarget(request) {
553
+ if (request.method === "tools/call" || request.method === "prompts/get") {
554
+ const name = request.params?.["name"];
555
+ return (0, _orkestrel_contract.isString)(name) ? name : void 0;
556
+ }
557
+ if (request.method === "resources/read") {
558
+ const uri = request.params?.["uri"];
559
+ return (0, _orkestrel_contract.isString)(uri) ? uri : void 0;
560
+ }
561
+ }
562
+ /**
530
563
  * Infers the first required MCP HTTP header that is missing or mismatched.
531
564
  *
532
565
  * @remarks
533
- * A modern request derives its protocol, method, and tools/call-only name expectations from
534
- * the JSON-RPC body. A legacy request body requires a protocol header after initialization,
535
- * while a supplied legacy session version additionally diagnoses a header that disagrees with
536
- * the active session. Messages name the expected value but never echo the client-supplied one.
566
+ * A modern request derives its protocol, method, and name expectations from the JSON-RPC body,
567
+ * the name expectation scoped to the methods {@link inferHeaderTarget} reads a target for. A
568
+ * name header carrying the Base64 sentinel is decoded through
569
+ * {@link import('@orkestrel/mcp').decodeSentinel} before the comparison, so a peer that had
570
+ * to encode its value still matches; a sentinel whose payload is invalid decodes to nothing
571
+ * and therefore mismatches, which is how an invalid header value is refused. A legacy request
572
+ * body requires a protocol header after initialization, while a supplied legacy session
573
+ * version additionally diagnoses a header that disagrees with the active session. Messages
574
+ * name the expected value but never echo the client-supplied one.
537
575
  *
538
576
  * @param request - The HTTP request carrying the headers
539
577
  * @param reference - The parsed invocation body, or the active legacy session version
@@ -592,22 +630,73 @@ function inferHeaderIssue(request, reference) {
592
630
  reason: "mismatched",
593
631
  message: `Mcp-Method header does not match the request body method '${message.method}'.`
594
632
  };
595
- if (message.method !== "tools/call") return void 0;
596
- const name = message.params?.["name"];
597
- if (!(0, _orkestrel_contract.isString)(name)) return void 0;
633
+ const target = inferHeaderTarget(message);
634
+ if (target === void 0) return void 0;
598
635
  const header = request.headers.get(MCP_NAME_HEADER);
599
636
  if (header === null) return {
600
637
  header: "Mcp-Name",
601
638
  reason: "missing",
602
- message: `Required Mcp-Name header is missing; the request body tool name is '${name}'.`
639
+ message: `Required Mcp-Name header is missing; the request body target is '${target}'.`
603
640
  };
604
- if (header !== name) return {
641
+ if ((0, _src_core.decodeSentinel)(header) !== target) return {
605
642
  header: "Mcp-Name",
606
643
  reason: "mismatched",
607
- message: `Mcp-Name header does not match the request body tool name '${name}'.`
644
+ message: `Mcp-Name header does not match the request body target '${target}'.`
608
645
  };
609
646
  }
610
647
  /**
648
+ * Infers the refusal one `tools/call` earns for a `Mcp-Param-*` header the body contradicts.
649
+ *
650
+ * @remarks
651
+ * The custom-header half of the standard-header seam {@link inferHeaderIssue} owns, and it
652
+ * takes the SERVED definition's projections rather than a header issue: SEP-2243 scopes the
653
+ * rule to the `Mcp-Param-*` names the server's OWN tool definitions annotate, so a name no
654
+ * parameter claims is another party's header and travels through untouched.
655
+ *
656
+ * For each recognized parameter the body's value at the parameter's own property path fixes
657
+ * the expectation. A value the call omits or supplies as `null` requires no header, and a
658
+ * header sent anyway is refused because it asserts something the body never said. A value the
659
+ * call does supply requires its header: an absent one, a Base64 sentinel whose payload is
660
+ * invalid, and a decoded value that disagrees are each refused. An `integer` parameter
661
+ * compares numerically, so a peer that padded its decimal still matches. A supplied value
662
+ * whose runtime shape contradicts the declared type is left alone — the tool's own argument
663
+ * validation owns that disagreement, and refusing it here would report an argument fault as a
664
+ * header fault.
665
+ *
666
+ * Messages name the field and the body path the expectation came from, and never echo the
667
+ * value the peer supplied.
668
+ *
669
+ * @param request - The HTTP request carrying the headers
670
+ * @param parameters - The projections the served tool definition declares
671
+ * @param values - The call's `arguments` record
672
+ * @returns The refusal message for the first disagreeing parameter, or `undefined`
673
+ *
674
+ * @example
675
+ * ```ts
676
+ * inferParameterRefusal(request, [{ name: 'Region', path: ['region'], primitive: 'string' }], {})
677
+ * // → undefined when the request carries no `Mcp-Param-Region` either
678
+ * ```
679
+ */
680
+ function inferParameterRefusal(request, parameters, values) {
681
+ for (const parameter of parameters) {
682
+ let carried = values;
683
+ for (const key of parameter.path) carried = (0, _orkestrel_contract.isRecord)(carried) ? carried[key] : void 0;
684
+ const field = `${_src_core.MCP_PARAM_PREFIX}${parameter.name}`;
685
+ const path = parameter.path.join(".");
686
+ const header = request.headers.get(field);
687
+ if (carried === void 0 || carried === null) {
688
+ if (header === null) continue;
689
+ return `${field} header carries a value the request body omits at '${path}'.`;
690
+ }
691
+ const expected = (0, _src_core.renderHeaderValue)(carried, parameter.primitive);
692
+ if (expected === void 0) continue;
693
+ if (header === null) return `Required ${field} header is missing; the request body carries '${path}'.`;
694
+ const decoded = (0, _src_core.decodeSentinel)(header);
695
+ if (decoded === void 0) return `${field} header value is not a valid Base64 sentinel.`;
696
+ if (!(parameter.primitive === "integer" ? decoded.trim() !== "" && Number(decoded) === Number(expected) : decoded === expected)) return `${field} header does not match the request body value at '${path}'.`;
697
+ }
698
+ }
699
+ /**
611
700
  * Infers the legacy revision an `initialize` request negotiates.
612
701
  *
613
702
  * @remarks
@@ -785,12 +874,17 @@ var HTTPDisconnect = class {
785
874
  * Creates the Streamable-HTTP POST handler used by `createMCPRoutes`.
786
875
  *
787
876
  * @remarks
788
- * Modern requests require matching protocol/method headers and a matching name header only
789
- * for `tools/call`; mismatch returns HTTP `400` + `-32020`. Headerless `initialize` is
790
- * accepted, while every other headerless request needs a live legacy session to supply its
791
- * pinned version. A legacy-shaped request carrying a protocol header is admitted only for a
792
- * legacy revision; any other value, the modern revision included, returns HTTP `400` + `-32022`
793
- * whose `supported` names the legacy revisions this door accepts. A present origin must occur in `origin.origins` unless validation is
877
+ * Modern requests require matching protocol/method headers and a matching name header on each
878
+ * method carrying a named target — `tools/call` and `prompts/get` against `params.name`,
879
+ * `resources/read` against `params.uri` with a Base64-sentinel value decoded before the
880
+ * comparison; a missing, mismatched, or invalidly encoded value returns HTTP `400` + `-32020`.
881
+ * A protocol header naming a MODERN revision holds the request to that revision whatever shape
882
+ * its body arrived in, so a body with no parsable modern `_meta` returns HTTP `400` + `-32602`.
883
+ * Headerless `initialize` is accepted, while every other headerless request needs a live legacy
884
+ * session to supply its pinned version. A legacy-shaped request carrying a protocol header is
885
+ * otherwise admitted only for a legacy revision; a revision this server does not implement
886
+ * returns HTTP `400` + `-32022` whose `supported` names the legacy revisions this door accepts.
887
+ * A present origin must occur in `origin.origins` unless validation is
794
888
  * explicitly delegated upstream. Modern dispatch errors use their protocol status map; legacy
795
889
  * errors remain in-band at HTTP `200`. A streamed response composes the fetch-standard request
796
890
  * signal with response-body cancellation and supplies the result to every dispatched modern
@@ -839,11 +933,42 @@ function createMCPPostHandler(mcp, options) {
839
933
  const era = (0, _src_core.isModernRequest)(invocation) ? "modern" : "legacy";
840
934
  const id = invocation.id;
841
935
  const protocol = request.headers.get(MCP_PROTOCOL_VERSION_HEADER);
842
- if (era === "modern") {
936
+ if (era === "modern" || (0, _src_core.isMCPModernVersion)(protocol)) {
843
937
  if ((0, _src_core.parseRequestContext)(invocation) === void 0) return Response.json((0, _src_core.buildJSONRPCError)(id, _src_core.JSONRPC_INVALID_PARAMS, "Invalid params: malformed modern request metadata"), { status: 400 });
844
938
  }
845
939
  const issue = inferHeaderIssue(request, invocation);
846
940
  if (issue !== void 0) return Response.json((0, _src_core.buildJSONRPCError)(id, _src_core.MCP_HEADER_MISMATCH, issue.message), { status: 400 });
941
+ const called = invocation.params?.["name"];
942
+ if (era === "modern" && invocation.method === "tools/call" && (0, _orkestrel_contract.isString)(called)) {
943
+ let parameters = [];
944
+ let cursor = void 0;
945
+ for (let page = 0; page < _src_core.MCP_LOOKUP_PAGES; page += 1) {
946
+ const answer = await mcp.dispatch({
947
+ jsonrpc: "2.0",
948
+ id: 0,
949
+ method: "tools/list",
950
+ params: {
951
+ _meta: invocation.params?.["_meta"],
952
+ ...cursor === void 0 ? {} : { cursor }
953
+ }
954
+ });
955
+ if (Symbol.asyncIterator in answer) {
956
+ answer.stop();
957
+ break;
958
+ }
959
+ const schema = (0, _src_core.extractToolSchema)(answer, called);
960
+ if (schema !== void 0) {
961
+ parameters = (0, _src_core.buildHeaderParameters)(schema) ?? [];
962
+ break;
963
+ }
964
+ const listing = answer.result;
965
+ const next = (0, _orkestrel_contract.isRecord)(listing) ? listing["nextCursor"] : void 0;
966
+ if (!(0, _orkestrel_contract.isString)(next)) break;
967
+ cursor = next;
968
+ }
969
+ const refusal = inferParameterRefusal(request, parameters, invocation.params?.["arguments"]);
970
+ if (refusal !== void 0) return Response.json((0, _src_core.buildJSONRPCError)(id, _src_core.MCP_HEADER_MISMATCH, refusal), { status: 400 });
971
+ }
847
972
  if (era === "legacy") {
848
973
  if (protocol !== null && !(0, _src_core.isMCPLegacyVersion)(protocol)) return Response.json((0, _src_core.buildJSONRPCError)(id, _src_core.MCP_UNSUPPORTED_VERSION, `Unsupported MCP protocol version '${protocol}'`, {
849
974
  supported: _src_core.SUPPORTED_LEGACY_PROTOCOL_VERSIONS,
@@ -904,7 +1029,8 @@ function createMCPPostHandler(mcp, options) {
904
1029
  * initialize result's `protocolVersion` is likewise captured, but only
905
1030
  * when it is a SUPPORTED value, and echoed as `mcp-protocol-version` alone on
906
1031
  * subsequent legacy requests. Modern requests instead derive protocol and method
907
- * headers from the message, plus the name header only for `tools/call`.
1032
+ * headers from the message, plus the name header only for `tools/call` — carried in the
1033
+ * protocol's Base64 sentinel form whenever the tool name cannot ride as plain ASCII.
908
1034
  * Before initialize returns, neither captured legacy header is sent.
909
1035
  * `close()` clears the captured protocol so a reconnect's `initialize`
910
1036
  * POST is headerless; the captured `session` persists across `close()`.
@@ -935,8 +1061,11 @@ var HTTPClientTransport = class {
935
1061
  #fetch;
936
1062
  #timeout;
937
1063
  #pending = /* @__PURE__ */ new Set();
1064
+ #parameters = /* @__PURE__ */ new Map();
1065
+ #stamps = /* @__PURE__ */ new WeakMap();
938
1066
  #session = void 0;
939
1067
  #protocol = void 0;
1068
+ #generation = 0;
940
1069
  #closed = false;
941
1070
  constructor(options) {
942
1071
  this.#emitter = new _orkestrel_emitter.Emitter();
@@ -958,6 +1087,7 @@ var HTTPClientTransport = class {
958
1087
  this.#closed = false;
959
1088
  }
960
1089
  async send(message) {
1090
+ this.#stamp(message);
961
1091
  const request = new AbortController();
962
1092
  this.#pending.add(request);
963
1093
  try {
@@ -966,6 +1096,11 @@ var HTTPClientTransport = class {
966
1096
  this.#pending.delete(request);
967
1097
  }
968
1098
  }
1099
+ #stamp(message) {
1100
+ if (!(0, _src_core.isModernRequest)(message) || message.method !== "tools/list") return;
1101
+ if (message.params?.["cursor"] === void 0) this.#generation += 1;
1102
+ this.#stamps.set(message, this.#generation);
1103
+ }
969
1104
  async #exchange(message, signal) {
970
1105
  let response;
971
1106
  try {
@@ -987,7 +1122,7 @@ var HTTPClientTransport = class {
987
1122
  }
988
1123
  const session = response.headers.get(MCP_SESSION_HEADER);
989
1124
  if (session !== null) this.#session = session;
990
- await this.#deliver(response);
1125
+ await this.#deliver(response, message);
991
1126
  }
992
1127
  async close() {
993
1128
  if (this.#closed) return;
@@ -1004,12 +1139,15 @@ var HTTPClientTransport = class {
1004
1139
  return {
1005
1140
  ...version === void 0 ? {} : { [MCP_PROTOCOL_VERSION_HEADER]: version },
1006
1141
  [MCP_METHOD_HEADER]: message.method,
1007
- ...message.method === "tools/call" && (0, _orkestrel_contract.isString)(name) ? { [MCP_NAME_HEADER]: name } : {}
1142
+ ...message.method === "tools/call" && (0, _orkestrel_contract.isString)(name) ? {
1143
+ [MCP_NAME_HEADER]: (0, _src_core.encodeSentinel)(name),
1144
+ ...(0, _src_core.buildHeaderProjection)(this.#parameters.get(name) ?? [], message.params?.["arguments"])
1145
+ } : {}
1008
1146
  };
1009
1147
  }
1010
1148
  return this.#protocol === void 0 ? {} : { [MCP_PROTOCOL_VERSION_HEADER]: this.#protocol };
1011
1149
  }
1012
- async #deliver(response) {
1150
+ async #deliver(response, sent) {
1013
1151
  if (response.status === 202) return;
1014
1152
  const type = response.headers.get("content-type") ?? "";
1015
1153
  let messages = [];
@@ -1023,13 +1161,43 @@ var HTTPClientTransport = class {
1023
1161
  } catch (error) {
1024
1162
  failure = { error };
1025
1163
  }
1026
- for (const message of messages) this.#capture(message);
1164
+ for (const message of messages) this.#capture(message, sent);
1027
1165
  if (!response.ok && messages.length === 0) throw buildResponseError(response, type);
1028
1166
  if (failure !== void 0) this.#emitter.emit("error", failure.error);
1029
1167
  }
1030
- #capture(message) {
1168
+ #capture(message, sent) {
1031
1169
  if ((0, _src_core.isJSONRPCResponse)(message) && (0, _orkestrel_contract.isRecord)(message.result) && (0, _src_core.isMCPVersion)(message.result["protocolVersion"])) this.#protocol = message.result["protocolVersion"];
1032
- this.#emitter.emit("message", message);
1170
+ this.#emitter.emit("message", this.#select(message, sent));
1171
+ }
1172
+ #select(message, sent) {
1173
+ if (!(0, _src_core.isModernRequest)(sent) || sent.method !== "tools/list") return message;
1174
+ if (!(0, _src_core.isJSONRPCResponse)(message) || message.error !== void 0) return message;
1175
+ const result = message.result;
1176
+ const listed = (0, _orkestrel_contract.isRecord)(result) ? result["tools"] : void 0;
1177
+ if (!(0, _orkestrel_contract.isRecord)(result) || !(0, _orkestrel_contract.isArray)(listed)) return message;
1178
+ const current = this.#stamps.get(sent) === this.#generation;
1179
+ if (current && sent.params?.["cursor"] === void 0) this.#parameters.clear();
1180
+ const kept = [];
1181
+ for (const tool of listed) {
1182
+ if (!(0, _orkestrel_contract.isRecord)(tool) || !(0, _orkestrel_contract.isString)(tool["name"])) {
1183
+ kept.push(tool);
1184
+ continue;
1185
+ }
1186
+ const parameters = (0, _src_core.buildHeaderParameters)(tool["inputSchema"]);
1187
+ if (parameters === void 0) {
1188
+ this.#emitter.emit("error", /* @__PURE__ */ new Error(`MCP tool '${tool["name"]}' is excluded from tools/list: its inputSchema carries an invalid x-mcp-header annotation`));
1189
+ continue;
1190
+ }
1191
+ if (current) this.#parameters.set(tool["name"], parameters);
1192
+ kept.push(tool);
1193
+ }
1194
+ return {
1195
+ ...message,
1196
+ result: {
1197
+ ...result,
1198
+ tools: kept
1199
+ }
1200
+ };
1033
1201
  }
1034
1202
  };
1035
1203
  //#endregion
@@ -1173,8 +1341,13 @@ var MCPSession = class {
1173
1341
  * a non-JSON or non-message frame is surfaced on `error` and DROPPED, never thrown. It
1174
1342
  * also bridges the socket's `close` → this transport's `close`, and the socket's `error`.
1175
1343
  * - **Outbound (`send`).** `send(message)` writes one text frame
1176
- * (`nodeWs.send(JSON.stringify(message))`); the underlying wrapper no-ops a write on a
1177
- * non-open socket, so a closed connection drops silently rather than throwing.
1344
+ * (`nodeWs.send(JSON.stringify(message))`). The underlying wrapper no-ops a write on a
1345
+ * non-open socket and confirms nothing, so this bridge answers a closed channel from its own
1346
+ * state and the socket's `readyState`: a `send` after `close()`, after the peer's close, or on
1347
+ * a socket that is not `OPEN` REJECTS with `WebSocket transport is not connected` rather than
1348
+ * resolving on a frame nobody wrote. `bindServer` catches that rejection and routes it to the
1349
+ * dispatcher's `error` event, and it aborts every in-flight request the moment this transport's
1350
+ * `close` fires — so a peer that disconnects mid-request is answered by no write at all.
1178
1351
  * - **`close()`** removes the subscriptions `start()` installed on the socket, closes the
1179
1352
  * underlying socket (the RFC 6455 close handshake), and fires the transport's `close` event
1180
1353
  * (idempotent — a second `close`, or a socket-driven close, emits once). A frame that arrives
@@ -1211,6 +1384,7 @@ var WebSocketServerTransport = class {
1211
1384
  this.#socket.emitter.on("error", this.#failure);
1212
1385
  }
1213
1386
  async send(message) {
1387
+ if (this.#closed || this.#socket.readyState !== _orkestrel_websocket.WEBSOCKET_READY_OPEN) throw new Error("WebSocket transport is not connected");
1214
1388
  this.#socket.send(JSON.stringify(message));
1215
1389
  }
1216
1390
  async close() {
@@ -1277,10 +1451,12 @@ var WebSocketServerTransport = class {
1277
1451
  * non-JSON / non-message frame surfaces on `error` and is dropped. The socket's `close`
1278
1452
  * / `error` bridge to this transport's events.
1279
1453
  * - **Outbound (`send`).** `send(message)` writes one masked text frame. A socket write is not
1280
- * confirmed, so this transport answers a closed channel from its OWN state: a `send` with no
1281
- * bound socket — before `start()`, after `close()`, or after the peer ended the socket —
1282
- * REJECTS with `WebSocket transport is not connected`. It neither drops the message (the
1283
- * browser face's posture) nor queues it for a connection this transport is not holding.
1454
+ * confirmed, so this transport answers a closed channel from its own state AND the socket's
1455
+ * `readyState`: a `send` with no bound socket — before `start()`, after `close()`, or after the
1456
+ * peer ended the socket — and a `send` on a bound socket that is not `OPEN` both REJECT with
1457
+ * `WebSocket transport is not connected`. It neither drops the message nor queues it for a
1458
+ * connection this transport is not holding — the browser face queues a pre-open send, and this
1459
+ * one, holding no connection to flush it onto, rejects that too.
1284
1460
  * - **`close()`** unsubscribes from the socket, closes it, and fires `close` (idempotent). An
1285
1461
  * upgrade still on the wire is DESTROYED, so a `close()` during the handshake ends the
1286
1462
  * transport at once instead of waiting for a peer that may never answer — the suspended
@@ -1332,7 +1508,7 @@ var WebSocketClientTransport = class {
1332
1508
  }
1333
1509
  async send(message) {
1334
1510
  const socket = this.#socket;
1335
- if (socket === void 0) throw new Error("WebSocket transport is not connected");
1511
+ if (socket === void 0 || socket.readyState !== _orkestrel_websocket.WEBSOCKET_READY_OPEN) throw new Error("WebSocket transport is not connected");
1336
1512
  socket.send(JSON.stringify(message));
1337
1513
  }
1338
1514
  async close() {
@@ -2032,7 +2208,8 @@ function createStdioClientTransport(options) {
2032
2208
  * @param mcp - The transport-agnostic {@link MCPDispatcherInterface} to expose over stdio
2033
2209
  * @param options - Optional injectable `input` / `output` streams; see
2034
2210
  * {@link StdioServerOptions}
2035
- * @returns A `{ start(): void; stop(): void }` handle to arm / tear down the pump
2211
+ * @returns A {@link StdioServerInterface} handle to arm / tear down the pump; `stop()` ends
2212
+ * that handle's lifetime permanently
2036
2213
  *
2037
2214
  * @example
2038
2215
  * ```ts
@@ -2271,7 +2448,9 @@ exports.decodeEvent = decodeEvent;
2271
2448
  exports.dispatchLines = dispatchLines;
2272
2449
  exports.extractLines = extractLines;
2273
2450
  exports.inferHeaderIssue = inferHeaderIssue;
2451
+ exports.inferHeaderTarget = inferHeaderTarget;
2274
2452
  exports.inferLegacyVersion = inferLegacyVersion;
2453
+ exports.inferParameterRefusal = inferParameterRefusal;
2275
2454
  exports.inferStatus = inferStatus;
2276
2455
  exports.readEventStream = readEventStream;
2277
2456
  exports.readLastEventId = readLastEventId;