@fitzzero/quickdraw-core 3.6.0 → 3.7.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 (30) hide show
  1. package/README.md +113 -64
  2. package/dist/client/index.js +32 -61
  3. package/dist/client/index.js.map +1 -1
  4. package/dist/client/testing.js.map +1 -1
  5. package/dist/server/express/index.d.ts +37 -0
  6. package/dist/server/express/index.js +60 -0
  7. package/dist/server/express/index.js.map +1 -0
  8. package/dist/server/index.d.ts +227 -1
  9. package/dist/server/index.js +400 -142
  10. package/dist/server/index.js.map +1 -1
  11. package/dist/server/testing-prisma.d.ts +101 -0
  12. package/dist/server/testing-prisma.js +172 -0
  13. package/dist/server/testing-prisma.js.map +1 -0
  14. package/dist/server/testing.js +5 -22
  15. package/dist/server/testing.js.map +1 -1
  16. package/dist/shared/index.js.map +1 -1
  17. package/eslint-config/index.mjs +1 -2
  18. package/eslint-plugin-quickdraw/index.mjs +15 -7
  19. package/eslint-plugin-quickdraw/rules/no-cross-service-mutations.mjs +31 -32
  20. package/eslint-plugin-quickdraw/rules/no-direct-prisma-mutations.mjs +17 -18
  21. package/eslint-plugin-quickdraw/rules/no-raw-button-strings.mjs +52 -0
  22. package/eslint-plugin-quickdraw/rules/no-raw-service-room-string.mjs +140 -0
  23. package/eslint-plugin-quickdraw/rules/no-raw-socket-emit.mjs +20 -22
  24. package/eslint-plugin-quickdraw/rules/no-raw-socket-on.mjs +17 -17
  25. package/eslint-plugin-quickdraw/rules/no-raw-tooltip-strings.mjs +59 -0
  26. package/eslint-plugin-quickdraw/rules/no-raw-typography-strings.mjs +52 -0
  27. package/eslint-plugin-quickdraw/rules/no-service-method-record.mjs +3 -3
  28. package/eslint-plugin-quickdraw/rules/no-unsafe-payload-cast.mjs +8 -8
  29. package/eslint-plugin-quickdraw/rules/require-zod-schema.mjs +15 -20
  30. package/package.json +74 -50
@@ -2,6 +2,7 @@ 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';
@@ -16,18 +17,8 @@ var consoleLogger = {
16
17
  };
17
18
 
18
19
  // 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
- ]);
20
+ var NON_EDITABLE_FIELDS = /* @__PURE__ */ new Set(["id", "createdAt", "updatedAt", "created_at", "updated_at"]);
21
+ var DEFAULT_HIDDEN_FIELDS = /* @__PURE__ */ new Set(["acl", "serviceAccess", "service_access"]);
31
22
  function toLabel(fieldName) {
32
23
  return fieldName.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
33
24
  }
@@ -68,9 +59,7 @@ function getEnumValues(zodType) {
68
59
  }
69
60
  if (typeName === "ZodNativeEnum") {
70
61
  const enumObj = zodType._def.values;
71
- return Object.values(enumObj).filter(
72
- (v) => typeof v === "string"
73
- );
62
+ return Object.values(enumObj).filter((v) => typeof v === "string");
74
63
  }
75
64
  return void 0;
76
65
  }
@@ -167,13 +156,9 @@ function getDefaultEntityFields() {
167
156
  }
168
157
  function mergeWithDefaultFields(schemaFields, options = {}) {
169
158
  const { includeDefaults = ["id", "createdAt"] } = options;
170
- const defaultFields = getDefaultEntityFields().filter(
171
- (f) => includeDefaults.includes(f.name)
172
- );
159
+ const defaultFields = getDefaultEntityFields().filter((f) => includeDefaults.includes(f.name));
173
160
  const defaultNames = new Set(defaultFields.map((f) => f.name));
174
- const filteredSchemaFields = schemaFields.filter(
175
- (f) => !defaultNames.has(f.name)
176
- );
161
+ const filteredSchemaFields = schemaFields.filter((f) => !defaultNames.has(f.name));
177
162
  return [...defaultFields, ...filteredSchemaFields];
178
163
  }
179
164
 
@@ -258,9 +243,7 @@ var BaseService = class {
258
243
  this.subscribers.get(entryId).add(socket);
259
244
  const roomName = this.getRoomName(entryId);
260
245
  void socket.join(roomName);
261
- this.logger.debug(
262
- `User ${socket.userId} subscribed to ${entryId} (room: ${roomName})`
263
- );
246
+ this.logger.debug(`User ${socket.userId} subscribed to ${entryId} (room: ${roomName})`);
264
247
  const entity = await this.findById(entryId);
265
248
  if (!entity) return null;
266
249
  return this.filterEntityForSubscriber(entity, socket, entryId);
@@ -303,11 +286,7 @@ var BaseService = class {
303
286
  results[entryId] = null;
304
287
  continue;
305
288
  }
306
- results[entryId] = this.filterEntityForSubscriber(
307
- entity,
308
- socket,
309
- entryId
310
- );
289
+ results[entryId] = this.filterEntityForSubscriber(entity, socket, entryId);
311
290
  }
