@fitzzero/quickdraw-core 3.6.0 → 3.8.0

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.
Files changed (37) hide show
  1. package/README.md +218 -64
  2. package/dist/client/index.d.ts +34 -4
  3. package/dist/client/index.js +57 -65
  4. package/dist/client/index.js.map +1 -1
  5. package/dist/client/testing.d.ts +1 -1
  6. package/dist/client/testing.js.map +1 -1
  7. package/dist/server/express/index.d.ts +37 -0
  8. package/dist/server/express/index.js +60 -0
  9. package/dist/server/express/index.js.map +1 -0
  10. package/dist/server/index.d.ts +330 -5
  11. package/dist/server/index.js +593 -144
  12. package/dist/server/index.js.map +1 -1
  13. package/dist/server/testing-prisma.d.ts +101 -0
  14. package/dist/server/testing-prisma.js +172 -0
  15. package/dist/server/testing-prisma.js.map +1 -0
  16. package/dist/server/testing.d.ts +1 -1
  17. package/dist/server/testing.js +87 -22
  18. package/dist/server/testing.js.map +1 -1
  19. package/dist/shared/index.d.ts +77 -1
  20. package/dist/shared/index.js +5 -1
  21. package/dist/shared/index.js.map +1 -1
  22. package/dist/{types-DniMxkQs.d.ts → types-ClgSr90l.d.ts} +5 -1
  23. package/dist/{types-B1z5aXdR.d.ts → types-DUHPIy_5.d.ts} +20 -1
  24. package/eslint-config/index.mjs +1 -2
  25. package/eslint-plugin-quickdraw/index.mjs +15 -7
  26. package/eslint-plugin-quickdraw/rules/no-cross-service-mutations.mjs +31 -32
  27. package/eslint-plugin-quickdraw/rules/no-direct-prisma-mutations.mjs +17 -18
  28. package/eslint-plugin-quickdraw/rules/no-raw-button-strings.mjs +52 -0
  29. package/eslint-plugin-quickdraw/rules/no-raw-service-room-string.mjs +140 -0
  30. package/eslint-plugin-quickdraw/rules/no-raw-socket-emit.mjs +20 -22
  31. package/eslint-plugin-quickdraw/rules/no-raw-socket-on.mjs +17 -17
  32. package/eslint-plugin-quickdraw/rules/no-raw-tooltip-strings.mjs +59 -0
  33. package/eslint-plugin-quickdraw/rules/no-raw-typography-strings.mjs +52 -0
  34. package/eslint-plugin-quickdraw/rules/no-service-method-record.mjs +3 -3
  35. package/eslint-plugin-quickdraw/rules/no-unsafe-payload-cast.mjs +8 -8
  36. package/eslint-plugin-quickdraw/rules/require-zod-schema.mjs +15 -20
  37. package/package.json +74 -50
@@ -2,11 +2,15 @@ import express from 'express';
2
2
  import { createServer } from 'http';
3
3
  import { Server } from 'socket.io';
4
4
  import * as jose from 'jose';
5
+ import { randomBytes, createCipheriv, createDecipheriv } from 'crypto';
5
6
  import * as readline from 'readline';
6
7
  import * as fs from 'fs';
7
8
  import * as path from 'path';
8
9
 
9
10
  // src/shared/types.ts
11
+ function channelEventName(serviceName, channelName) {
12
+ return `channel:${serviceName}:${channelName}`;
13
+ }
10
14
  var consoleLogger = {
11
15
  info: (message, meta) => console.log(`[INFO] ${message}`, meta ?? ""),
12
16
  warn: (message, meta) => console.warn(`[WARN] ${message}`, meta ?? ""),
@@ -16,18 +20,8 @@ var consoleLogger = {
16
20
  };
17
21
 
18
22
  // src/server/utils/zodToAdminFields.ts
19
- var NON_EDITABLE_FIELDS = /* @__PURE__ */ new Set([
20
- "id",
21
- "createdAt",
22
- "updatedAt",
23
- "created_at",
24
- "updated_at"
25
- ]);
26
- var DEFAULT_HIDDEN_FIELDS = /* @__PURE__ */ new Set([
27
- "acl",
28
- "serviceAccess",
29
- "service_access"
30
- ]);
23
+ var NON_EDITABLE_FIELDS = /* @__PURE__ */ new Set(["id", "createdAt", "updatedAt", "created_at", "updated_at"]);
24
+ var DEFAULT_HIDDEN_FIELDS = /* @__PURE__ */ new Set(["acl", "serviceAccess", "service_access"]);
31
25
  function toLabel(fieldName) {
32
26
  return fieldName.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
33
27
  }
@@ -68,9 +62,7 @@ function getEnumValues(zodType) {
68
62
  }
69
63
  if (typeName === "ZodNativeEnum") {
70
64
  const enumObj = zodType._def.values;
71
- return Object.values(enumObj).filter(
72
- (v) => typeof v === "string"
73
- );
65
+ return Object.values(enumObj).filter((v) => typeof v === "string");
74
66
  }
75
67
  return void 0;
76
68
  }
@@ -167,13 +159,9 @@ function getDefaultEntityFields() {
167
159
  }
168
160
  function mergeWithDefaultFields(schemaFields, options = {}) {
169
161
  const { includeDefaults = ["id", "createdAt"] } = options;
170
- const defaultFields = getDefaultEntityFields().filter(
171
- (f) => includeDefaults.includes(f.name)
172
- );
162
+ const defaultFields = getDefaultEntityFields().filter((f) => includeDefaults.includes(f.name));
173
163
  const defaultNames = new Set(defaultFields.map((f) => f.name));
174
- const filteredSchemaFields = schemaFields.filter(
175
- (f) => !defaultNames.has(f.name)
176
- );
164
+ const filteredSchemaFields = schemaFields.filter((f) => !defaultNames.has(f.name));
177
165
  return [...defaultFields, ...filteredSchemaFields];
178
166
  }
179
167
 