312
291
  this.logger.debug(
313
292
  `User ${socket.userId} batch-subscribed to ${allowedIds.length}/${entryIds.length} entities`
@@ -325,12 +304,7 @@ var BaseService = class {
325
304
  const results = /* @__PURE__ */ new Map();
326
305
  await Promise.all(
327
306
  entryIds.map(async (id) => {
328
- const allowed = await this.checkSubscriptionAccess(
329
- userId,
330
- id,
331
- requiredLevel,
332
- socket
333
- );
307
+ const allowed = await this.checkSubscriptionAccess(userId, id, requiredLevel, socket);
334
308
  results.set(id, allowed);
335
309
  })
336
310
  );
@@ -344,9 +318,7 @@ var BaseService = class {
344
318
  * Default: fetches each entity individually in parallel.
345
319
  */
346
320
  async findByIds(ids) {
347
- const results = await Promise.all(
348
- ids.map((id) => this.findById(id))
349
- );
321
+ const results = await Promise.all(ids.map((id) => this.findById(id)));
350
322
  return results.filter((e) => e !== null);
351
323
  }
352
324
  /**
@@ -410,9 +382,7 @@ var BaseService = class {
410
382
  return;
411
383
  }
412
384
  anySocket.to(roomName).emit(eventName, data);
413
- this.logger.debug(
414
- `Emitted ${eventName} to room ${roomName} (via socket fallback)`
415
- );
385
+ this.logger.debug(`Emitted ${eventName} to room ${roomName} (via socket fallback)`);
416
386
  }
417
387
  /**
418
388
  * Emit a custom event to a specific user's room.
@@ -689,10 +659,7 @@ var BaseService = class {
689
659
  schema: options?.schema,
690
660
  resolveEntryId: options?.resolveEntryId
691
661
  };
692
- this.publicMethods.set(
693
- name,
694
- definition
695
- );
662
+ this.publicMethods.set(name, definition);
696
663
  return definition;
697
664
  }
698
665
  /**
@@ -725,9 +692,7 @@ var BaseService = class {
725
692
  }
726
693
  }
727
694
  if (missing.length > 0) {
728
- throw new Error(
729
- `${this.serviceName}: Missing method implementations: ${missing.join(", ")}`
730
- );
695
+ throw new Error(`${this.serviceName}: Missing method implementations: ${missing.join(", ")}`);
731
696
  }
732
697
  }
733
698
  // ===========================================================================
@@ -758,15 +723,7 @@ var BaseService = class {
758
723
  * ```
759
724
  */
760
725
  installAdminMethods(options) {
761
- const {
762
- expose,
763
- access,
764
- schema,
765
- displayName,
766
- tableColumns,
767
- hiddenFields,
768
- fieldOverrides
769
- } = options;
726
+ const { expose, access, schema, displayName, tableColumns, hiddenFields, fieldOverrides } = options;
770
727
  if (schema) {
771
728
  const schemaFields = zodToAdminFields(schema, {
772
729
  hiddenFields,
@@ -820,9 +777,7 @@ var BaseService = class {
820
777
  name: "adminCreate",
821
778
  access: access.create,
822
779
  handler: async (payload, _context) => {
823
- return await this.adminCreate(
824
- payload.data
825
- );
780
+ return await this.adminCreate(payload.data);
826
781
  }
827
782
  });
828
783
  }
@@ -862,9 +817,7 @@ var BaseService = class {
862
817
  name: "adminGetSubscribers",
863
818
  access: access.getSubscribers,
864
819
  handler: async (payload, _context) => {
865
- return this.adminGetSubscribers(
866
- payload.entryId
867
- );
820
+ return this.adminGetSubscribers(payload.entryId);
868
821
  },
869
822
  resolveEntryId: (payload) => payload.entryId
870
823
  });
@@ -874,9 +827,7 @@ var BaseService = class {
874
827
  name: "adminReemit",
875
828
  access: access.reemit,
876
829
  handler: async (payload, _context) => {
877
- return await this.adminReemit(
878
- payload.entryId
879
- );
830
+ return await this.adminReemit(payload.entryId);
880
831
  },
881
832
  resolveEntryId: (payload) => payload.entryId
882
833
  });
@@ -886,16 +837,12 @@ var BaseService = class {
886
837
  name: "adminUnsubscribeAll",
887
838
  access: access.unsubscribeAll,
888
839
  handler: async (payload, _context) => {
889
- return this.adminUnsubscribeAll(
890
- payload.entryId
891
- );
840
+ return this.adminUnsubscribeAll(payload.entryId);
892
841
  },
893
842
  resolveEntryId: (payload) => payload.entryId
894
843
  });
895
844
  }
896
- const installedMethods = Object.keys(expose).filter(
897
- (k) => expose[k]
898
- );
845
+ const installedMethods = Object.keys(expose).filter((k) => expose[k]);
899
846
  if (shouldExposeMeta) installedMethods.push("meta");
900
847
  this.logger.info(`Installed admin methods: ${installedMethods.join(", ")}`);
901
848
  }
@@ -1091,21 +1038,9 @@ var ServiceRegistry = class {
1091
1038
  this.registerMethodListener(socket, eventName, method, service);
1092
1039
  }
1093
1040
  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
- );
1041
+ this.registerSubscriptionListener(socket, `${serviceName}:subscribe`, service);
1042
+ this.registerBatchSubscriptionListener(socket, `${serviceName}:batchSubscribe`, service);
1043
+ this.registerUnsubscriptionListener(socket, `${serviceName}:unsubscribe`, service);
1109
1044
  }
1110
1045
  }
1111
1046
  /**
@@ -1238,10 +1173,7 @@ var ServiceRegistry = class {
1238
1173
  if (this.logPayloads) {
1239
1174
  logContext.payload = payload;
1240
1175
  }
1241
- this.logger.error(
1242
- `User ${userIdShort} ${serviceName}.${methodName} -> fail`,
1243
- logContext
1244
- );
1176
+ this.logger.error(`User ${userIdShort} ${serviceName}.${methodName} -> fail`, logContext);
1245
1177
  const errorResponse = {
1246
1178
  success: false,
1247
1179
  error: error instanceof Error ? error.message : "Internal error",
@@ -1437,9 +1369,7 @@ function createQuickdrawServer(options) {
1437
1369
  });
1438
1370
  httpServer.listen(options.port, "0.0.0.0", () => {
1439
1371
  serverLogger.info(`Server listening on port ${options.port}`);
1440
- serverLogger.info(
1441
- `Registered services: ${registry.getServices().join(", ")}`
1442
- );
1372
+ serverLogger.info(`Registered services: ${registry.getServices().join(", ")}`);
1443
1373
  });
1444
1374
  const shutdown = async (signal) => {
1445
1375
  serverLogger.info(`Received ${signal}, starting graceful shutdown...`);
@@ -1475,13 +1405,7 @@ function createQuickdrawServer(options) {
1475
1405
  // src/server/redis.ts
1476
1406
  async function setupRedisAdapter(io, options = {}) {
1477
1407
  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;
1408
+ const { host = "localhost", port = 6379, password, db = 0, keyPrefix = "socket.io" } = options;
1485
1409
  try {
1486
1410
  const dynamicImport = new Function("specifier", "return import(specifier)");
1487
1411
  const [redisAdapterModule, redisModule] = await Promise.all([
@@ -1497,7 +1421,9 @@ async function setupRedisAdapter(io, options = {}) {
1497
1421
  });
1498
1422
  const subClient = pubClient.duplicate();
1499
1423
  await Promise.all([pubClient.connect(), subClient.connect()]);
1500
- io.adapter(createAdapter(pubClient, subClient, { key: keyPrefix }));
1424
+ io.adapter(
1425
+ createAdapter(pubClient, subClient, { key: keyPrefix })
1426
+ );
1501
1427
  logger.info(`Redis adapter connected to ${host}:${port}`);
1502
1428
  const cleanup = async () => {
1503
1429
  try {
@@ -1529,10 +1455,7 @@ async function setupRedisAdapter(io, options = {}) {
1529
1455
  async function isRedisAdapterAvailable() {
1530
1456
  try {
1531
1457
  const dynamicImport = new Function("specifier", "return import(specifier)");
1532
- await Promise.all([
1533
- dynamicImport("@socket.io/redis-adapter"),
1534
- dynamicImport("redis")
1535
- ]);
1458
+ await Promise.all([dynamicImport("@socket.io/redis-adapter"), dynamicImport("redis")]);
1536
1459
  return true;
1537
1460
  } catch {
1538
1461
  return false;
@@ -1541,11 +1464,7 @@ async function isRedisAdapterAvailable() {
1541
1464
 
1542
1465
  // src/server/rateLimit.ts
1543
1466
  function createRateLimiter(options = {}) {
1544
- const {
1545
- windowMs = 6e4,
1546
- maxRequests = 100,
1547
- excludeEvents = []
1548
- } = options;
1467
+ const { windowMs = 6e4, maxRequests = 100, excludeEvents = [] } = options;
1549
1468
  const entries = /* @__PURE__ */ new Map();