@@ -191,6 +179,8 @@ var BaseService = class {
191
179
  io = null;
192
180
  // Collection of public methods for registry discovery
193
181
  publicMethods = /* @__PURE__ */ new Map();
182
+ // Collection of channels for registry discovery
183
+ publicChannels = /* @__PURE__ */ new Map();
194
184
  // Admin metadata configuration (set by installAdminMethods)
195
185
  adminMeta = null;
196
186
  constructor(options) {
@@ -258,9 +248,7 @@ var BaseService = class {
258
248
  this.subscribers.get(entryId).add(socket);
259
249
  const roomName = this.getRoomName(entryId);
260
250
  void socket.join(roomName);
261
- this.logger.debug(
262
- `User ${socket.userId} subscribed to ${entryId} (room: ${roomName})`
263
- );
251
+ this.logger.debug(`User ${socket.userId} subscribed to ${entryId} (room: ${roomName})`);
264
252
  const entity = await this.findById(entryId);
265
253
  if (!entity) return null;
266
254
  return this.filterEntityForSubscriber(entity, socket, entryId);
@@ -303,11 +291,7 @@ var BaseService = class {
303
291
  results[entryId] = null;
304
292
  continue;
305
293
  }
306
- results[entryId] = this.filterEntityForSubscriber(
307
- entity,
308
- socket,
309
- entryId
310
- );
294
+ results[entryId] = this.filterEntityForSubscriber(entity, socket, entryId);
311
295
  }
312
296
  this.logger.debug(
313
297
  `User ${socket.userId} batch-subscribed to ${allowedIds.length}/${entryIds.length} entities`
@@ -325,12 +309,7 @@ var BaseService = class {
325
309
  const results = /* @__PURE__ */ new Map();
326
310
  await Promise.all(
327
311
  entryIds.map(async (id) => {
328
- const allowed = await this.checkSubscriptionAccess(
329
- userId,
330
- id,
331
- requiredLevel,
332
- socket
333
- );
312
+ const allowed = await this.checkSubscriptionAccess(userId, id, requiredLevel, socket);
334
313
  results.set(id, allowed);
335
314
  })
336
315
  );
@@ -344,9 +323,7 @@ var BaseService = class {
344
323
  * Default: fetches each entity individually in parallel.
345
324
  */
346
325
  async findByIds(ids) {
347
- const results = await Promise.all(
348
- ids.map((id) => this.findById(id))
349
- );
326
+ const results = await Promise.all(ids.map((id) => this.findById(id)));
350
327
  return results.filter((e) => e !== null);
351
328
  }
352
329
  /**
@@ -410,9 +387,37 @@ var BaseService = class {
410
387
  return;
411
388
  }
412
389
  anySocket.to(roomName).emit(eventName, data);
413
- this.logger.debug(
414
- `Emitted ${eventName} to room ${roomName} (via socket fallback)`
415
- );
390
+ this.logger.debug(`Emitted ${eventName} to room ${roomName} (via socket fallback)`);
391
+ }
392
+ /**
393
+ * Emit a custom event to a room using Socket.io's volatile flag.
394
+ *
395
+ * Volatile events are dropped for clients whose connection is backpressured
396
+ * instead of being buffered — exactly what you want for high-frequency state
397
+ * that is superseded by the next emit (game snapshots, cursor positions).
398
+ * Never use this for events a client must not miss.
399
+ *
400
+ * Intentionally does no per-call logging: this is a hot path, typically
401
+ * called at tick rate (10–60Hz).
402
+ *
403
+ * @example
404
+ * ```typescript
405
+ * // In a 20Hz game loop
406
+ * this.emitToRoomVolatile(
407
+ * this.getRoomName(worldId),
408
+ * 'game:snapshot',
409
+ * snapshot
410
+ * );
411
+ * ```
412
+ */
413
+ emitToRoomVolatile(roomName, eventName, data) {
414
+ if (this.io) {
415
+ this.io.to(roomName).volatile.emit(eventName, data);
416
+ return;
417
+ }
418
+ const anySocket = this.getAnySubscriberSocket();
419
+ if (!anySocket) return;
420
+ anySocket.to(roomName).volatile.emit(eventName, data);
416
421
  }
417
422
  /**
418
423
  * Emit a custom event to a specific user's room.
@@ -689,10 +694,7 @@ var BaseService = class {
689
694
  schema: options?.schema,
690
695
  resolveEntryId: options?.resolveEntryId
691
696
  };
692
- this.publicMethods.set(
693
- name,
694
- definition
695
- );
697
+ this.publicMethods.set(name, definition);
696
698
  return definition;
697
699
  }
698
700
  /**
@@ -701,6 +703,78 @@ var BaseService = class {
701
703
  getPublicMethods() {
702
704
  return Array.from(this.publicMethods.values());
703
705
  }
706
+ // ===========================================================================
707
+ // Channel Definition (high-frequency, fire-and-forget events)
708
+ // ===========================================================================
709
+ /**
710
+ * Define a channel: a fire-and-forget event for high-frequency traffic
711
+ * (game input, cursor positions, typing indicators).
712
+ *
713
+ * Channels differ from methods:
714
+ * - No ack, no response — the handler returns void and errors are logged,
715
+ * never sent to the client.
716
+ * - Exempt from the global rate limiter; each channel enforces its own
717
+ * per-socket token bucket (`ratePerSecond`/`burst`). Excess messages are
718
+ * silently dropped; sustained extreme abuse disconnects the socket.
719
+ * - Access checks are fully synchronous and in-memory: authentication is
720
+ * always required; "Moderate"/"Admin" check the socket's serviceAccess;
721
+ * entry-level access is expressed via `requireRoom` (the socket must
722
+ * already be in that room, which was ACL-gated at subscribe time).
723
+ * - `schema` is required — channel payloads are untrusted input arriving
724
+ * at tick rate.
725
+ *
726
+ * Routes as the Socket.io event `channel:<serviceName>:<name>`.
727
+ *
728
+ * @example
729
+ * ```typescript
730
+ * this.defineChannel(
731
+ * "input",
732
+ * "Read",
733
+ * (payload, ctx) => this.sim.applyInput(ctx.userId, payload),
734
+ * {
735
+ * schema: gameInputSchema,
736
+ * ratePerSecond: 30,
737
+ * requireRoom: () => this.getRoomName(GLOBAL_WORLD_ID),
738
+ * },
739
+ * );
740
+ * ```
741
+ */
742
+ defineChannel(name, access, handler, options) {
743
+ const ratePerSecond = options.ratePerSecond ?? 30;
744
+ const definition = {
745
+ name,
746
+ access,
747
+ handler,
748
+ schema: options.schema,
749
+ ratePerSecond,
750
+ burst: options.burst ?? ratePerSecond * 2,
751
+ requireRoom: options.requireRoom
752
+ };
753
+ this.publicChannels.set(name, definition);
754
+ return definition;
755
+ }
756
+ /**
757
+ * Get all channels for registry discovery.
758
+ */
759
+ getPublicChannels() {
760
+ return Array.from(this.publicChannels.values());
761
+ }
762
+ /**
763
+ * Synchronous, in-memory access check for a channel message.
764
+ * Called by the registry on the hot path — must not touch the database.
765
+ * Returns false to silently drop the message.
766
+ */
767
+ checkChannelAccess(channel, socket, payload) {
768
+ if (!socket.userId) return false;
769
+ if (channel.access !== "Public" && channel.access !== "Read" && !this.hasServiceAccess(socket, channel.access)) {
770
+ return false;
771
+ }
772
+ if (channel.requireRoom) {
773
+ const roomName = channel.requireRoom(payload);
774
+ if (roomName && !socket.rooms.has(roomName)) return false;
775
+ }
776
+ return true;
777
+ }
704
778
  /**
705
779
  * Verify that all expected service methods have been defined via defineMethod().
706
780
  * Call at the end of initMethods() to catch missing implementations at boot time.
@@ -725,9 +799,7 @@ var BaseService = class {
725
799
  }
726
800
  }
727
801
  if (missing.length > 0) {
728
- throw new Error(
729
- `${this.serviceName}: Missing method implementations: ${missing.join(", ")}`
730
- );
802
+ throw new Error(`${this.serviceName}: Missing method implementations: ${missing.join(", ")}`);
731
803
  }
732
804
  }
733
805
  // ===========================================================================
@@ -758,15 +830,7 @@ var BaseService = class {
758
830
  * ```
759
831
  */
760
832
  installAdminMethods(options) {
761
- const {
762
- expose,
763
- access,
764
- schema,
765
- displayName,
766
- tableColumns,
767
- hiddenFields,
768
- fieldOverrides
769
- } = options;
833
+ const { expose, access, schema, displayName, tableColumns, hiddenFields, fieldOverrides } = options;
770
834
  if (schema) {
771
835
  const schemaFields = zodToAdminFields(schema, {
772
836
  hiddenFields,
@@ -820,9 +884,7 @@ var BaseService = class {
820
884
  name: "adminCreate",
821
885
  access: access.create,
822
886
  handler: async (payload, _context) => {
823
- return await this.adminCreate(
824
- payload.data
825
- );
887
+ return await this.adminCreate(payload.data);
826
888
  }
827
889
  });
828
890
  }
@@ -862,9 +924,7 @@ var BaseService = class {
862
924
  name: "adminGetSubscribers",
863
925
  access: access.getSubscribers,
864
926
  handler: async (payload, _context) => {
865
- return this.adminGetSubscribers(
866
- payload.entryId
867
- );
927
+ return this.adminGetSubscribers(payload.entryId);
868
928
  },
869
929
  resolveEntryId: (payload) => payload.entryId
870
930
  });
@@ -874,9 +934,7 @@ var BaseService = class {
874
934
  name: "adminReemit",
875
935
  access: access.reemit,
876
936
  handler: async (payload, _context) => {
877
- return await this.adminReemit(
878
- payload.entryId
879
- );
937
+ return await this.adminReemit(payload.entryId);
880
938
  },
881
939
  resolveEntryId: (payload) => payload.entryId
882
940
  });
@@ -886,16 +944,12 @@ var BaseService = class {
886
944
  name: "adminUnsubscribeAll",
887
945
  access: access.unsubscribeAll,
888
946
  handler: async (payload, _context) => {
889
- return this.adminUnsubscribeAll(
890
- payload.entryId
891
- );
947
+ return this.adminUnsubscribeAll(payload.entryId);
892
948
  },
893
949
  resolveEntryId: (payload) => payload.entryId
894
950
  });
895
951
  }
896
- const installedMethods = Object.keys(expose).filter(
897
- (k) => expose[k]
898
- );
952
+ const installedMethods = Object.keys(expose).filter((k) => expose[k]);
899
953
  if (shouldExposeMeta) installedMethods.push("meta");
900
954
  this.logger.info(`Installed admin methods: ${installedMethods.join(", ")}`);
901
955
  }
@@ -1047,6 +1101,8 @@ var BaseService = class {
1047
1101
  };
1048
1102
 
1049
1103
  // src/server/ServiceRegistry.ts
1104
+ var CHANNEL_ABUSE_WINDOW_MS = 1e4;
1105
+ var CHANNEL_ABUSE_MULTIPLIER = 100;
1050
1106
  var ServiceRegistry = class {
1051
1107
  io;
1052
1108
  services = /* @__PURE__ */ new Map();
@@ -1057,6 +1113,8 @@ var ServiceRegistry = class {
1057
1113
  connectionHandlerInstalled = false;
1058
1114
  // Store method metadata for deferred registration
1059
1115
  methodRegistry = /* @__PURE__ */ new Map();
1116
+ // Store channel metadata for deferred registration
1117
+ channelRegistry = /* @__PURE__ */ new Map();
1060
1118
  constructor(io, options) {
1061
1119
  this.io = io;
1062
1120
  this.logger = options?.logger?.child({ service: "ServiceRegistry" }) ?? consoleLogger.child({ service: "ServiceRegistry" });
@@ -1090,22 +1148,13 @@ var ServiceRegistry = class {
1090
1148
  for (const [eventName, { method, service }] of this.methodRegistry) {
1091
1149
  this.registerMethodListener(socket, eventName, method, service);
1092
1150
  }
1151
+ for (const [eventName, { channel, service }] of this.channelRegistry) {
1152
+ this.registerChannelListener(socket, eventName, channel, service);
1153
+ }
1093
1154
  for (const [serviceName, service] of this.services) {
1094
- this.registerSubscriptionListener(
1095
- socket,
1096
- `${serviceName}:subscribe`,
1097
- service
1098
- );
1099
- this.registerBatchSubscriptionListener(
1100
- socket,
1101
- `${serviceName}:batchSubscribe`,
1102
- service
1103
- );
1104
- this.registerUnsubscriptionListener(
1105
- socket,
1106
- `${serviceName}:unsubscribe`,
1107
- service
1108
- );
1155
+ this.registerSubscriptionListener(socket, `${serviceName}:subscribe`, service);
1156
+ this.registerBatchSubscriptionListener(socket, `${serviceName}:batchSubscribe`, service);
1157
+ this.registerUnsubscriptionListener(socket, `${serviceName}:unsubscribe`, service);
1109
1158
  }
1110
1159
  }
1111
1160
  /**
@@ -1118,6 +1167,7 @@ var ServiceRegistry = class {
1118
1167
  }
1119
1168
  this.logger.info(`Registered service: ${serviceName}`);
1120
1169
  this.discoverServiceMethods(serviceName, service);
1170
+ this.discoverServiceChannels(serviceName, service);
1121
1171
  this.installConnectionHandler();
1122
1172
  }
1123
1173
  /**
@@ -1135,6 +1185,77 @@ var ServiceRegistry = class {
1135
1185
  });
1136
1186
  }
1137
1187
  }
1188
+ /**
1189
+ * Discover and store all channels from a service.
1190
+ */
1191
+ discoverServiceChannels(serviceName, service) {
1192
+ const channels = service.getPublicChannels?.() ?? [];
1193
+ for (const channel of channels) {
1194
+ const eventName = channelEventName(serviceName, channel.name);
1195
+ this.logger.info(
1196
+ `Registering channel event: ${eventName} (${channel.ratePerSecond}/s, burst ${channel.burst})`
1197
+ );
1198
+ this.channelRegistry.set(eventName, {
1199
+ serviceName,
1200
+ channel,
1201
+ service
1202
+ });
1203
+ }
1204
+ }
1205
+ /**
1206
+ * Register a channel as a socket event listener.
1207
+ *
1208
+ * This is the hot path for high-frequency traffic: no ack, no async access
1209
+ * checks, no per-message logging. Order of operations per message:
1210
+ * token bucket → schema validation → in-memory access check → handler.
1211
+ * Any failure silently drops the message; sustained extreme flooding
1212
+ * disconnects the socket.
1213
+ */
1214
+ registerChannelListener(socket, eventName, channel, service) {
1215
+ let tokens = channel.burst;
1216
+ let lastRefillAt = Date.now();
1217
+ let abuseWindowStart = lastRefillAt;
1218
+ let droppedInWindow = 0;
1219
+ socket.on(eventName, (payload) => {
1220
+ const now = Date.now();
1221
+ tokens = Math.min(
1222
+ channel.burst,
1223
+ tokens + (now - lastRefillAt) / 1e3 * channel.ratePerSecond
1224
+ );
1225
+ lastRefillAt = now;
1226
+ if (tokens < 1) {
1227
+ if (now - abuseWindowStart > CHANNEL_ABUSE_WINDOW_MS) {
1228
+ abuseWindowStart = now;
1229
+ droppedInWindow = 0;
1230
+ }
1231
+ droppedInWindow++;
1232
+ if (droppedInWindow > channel.ratePerSecond * CHANNEL_ABUSE_MULTIPLIER) {
1233
+ this.logger.warn(
1234
+ `Disconnecting socket ${socket.id} for sustained channel flooding on ${eventName}`,
1235
+ { userId: socket.userId, droppedInWindow }
1236
+ );
1237
+ socket.disconnect(true);
1238
+ }
1239
+ return;
1240
+ }
1241
+ tokens -= 1;
1242
+ const parsed = channel.schema.safeParse(payload);
1243
+ if (!parsed.success) return;
1244
+ if (!service.checkChannelAccess?.(channel, socket, parsed.data)) return;
1245
+ try {
1246
+ channel.handler(parsed.data, {
1247
+ userId: socket.userId,
1248
+ socketId: socket.id,
1249
+ serviceAccess: socket.serviceAccess ?? {}
1250
+ });
1251
+ } catch (error) {
1252
+ this.logger.error(`Channel handler error for ${eventName}`, {
1253
+ userId: socket.userId,
1254
+ error: error instanceof Error ? error.message : "Unknown error"
1255
+ });
1256
+ }
1257
+ });
1258
+ }
1138
1259
  /**
1139
1260
  * Register a method as a socket event listener.
1140
1261
  */
@@ -1238,10 +1359,7 @@ var ServiceRegistry = class {
1238
1359
  if (this.logPayloads) {
1239
1360
  logContext.payload = payload;
1240
1361
  }
1241
- this.logger.error(
1242
- `User ${userIdShort} ${serviceName}.${methodName} -> fail`,
1243
- logContext
1244
- );
1362
+ this.logger.error(`User ${userIdShort} ${serviceName}.${methodName} -> fail`, logContext);
1245
1363
  const errorResponse = {
1246
1364
  success: false,
1247
1365
  error: error instanceof Error ? error.message : "Internal error",
@@ -1437,9 +1555,7 @@ function createQuickdrawServer(options) {
1437
1555
  });
1438
1556
  httpServer.listen(options.port, "0.0.0.0", () => {
1439
1557
  serverLogger.info(`Server listening on port ${options.port}`);
1440
- serverLogger.info(
1441
- `Registered services: ${registry.getServices().join(", ")}`
1442
- );
1558
+ serverLogger.info(`Registered services: ${registry.getServices().join(", ")}`);
1443
1559
  });
1444
1560
  const shutdown = async (signal) => {
1445
1561
  serverLogger.info(`Received ${signal}, starting graceful shutdown...`);
@@ -1475,13 +1591,7 @@ function createQuickdrawServer(options) {
1475
1591
  // src/server/redis.ts
1476
1592
  async function setupRedisAdapter(io, options = {}) {
1477
1593
  const logger = options.logger?.child({ service: "RedisAdapter" }) ?? consoleLogger.child({ service: "RedisAdapter" });
1478
- const {
1479
- host = "localhost",
1480
- port = 6379,
1481
- password,
1482
- db = 0,
1483
- keyPrefix = "socket.io"
1484
- } = options;
1594
+ const { host = "localhost", port = 6379, password, db = 0, keyPrefix = "socket.io" } = options;
1485
1595
  try {
1486
1596
  const dynamicImport = new Function("specifier", "return import(specifier)");
1487
1597
  const [redisAdapterModule, redisModule] = await Promise.all([
@@ -1497,7 +1607,9 @@ async function setupRedisAdapter(io, options = {}) {
1497
1607
  });
1498
1608
  const subClient = pubClient.duplicate();
1499
1609
  await Promise.all([pubClient.connect(), subClient.connect()]);
1500
- io.adapter(createAdapter(pubClient, subClient, { key: keyPrefix }));
1610
+ io.adapter(
1611
+ createAdapter(pubClient, subClient, { key: keyPrefix })
1612
+ );
1501
1613
  logger.info(`Redis adapter connected to ${host}:${port}`);
1502
1614
  const cleanup = async () => {
1503
1615
  try {
@@ -1529,10 +1641,7 @@ async function setupRedisAdapter(io, options = {}) {
1529
1641
  async function isRedisAdapterAvailable() {
1530
1642
  try {
1531
1643
  const dynamicImport = new Function("specifier", "return import(specifier)");
1532
- await Promise.all([
1533
- dynamicImport("@socket.io/redis-adapter"),
1534
- dynamicImport("redis")
1535
- ]);
1644
+ await Promise.all([dynamicImport("@socket.io/redis-adapter"), dynamicImport("redis")]);
1536
1645
  return true;
1537
1646
  } catch {
1538
1647
  return false;
@@ -1541,11 +1650,7 @@ async function isRedisAdapterAvailable() {
1541
1650
 
1542
1651
  // src/server/rateLimit.ts
1543
1652
  function createRateLimiter(options = {}) {
1544
- const {
1545
- windowMs = 6e4,
1546
- maxRequests = 100,
1547
- excludeEvents = []
1548
- } = options;
1653
+ const { windowMs = 6e4, maxRequests = 100, excludeEvents = [], excludePrefixes = [] } = options;
1549
1654
  const entries = /* @__PURE__ */ new Map();
1550
1655
  const cleanupInterval = setInterval(() => {
1551
1656
  const now = Date.now();
@@ -1597,7 +1702,8 @@ function createRateLimiter(options = {}) {
1597
1702
  options: {
1598
1703
  windowMs,
1599
1704
  maxRequests,
1600
- excludeEvents
1705
+ excludeEvents,
1706
+ excludePrefixes
1601
1707
  }
1602
1708
  };
1603
1709
  }
@@ -1611,13 +1717,17 @@ function applyRateLimitMiddleware(io, rateLimiter, options = {}) {
1611
1717
  retryAfter: rateLimiter.getResetTime(keyGenerator(socket, eventName))
1612
1718
  });
1613
1719
  });
1614
- const { excludeEvents } = rateLimiter.options;
1720
+ const { excludeEvents, excludePrefixes } = rateLimiter.options;
1615
1721
  io.on("connection", (socket) => {
1616
1722
  socket.use(([eventName, ...args], next) => {
1617
1723
  if (excludeEvents.includes(eventName)) {
1618
1724
  next();
1619
1725
  return;
1620
1726
  }
1727
+ if (excludePrefixes.some((prefix) => eventName.startsWith(prefix))) {
1728
+ next();
1729
+ return;
1730
+ }
1621
1731
  if (eventName.startsWith("$")) {
1622
1732
  next();
1623
1733
  return;
@@ -1769,6 +1879,13 @@ var discordProvider = {
1769
1879
  };
1770
1880
  }
1771
1881
  };
1882
+ function getDiscordAvatarUrl(user) {
1883
+ if (user.avatar) {
1884
+ return `https://cdn.discordapp.com/avatars/${user.id}/${user.avatar}.png`;
1885
+ }
1886
+ const index = user.discriminator !== "0" ? parseInt(user.discriminator, 10) % 5 : (BigInt(user.id) >> BigInt(22)) % BigInt(6);
1887
+ return `https://cdn.discordapp.com/embed/avatars/${index}.png`;
1888
+ }
1772
1889
 
1773
1890
  // src/server/auth/google.ts
1774
1891
  var googleProvider = {
@@ -1789,6 +1906,306 @@ var googleProvider = {
1789
1906
  }
1790
1907
  };
1791
1908
 
1909
+ // src/server/auth/validateOrigin.ts
1910
+ var OAUTH_RETURN_ORIGIN_COOKIE = "oauth_return_origin";
1911
+ var CODESPACE_REGEX = /^https:\/\/[a-z0-9-]+-[a-z0-9-]+-\d+\.app\.github\.dev$/;
1912
+ var LOCALHOST_REGEX = /^http:\/\/localhost:\d+$/;
1913
+ function envExtraOrigins() {
1914
+ const raw = process.env.EXTRA_ALLOWED_ORIGINS;
1915
+ if (!raw) return [];
1916
+ return raw.split(",").map((s) => s.trim()).filter(Boolean);
1917
+ }
1918
+ function validateRedirectOrigin(origin, options = {}) {
1919
+ if (!origin) return null;
1920
+ const clientUrl = options.clientUrl ?? process.env.CLIENT_URL;
1921
+ let parsedOrigin;
1922
+ try {
1923
+ parsedOrigin = new URL(origin);
1924
+ } catch {
1925
+ return null;
1926
+ }
1927
+ const cleanOrigin = parsedOrigin.origin;
1928
+ if (clientUrl && cleanOrigin === clientUrl) {
1929
+ return cleanOrigin;
1930
+ }
1931
+ const extra = options.extraAllowedOrigins ?? envExtraOrigins();
1932
+ if (extra.includes(cleanOrigin)) {
1933
+ return cleanOrigin;
1934
+ }
1935
+ if (options.allowedPatterns?.some((pattern) => pattern.test(cleanOrigin))) {
1936
+ return cleanOrigin;
1937
+ }
1938
+ if ((options.allowCodespaces ?? true) && CODESPACE_REGEX.test(cleanOrigin)) {
1939
+ return cleanOrigin;
1940
+ }
1941
+ if ((options.allowLocalhostInDev ?? true) && process.env.NODE_ENV !== "production" && LOCALHOST_REGEX.test(cleanOrigin)) {
1942
+ return cleanOrigin;
1943
+ }
1944
+ return null;
1945
+ }
1946
+
1947
+ // src/server/auth/mock.ts
1948
+ var DEFAULT_MOCK_PATH_PREFIX = "/auth/mock/provider";
1949
+ var CODE_TTL_MS = 5 * 60 * 1e3;
1950
+ var TOKEN_TTL_MS = 60 * 60 * 1e3;
1951
+ var DEV_PRIVATE_ORIGIN_REGEX = /^http:\/\/(?:localhost|127\.0\.0\.1|0\.0\.0\.0|10\.\d{1,3}\.\d{1,3}\.\d{1,3}|192\.168\.\d{1,3}\.\d{1,3}|172\.(?:1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3}):\d+$/;
1952
+ function isMockOAuthEnabled() {
1953
+ return process.env.ENABLE_MOCK_OAUTH === "true" && process.env.NODE_ENV !== "production";
1954
+ }
1955
+ function createMockOAuthProvider(apiBaseUrl, options = {}) {
1956
+ const prefix = options.pathPrefix ?? DEFAULT_MOCK_PATH_PREFIX;
1957
+ const publicBase = apiBaseUrl.replace(/\/$/, "");
1958
+ const internalBase = (options.internalBaseUrl ?? apiBaseUrl).replace(/\/$/, "");
1959
+ return {
1960
+ name: "mock",
1961
+ authorizationUrl: `${publicBase}${prefix}/authorize`,
1962
+ tokenUrl: `${internalBase}${prefix}/token`,
1963
+ userInfoUrl: `${internalBase}${prefix}/userinfo`,
1964
+ scopes: ["openid", "email", "profile"],
1965
+ parseUserInfo: (data) => {
1966
+ const d = data;
1967
+ return {
1968
+ id: String(d.id),
1969
+ email: String(d.email),
1970
+ name: String(d.name),
1971
+ picture: d.picture ? String(d.picture) : null,
1972
+ verified_email: true
1973
+ };
1974
+ }
1975
+ };
1976
+ }
1977
+ function getQueryParam(req, name) {
1978
+ const fromQuery = req.query?.[name];
1979
+ if (typeof fromQuery === "string") return fromQuery;
1980
+ const url = req.originalUrl ?? req.url;
1981
+ if (!url) return null;
1982
+ const idx = url.indexOf("?");
1983
+ if (idx === -1) return null;
1984
+ return new URLSearchParams(url.slice(idx + 1)).get(name);
1985
+ }
1986
+ async function readBodyParams(req) {
1987
+ if (typeof req.body === "string") return new URLSearchParams(req.body);
1988
+ if (req.body && typeof req.body === "object" && Object.keys(req.body).length > 0) {
1989
+ return new URLSearchParams(req.body);
1990
+ }
1991
+ const chunks = [];
1992
+ for await (const chunk of req) {
1993
+ chunks.push(Buffer.from(chunk));
1994
+ }
1995
+ return new URLSearchParams(Buffer.concat(chunks).toString("utf8"));
1996
+ }
1997
+ function escapeHtml(value) {
1998
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
1999
+ }
2000
+ function isAllowedRedirectUri(uri, allowedOrigins) {
2001
+ let parsed;
2002
+ try {
2003
+ parsed = new URL(uri);
2004
+ } catch {
2005
+ return false;
2006
+ }
2007
+ const origin = parsed.origin;
2008
+ if (allowedOrigins.includes(origin)) return true;
2009
+ if (DEV_PRIVATE_ORIGIN_REGEX.test(origin)) return true;
2010
+ return validateRedirectOrigin(origin) !== null;
2011
+ }
2012
+ function renderPickerPage(users, authorizePath, redirectUri, state) {
2013
+ const links = users.map((user) => {
2014
+ const params = new URLSearchParams({ redirect_uri: redirectUri, email: user.email });
2015
+ if (state) params.set("state", state);
2016
+ const href = `${authorizePath}?${params.toString()}`;
2017
+ return `<li><a href="${escapeHtml(href)}"><strong>${escapeHtml(user.name)}</strong><span>${escapeHtml(user.email)}</span></a></li>`;
2018
+ }).join("\n");
2019
+ return `<!doctype html>
2020
+ <html lang="en">
2021
+ <head>
2022
+ <meta charset="utf-8" />
2023
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
2024
+ <title>Mock sign-in</title>
2025
+ <style>
2026
+ body { font-family: system-ui, sans-serif; background: #111; color: #eee; display: flex; min-height: 100vh; align-items: center; justify-content: center; margin: 0; }
2027
+ main { max-width: 420px; width: 100%; padding: 2rem; }
2028
+ h1 { font-size: 1.25rem; }
2029
+ p { color: #999; font-size: 0.875rem; }
2030
+ ul { list-style: none; padding: 0; display: flex; flex-direction: column; gap: 0.5rem; }
2031
+ a { display: flex; flex-direction: column; gap: 0.125rem; padding: 0.75rem 1rem; border: 1px solid #333; border-radius: 8px; text-decoration: none; color: inherit; }
2032
+ a:hover { border-color: #888; background: #1a1a1a; }
2033
+ a span { color: #999; font-size: 0.8125rem; }
2034
+ </style>
2035
+ </head>
2036
+ <body>
2037
+ <main>
2038
+ <h1>Mock sign-in</h1>
2039
+ <p>Development only \u2014 pick an account to continue. Run your seed script if this list is empty.</p>
2040
+ <ul>
2041
+ ${links || "<li><em>No users found.</em></li>"}
2042
+ </ul>
2043
+ </main>
2044
+ </body>
2045
+ </html>`;
2046
+ }
2047
+ function registerMockOAuthProvider(router, options) {
2048
+ const logger = options.logger ?? consoleLogger;
2049
+ if (!isMockOAuthEnabled()) {
2050
+ if (process.env.ENABLE_MOCK_OAUTH === "true") {
2051
+ logger.error("Mock OAuth is enabled but NODE_ENV is production \u2014 refusing to mount routes");
2052
+ }
2053
+ return false;
2054
+ }
2055
+ const prefix = options.pathPrefix ?? DEFAULT_MOCK_PATH_PREFIX;
2056
+ const allowedOrigins = options.allowedRedirectOrigins ?? [];
2057
+ const codes = /* @__PURE__ */ new Map();
2058
+ const tokens = /* @__PURE__ */ new Map();
2059
+ function sweep(store) {
2060
+ const now = Date.now();
2061
+ for (const [key, entry] of store) {
2062
+ if (entry.expiresAt < now) store.delete(key);
2063
+ }
2064
+ }
2065
+ function guard(res) {
2066
+ if (process.env.NODE_ENV === "production") {
2067
+ res.status(404).json({ error: "Not found" });
2068
+ return false;
2069
+ }
2070
+ return true;
2071
+ }
2072
+ router.get(`${prefix}/authorize`, async (req, res) => {
2073
+ if (!guard(res)) return;
2074
+ const redirectUri = getQueryParam(req, "redirect_uri");
2075
+ const state = getQueryParam(req, "state");
2076
+ const email = getQueryParam(req, "email");
2077
+ if (!redirectUri || !isAllowedRedirectUri(redirectUri, allowedOrigins)) {
2078
+ res.status(400).json({ error: "Invalid redirect_uri" });
2079
+ return;
2080
+ }
2081
+ const users = await options.listUsers();
2082
+ if (!email) {
2083
+ res.status(200).send(renderPickerPage(users, `${prefix}/authorize`, redirectUri, state));
2084
+ return;
2085
+ }
2086
+ const user = users.find((u) => u.email === email);
2087
+ if (!user) {
2088
+ res.status(400).json({ error: "Unknown mock user" });
2089
+ return;
2090
+ }
2091
+ sweep(codes);
2092
+ const code = randomBytes(24).toString("hex");
2093
+ codes.set(code, { email: user.email, expiresAt: Date.now() + CODE_TTL_MS });
2094
+ const params = new URLSearchParams({ code });
2095
+ if (state) params.set("state", state);
2096
+ res.redirect(`${redirectUri}${redirectUri.includes("?") ? "&" : "?"}${params.toString()}`);
2097
+ });
2098
+ router.post(`${prefix}/token`, async (req, res) => {
2099
+ if (!guard(res)) return;
2100
+ const body = await readBodyParams(req);
2101
+ const code = body.get("code");
2102
+ const entry = code ? codes.get(code) : void 0;
2103
+ if (code) codes.delete(code);
2104
+ if (!entry || entry.expiresAt < Date.now()) {
2105
+ res.status(400).json({ error: "invalid_grant" });
2106
+ return;
2107
+ }
2108
+ sweep(tokens);
2109
+ const accessToken = randomBytes(24).toString("hex");
2110
+ tokens.set(accessToken, { email: entry.email, expiresAt: Date.now() + TOKEN_TTL_MS });
2111
+ res.status(200).json({
2112
+ access_token: accessToken,
2113
+ token_type: "Bearer",
2114
+ expires_in: Math.floor(TOKEN_TTL_MS / 1e3),
2115
+ scope: "openid email profile"
2116
+ });
2117
+ });
2118
+ router.get(`${prefix}/userinfo`, async (req, res) => {
2119
+ if (!guard(res)) return;
2120
+ const auth = req.headers.authorization;
2121
+ const accessToken = typeof auth === "string" && auth.startsWith("Bearer ") ? auth.slice(7) : null;
2122
+ const entry = accessToken ? tokens.get(accessToken) : void 0;
2123
+ if (!entry || entry.expiresAt < Date.now()) {
2124
+ res.status(401).json({ error: "invalid_token" });
2125
+ return;
2126
+ }
2127
+ const users = await options.listUsers();
2128
+ const user = users.find((u) => u.email === entry.email);
2129
+ if (!user) {
2130
+ res.status(401).json({ error: "invalid_token" });
2131
+ return;
2132
+ }
2133
+ res.status(200).json({
2134
+ // providerAccountId = email: stable across re-seeds, unlike row ids.
2135
+ id: user.email,
2136
+ email: user.email,
2137
+ name: user.name,
2138
+ picture: user.picture ?? null,
2139
+ verified_email: true
2140
+ });
2141
+ });
2142
+ logger.info(`Mock OAuth provider mounted at ${prefix} (development only)`);
2143
+ return true;
2144
+ }
2145
+
2146
+ // src/server/auth/sessionCookie.ts
2147
+ var SESSION_COOKIE = "session";
2148
+ var DEFAULT_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1e3;
2149
+ function cookieOptions(options) {
2150
+ const isProd = process.env.NODE_ENV === "production";
2151
+ const domain = options.domain ?? process.env.COOKIE_DOMAIN;
2152
+ return {
2153
+ httpOnly: true,
2154
+ secure: isProd,
2155
+ // SameSite=None (with Secure) lets the session cookie ride cross-site
2156
+ // fetches, so a secondary web origin can hit the primary API host. The
2157
+ // CORS allowlist (validateRedirectOrigin) is the actual origin gate. Dev
2158
+ // keeps Lax — localhost is http-only and SameSite=None requires Secure.
2159
+ sameSite: isProd ? "none" : "lax",
2160
+ path: "/",
2161
+ ...domain && { domain }
2162
+ };
2163
+ }
2164
+ function setSessionCookie(res, jwt, options = {}) {
2165
+ res.cookie(options.cookieName ?? SESSION_COOKIE, jwt, {
2166
+ ...cookieOptions(options),
2167
+ maxAge: options.maxAgeMs ?? DEFAULT_MAX_AGE_MS
2168
+ });
2169
+ }
2170
+ function clearSessionCookie(res, options = {}) {
2171
+ res.clearCookie(options.cookieName ?? SESSION_COOKIE, cookieOptions(options));
2172
+ }
2173
+
2174
+ // src/server/auth/restMiddleware.ts
2175
+ function extractBearerOrCookieToken(req, cookieName = SESSION_COOKIE) {
2176
+ const cookieToken = req.cookies?.[cookieName];
2177
+ if (cookieToken) return cookieToken;
2178
+ const authHeader = req.headers.authorization;
2179
+ if (authHeader?.startsWith("Bearer ")) return authHeader.slice(7);
2180
+ return null;
2181
+ }
2182
+ function createRequireAuth(options) {
2183
+ return async (req, res, next) => {
2184
+ const token = extractBearerOrCookieToken(req, options.cookieName);
2185
+ if (!token) {
2186
+ res.status(401).json({ error: "Missing or invalid authorization" });
2187
+ return;
2188
+ }
2189
+ const secret = options.jwtSecret ?? process.env.JWT_SECRET;
2190
+ if (!secret) {
2191
+ res.status(401).json({ error: "Authentication is not configured" });
2192
+ return;
2193
+ }
2194
+ const payload = await verifyJWT(token, secret);
2195
+ if (!payload?.userId) {
2196
+ res.status(401).json({ error: "Invalid or expired token" });
2197
+ return;
2198
+ }
2199
+ const session = await options.getSession(token);
2200
+ if (!session || session.expiresAt < /* @__PURE__ */ new Date()) {
2201
+ res.status(401).json({ error: "Session expired or revoked" });
2202
+ return;
2203
+ }
2204
+ req.userId = payload.userId;
2205
+ next();
2206
+ };
2207
+ }
2208
+
1792
2209
  // src/server/utils/env.ts
1793
2210
  function validateEnv(options) {
1794
2211
  const result = checkEnv(options);
@@ -1797,7 +2214,11 @@ function validateEnv(options) {
1797
2214
  }
1798
2215
  }
1799
2216
  function checkEnv(options) {
1800
- const { required, productionOnly = true, errorPrefix = "Missing required environment variables" } = options;
2217
+ const {
2218
+ required,
2219
+ productionOnly = true,
2220
+ errorPrefix = "Missing required environment variables"
2221
+ } = options;
1801
2222
  if (productionOnly && process.env.NODE_ENV !== "production") {
1802
2223
  return { success: true, missing: [] };
1803
2224
  }
@@ -1827,6 +2248,54 @@ function requireEnv(name, defaultValue) {
1827
2248
  }
1828
2249
  return value;
1829
2250
  }
2251
+ var ALGORITHM = "aes-256-gcm";
2252
+ var IV_LENGTH = 12;
2253
+ var AUTH_TAG_LENGTH = 16;
2254
+ function getKey() {
2255
+ const hex = process.env.ENCRYPTION_KEY;
2256
+ if (!hex || hex.length !== 64) {
2257
+ throw new Error(
2258
+ "ENCRYPTION_KEY must be set as a 64-character hex string (32 bytes). Check your environment configuration."
2259
+ );
2260
+ }
2261
+ return Buffer.from(hex, "hex");
2262
+ }
2263
+ function encrypt(plaintext) {
2264
+ const key = getKey();
2265
+ const iv = randomBytes(IV_LENGTH);
2266
+ const cipher = createCipheriv(ALGORITHM, key, iv, { authTagLength: AUTH_TAG_LENGTH });
2267
+ const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
2268
+ const authTag = cipher.getAuthTag();
2269
+ return `${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted.toString("hex")}`;
2270
+ }
2271
+ function decrypt(ciphertext) {
2272
+ const key = getKey();
2273
+ const [ivHex, authTagHex, encryptedHex] = ciphertext.split(":");
2274
+ if (!ivHex || !authTagHex || !encryptedHex) {
2275
+ throw new Error("Invalid ciphertext format");
2276
+ }
2277
+ const iv = Buffer.from(ivHex, "hex");
2278
+ const authTag = Buffer.from(authTagHex, "hex");
2279
+ const encrypted = Buffer.from(encryptedHex, "hex");
2280
+ const decipher = createDecipheriv(ALGORITHM, key, iv, { authTagLength: AUTH_TAG_LENGTH });
2281
+ decipher.setAuthTag(authTag);
2282
+ return decipher.update(encrypted) + decipher.final("utf8");
2283
+ }
2284
+ function isEncrypted(value) {
2285
+ const parts = value.split(":");
2286
+ return parts.length === 3 && parts.every((p) => /^[0-9a-f]+$/.test(p));
2287
+ }
2288
+ function decryptIfEncrypted(value) {
2289
+ return isEncrypted(value) ? decrypt(value) : value;
2290
+ }
2291
+ function tryDecrypt(ciphertext, logger = consoleLogger) {
2292
+ try {
2293
+ return decryptIfEncrypted(ciphertext);
2294
+ } catch {
2295
+ logger.warn("Failed to decrypt stored token \u2014 likely stale or re-keyed");
2296
+ return null;
2297
+ }
2298
+ }
1830
2299
 
1831
2300
  // src/server/mcp/McpRegistry.ts
1832
2301
  var McpRegistry = class {
@@ -1863,7 +2332,9 @@ var McpRegistry = class {
1863
2332
  listTools(options) {
1864
2333
  const hasDefaultUser = !!options?.defaultUserId;
1865
2334
  if (this.customToolSpecs) {
1866
- return this.customToolSpecs.filter((s) => this.services.has(s.name)).map((svc) => this.buildToolDefinition(svc.name, svc.description, svc.methodNames, hasDefaultUser));
2335
+ return this.customToolSpecs.filter((s) => this.services.has(s.name)).map(
2336
+ (svc) => this.buildToolDefinition(svc.name, svc.description, svc.methodNames, hasDefaultUser)
2337
+ );
1867
2338
  }
1868
2339
  const tools = [];
1869
2340
  for (const [serviceName] of this.services) {
@@ -1898,11 +2369,7 @@ var McpRegistry = class {
1898
2369
  const startTime = Date.now();
1899
2370
  try {
1900
2371
  const validatedPayload = this.validatePayload(method, payload);
1901
- await instance.ensureAccessForMethod(
1902
- method.access,
1903
- ctx,
1904
- entryId
1905
- );
2372
+ await instance.ensureAccessForMethod(method.access, ctx, entryId);
1906
2373
  const result = await method.handler(validatedPayload, {
1907
2374
  userId,
1908
2375
  socketId: ctx.id,
@@ -1954,9 +2421,7 @@ var McpRegistry = class {
1954
2421
  if (method.schema) {
1955
2422
  const result = method.schema.safeParse(payload);
1956
2423
  if (!result.success) {
1957
- throw new Error(
1958
- `Validation error: ${result.error?.message ?? "invalid payload"}`
1959
- );
2424
+ throw new Error(`Validation error: ${result.error?.message ?? "invalid payload"}`);
1960
2425
  }
1961
2426
  return result.data;
1962
2427
  }
@@ -2053,12 +2518,7 @@ function createMcpStdioServer(options) {
2053
2518
  }
2054
2519
  const userId = args?.userId ?? defaultUserId ?? process.env.MCP_USER_ID ?? "mcp-anonymous";
2055
2520
  const payload = args?.payload;
2056
- const result = await registry.invoke(
2057
- serviceName,
2058
- methodName,
2059
- payload,
2060
- userId
2061
- );
2521
+ const result = await registry.invoke(serviceName, methodName, payload, userId);
2062
2522
  return {
2063
2523
  jsonrpc: "2.0",
2064
2524
  id,
@@ -2181,12 +2641,7 @@ function createMcpRoutes(router, options) {
2181
2641
  res.status(400).json({ error: "Missing service or method" });
2182
2642
  return;
2183
2643
  }
2184
- const result = await registry.invoke(
2185
- service,
2186
- method,
2187
- toolPayload,
2188
- tokenPayload.userId
2189
- );
2644
+ const result = await registry.invoke(service, method, toolPayload, tokenPayload.userId);
2190
2645
  res.json({ success: true, data: result });
2191
2646
  } catch (error) {
2192
2647
  const message = error instanceof Error ? error.message : "Internal error";
@@ -2464,10 +2919,7 @@ function generateToolMetadata(options) {
2464
2919
  allServices.push(serviceInfo);
2465
2920
  if (docsOutputDir) {
2466
2921
  const doc = generateServiceDoc(serviceInfo);
2467
- const outputPath = path.join(
2468
- docsOutputDir,
2469
- `${serviceInfo.className}.md`
2470
- );
2922
+ const outputPath = path.join(docsOutputDir, `${serviceInfo.className}.md`);
2471
2923
  fs.writeFileSync(outputPath, doc);
2472
2924
  }
2473
2925
  }
@@ -2488,15 +2940,12 @@ Auto-generated documentation for all services.
2488
2940
  }
2489
2941
  const toolsContent = generateToolsFileContent(allServices);
2490
2942
  fs.writeFileSync(toolsOutputPath, toolsContent);
2491
- const totalMethods = allServices.reduce(
2492
- (sum, s) => sum + s.methods.length,
2493
- 0
2494
- );
2943
+ const totalMethods = allServices.reduce((sum, s) => sum + s.methods.length, 0);
2495
2944
  console.log(
2496
2945
  `Generated ${allServices.length} service tools (${totalMethods} methods) -> ${toolsOutputPath}`
2497
2946
  );
2498
2947
  }
2499
2948
 
2500
- export { BaseService, McpRegistry, ServiceRegistry, applyRateLimitMiddleware, bootstrapMcpServer, checkEnv, createJWT, createMcpRoutes, createMcpStdioServer, createOAuthURL, createQuickdrawServer, createRateLimiter, createTieredRateLimiter, discordProvider, exchangeOAuthCode, generateToolMetadata, getDefaultEntityFields, googleProvider, isRedisAdapterAvailable, mergeWithDefaultFields, requireEnv, setupRedisAdapter, validateEnv, verifyJWT, zodToAdminFields };
2949
+ export { BaseService, DEFAULT_MOCK_PATH_PREFIX, McpRegistry, OAUTH_RETURN_ORIGIN_COOKIE, SESSION_COOKIE, ServiceRegistry, applyRateLimitMiddleware, bootstrapMcpServer, checkEnv, clearSessionCookie, createJWT, createMcpRoutes, createMcpStdioServer, createMockOAuthProvider, createOAuthURL, createQuickdrawServer, createRateLimiter, createRequireAuth, createTieredRateLimiter, decrypt, decryptIfEncrypted, discordProvider, encrypt, exchangeOAuthCode, extractBearerOrCookieToken, generateToolMetadata, getDefaultEntityFields, getDiscordAvatarUrl, googleProvider, isEncrypted, isMockOAuthEnabled, isRedisAdapterAvailable, mergeWithDefaultFields, registerMockOAuthProvider, requireEnv, setSessionCookie, setupRedisAdapter, tryDecrypt, validateEnv, validateRedirectOrigin, verifyJWT, zodToAdminFields };
2501
2950
  //# sourceMappingURL=index.js.map
2502
2951
  //# sourceMappingURL=index.js.map