1550
1469
  const cleanupInterval = setInterval(() => {
1551
1470
  const now = Date.now();
@@ -1769,6 +1688,13 @@ var discordProvider = {
1769
1688
  };
1770
1689
  }
1771
1690
  };
1691
+ function getDiscordAvatarUrl(user) {
1692
+ if (user.avatar) {
1693
+ return `https://cdn.discordapp.com/avatars/${user.id}/${user.avatar}.png`;
1694
+ }
1695
+ const index = user.discriminator !== "0" ? parseInt(user.discriminator, 10) % 5 : (BigInt(user.id) >> BigInt(22)) % BigInt(6);
1696
+ return `https://cdn.discordapp.com/embed/avatars/${index}.png`;
1697
+ }
1772
1698
 
1773
1699
  // src/server/auth/google.ts
1774
1700
  var googleProvider = {
@@ -1789,6 +1715,306 @@ var googleProvider = {
1789
1715
  }
1790
1716
  };
1791
1717
 
1718
+ // src/server/auth/validateOrigin.ts
1719
+ var OAUTH_RETURN_ORIGIN_COOKIE = "oauth_return_origin";
1720
+ var CODESPACE_REGEX = /^https:\/\/[a-z0-9-]+-[a-z0-9-]+-\d+\.app\.github\.dev$/;
1721
+ var LOCALHOST_REGEX = /^http:\/\/localhost:\d+$/;
1722
+ function envExtraOrigins() {
1723
+ const raw = process.env.EXTRA_ALLOWED_ORIGINS;
1724
+ if (!raw) return [];
1725
+ return raw.split(",").map((s) => s.trim()).filter(Boolean);
1726
+ }
1727
+ function validateRedirectOrigin(origin, options = {}) {
1728
+ if (!origin) return null;
1729
+ const clientUrl = options.clientUrl ?? process.env.CLIENT_URL;
1730
+ let parsedOrigin;
1731
+ try {
1732
+ parsedOrigin = new URL(origin);
1733
+ } catch {
1734
+ return null;
1735
+ }
1736
+ const cleanOrigin = parsedOrigin.origin;
1737
+ if (clientUrl && cleanOrigin === clientUrl) {
1738
+ return cleanOrigin;
1739
+ }
1740
+ const extra = options.extraAllowedOrigins ?? envExtraOrigins();
1741
+ if (extra.includes(cleanOrigin)) {
1742
+ return cleanOrigin;
1743
+ }
1744
+ if (options.allowedPatterns?.some((pattern) => pattern.test(cleanOrigin))) {
1745
+ return cleanOrigin;
1746
+ }
1747
+ if ((options.allowCodespaces ?? true) && CODESPACE_REGEX.test(cleanOrigin)) {
1748
+ return cleanOrigin;
1749
+ }
1750
+ if ((options.allowLocalhostInDev ?? true) && process.env.NODE_ENV !== "production" && LOCALHOST_REGEX.test(cleanOrigin)) {
1751
+ return cleanOrigin;
1752
+ }
1753
+ return null;
1754
+ }
1755
+
1756
+ // src/server/auth/mock.ts
1757
+ var DEFAULT_MOCK_PATH_PREFIX = "/auth/mock/provider";
1758
+ var CODE_TTL_MS = 5 * 60 * 1e3;
1759
+ var TOKEN_TTL_MS = 60 * 60 * 1e3;
1760
+ 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+$/;
1761
+ function isMockOAuthEnabled() {
1762
+ return process.env.ENABLE_MOCK_OAUTH === "true" && process.env.NODE_ENV !== "production";
1763
+ }
1764
+ function createMockOAuthProvider(apiBaseUrl, options = {}) {
1765
+ const prefix = options.pathPrefix ?? DEFAULT_MOCK_PATH_PREFIX;
1766
+ const publicBase = apiBaseUrl.replace(/\/$/, "");
1767
+ const internalBase = (options.internalBaseUrl ?? apiBaseUrl).replace(/\/$/, "");
1768
+ return {
1769
+ name: "mock",
1770
+ authorizationUrl: `${publicBase}${prefix}/authorize`,
1771
+ tokenUrl: `${internalBase}${prefix}/token`,
1772
+ userInfoUrl: `${internalBase}${prefix}/userinfo`,
1773
+ scopes: ["openid", "email", "profile"],
1774
+ parseUserInfo: (data) => {
1775
+ const d = data;
1776
+ return {
1777
+ id: String(d.id),
1778
+ email: String(d.email),
1779
+ name: String(d.name),
1780
+ picture: d.picture ? String(d.picture) : null,
1781
+ verified_email: true
1782
+ };
1783
+ }
1784
+ };
1785
+ }
1786
+ function getQueryParam(req, name) {
1787
+ const fromQuery = req.query?.[name];
1788
+ if (typeof fromQuery === "string") return fromQuery;
1789
+ const url = req.originalUrl ?? req.url;
1790
+ if (!url) return null;
1791
+ const idx = url.indexOf("?");
1792
+ if (idx === -1) return null;
1793
+ return new URLSearchParams(url.slice(idx + 1)).get(name);
1794
+ }
1795
+ async function readBodyParams(req) {
1796
+ if (typeof req.body === "string") return new URLSearchParams(req.body);
1797
+ if (req.body && typeof req.body === "object" && Object.keys(req.body).length > 0) {
1798
+ return new URLSearchParams(req.body);
1799
+ }
1800
+ const chunks = [];
1801
+ for await (const chunk of req) {
1802
+ chunks.push(Buffer.from(chunk));
1803
+ }
1804
+ return new URLSearchParams(Buffer.concat(chunks).toString("utf8"));
1805
+ }
1806
+ function escapeHtml(value) {
1807
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
1808
+ }
1809
+ function isAllowedRedirectUri(uri, allowedOrigins) {
1810
+ let parsed;
1811
+ try {
1812
+ parsed = new URL(uri);
1813
+ } catch {
1814
+ return false;
1815
+ }
1816
+ const origin = parsed.origin;
1817
+ if (allowedOrigins.includes(origin)) return true;
1818
+ if (DEV_PRIVATE_ORIGIN_REGEX.test(origin)) return true;
1819
+ return validateRedirectOrigin(origin) !== null;
1820
+ }
1821
+ function renderPickerPage(users, authorizePath, redirectUri, state) {
1822
+ const links = users.map((user) => {
1823
+ const params = new URLSearchParams({ redirect_uri: redirectUri, email: user.email });
1824
+ if (state) params.set("state", state);
1825
+ const href = `${authorizePath}?${params.toString()}`;
1826
+ return `<li><a href="${escapeHtml(href)}"><strong>${escapeHtml(user.name)}</strong><span>${escapeHtml(user.email)}</span></a></li>`;
1827
+ }).join("\n");
1828
+ return `<!doctype html>
1829
+ <html lang="en">
1830
+ <head>
1831
+ <meta charset="utf-8" />
1832
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
1833
+ <title>Mock sign-in</title>
1834
+ <style>
1835
+ body { font-family: system-ui, sans-serif; background: #111; color: #eee; display: flex; min-height: 100vh; align-items: center; justify-content: center; margin: 0; }
1836
+ main { max-width: 420px; width: 100%; padding: 2rem; }
1837
+ h1 { font-size: 1.25rem; }
1838
+ p { color: #999; font-size: 0.875rem; }
1839
+ ul { list-style: none; padding: 0; display: flex; flex-direction: column; gap: 0.5rem; }
1840
+ 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; }
1841
+ a:hover { border-color: #888; background: #1a1a1a; }
1842
+ a span { color: #999; font-size: 0.8125rem; }
1843
+ </style>
1844
+ </head>
1845
+ <body>
1846
+ <main>
1847
+ <h1>Mock sign-in</h1>
1848
+ <p>Development only \u2014 pick an account to continue. Run your seed script if this list is empty.</p>
1849
+ <ul>
1850
+ ${links || "<li><em>No users found.</em></li>"}
1851
+ </ul>
1852
+ </main>
1853
+ </body>
1854
+ </html>`;
1855
+ }
1856
+ function registerMockOAuthProvider(router, options) {
1857
+ const logger = options.logger ?? consoleLogger;
1858
+ if (!isMockOAuthEnabled()) {
1859
+ if (process.env.ENABLE_MOCK_OAUTH === "true") {
1860
+ logger.error("Mock OAuth is enabled but NODE_ENV is production \u2014 refusing to mount routes");
1861
+ }
1862
+ return false;
1863
+ }
1864
+ const prefix = options.pathPrefix ?? DEFAULT_MOCK_PATH_PREFIX;
1865
+ const allowedOrigins = options.allowedRedirectOrigins ?? [];
1866
+ const codes = /* @__PURE__ */ new Map();
1867
+ const tokens = /* @__PURE__ */ new Map();
1868
+ function sweep(store) {
1869
+ const now = Date.now();
1870
+ for (const [key, entry] of store) {
1871
+ if (entry.expiresAt < now) store.delete(key);
1872
+ }
1873
+ }
1874
+ function guard(res) {
1875
+ if (process.env.NODE_ENV === "production") {
1876
+ res.status(404).json({ error: "Not found" });
1877
+ return false;
1878
+ }
1879
+ return true;
1880
+ }
1881
+ router.get(`${prefix}/authorize`, async (req, res) => {
1882
+ if (!guard(res)) return;
1883
+ const redirectUri = getQueryParam(req, "redirect_uri");
1884
+ const state = getQueryParam(req, "state");
1885
+ const email = getQueryParam(req, "email");
1886
+ if (!redirectUri || !isAllowedRedirectUri(redirectUri, allowedOrigins)) {
1887
+ res.status(400).json({ error: "Invalid redirect_uri" });
1888
+ return;
1889
+ }
1890
+ const users = await options.listUsers();
1891
+ if (!email) {
1892
+ res.status(200).send(renderPickerPage(users, `${prefix}/authorize`, redirectUri, state));
1893
+ return;
1894
+ }
1895
+ const user = users.find((u) => u.email === email);
1896
+ if (!user) {
1897
+ res.status(400).json({ error: "Unknown mock user" });
1898
+ return;
1899
+ }
1900
+ sweep(codes);
1901
+ const code = randomBytes(24).toString("hex");
1902
+ codes.set(code, { email: user.email, expiresAt: Date.now() + CODE_TTL_MS });
1903
+ const params = new URLSearchParams({ code });
1904
+ if (state) params.set("state", state);
1905
+ res.redirect(`${redirectUri}${redirectUri.includes("?") ? "&" : "?"}${params.toString()}`);
1906
+ });
1907
+ router.post(`${prefix}/token`, async (req, res) => {
1908
+ if (!guard(res)) return;
1909
+ const body = await readBodyParams(req);
1910
+ const code = body.get("code");
1911
+ const entry = code ? codes.get(code) : void 0;
1912
+ if (code) codes.delete(code);
1913
+ if (!entry || entry.expiresAt < Date.now()) {
1914
+ res.status(400).json({ error: "invalid_grant" });
1915
+ return;
1916
+ }
1917
+ sweep(tokens);
1918
+ const accessToken = randomBytes(24).toString("hex");
1919
+ tokens.set(accessToken, { email: entry.email, expiresAt: Date.now() + TOKEN_TTL_MS });
1920
+ res.status(200).json({
1921
+ access_token: accessToken,
1922
+ token_type: "Bearer",
1923
+ expires_in: Math.floor(TOKEN_TTL_MS / 1e3),
1924
+ scope: "openid email profile"
1925
+ });
1926
+ });
1927
+ router.get(`${prefix}/userinfo`, async (req, res) => {
1928
+ if (!guard(res)) return;
1929
+ const auth = req.headers.authorization;
1930
+ const accessToken = typeof auth === "string" && auth.startsWith("Bearer ") ? auth.slice(7) : null;
1931
+ const entry = accessToken ? tokens.get(accessToken) : void 0;
1932
+ if (!entry || entry.expiresAt < Date.now()) {
1933
+ res.status(401).json({ error: "invalid_token" });
1934
+ return;
1935
+ }
1936
+ const users = await options.listUsers();
1937
+ const user = users.find((u) => u.email === entry.email);
1938
+ if (!user) {
1939
+ res.status(401).json({ error: "invalid_token" });
1940
+ return;
1941
+ }
1942
+ res.status(200).json({
1943
+ // providerAccountId = email: stable across re-seeds, unlike row ids.
1944
+ id: user.email,
1945
+ email: user.email,
1946
+ name: user.name,
1947
+ picture: user.picture ?? null,
1948
+ verified_email: true
1949
+ });
1950
+ });
1951
+ logger.info(`Mock OAuth provider mounted at ${prefix} (development only)`);
1952
+ return true;
1953
+ }
1954
+
1955
+ // src/server/auth/sessionCookie.ts
1956
+ var SESSION_COOKIE = "session";
1957
+ var DEFAULT_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1e3;
1958
+ function cookieOptions(options) {
1959
+ const isProd = process.env.NODE_ENV === "production";
1960
+ const domain = options.domain ?? process.env.COOKIE_DOMAIN;
1961
+ return {
1962
+ httpOnly: true,
1963
+ secure: isProd,
1964
+ // SameSite=None (with Secure) lets the session cookie ride cross-site
1965
+ // fetches, so a secondary web origin can hit the primary API host. The
1966
+ // CORS allowlist (validateRedirectOrigin) is the actual origin gate. Dev
1967
+ // keeps Lax — localhost is http-only and SameSite=None requires Secure.
1968
+ sameSite: isProd ? "none" : "lax",
1969
+ path: "/",
1970
+ ...domain && { domain }
1971
+ };
1972
+ }
1973
+ function setSessionCookie(res, jwt, options = {}) {
1974
+ res.cookie(options.cookieName ?? SESSION_COOKIE, jwt, {
1975
+ ...cookieOptions(options),
1976
+ maxAge: options.maxAgeMs ?? DEFAULT_MAX_AGE_MS
1977
+ });
1978
+ }
1979
+ function clearSessionCookie(res, options = {}) {
1980
+ res.clearCookie(options.cookieName ?? SESSION_COOKIE, cookieOptions(options));
1981
+ }
1982
+
1983
+ // src/server/auth/restMiddleware.ts
1984
+ function extractBearerOrCookieToken(req, cookieName = SESSION_COOKIE) {
1985
+ const cookieToken = req.cookies?.[cookieName];
1986
+ if (cookieToken) return cookieToken;
1987
+ const authHeader = req.headers.authorization;
1988
+ if (authHeader?.startsWith("Bearer ")) return authHeader.slice(7);
1989
+ return null;
1990
+ }
1991
+ function createRequireAuth(options) {
1992
+ return async (req, res, next) => {
1993
+ const token = extractBearerOrCookieToken(req, options.cookieName);
1994
+ if (!token) {
1995
+ res.status(401).json({ error: "Missing or invalid authorization" });
1996
+ return;
1997
+ }
1998
+ const secret = options.jwtSecret ?? process.env.JWT_SECRET;
1999
+ if (!secret) {
2000
+ res.status(401).json({ error: "Authentication is not configured" });
2001
+ return;
2002
+ }
2003
+ const payload = await verifyJWT(token, secret);
2004
+ if (!payload?.userId) {
2005
+ res.status(401).json({ error: "Invalid or expired token" });
2006
+ return;
2007
+ }
2008
+ const session = await options.getSession(token);
2009
+ if (!session || session.expiresAt < /* @__PURE__ */ new Date()) {
2010
+ res.status(401).json({ error: "Session expired or revoked" });
2011
+ return;
2012
+ }
2013
+ req.userId = payload.userId;
2014
+ next();
2015
+ };
2016
+ }
2017
+
1792
2018
  // src/server/utils/env.ts
1793
2019
  function validateEnv(options) {
1794
2020
  const result = checkEnv(options);
@@ -1797,7 +2023,11 @@ function validateEnv(options) {
1797
2023
  }
1798
2024
  }
1799
2025
  function checkEnv(options) {
1800
- const { required, productionOnly = true, errorPrefix = "Missing required environment variables" } = options;
2026
+ const {
2027
+ required,
2028
+ productionOnly = true,
2029
+ errorPrefix = "Missing required environment variables"
2030
+ } = options;
1801
2031
  if (productionOnly && process.env.NODE_ENV !== "production") {
1802
2032
  return { success: true, missing: [] };
1803
2033
  }
@@ -1827,6 +2057,54 @@ function requireEnv(name, defaultValue) {
1827
2057
  }
1828
2058
  return value;
1829
2059
  }
2060
+ var ALGORITHM = "aes-256-gcm";
2061
+ var IV_LENGTH = 12;
2062
+ var AUTH_TAG_LENGTH = 16;
2063
+ function getKey() {
2064
+ const hex = process.env.ENCRYPTION_KEY;
2065
+ if (!hex || hex.length !== 64) {
2066
+ throw new Error(
2067
+ "ENCRYPTION_KEY must be set as a 64-character hex string (32 bytes). Check your environment configuration."
2068
+ );
2069
+ }
2070
+ return Buffer.from(hex, "hex");
2071
+ }
2072
+ function encrypt(plaintext) {
2073
+ const key = getKey();
2074
+ const iv = randomBytes(IV_LENGTH);
2075
+ const cipher = createCipheriv(ALGORITHM, key, iv, { authTagLength: AUTH_TAG_LENGTH });
2076
+ const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
2077
+ const authTag = cipher.getAuthTag();
2078
+ return `${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted.toString("hex")}`;
2079
+ }
2080
+ function decrypt(ciphertext) {
2081
+ const key = getKey();
2082
+ const [ivHex, authTagHex, encryptedHex] = ciphertext.split(":");
2083
+ if (!ivHex || !authTagHex || !encryptedHex) {
2084
+ throw new Error("Invalid ciphertext format");
2085
+ }
2086
+ const iv = Buffer.from(ivHex, "hex");
2087
+ const authTag = Buffer.from(authTagHex, "hex");
2088
+ const encrypted = Buffer.from(encryptedHex, "hex");
2089
+ const decipher = createDecipheriv(ALGORITHM, key, iv, { authTagLength: AUTH_TAG_LENGTH });
2090
+ decipher.setAuthTag(authTag);
2091
+ return decipher.update(encrypted) + decipher.final("utf8");
2092
+ }
2093
+ function isEncrypted(value) {
2094
+ const parts = value.split(":");
2095
+ return parts.length === 3 && parts.every((p) => /^[0-9a-f]+$/.test(p));
2096
+ }
2097
+ function decryptIfEncrypted(value) {
2098
+ return isEncrypted(value) ? decrypt(value) : value;
2099
+ }
2100
+ function tryDecrypt(ciphertext, logger = consoleLogger) {
2101
+ try {
2102
+ return decryptIfEncrypted(ciphertext);
2103
+ } catch {
2104
+ logger.warn("Failed to decrypt stored token \u2014 likely stale or re-keyed");
2105
+ return null;
2106
+ }
2107
+ }
1830
2108
 
1831
2109
  // src/server/mcp/McpRegistry.ts
1832
2110
  var McpRegistry = class {
@@ -1863,7 +2141,9 @@ var McpRegistry = class {
1863
2141
  listTools(options) {
1864
2142
  const hasDefaultUser = !!options?.defaultUserId;
1865
2143
  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));
2144
+ return this.customToolSpecs.filter((s) => this.services.has(s.name)).map(
2145
+ (svc) => this.buildToolDefinition(svc.name, svc.description, svc.methodNames, hasDefaultUser)
2146
+ );
1867
2147
  }
1868
2148
  const tools = [];
1869
2149
  for (const [serviceName] of this.services) {
@@ -1898,11 +2178,7 @@ var McpRegistry = class {
1898
2178
  const startTime = Date.now();
1899
2179
  try {
1900
2180
  const validatedPayload = this.validatePayload(method, payload);
1901
- await instance.ensureAccessForMethod(
1902
- method.access,
1903
- ctx,
1904
- entryId
1905
- );
2181
+ await instance.ensureAccessForMethod(method.access, ctx, entryId);
1906
2182
  const result = await method.handler(validatedPayload, {
1907
2183
  userId,
1908
2184
  socketId: ctx.id,
@@ -1954,9 +2230,7 @@ var McpRegistry = class {
1954
2230
  if (method.schema) {
1955
2231
  const result = method.schema.safeParse(payload);
1956
2232
  if (!result.success) {
1957
- throw new Error(
1958
- `Validation error: ${result.error?.message ?? "invalid payload"}`
1959
- );
2233
+ throw new Error(`Validation error: ${result.error?.message ?? "invalid payload"}`);
1960
2234
  }
1961
2235
  return result.data;
1962
2236
  }
@@ -2053,12 +2327,7 @@ function createMcpStdioServer(options) {
2053
2327
  }
2054
2328
  const userId = args?.userId ?? defaultUserId ?? process.env.MCP_USER_ID ?? "mcp-anonymous";
2055
2329
  const payload = args?.payload;
2056
- const result = await registry.invoke(
2057
- serviceName,
2058
- methodName,
2059
- payload,
2060
- userId
2061
- );
2330
+ const result = await registry.invoke(serviceName, methodName, payload, userId);
2062
2331
  return {
2063
2332
  jsonrpc: "2.0",
2064
2333
  id,
@@ -2181,12 +2450,7 @@ function createMcpRoutes(router, options) {
2181
2450
  res.status(400).json({ error: "Missing service or method" });
2182
2451
  return;
2183
2452
  }
2184
- const result = await registry.invoke(
2185
- service,
2186
- method,
2187
- toolPayload,
2188
- tokenPayload.userId
2189
- );
2453
+ const result = await registry.invoke(service, method, toolPayload, tokenPayload.userId);
2190
2454
  res.json({ success: true, data: result });
2191
2455
  } catch (error) {
2192
2456
  const message = error instanceof Error ? error.message : "Internal error";
@@ -2464,10 +2728,7 @@ function generateToolMetadata(options) {
2464
2728
  allServices.push(serviceInfo);
2465
2729
  if (docsOutputDir) {
2466
2730
  const doc = generateServiceDoc(serviceInfo);
2467
- const outputPath = path.join(
2468
- docsOutputDir,
2469
- `${serviceInfo.className}.md`
2470
- );
2731
+ const outputPath = path.join(docsOutputDir, `${serviceInfo.className}.md`);
2471
2732
  fs.writeFileSync(outputPath, doc);
2472
2733
  }
2473
2734
  }
@@ -2488,15 +2749,12 @@ Auto-generated documentation for all services.
2488
2749
  }
2489
2750
  const toolsContent = generateToolsFileContent(allServices);
2490
2751
  fs.writeFileSync(toolsOutputPath, toolsContent);
2491
- const totalMethods = allServices.reduce(
2492
- (sum, s) => sum + s.methods.length,
2493
- 0
2494
- );
2752
+ const totalMethods = allServices.reduce((sum, s) => sum + s.methods.length, 0);
2495
2753
  console.log(
2496
2754
  `Generated ${allServices.length} service tools (${totalMethods} methods) -> ${toolsOutputPath}`
2497
2755
  );
2498
2756
  }
2499
2757
 
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 };
2758
+ 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
2759
  //# sourceMappingURL=index.js.map
2502
2760
  //# sourceMappingURL=index.js.map