@fitzzero/quickdraw-core 3.7.0 → 3.9.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.
@@ -1,5 +1,5 @@
1
1
  import { Socket } from 'socket.io-client';
2
- import { B as BaseServiceInstance, Q as QuickdrawServerResult } from '../types-DniMxkQs.js';
2
+ import { B as BaseServiceInstance, Q as QuickdrawServerResult } from '../types-ClgSr90l.js';
3
3
  import 'socket.io';
4
4
  import 'http';
5
5
  import 'zod';
@@ -6,6 +6,9 @@ import { Server } from 'socket.io';
6
6
  // src/server/testing.ts
7
7
 
8
8
  // src/shared/types.ts
9
+ function channelEventName(serviceName, channelName) {
10
+ return `channel:${serviceName}:${channelName}`;
11
+ }
9
12
  var consoleLogger = {
10
13
  info: (message, meta) => console.log(`[INFO] ${message}`, meta ?? ""),
11
14
  warn: (message, meta) => console.warn(`[WARN] ${message}`, meta ?? ""),
@@ -15,6 +18,8 @@ var consoleLogger = {
15
18
  };
16
19
 
17
20
  // src/server/ServiceRegistry.ts
21
+ var CHANNEL_ABUSE_WINDOW_MS = 1e4;
22
+ var CHANNEL_ABUSE_MULTIPLIER = 100;
18
23
  var ServiceRegistry = class {
19
24
  io;
20
25
  services = /* @__PURE__ */ new Map();
@@ -25,6 +30,8 @@ var ServiceRegistry = class {
25
30
  connectionHandlerInstalled = false;
26
31
  // Store method metadata for deferred registration
27
32
  methodRegistry = /* @__PURE__ */ new Map();
33
+ // Store channel metadata for deferred registration
34
+ channelRegistry = /* @__PURE__ */ new Map();
28
35
  constructor(io, options) {
29
36
  this.io = io;
30
37
  this.logger = options?.logger?.child({ service: "ServiceRegistry" }) ?? consoleLogger.child({ service: "ServiceRegistry" });
@@ -58,6 +65,9 @@ var ServiceRegistry = class {
58
65
  for (const [eventName, { method, service }] of this.methodRegistry) {
59
66
  this.registerMethodListener(socket, eventName, method, service);
60
67
  }
68
+ for (const [eventName, { channel, service }] of this.channelRegistry) {
69
+ this.registerChannelListener(socket, eventName, channel, service);
70
+ }
61
71
  for (const [serviceName, service] of this.services) {
62
72
  this.registerSubscriptionListener(socket, `${serviceName}:subscribe`, service);
63
73
  this.registerBatchSubscriptionListener(socket, `${serviceName}:batchSubscribe`, service);
@@ -74,6 +84,7 @@ var ServiceRegistry = class {
74
84
  }
75
85
  this.logger.info(`Registered service: ${serviceName}`);
76
86
  this.discoverServiceMethods(serviceName, service);
87
+ this.discoverServiceChannels(serviceName, service);
77
88
  this.installConnectionHandler();
78
89
  }
79
90
  /**
@@ -91,6 +102,77 @@ var ServiceRegistry = class {
91
102
  });
92
103
  }
93
104
  }
105
+ /**
106
+ * Discover and store all channels from a service.
107
+ */
108
+ discoverServiceChannels(serviceName, service) {
109
+ const channels = service.getPublicChannels?.() ?? [];
110
+ for (const channel of channels) {
111
+ const eventName = channelEventName(serviceName, channel.name);
112
+ this.logger.info(
113
+ `Registering channel event: ${eventName} (${channel.ratePerSecond}/s, burst ${channel.burst})`
114
+ );
115
+ this.channelRegistry.set(eventName, {
116
+ serviceName,
117
+ channel,
118
+ service
119
+ });
120
+ }
121
+ }
122
+ /**
123
+ * Register a channel as a socket event listener.
124
+ *
125
+ * This is the hot path for high-frequency traffic: no ack, no async access
126
+ * checks, no per-message logging. Order of operations per message:
127
+ * token bucket → schema validation → in-memory access check → handler.
128
+ * Any failure silently drops the message; sustained extreme flooding
129
+ * disconnects the socket.
130
+ */
131
+ registerChannelListener(socket, eventName, channel, service) {
132
+ let tokens = channel.burst;
133
+ let lastRefillAt = Date.now();
134
+ let abuseWindowStart = lastRefillAt;
135
+ let droppedInWindow = 0;
136
+ socket.on(eventName, (payload) => {
137
+ const now = Date.now();
138
+ tokens = Math.min(
139
+ channel.burst,
140
+ tokens + (now - lastRefillAt) / 1e3 * channel.ratePerSecond
141
+ );
142
+ lastRefillAt = now;
143
+ if (tokens < 1) {
144
+ if (now - abuseWindowStart > CHANNEL_ABUSE_WINDOW_MS) {
145
+ abuseWindowStart = now;
146
+ droppedInWindow = 0;
147
+ }
148
+ droppedInWindow++;
149
+ if (droppedInWindow > channel.ratePerSecond * CHANNEL_ABUSE_MULTIPLIER) {
150
+ this.logger.warn(
151
+ `Disconnecting socket ${socket.id} for sustained channel flooding on ${eventName}`,
152
+ { userId: socket.userId, droppedInWindow }
153
+ );
154
+ socket.disconnect(true);
155
+ }
156
+ return;
157
+ }
158
+ tokens -= 1;
159
+ const parsed = channel.schema.safeParse(payload);
160
+ if (!parsed.success) return;
161
+ if (!service.checkChannelAccess?.(channel, socket, parsed.data)) return;
162
+ try {
163
+ channel.handler(parsed.data, {
164
+ userId: socket.userId,
165
+ socketId: socket.id,
166
+ serviceAccess: socket.serviceAccess ?? {}
167
+ });
168
+ } catch (error) {
169
+ this.logger.error(`Channel handler error for ${eventName}`, {
170
+ userId: socket.userId,
171
+ error: error instanceof Error ? error.message : "Unknown error"
172
+ });
173
+ }
174
+ });
175
+ }
94
176
  /**
95
177
  * Register a method as a socket event listener.
96
178
  */
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/shared/types.ts","../../src/server/ServiceRegistry.ts","../../src/server/createServer.ts","../../src/server/testing.ts"],"names":["result","SocketIOServer","ioClient"],"mappings":";;;;;;;;AA0SO,IAAM,aAAA,GAAwB;AAAA,EACnC,IAAA,EAAM,CAAC,OAAA,EAAS,IAAA,KAAS,OAAA,CAAQ,IAAI,CAAA,OAAA,EAAU,OAAO,CAAA,CAAA,EAAI,IAAA,IAAQ,EAAE,CAAA;AAAA,EACpE,IAAA,EAAM,CAAC,OAAA,EAAS,IAAA,KAAS,OAAA,CAAQ,KAAK,CAAA,OAAA,EAAU,OAAO,CAAA,CAAA,EAAI,IAAA,IAAQ,EAAE,CAAA;AAAA,EACrE,KAAA,EAAO,CAAC,OAAA,EAAS,IAAA,KAAS,OAAA,CAAQ,MAAM,CAAA,QAAA,EAAW,OAAO,CAAA,CAAA,EAAI,IAAA,IAAQ,EAAE,CAAA;AAAA,EACxE,KAAA,EAAO,CAAC,OAAA,EAAS,IAAA,KAAS,OAAA,CAAQ,MAAM,CAAA,QAAA,EAAW,OAAO,CAAA,CAAA,EAAI,IAAA,IAAQ,EAAE,CAAA;AAAA,EACxE,OAAO,MAAM;AACf,CAAA;;;AC7QO,IAAM,kBAAN,MAAyD;AAAA,EAC7C,EAAA;AAAA,EACA,QAAA,uBAAiD,GAAA,EAAI;AAAA,EACrD,MAAA;AAAA,EACA,oBAAA;AAAA,EACA,WAAA;AAAA,EACA,YAAA;AAAA,EACT,0BAAA,GAA6B,KAAA;AAAA;AAAA,EAGpB,cAAA,uBAOT,GAAA,EAAI;AAAA,EAEZ,WAAA,CAAY,IAAoB,OAAA,EAAkC;AAChE,IAAA,IAAA,CAAK,EAAA,GAAK,EAAA;AACV,IAAA,IAAA,CAAK,MAAA,GACH,OAAA,EAAS,MAAA,EAAQ,KAAA,CAAM,EAAE,OAAA,EAAS,iBAAA,EAAmB,CAAA,IACrD,aAAA,CAAc,KAAA,CAAM,EAAE,OAAA,EAAS,mBAAmB,CAAA;AAGpD,IAAA,IAAA,CAAK,oBAAA,GAAuB,OAAA,EAAS,aAAA,EAAe,OAAA,IAAW,IAAA;AAC/D,IAAA,IAAA,CAAK,WAAA,GAAc,OAAA,EAAS,aAAA,EAAe,WAAA,IAAe,KAAA;AAC1D,IAAA,IAAA,CAAK,YAAA,GAAe,OAAA,EAAS,aAAA,EAAe,YAAA,IAAgB,KAAA;AAE5D,IAAA,IAAI,KAAK,oBAAA,EAAsB;AAC7B,MAAA,IAAA,CAAK,MAAA,CAAO,KAAK,kCAAA,EAAoC;AAAA,QACnD,aAAa,IAAA,CAAK,WAAA;AAAA,QAClB,cAAc,IAAA,CAAK;AAAA,OACpB,CAAA;AAAA,IACH;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMQ,wBAAA,GAAiC;AACvC,IAAA,IAAI,KAAK,0BAAA,EAA4B;AACrC,IAAA,IAAA,CAAK,0BAAA,GAA6B,IAAA;AAElC,IAAA,IAAA,CAAK,EAAA,CAAG,EAAA,CAAG,YAAA,EAAc,CAAC,MAAA,KAAW;AACnC,MAAA,MAAM,eAAA,GAAkB,MAAA;AACxB,MAAA,IAAA,CAAK,qBAAqB,eAAe,CAAA;AAAA,IAC3C,CAAC,CAAA;AAED,IAAA,IAAA,CAAK,MAAA,CAAO,KAAK,sDAAsD,CAAA;AAAA,EACzE;AAAA;AAAA;AAAA;AAAA,EAKQ,qBAAqB,MAAA,EAA+B;AAE1D,IAAA,KAAA,MAAW,CAAC,WAAW,EAAE,MAAA,EAAQ,SAAS,CAAA,IAAK,KAAK,cAAA,EAAgB;AAClE,MAAA,IAAA,CAAK,sBAAA,CAAuB,MAAA,EAAQ,SAAA,EAAW,MAAA,EAAQ,OAAO,CAAA;AAAA,IAChE;AAGA,IAAA,KAAA,MAAW,CAAC,WAAA,EAAa,OAAO,CAAA,IAAK,KAAK,QAAA,EAAU;AAClD,MAAA,IAAA,CAAK,4BAAA,CAA6B,MAAA,EAAQ,CAAA,EAAG,WAAW,cAAc,OAAO,CAAA;AAC7E,MAAA,IAAA,CAAK,iCAAA,CAAkC,MAAA,EAAQ,CAAA,EAAG,WAAW,mBAAmB,OAAO,CAAA;AACvF,MAAA,IAAA,CAAK,8BAAA,CAA+B,MAAA,EAAQ,CAAA,EAAG,WAAW,gBAAgB,OAAO,CAAA;AAAA,IACnF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKO,eAAA,CAAgB,aAAqB,OAAA,EAAoC;AAC9E,IAAA,IAAA,CAAK,QAAA,CAAS,GAAA,CAAI,WAAA,EAAa,OAAO,CAAA;AAGtC,IAAA,IAAI,QAAQ,KAAA,EAAO;AACjB,MAAA,OAAA,CAAQ,KAAA,CAAM,KAAK,EAAE,CAAA;AAAA,IACvB;AAEA,IAAA,IAAA,CAAK,MAAA,CAAO,IAAA,CAAK,CAAA,oBAAA,EAAuB,WAAW,CAAA,CAAE,CAAA;AAGrD,IAAA,IAAA,CAAK,sBAAA,CAAuB,aAAa,OAAO,CAAA;AAGhD,IAAA,IAAA,CAAK,wBAAA,EAAyB;AAAA,EAChC;AAAA;AAAA;AAAA;AAAA,EAKQ,sBAAA,CAAuB,aAAqB,OAAA,EAAoC;AACtF,IAAA,MAAM,OAAA,GAAU,QAAQ,gBAAA,EAAiB;AAEzC,IAAA,KAAA,MAAW,UAAU,OAAA,EAAS;AAC5B,MAAA,MAAM,SAAA,GAAY,CAAA,EAAG,WAAW,CAAA,CAAA,EAAI,OAAO,IAAI,CAAA,CAAA;AAC/C,MAAA,IAAA,CAAK,MAAA,CAAO,IAAA,CAAK,CAAA,0BAAA,EAA6B,SAAS,CAAA,CAAE,CAAA;AAGzD,MAAA,IAAA,CAAK,cAAA,CAAe,IAAI,SAAA,EAAW;AAAA,QACjC,WAAA;AAAA,QACA,MAAA;AAAA,QACA;AAAA,OACD,CAAA;AAAA,IACH;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKQ,sBAAA,CACN,MAAA,EACA,SAAA,EACA,MAAA,EACA,OAAA,EACM;AACN,IAAA,MAAA,CAAO,EAAA;AAAA,MACL,SAAA;AAAA,MACA,OAAO,SAAkB,QAAA,KAA4D;AACnF,QAAA,MAAM,SAAA,GAAY,KAAK,GAAA,EAAI;AAC3B,QAAA,MAAM,CAAC,WAAA,EAAa,UAAU,CAAA,GAAI,SAAA,CAAU,MAAM,GAAG,CAAA;AACrD,QAAA,MAAM,cAAc,MAAA,CAAO,MAAA,EAAQ,KAAA,CAAM,CAAA,EAAG,CAAC,CAAA,IAAK,MAAA;AAElD,QAAA,IAAI;AAEF,UAAA,IAAI,CAAC,MAAA,CAAO,MAAA,IAAU,MAAA,CAAO,WAAW,QAAA,EAAU;AAChD,YAAA,MAAM,aAAA,GAA0C;AAAA,cAC9C,OAAA,EAAS,KAAA;AAAA,cACT,KAAA,EAAO,yBAAA;AAAA,cACP,IAAA,EAAM;AAAA,aACR;AACA,YAAA,QAAA,GAAW,aAAa,CAAA;AACxB,YAAA;AAAA,UACF;AAGA,UAAA,IAAI,gBAAA,GAAmB,OAAA;AACvB,UAAA,IAAI,OAAO,MAAA,EAAQ;AACjB,YAAA,MAAMA,OAAAA,GAAS,MAAA,CAAO,MAAA,CAAO,SAAA,CAAU,OAAO,CAAA;AAC9C,YAAA,IAAI,CAACA,QAAO,OAAA,EAAS;AACnB,cAAA,MAAM,aAAA,GAA0C;AAAA,gBAC9C,OAAA,EAAS,KAAA;AAAA,gBACT,KAAA,EAAO,CAAA,kBAAA,EAAqBA,OAAAA,CAAO,KAAA,CAAM,OAAO,CAAA,CAAA;AAAA,gBAChD,IAAA,EAAM;AAAA,eACR;AACA,cAAA,QAAA,GAAW,aAAa,CAAA;AACxB,cAAA;AAAA,YACF;AACA,YAAA,gBAAA,GAAmBA,OAAAA,CAAO,IAAA;AAAA,UAC5B;AAGA,UAAA,IAAI,OAAA;AACJ,UAAA,IAAI,OAAO,cAAA,EAAgB;AACzB,YAAA,OAAA,GAAU,MAAA,CAAO,cAAA,CAAe,gBAAgB,CAAA,IAAK,KAAA,CAAA;AAAA,UACvD,WACE,gBAAA,IACA,OAAO,gBAAA,KAAqB,QAAA,IAC5B,QAAQ,gBAAA,EACR;AACA,YAAA,MAAM,UAAW,gBAAA,CAA6C,EAAA;AAC9D,YAAA,IAAI,OAAO,YAAY,QAAA,EAAU;AAC/B,cAAA,OAAA,GAAU,OAAA;AAAA,YACZ;AAAA,UACF;AAGA,UAAA,MAAM,OAAA,CAAQ,qBAAA,CAAsB,MAAA,CAAO,MAAA,EAAQ,QAAQ,OAAO,CAAA;AAGlE,UAAA,IAAI,KAAK,oBAAA,EAAsB;AAC7B,YAAA,MAAM,UAAA,GAAsC;AAAA,cAC1C,QAAA,EAAU,oBAAA;AAAA,cACV,WAAA;AAAA,cACA,UAAA;AAAA,cACA,QAAQ,MAAA,CAAO,MAAA;AAAA,cACf,UAAU,MAAA,CAAO;AAAA,aACnB;AACA,YAAA,IAAI,KAAK,WAAA,EAAa;AACpB,cAAA,UAAA,CAAW,OAAA,GAAU,gBAAA;AAAA,YACvB;AACA,YAAA,IAAA,CAAK,MAAA,CAAO,IAAA;AAAA,cACV,CAAA,KAAA,EAAQ,WAAW,CAAA,CAAA,EAAI,WAAW,IAAI,UAAU,CAAA,SAAA,CAAA;AAAA,cAChD;AAAA,aACF;AAAA,UACF;AAGA,UAAA,MAAM,MAAA,GAAS,MAAM,MAAA,CAAO,OAAA,CAAQ,gBAAA,EAAkB;AAAA,YACpD,QAAQ,MAAA,CAAO,MAAA;AAAA,YACf,UAAU,MAAA,CAAO,EAAA;AAAA,YACjB,aAAA,EAAe,MAAA,CAAO,aAAA,IAAiB;AAAC,WACzC,CAAA;AAGD,UAAA,IAAI,KAAK,oBAAA,EAAsB;AAC7B,YAAA,MAAM,UAAA,GAAa,IAAA,CAAK,GAAA,EAAI,GAAI,SAAA;AAChC,YAAA,MAAM,UAAA,GAAsC;AAAA,cAC1C,QAAA,EAAU,oBAAA;AAAA,cACV,OAAA,EAAS,SAAA;AAAA,cACT,UAAA;AAAA,cACA,WAAA;AAAA,cACA,UAAA;AAAA,cACA,QAAQ,MAAA,CAAO;AAAA,aACjB;AACA,YAAA,IAAI,KAAK,YAAA,EAAc;AACrB,cAAA,UAAA,CAAW,QAAA,GAAW,MAAA;AAAA,YACxB;AACA,YAAA,IAAA,CAAK,MAAA,CAAO,IAAA;AAAA,cACV,CAAA,KAAA,EAAQ,WAAW,CAAA,CAAA,EAAI,WAAW,IAAI,UAAU,CAAA,WAAA,CAAA;AAAA,cAChD;AAAA,aACF;AAAA,UACF;AAEA,UAAA,MAAM,eAAA,GAA4C;AAAA,YAChD,OAAA,EAAS,IAAA;AAAA,YACT,IAAA,EAAM;AAAA,WACR;AACA,UAAA,QAAA,GAAW,eAAe,CAAA;AAAA,QAC5B,SAAS,KAAA,EAAO;AAEd,UAAA,MAAM,UAAA,GAAa,IAAA,CAAK,GAAA,EAAI,GAAI,SAAA;AAChC,UAAA,MAAM,UAAA,GAAsC;AAAA,YAC1C,QAAA,EAAU,oBAAA;AAAA,YACV,OAAA,EAAS,MAAA;AAAA,YACT,UAAA;AAAA,YACA,WAAA;AAAA,YACA,UAAA;AAAA,YACA,QAAQ,MAAA,CAAO,MAAA;AAAA,YACf,UAAU,MAAA,CAAO,EAAA;AAAA,YACjB,KAAA,EAAO,KAAA,YAAiB,KAAA,GAAQ,KAAA,CAAM,OAAA,GAAU;AAAA,WAClD;AACA,UAAA,IAAI,KAAK,WAAA,EAAa;AACpB,YAAA,UAAA,CAAW,OAAA,GAAU,OAAA;AAAA,UACvB;AACA,UAAA,IAAA,CAAK,MAAA,CAAO,MAAM,CAAA,KAAA,EAAQ,WAAW,IAAI,WAAW,CAAA,CAAA,EAAI,UAAU,CAAA,QAAA,CAAA,EAAY,UAAU,CAAA;AAExF,UAAA,MAAM,aAAA,GAA0C;AAAA,YAC9C,OAAA,EAAS,KAAA;AAAA,YACT,KAAA,EAAO,KAAA,YAAiB,KAAA,GAAQ,KAAA,CAAM,OAAA,GAAU,gBAAA;AAAA,YAChD,IAAA,EAAM;AAAA,WACR;AACA,UAAA,QAAA,GAAW,aAAa,CAAA;AAAA,QAC1B;AAAA,MACF;AAAA,KACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKQ,4BAAA,CACN,MAAA,EACA,SAAA,EACA,OAAA,EACM;AACN,IAAA,MAAA,CAAO,EAAA;AAAA,MACL,SAAA;AAAA,MACA,OACE,SACA,QAAA,KACG;AACH,QAAA,IAAI;AACF,UAAA,IAAI,CAAC,OAAO,MAAA,EAAQ;AAClB,YAAA,QAAA,GAAW;AAAA,cACT,OAAA,EAAS,KAAA;AAAA,cACT,KAAA,EAAO,yBAAA;AAAA,cACP,IAAA,EAAM;AAAA,aACP,CAAA;AACD,YAAA;AAAA,UACF;AAEA,UAAA,MAAM,IAAA,GAAO,MAAM,OAAA,CAAQ,SAAA;AAAA,YACzB,OAAA,CAAQ,OAAA;AAAA,YACR,MAAA;AAAA,YACC,QAAQ,aAAA,IAAmD;AAAA,WAC9D;AAEA,UAAA,IAAI,SAAS,IAAA,EAAM;AACjB,YAAA,QAAA,GAAW;AAAA,cACT,OAAA,EAAS,KAAA;AAAA,cACT,KAAA,EAAO,kCAAA;AAAA,cACP,IAAA,EAAM;AAAA,aACP,CAAA;AACD,YAAA;AAAA,UACF;AAEA,UAAA,QAAA,GAAW,EAAE,OAAA,EAAS,IAAA,EAAM,IAAA,EAAM,CAAA;AAAA,QACpC,SAAS,KAAA,EAAO;AACd,UAAA,IAAA,CAAK,MAAA,CAAO,KAAA,CAAM,CAAA,sBAAA,EAAyB,SAAS,CAAA,CAAA,CAAA,EAAK;AAAA,YACvD,KAAA,EAAO,KAAA,YAAiB,KAAA,GAAQ,KAAA,CAAM,OAAA,GAAU;AAAA,WACjD,CAAA;AACD,UAAA,QAAA,GAAW;AAAA,YACT,OAAA,EAAS,KAAA;AAAA,YACT,KAAA,EAAO,KAAA,YAAiB,KAAA,GAAQ,KAAA,CAAM,OAAA,GAAU;AAAA,WACjD,CAAA;AAAA,QACH;AAAA,MACF;AAAA,KACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMQ,iCAAA,CACN,MAAA,EACA,SAAA,EACA,OAAA,EACM;AACN,IAAA,MAAA,CAAO,EAAA;AAAA,MACL,SAAA;AAAA,MACA,OACE,SACA,QAAA,KACG;AACH,QAAA,IAAI;AACF,UAAA,IAAI,CAAC,OAAO,MAAA,EAAQ;AAClB,YAAA,QAAA,GAAW;AAAA,cACT,OAAA,EAAS,KAAA;AAAA,cACT,KAAA,EAAO,yBAAA;AAAA,cACP,IAAA,EAAM;AAAA,aACP,CAAA;AACD,YAAA;AAAA,UACF;AAEA,UAAA,IAAI,CAAC,MAAM,OAAA,CAAQ,OAAA,CAAQ,QAAQ,CAAA,IAAK,OAAA,CAAQ,QAAA,CAAS,MAAA,KAAW,CAAA,EAAG;AACrE,YAAA,QAAA,GAAW;AAAA,cACT,OAAA,EAAS,KAAA;AAAA,cACT,KAAA,EAAO,oCAAA;AAAA,cACP,IAAA,EAAM;AAAA,aACP,CAAA;AACD,YAAA;AAAA,UACF;AAEA,UAAA,MAAM,IAAA,GAAO,MAAM,OAAA,CAAQ,cAAA;AAAA,YACzB,OAAA,CAAQ,QAAA;AAAA,YACR,MAAA;AAAA,YACC,QAAQ,aAAA,IAAmD;AAAA,WAC9D;AAEA,UAAA,QAAA,GAAW,EAAE,OAAA,EAAS,IAAA,EAAM,IAAA,EAAM,CAAA;AAAA,QACpC,SAAS,KAAA,EAAO;AACd,UAAA,IAAA,CAAK,MAAA,CAAO,KAAA,CAAM,CAAA,4BAAA,EAA+B,SAAS,CAAA,CAAA,CAAA,EAAK;AAAA,YAC7D,KAAA,EAAO,KAAA,YAAiB,KAAA,GAAQ,KAAA,CAAM,OAAA,GAAU;AAAA,WACjD,CAAA;AACD,UAAA,QAAA,GAAW;AAAA,YACT,OAAA,EAAS,KAAA;AAAA,YACT,KAAA,EAAO,KAAA,YAAiB,KAAA,GAAQ,KAAA,CAAM,OAAA,GAAU;AAAA,WACjD,CAAA;AAAA,QACH;AAAA,MACF;AAAA,KACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKQ,8BAAA,CACN,MAAA,EACA,SAAA,EACA,OAAA,EACM;AACN,IAAA,MAAA,CAAO,EAAA;AAAA,MACL,SAAA;AAAA,MACA,CACE,SACA,QAAA,KACG;AACH,QAAA,IAAI;AACF,UAAA,OAAA,CAAQ,WAAA,CAAY,OAAA,CAAQ,OAAA,EAAS,MAAM,CAAA;AAC3C,UAAA,QAAA,GAAW;AAAA,YACT,OAAA,EAAS,IAAA;AAAA,YACT,MAAM,EAAE,YAAA,EAAc,IAAA,EAAM,OAAA,EAAS,QAAQ,OAAA;AAAQ,WACtD,CAAA;AAAA,QACH,SAAS,KAAA,EAAO;AACd,UAAA,IAAA,CAAK,MAAA,CAAO,KAAA,CAAM,CAAA,wBAAA,EAA2B,SAAS,CAAA,CAAA,CAAA,EAAK;AAAA,YACzD,KAAA,EAAO,KAAA,YAAiB,KAAA,GAAQ,KAAA,CAAM,OAAA,GAAU;AAAA,WACjD,CAAA;AACD,UAAA,QAAA,GAAW;AAAA,YACT,OAAA,EAAS,KAAA;AAAA,YACT,KAAA,EAAO,KAAA,YAAiB,KAAA,GAAQ,KAAA,CAAM,OAAA,GAAU;AAAA,WACjD,CAAA;AAAA,QACH;AAAA,MACF;AAAA,KACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKO,WAAA,GAAwB;AAC7B,IAAA,OAAO,KAAA,CAAM,IAAA,CAAK,IAAA,CAAK,QAAA,CAAS,MAAM,CAAA;AAAA,EACxC;AAAA;AAAA;AAAA;AAAA,EAKO,mBAAA,GAA6C;AAClD,IAAA,OAAO,KAAA,CAAM,IAAA,CAAK,IAAA,CAAK,QAAA,CAAS,QAAQ,CAAA;AAAA,EAC1C;AACF,CAAA;;;ACvZO,SAAS,sBAAsB,OAAA,EAAwD;AAC5F,EAAA,MAAM,MAAA,GAAS,QAAQ,MAAA,IAAU,aAAA;AACjC,EAAA,MAAM,eAAe,MAAA,CAAO,KAAA,CAAM,EAAE,OAAA,EAAS,mBAAmB,CAAA;AAGhE,EAAA,MAAM,MAAM,OAAA,EAAQ;AACpB,EAAA,GAAA,CAAI,GAAA,CAAI,OAAA,CAAQ,IAAA,EAAM,CAAA;AAGtB,EAAA,GAAA,CAAI,GAAA,CAAI,SAAA,EAAW,CAAC,IAAA,EAAM,GAAA,KAAQ;AAChC,IAAA,GAAA,CAAI,IAAA,CAAK,EAAE,MAAA,EAAQ,IAAA,EAAM,SAAA,EAAA,qBAAe,IAAA,EAAK,EAAE,WAAA,EAAY,EAAG,CAAA;AAAA,EAChE,CAAC,CAAA;AAGD,EAAA,MAAM,UAAA,GAAa,aAAa,GAAG,CAAA;AAGnC,EAAA,MAAM,EAAA,GAAK,IAAIC,MAAA,CAAe,UAAA,EAAY;AAAA,IACxC,IAAA,EAAM;AAAA,MACJ,MAAA,EAAQ,OAAA,CAAQ,IAAA,EAAM,MAAA,IAAU,GAAA;AAAA,MAChC,SAAS,OAAA,CAAQ,IAAA,EAAM,OAAA,IAAW,CAAC,OAAO,MAAM,CAAA;AAAA,MAChD,WAAA,EAAa,OAAA,CAAQ,IAAA,EAAM,WAAA,IAAe;AAAA;AAC5C,GACD,CAAA;AAGD,EAAA,MAAM,QAAA,GAAW,IAAI,eAAA,CAAgB,EAAA,EAAI;AAAA,IACvC,MAAA;AAAA,IACA,eAAe,OAAA,CAAQ;AAAA,GACxB,CAAA;AAGD,EAAA,EAAA,CAAG,GAAA,CAAI,OAAO,MAAA,EAAQ,IAAA,KAAS;AAC7B,IAAA,MAAM,eAAA,GAAkB,MAAA;AAExB,IAAA,IAAI;AACF,MAAA,IAAI,OAAA,CAAQ,MAAM,YAAA,EAAc;AAC9B,QAAA,MAAM,IAAA,GAAO,OAAO,SAAA,CAAU,IAAA;AAC9B,QAAA,MAAM,SAAS,MAAM,OAAA,CAAQ,IAAA,CAAK,YAAA,CAAa,iBAAiB,IAAI,CAAA;AACpE,QAAA,eAAA,CAAgB,MAAA,GAAS,MAAA;AAIzB,QAAA,eAAA,CAAgB,gBAAgB,EAAC;AAAA,MACnC;AACA,MAAA,IAAA,EAAK;AAAA,IACP,SAAS,KAAA,EAAO;AACd,MAAA,YAAA,CAAa,MAAM,uBAAA,EAAyB;AAAA,QAC1C,KAAA,EAAO,KAAA,YAAiB,KAAA,GAAQ,KAAA,CAAM,OAAA,GAAU,eAAA;AAAA,QAChD,UAAU,MAAA,CAAO;AAAA,OAClB,CAAA;AACD,MAAA,IAAA,CAAK,IAAI,KAAA,CAAM,uBAAuB,CAAC,CAAA;AAAA,IACzC;AAAA,EACF,CAAC,CAAA;AAGD,EAAA,KAAA,MAAW,CAAC,aAAa,OAAO,CAAA,IAAK,OAAO,OAAA,CAAQ,OAAA,CAAQ,QAAQ,CAAA,EAAG;AACrE,IAAA,QAAA,CAAS,eAAA,CAAgB,aAAa,OAAO,CAAA;AAAA,EAC/C;AAGA,EAAA,EAAA,CAAG,EAAA,CAAG,YAAA,EAAc,CAAC,MAAA,KAAW;AAC9B,IAAA,MAAM,eAAA,GAAkB,MAAA;AACxB,IAAA,YAAA,CAAa,KAAK,kBAAA,EAAoB;AAAA,MACpC,UAAU,eAAA,CAAgB,EAAA;AAAA,MAC1B,QAAQ,eAAA,CAAgB;AAAA,KACzB,CAAA;AAED,IAAA,eAAA,CAAgB,EAAA,CAAG,cAAc,MAAM;AACrC,MAAA,YAAA,CAAa,KAAK,qBAAA,EAAuB;AAAA,QACvC,UAAU,eAAA,CAAgB,EAAA;AAAA,QAC1B,QAAQ,eAAA,CAAgB;AAAA,OACzB,CAAA;AAGD,MAAA,KAAA,MAAW,OAAA,IAAW,QAAA,CAAS,mBAAA,EAAoB,EAAG;AACpD,QAAA,IAAI;AACF,UAAA,OAAA,CAAQ,kBAAkB,eAAe,CAAA;AAAA,QAC3C,CAAA,CAAA,MAAQ;AAAA,QAER;AAAA,MACF;AAAA,IACF,CAAC,CAAA;AAAA,EACH,CAAC,CAAA;AAGD,EAAA,UAAA,CAAW,MAAA,CAAO,OAAA,CAAQ,IAAA,EAAM,SAAA,EAAW,MAAM;AAC/C,IAAA,YAAA,CAAa,IAAA,CAAK,CAAA,yBAAA,EAA4B,OAAA,CAAQ,IAAI,CAAA,CAAE,CAAA;AAC5D,IAAA,YAAA,CAAa,IAAA,CAAK,wBAAwB,QAAA,CAAS,WAAA,GAAc,IAAA,CAAK,IAAI,CAAC,CAAA,CAAE,CAAA;AAAA,EAC/E,CAAC,CAAA;AAGD,EAAA,MAAM,QAAA,GAAW,OAAO,MAAA,KAAmB;AACzC,IAAA,YAAA,CAAa,IAAA,CAAK,CAAA,SAAA,EAAY,MAAM,CAAA,+BAAA,CAAiC,CAAA;AAGrE,IAAA,UAAA,CAAW,MAAM,MAAM;AACrB,MAAA,YAAA,CAAa,KAAK,oBAAoB,CAAA;AAAA,IACxC,CAAC,CAAA;AAGD,IAAA,MAAM,OAAA,GAAU,MAAM,EAAA,CAAG,YAAA,EAAa;AACtC,IAAA,YAAA,CAAa,IAAA,CAAK,CAAA,QAAA,EAAW,OAAA,CAAQ,MAAM,CAAA,6BAAA,CAA+B,CAAA;AAE1E,IAAA,KAAA,MAAW,UAAU,OAAA,EAAS;AAC5B,MAAA,MAAM,eAAA,GAAkB,MAAA;AAGxB,MAAA,KAAA,MAAW,OAAA,IAAW,QAAA,CAAS,mBAAA,EAAoB,EAAG;AACpD,QAAA,IAAI;AACF,UAAA,OAAA,CAAQ,kBAAkB,eAAe,CAAA;AAAA,QAC3C,CAAA,CAAA,MAAQ;AAAA,QAER;AAAA,MACF;AAGA,MAAA,MAAA,CAAO,WAAW,IAAI,CAAA;AAAA,IACxB;AAGA,IAAA,EAAA,CAAG,MAAM,MAAM;AACb,MAAA,YAAA,CAAa,KAAK,yBAAyB,CAAA;AAC3C,MAAA,OAAA,CAAQ,KAAK,CAAC,CAAA;AAAA,IAChB,CAAC,CAAA;AAGD,IAAA,UAAA,CAAW,MAAM;AACf,MAAA,YAAA,CAAa,MAAM,yCAAyC,CAAA;AAC5D,MAAA,OAAA,CAAQ,KAAK,CAAC,CAAA;AAAA,IAChB,GAAG,GAAK,CAAA;AAAA,EACV,CAAA;AAGA,EAAA,OAAA,CAAQ,EAAA,CAAG,SAAA,EAAW,MAAM,QAAA,CAAS,SAAS,CAAC,CAAA;AAC/C,EAAA,OAAA,CAAQ,EAAA,CAAG,QAAA,EAAU,MAAM,QAAA,CAAS,QAAQ,CAAC,CAAA;AAE7C,EAAA,OAAO,EAAE,EAAA,EAAI,UAAA,EAAY,QAAA,EAAS;AACpC;;;AC1IA,IAAI,QAAA,GAAW,GAAA;AAKR,SAAS,gBAAA,GAA2B;AACzC,EAAA,OAAO,QAAA,EAAA;AACT;AAsBA,eAAsB,iBAAiB,OAAA,EAAuD;AAC5F,EAAA,MAAM,OAAO,gBAAA,EAAiB;AAE9B,EAAA,MAAM,SAAS,qBAAA,CAAsB;AAAA,IACnC,IAAA;AAAA,IACA,UAAU,OAAA,CAAQ,QAAA;AAAA,IAClB,IAAA,EAAM;AAAA;AAAA,MAEJ,YAAA,EAAc,OAAO,OAAA,EAAS,IAAA,KAAS;AACrC,QAAA,OAAO,IAAA,CAAK,MAAA;AAAA,MACd;AAAA;AACF,GACD,CAAA;AAGD,EAAA,MAAM,IAAI,OAAA,CAAc,CAAC,OAAA,KAAY;AACnC,IAAA,MAAA,CAAO,UAAA,CAAW,IAAA,CAAK,WAAA,EAAa,OAAO,CAAA;AAAA,EAC7C,CAAC,CAAA;AAGD,EAAA,IAAI,QAAQ,MAAA,EAAQ;AAClB,IAAA,MAAM,QAAQ,MAAA,EAAO;AAAA,EACvB;AAEA,EAAA,OAAO;AAAA,IACL,IAAA;AAAA,IACA,MAAA;AAAA,IACA,IAAA,EAAM,MACJ,IAAI,OAAA,CAAc,CAAC,OAAA,KAAY;AAC7B,MAAA,MAAA,CAAO,GAAG,KAAA,EAAM;AAChB,MAAA,MAAA,CAAO,UAAA,CAAW,KAAA,CAAM,MAAM,OAAA,EAAS,CAAA;AAAA,IACzC,CAAC,CAAA;AAAA,IACH,SAAA,EAAW,CAAC,MAAA,KAAmB,aAAA,CAAc,MAAM,MAAM;AAAA,GAC3D;AACF;AAKA,eAAsB,aAAA,CAAc,MAAc,MAAA,EAAqC;AACrF,EAAA,MAAM,MAAA,GAASC,EAAA,CAAS,CAAA,iBAAA,EAAoB,IAAI,CAAA,CAAA,EAAI;AAAA,IAClD,IAAA,EAAM,EAAE,MAAA,EAAO;AAAA,IACf,UAAA,EAAY,CAAC,WAAW,CAAA;AAAA,IACxB,WAAA,EAAa;AAAA,GACd,CAAA;AAED,EAAA,MAAM,IAAI,OAAA,CAAc,CAAC,OAAA,EAAS,MAAA,KAAW;AAC3C,IAAA,MAAM,OAAA,GAAU,WAAW,MAAM;AAC/B,MAAA,MAAA,CAAO,IAAI,KAAA,CAAM,oBAAoB,CAAC,CAAA;AAAA,IACxC,GAAG,GAAI,CAAA;AAEP,IAAA,MAAA,CAAO,EAAA,CAAG,WAAW,MAAM;AACzB,MAAA,YAAA,CAAa,OAAO,CAAA;AACpB,MAAA,OAAA,EAAQ;AAAA,IACV,CAAC,CAAA;AAED,IAAA,MAAA,CAAO,EAAA,CAAG,eAAA,EAAiB,CAAC,GAAA,KAAQ;AAClC,MAAA,YAAA,CAAa,OAAO,CAAA;AACpB,MAAA,MAAA,CAAO,GAAG,CAAA;AAAA,IACZ,CAAC,CAAA;AAAA,EACH,CAAC,CAAA;AAED,EAAA,OAAO;AAAA,IACL,MAAA;AAAA,IACA,MAAM,CAAsB,KAAA,EAAe,YACzC,WAAA,CAAiC,MAAA,EAAQ,OAAO,OAAO,CAAA;AAAA,IACzD,KAAA,EAAO,MAAM,MAAA,CAAO,KAAA;AAAM,GAC5B;AACF;AAKO,SAAS,WAAA,CACd,MAAA,EACA,KAAA,EACA,OAAA,EACA,YAAoB,GAAA,EACA;AACpB,EAAA,OAAO,IAAI,OAAA,CAAQ,CAAC,OAAA,EAAS,MAAA,KAAW;AACtC,IAAA,MAAM,OAAA,GAAU,WAAW,MAAM;AAC/B,MAAA,MAAA,CAAO,IAAI,KAAA,CAAM,CAAA,oBAAA,EAAuB,KAAK,EAAE,CAAC,CAAA;AAAA,IAClD,GAAG,SAAS,CAAA;AAEZ,IAAA,MAAA,CAAO,IAAA,CAAK,KAAA,EAAO,OAAA,EAAS,CAAC,QAAA,KAAyC;AACpE,MAAA,YAAA,CAAa,OAAO,CAAA;AAEpB,MAAA,IAAI,SAAS,OAAA,EAAS;AACpB,QAAA,OAAA,CAAQ,SAAS,IAAI,CAAA;AAAA,MACvB,CAAA,MAAO;AACL,QAAA,MAAA,CAAO,IAAI,KAAA,CAAM,QAAA,CAAS,KAAK,CAAC,CAAA;AAAA,MAClC;AAAA,IACF,CAAC,CAAA;AAAA,EACH,CAAC,CAAA;AACH;AAKO,SAAS,YAAA,CACd,MAAA,EACA,KAAA,EACA,SAAA,GAAoB,GAAA,EACR;AACZ,EAAA,OAAO,IAAI,OAAA,CAAQ,CAAC,OAAA,EAAS,MAAA,KAAW;AACtC,IAAA,MAAM,OAAA,GAAU,WAAW,MAAM;AAC/B,MAAA,MAAA,CAAO,GAAA,CAAI,OAAO,OAAO,CAAA;AACzB,MAAA,MAAA,CAAO,IAAI,KAAA,CAAM,CAAA,0BAAA,EAA6B,KAAK,EAAE,CAAC,CAAA;AAAA,IACxD,GAAG,SAAS,CAAA;AAEZ,IAAA,MAAM,OAAA,GAAU,CAAC,IAAA,KAAY;AAC3B,MAAA,YAAA,CAAa,OAAO,CAAA;AACpB,MAAA,MAAA,CAAO,GAAA,CAAI,OAAO,OAAO,CAAA;AACzB,MAAA,OAAA,CAAQ,IAAI,CAAA;AAAA,IACd,CAAA;AAEA,IAAA,MAAA,CAAO,EAAA,CAAG,OAAO,OAAO,CAAA;AAAA,EAC1B,CAAC,CAAA;AACH","file":"testing.js","sourcesContent":["import type { z } from \"zod\";\n\n// ============================================================================\n// Access Control Types\n// ============================================================================\n\nexport type AccessLevel = \"Public\" | \"Read\" | \"Moderate\" | \"Admin\";\n\nexport type ACE = {\n userId: string;\n level: AccessLevel;\n};\n\nexport type ACL = ACE[];\n\n// ============================================================================\n// Entity and User Interfaces for ACL\n// ============================================================================\n\n/**\n * Interface for entities that support entry-level ACL stored as JSON.\n *\n * Use this pattern when:\n * - You want simple ACL without a separate membership table\n * - Access patterns are straightforward (read/write permissions)\n * - You don't need to query \"all entities user X can access\" efficiently\n *\n * The default `checkEntryACL` in BaseService expects entities to have this shape.\n *\n * @example\n * ```prisma\n * model Document {\n * id String @id @default(cuid())\n * title String\n * acl Json? // Stores ACL array\n * }\n * ```\n *\n * @example\n * ```typescript\n * class DocumentService extends BaseService<Document & ACLEntity, ...> {\n * constructor(prisma: PrismaClient) {\n * super({ serviceName: 'documentService', hasEntryACL: true });\n * this.setDelegate(prisma.document);\n * // Default checkEntryACL will work automatically\n * }\n * }\n * ```\n */\nexport interface ACLEntity {\n id: string;\n acl?: ACL | null;\n}\n\n/**\n * Interface for users with service-level access control.\n *\n * The `serviceAccess` field maps service names to access levels, granting\n * blanket permissions across all entries in that service.\n *\n * @example\n * ```prisma\n * model User {\n * id String @id @default(cuid())\n * serviceAccess Json? // { \"chatService\": \"Admin\", \"documentService\": \"Read\" }\n * }\n * ```\n *\n * @example\n * ```typescript\n * // User with Admin access to chatService can perform any operation\n * // on any chat, regardless of entry-level ACL\n * const user: QuickdrawUser = {\n * id: \"user-123\",\n * serviceAccess: { chatService: \"Admin\" }\n * };\n * ```\n */\nexport interface QuickdrawUser {\n id: string;\n serviceAccess?: Record<string, AccessLevel> | null;\n}\n\n// ============================================================================\n// Service Response Types\n// ============================================================================\n\nexport type ServiceResponse<T = unknown> =\n | { success: true; data: T }\n | { success: false; error: string; code?: number };\n\n// ============================================================================\n// Service Method Definition Types\n// ============================================================================\n\n/**\n * Defines the shape of a service method including its payload and response types.\n * Used to create a type-safe contract between server and client.\n */\nexport interface ServiceMethodDefinition<TPayload = unknown, TResponse = unknown> {\n name: string;\n access: AccessLevel;\n handler: (payload: TPayload, context: ServiceMethodContext) => Promise<TResponse>;\n schema?: z.ZodType<TPayload>;\n resolveEntryId?: (payload: TPayload) => string | null;\n}\n\n/**\n * Context provided to service method handlers\n */\nexport interface ServiceMethodContext {\n userId: string | undefined;\n socketId: string;\n serviceAccess: Record<string, AccessLevel>;\n}\n\n/**\n * Type helper for defining service method maps.\n * Each service should define its methods using this structure.\n *\n * @example\n * ```typescript\n * type ChatServiceMethods = ServiceMethodMap<{\n * createChat: {\n * payload: { title: string };\n * response: { id: string };\n * };\n * updateTitle: {\n * payload: { id: string; title: string };\n * response: { id: string; title: string };\n * };\n * }>;\n * ```\n */\nexport type ServiceMethodMap<T extends Record<string, { payload: unknown; response: unknown }>> = T;\n\n// ============================================================================\n// Admin Method Types (Generic shapes for all services)\n// ============================================================================\n\nexport type AdminListPayload = {\n page?: number;\n pageSize?: number;\n sort?: {\n field?: string;\n direction?: \"asc\" | \"desc\";\n };\n filter?: {\n id?: string;\n ids?: string[];\n createdAfter?: string;\n createdBefore?: string;\n updatedAfter?: string;\n updatedBefore?: string;\n } & Record<string, unknown>;\n};\n\nexport type AdminListResponse<T> = {\n rows: T[];\n page: number;\n pageSize: number;\n total: number;\n};\n\nexport type AdminGetPayload = { id: string };\n\nexport type AdminCreatePayload<TInsert> = { data: Partial<TInsert> };\n\nexport type AdminUpdatePayload<TInsert> = {\n id: string;\n data: Partial<TInsert>;\n};\n\nexport type AdminDeletePayload = { id: string };\nexport type AdminDeleteResponse = { id: string; deleted: true };\n\nexport type AdminSetEntryACLPayload = {\n id: string;\n acl: ACL;\n};\n\nexport type AdminGetSubscribersPayload = { id: string };\nexport type AdminGetSubscribersResponse = {\n id: string;\n subscribers: Array<{ socketId: string; userId?: string }>;\n};\n\nexport type AdminReemitPayload = { id: string };\nexport type AdminReemitResponse = { emitted: boolean };\n\nexport type AdminUnsubscribeAllPayload = { id: string };\nexport type AdminUnsubscribeAllResponse = { id: string; unsubscribed: number };\n\n// ============================================================================\n// Admin Field Configuration Types\n// ============================================================================\n\n/**\n * Supported field types for admin UI generation.\n */\nexport type AdminFieldType =\n | \"string\"\n | \"number\"\n | \"boolean\"\n | \"date\"\n | \"enum\"\n | \"json\"\n | \"relation\";\n\n/**\n * Configuration for a single field in the admin UI.\n * Derived from Zod schemas with optional overrides.\n */\nexport interface AdminFieldConfig {\n /** Field name (property key on the entity) */\n name: string;\n /** Field type for rendering appropriate input */\n type: AdminFieldType;\n /** Human-readable label */\n label: string;\n /** Whether the field is required for creation */\n required: boolean;\n /** Whether the field can be edited (false for id, createdAt, etc.) */\n editable: boolean;\n /** Whether to show this field as a table column */\n showInTable: boolean;\n /** Whether the table can be sorted by this field */\n sortable: boolean;\n /** For enum fields, the allowed values */\n enumValues?: string[];\n /** For relation fields, the related service name */\n relationService?: string;\n}\n\n/**\n * Service metadata for admin UI generation.\n * Returned by the adminMeta method.\n */\nexport interface AdminServiceMeta {\n /** Internal service name (e.g., \"chatService\") */\n serviceName: string;\n /** Human-readable display name (e.g., \"Chats\") */\n displayName: string;\n /** Field configurations derived from schema */\n fields: AdminFieldConfig[];\n}\n\n/**\n * Payload for adminMeta method (no parameters needed)\n */\nexport type AdminMetaPayload = Record<string, never>;\n\n/**\n * Response type for adminMeta method\n */\nexport type AdminMetaResponse = AdminServiceMeta;\n\n// ============================================================================\n// Subscription Types\n// ============================================================================\n\nexport type SubscribePayload = {\n entryId: string;\n requiredLevel?: AccessLevel;\n};\n\nexport type UnsubscribePayload = {\n entryId: string;\n};\n\n// ============================================================================\n// Utility Types\n// ============================================================================\n\n/**\n * Extracts payload type from a service method definition\n */\nexport type ExtractPayload<T> = T extends { payload: infer P } ? P : never;\n\n/**\n * Extracts response type from a service method definition\n */\nexport type ExtractResponse<T> = T extends { response: infer R } ? R : never;\n\n/**\n * Logger interface compatible with Winston and other loggers\n */\nexport interface Logger {\n info(message: string, meta?: Record<string, unknown>): void;\n warn(message: string, meta?: Record<string, unknown>): void;\n error(message: string, meta?: Record<string, unknown>): void;\n debug(message: string, meta?: Record<string, unknown>): void;\n child(options: Record<string, unknown>): Logger;\n}\n\n/**\n * Default console logger implementation\n */\nexport const consoleLogger: Logger = {\n info: (message, meta) => console.log(`[INFO] ${message}`, meta ?? \"\"),\n warn: (message, meta) => console.warn(`[WARN] ${message}`, meta ?? \"\"),\n error: (message, meta) => console.error(`[ERROR] ${message}`, meta ?? \"\"),\n debug: (message, meta) => console.debug(`[DEBUG] ${message}`, meta ?? \"\"),\n child: () => consoleLogger,\n};\n","import type { Server as SocketIOServer } from \"socket.io\";\nimport type { ServiceResponse, ServiceMethodDefinition, Logger } from \"../shared/types\";\nimport { consoleLogger } from \"../shared/types\";\nimport type { QuickdrawSocket, BaseServiceInstance, ServiceRegistryInstance } from \"./types\";\n\n/**\n * Central registry for auto-discovering and registering service methods as Socket.io events.\n *\n * Features:\n * - Auto-discovers public methods from registered services\n * - Wires methods to Socket.io events with standardized naming\n * - Handles subscription lifecycle\n * - Provides consistent error handling and logging\n * - Uses a single connection handler for all services (memory efficient)\n *\n * @example\n * ```typescript\n * const registry = new ServiceRegistry(io, { logger });\n * registry.registerService('chatService', chatService);\n * registry.registerService('userService', userService);\n * ```\n */\nexport interface ServiceRegistryOptions {\n logger?: Logger;\n /**\n * Automatic method logging configuration.\n * @default { enabled: true, logPayloads: false, logResponses: false }\n */\n methodLogging?: {\n enabled?: boolean;\n logPayloads?: boolean;\n logResponses?: boolean;\n };\n}\n\nexport class ServiceRegistry implements ServiceRegistryInstance {\n private readonly io: SocketIOServer;\n private readonly services: Map<string, BaseServiceInstance> = new Map();\n private readonly logger: Logger;\n private readonly methodLoggingEnabled: boolean;\n private readonly logPayloads: boolean;\n private readonly logResponses: boolean;\n private connectionHandlerInstalled = false;\n\n // Store method metadata for deferred registration\n private readonly methodRegistry: Map<\n string,\n {\n serviceName: string;\n method: ServiceMethodDefinition<unknown, unknown>;\n service: BaseServiceInstance;\n }\n > = new Map();\n\n constructor(io: SocketIOServer, options?: ServiceRegistryOptions) {\n this.io = io;\n this.logger =\n options?.logger?.child({ service: \"ServiceRegistry\" }) ??\n consoleLogger.child({ service: \"ServiceRegistry\" });\n\n // Method logging configuration (enabled by default)\n this.methodLoggingEnabled = options?.methodLogging?.enabled ?? true;\n this.logPayloads = options?.methodLogging?.logPayloads ?? false;\n this.logResponses = options?.methodLogging?.logResponses ?? false;\n\n if (this.methodLoggingEnabled) {\n this.logger.info(\"Automatic method logging enabled\", {\n logPayloads: this.logPayloads,\n logResponses: this.logResponses,\n });\n }\n }\n\n /**\n * Install the single connection handler that wires all registered services.\n * This is called lazily on first service registration.\n */\n private installConnectionHandler(): void {\n if (this.connectionHandlerInstalled) return;\n this.connectionHandlerInstalled = true;\n\n this.io.on(\"connection\", (socket) => {\n const quickdrawSocket = socket as QuickdrawSocket;\n this.setupSocketListeners(quickdrawSocket);\n });\n\n this.logger.info(\"Installed single connection handler for all services\");\n }\n\n /**\n * Set up all event listeners for a newly connected socket.\n */\n private setupSocketListeners(socket: QuickdrawSocket): void {\n // Register method listeners for all registered services\n for (const [eventName, { method, service }] of this.methodRegistry) {\n this.registerMethodListener(socket, eventName, method, service);\n }\n\n // Register subscription/unsubscription listeners for all services\n for (const [serviceName, service] of this.services) {\n this.registerSubscriptionListener(socket, `${serviceName}:subscribe`, service);\n this.registerBatchSubscriptionListener(socket, `${serviceName}:batchSubscribe`, service);\n this.registerUnsubscriptionListener(socket, `${serviceName}:unsubscribe`, service);\n }\n }\n\n /**\n * Register a service and auto-discover its public methods.\n */\n public registerService(serviceName: string, service: BaseServiceInstance): void {\n this.services.set(serviceName, service);\n\n // Pass io instance to service for room-based broadcasts\n if (service.setIo) {\n service.setIo(this.io);\n }\n\n this.logger.info(`Registered service: ${serviceName}`);\n\n // Auto-discover and store method metadata\n this.discoverServiceMethods(serviceName, service);\n\n // Ensure connection handler is installed\n this.installConnectionHandler();\n }\n\n /**\n * Discover and store all public methods from a service.\n */\n private discoverServiceMethods(serviceName: string, service: BaseServiceInstance): void {\n const methods = service.getPublicMethods();\n\n for (const method of methods) {\n const eventName = `${serviceName}:${method.name}`;\n this.logger.info(`Registering socket event: ${eventName}`);\n\n // Store method metadata for deferred registration on connection\n this.methodRegistry.set(eventName, {\n serviceName,\n method,\n service,\n });\n }\n }\n\n /**\n * Register a method as a socket event listener.\n */\n private registerMethodListener(\n socket: QuickdrawSocket,\n eventName: string,\n method: ServiceMethodDefinition<unknown, unknown>,\n service: BaseServiceInstance,\n ): void {\n socket.on(\n eventName,\n async (payload: unknown, callback?: (response: ServiceResponse<unknown>) => void) => {\n const startedAt = Date.now();\n const [serviceName, methodName] = eventName.split(\":\");\n const userIdShort = socket.userId?.slice(0, 8) ?? \"anon\";\n\n try {\n // Authentication check\n if (!socket.userId && method.access !== \"Public\") {\n const errorResponse: ServiceResponse<unknown> = {\n success: false,\n error: \"Authentication required\",\n code: 401,\n };\n callback?.(errorResponse);\n return;\n }\n\n // Validate payload with Zod schema if provided\n let validatedPayload = payload;\n if (method.schema) {\n const result = method.schema.safeParse(payload);\n if (!result.success) {\n const errorResponse: ServiceResponse<unknown> = {\n success: false,\n error: `Validation error: ${result.error.message}`,\n code: 400,\n };\n callback?.(errorResponse);\n return;\n }\n validatedPayload = result.data;\n }\n\n // Resolve entry ID for access check\n let entryId: string | undefined;\n if (method.resolveEntryId) {\n entryId = method.resolveEntryId(validatedPayload) ?? undefined;\n } else if (\n validatedPayload &&\n typeof validatedPayload === \"object\" &&\n \"id\" in validatedPayload\n ) {\n const idValue = (validatedPayload as Record<string, unknown>).id;\n if (typeof idValue === \"string\") {\n entryId = idValue;\n }\n }\n\n // Check access\n await service.ensureAccessForMethod(method.access, socket, entryId);\n\n // Log start (if enabled)\n if (this.methodLoggingEnabled) {\n const logContext: Record<string, unknown> = {\n category: \"request_processing\",\n serviceName,\n methodName,\n userId: socket.userId,\n socketId: socket.id,\n };\n if (this.logPayloads) {\n logContext.payload = validatedPayload;\n }\n this.logger.info(\n `User ${userIdShort} ${serviceName}.${methodName} -> start`,\n logContext,\n );\n }\n\n // Execute handler\n const result = await method.handler(validatedPayload, {\n userId: socket.userId,\n socketId: socket.id,\n serviceAccess: socket.serviceAccess ?? {},\n });\n\n // Log success (if enabled)\n if (this.methodLoggingEnabled) {\n const durationMs = Date.now() - startedAt;\n const logContext: Record<string, unknown> = {\n category: \"request_processing\",\n outcome: \"success\",\n durationMs,\n serviceName,\n methodName,\n userId: socket.userId,\n };\n if (this.logResponses) {\n logContext.response = result;\n }\n this.logger.info(\n `User ${userIdShort} ${serviceName}.${methodName} -> success`,\n logContext,\n );\n }\n\n const successResponse: ServiceResponse<unknown> = {\n success: true,\n data: result,\n };\n callback?.(successResponse);\n } catch (error) {\n // Always log errors, even if methodLogging is disabled\n const durationMs = Date.now() - startedAt;\n const logContext: Record<string, unknown> = {\n category: \"request_processing\",\n outcome: \"fail\",\n durationMs,\n serviceName,\n methodName,\n userId: socket.userId,\n socketId: socket.id,\n error: error instanceof Error ? error.message : \"Unknown error\",\n };\n if (this.logPayloads) {\n logContext.payload = payload;\n }\n this.logger.error(`User ${userIdShort} ${serviceName}.${methodName} -> fail`, logContext);\n\n const errorResponse: ServiceResponse<unknown> = {\n success: false,\n error: error instanceof Error ? error.message : \"Internal error\",\n code: 500,\n };\n callback?.(errorResponse);\n }\n },\n );\n }\n\n /**\n * Register subscription listener for a service.\n */\n private registerSubscriptionListener(\n socket: QuickdrawSocket,\n eventName: string,\n service: BaseServiceInstance,\n ): void {\n socket.on(\n eventName,\n async (\n payload: { entryId: string; requiredLevel?: string },\n callback?: (response: ServiceResponse<unknown>) => void,\n ) => {\n try {\n if (!socket.userId) {\n callback?.({\n success: false,\n error: \"Authentication required\",\n code: 401,\n });\n return;\n }\n\n const data = await service.subscribe(\n payload.entryId,\n socket,\n (payload.requiredLevel as \"Read\" | \"Moderate\" | \"Admin\") ?? \"Read\",\n );\n\n if (data === null) {\n callback?.({\n success: false,\n error: \"Access denied or entry not found\",\n code: 403,\n });\n return;\n }\n\n callback?.({ success: true, data });\n } catch (error) {\n this.logger.error(`Error in subscription ${eventName}:`, {\n error: error instanceof Error ? error.message : \"Unknown error\",\n });\n callback?.({\n success: false,\n error: error instanceof Error ? error.message : \"Subscription failed\",\n });\n }\n },\n );\n }\n\n /**\n * Register batch subscription listener for a service.\n * Handles subscribing to multiple entities in a single event.\n */\n private registerBatchSubscriptionListener(\n socket: QuickdrawSocket,\n eventName: string,\n service: BaseServiceInstance,\n ): void {\n socket.on(\n eventName,\n async (\n payload: { entryIds: string[]; requiredLevel?: string },\n callback?: (response: ServiceResponse<Record<string, unknown>>) => void,\n ) => {\n try {\n if (!socket.userId) {\n callback?.({\n success: false,\n error: \"Authentication required\",\n code: 401,\n });\n return;\n }\n\n if (!Array.isArray(payload.entryIds) || payload.entryIds.length === 0) {\n callback?.({\n success: false,\n error: \"entryIds must be a non-empty array\",\n code: 400,\n });\n return;\n }\n\n const data = await service.batchSubscribe(\n payload.entryIds,\n socket,\n (payload.requiredLevel as \"Read\" | \"Moderate\" | \"Admin\") ?? \"Read\",\n );\n\n callback?.({ success: true, data });\n } catch (error) {\n this.logger.error(`Error in batch subscription ${eventName}:`, {\n error: error instanceof Error ? error.message : \"Unknown error\",\n });\n callback?.({\n success: false,\n error: error instanceof Error ? error.message : \"Batch subscription failed\",\n });\n }\n },\n );\n }\n\n /**\n * Register unsubscription listener for a service.\n */\n private registerUnsubscriptionListener(\n socket: QuickdrawSocket,\n eventName: string,\n service: BaseServiceInstance,\n ): void {\n socket.on(\n eventName,\n (\n payload: { entryId: string },\n callback?: (response: ServiceResponse<{ unsubscribed: true; entryId: string }>) => void,\n ) => {\n try {\n service.unsubscribe(payload.entryId, socket);\n callback?.({\n success: true,\n data: { unsubscribed: true, entryId: payload.entryId },\n });\n } catch (error) {\n this.logger.error(`Error in unsubscription ${eventName}:`, {\n error: error instanceof Error ? error.message : \"Unknown error\",\n });\n callback?.({\n success: false,\n error: error instanceof Error ? error.message : \"Unsubscription failed\",\n });\n }\n },\n );\n }\n\n /**\n * Get list of registered service names.\n */\n public getServices(): string[] {\n return Array.from(this.services.keys());\n }\n\n /**\n * Get all service instances for lifecycle management.\n */\n public getServiceInstances(): BaseServiceInstance[] {\n return Array.from(this.services.values());\n }\n}\n","import express from \"express\";\nimport { createServer } from \"http\";\nimport { Server as SocketIOServer } from \"socket.io\";\nimport { consoleLogger } from \"../shared/types\";\nimport type { QuickdrawSocket, QuickdrawServerOptions, QuickdrawServerResult } from \"./types\";\nimport { ServiceRegistry } from \"./ServiceRegistry\";\n\n/**\n * Create a fully configured quickdraw server with one function call.\n *\n * @example\n * ```typescript\n * import { createQuickdrawServer } from '@quickdraw/core/server';\n * import { ChatService, UserService } from './services';\n *\n * const { io, httpServer, registry } = createQuickdrawServer({\n * port: 4000,\n * cors: { origin: 'http://localhost:3000' },\n * services: {\n * chatService: new ChatService(prisma),\n * userService: new UserService(prisma),\n * },\n * auth: {\n * authenticate: async (socket, auth) => {\n * // Verify JWT and return userId\n * const payload = await verifyJWT(auth.token);\n * return payload.userId;\n * },\n * },\n * });\n * ```\n */\nexport function createQuickdrawServer(options: QuickdrawServerOptions): QuickdrawServerResult {\n const logger = options.logger ?? consoleLogger;\n const serverLogger = logger.child({ service: \"QuickdrawServer\" });\n\n // Create Express app with basic middleware\n const app = express();\n app.use(express.json());\n\n // Health check endpoint\n app.get(\"/health\", (_req, res) => {\n res.json({ status: \"ok\", timestamp: new Date().toISOString() });\n });\n\n // Create HTTP server\n const httpServer = createServer(app);\n\n // Create Socket.io server\n const io = new SocketIOServer(httpServer, {\n cors: {\n origin: options.cors?.origin ?? \"*\",\n methods: options.cors?.methods ?? [\"GET\", \"POST\"],\n credentials: options.cors?.credentials ?? true,\n },\n });\n\n // Create service registry with method logging configuration\n const registry = new ServiceRegistry(io, {\n logger,\n methodLogging: options.methodLogging,\n });\n\n // Apply authentication middleware\n io.use(async (socket, next) => {\n const quickdrawSocket = socket as QuickdrawSocket;\n\n try {\n if (options.auth?.authenticate) {\n const auth = socket.handshake.auth as Record<string, unknown>;\n const userId = await options.auth.authenticate(quickdrawSocket, auth);\n quickdrawSocket.userId = userId;\n\n // TODO: Load service access from user record\n // For now, default to empty service access\n quickdrawSocket.serviceAccess = {};\n }\n next();\n } catch (error) {\n serverLogger.error(\"Authentication failed\", {\n error: error instanceof Error ? error.message : \"Unknown error\",\n socketId: socket.id,\n });\n next(new Error(\"Authentication failed\"));\n }\n });\n\n // Register services\n for (const [serviceName, service] of Object.entries(options.services)) {\n registry.registerService(serviceName, service);\n }\n\n // Handle connection lifecycle\n io.on(\"connection\", (socket) => {\n const quickdrawSocket = socket as QuickdrawSocket;\n serverLogger.info(\"Socket connected\", {\n socketId: quickdrawSocket.id,\n userId: quickdrawSocket.userId,\n });\n\n quickdrawSocket.on(\"disconnect\", () => {\n serverLogger.info(\"Socket disconnected\", {\n socketId: quickdrawSocket.id,\n userId: quickdrawSocket.userId,\n });\n\n // Cleanup subscriptions across all services\n for (const service of registry.getServiceInstances()) {\n try {\n service.unsubscribeSocket(quickdrawSocket);\n } catch {\n // Ignore cleanup errors\n }\n }\n });\n });\n\n // Start listening\n httpServer.listen(options.port, \"0.0.0.0\", () => {\n serverLogger.info(`Server listening on port ${options.port}`);\n serverLogger.info(`Registered services: ${registry.getServices().join(\", \")}`);\n });\n\n // Graceful shutdown handler\n const shutdown = async (signal: string) => {\n serverLogger.info(`Received ${signal}, starting graceful shutdown...`);\n\n // Stop accepting new connections\n httpServer.close(() => {\n serverLogger.info(\"HTTP server closed\");\n });\n\n // Close all Socket.io connections gracefully\n const sockets = await io.fetchSockets();\n serverLogger.info(`Closing ${sockets.length} active socket connections...`);\n\n for (const socket of sockets) {\n const quickdrawSocket = socket as unknown as QuickdrawSocket;\n\n // Unsubscribe from all services\n for (const service of registry.getServiceInstances()) {\n try {\n service.unsubscribeSocket(quickdrawSocket);\n } catch {\n // Ignore cleanup errors\n }\n }\n\n // Disconnect socket\n socket.disconnect(true);\n }\n\n // Close Socket.io server\n io.close(() => {\n serverLogger.info(\"Socket.io server closed\");\n process.exit(0);\n });\n\n // Force exit after timeout\n setTimeout(() => {\n serverLogger.error(\"Graceful shutdown timeout, forcing exit\");\n process.exit(1);\n }, 10000); // 10 second timeout\n };\n\n // Register shutdown handlers\n process.on(\"SIGTERM\", () => shutdown(\"SIGTERM\"));\n process.on(\"SIGINT\", () => shutdown(\"SIGINT\"));\n\n return { io, httpServer, registry };\n}\n","import { io as ioClient, Socket } from \"socket.io-client\";\nimport type { ServiceResponse } from \"../shared/types\";\nimport type { BaseServiceInstance, QuickdrawServerResult } from \"./types\";\nimport { createQuickdrawServer } from \"./createServer\";\n\n/**\n * Options for creating a test server.\n */\nexport interface CreateTestServerOptions {\n services: Record<string, BaseServiceInstance>;\n seedDb?: () => Promise<void>;\n}\n\n/**\n * Test server instance with helper methods.\n */\nexport interface TestServer {\n port: number;\n result: QuickdrawServerResult;\n stop: () => Promise<void>;\n connectAs: (userId: string) => Promise<TestClient>;\n}\n\n/**\n * Test client instance with helper methods.\n */\nexport interface TestClient {\n socket: Socket;\n emit: <TPayload, TResponse>(event: string, payload: TPayload) => Promise<TResponse>;\n close: () => void;\n}\n\nlet nextPort = 10000;\n\n/**\n * Get an available port for testing.\n */\nexport function getAvailablePort(): number {\n return nextPort++;\n}\n\n/**\n * Create a test server with dev authentication.\n *\n * @example\n * ```typescript\n * const server = await createTestServer({\n * services: {\n * chatService: new ChatService(testPrisma),\n * },\n * seedDb: async () => {\n * await testPrisma.user.create({ ... });\n * },\n * });\n *\n * const client = await server.connectAs('user-id-123');\n * const result = await client.emit('chatService:createChat', { title: 'Test' });\n *\n * await server.stop();\n * ```\n */\nexport async function createTestServer(options: CreateTestServerOptions): Promise<TestServer> {\n const port = getAvailablePort();\n\n const result = createQuickdrawServer({\n port,\n services: options.services,\n auth: {\n // Dev mode: accept userId directly from auth\n authenticate: async (_socket, auth) => {\n return auth.userId as string | undefined;\n },\n },\n });\n\n // Wait for server to start\n await new Promise<void>((resolve) => {\n result.httpServer.once(\"listening\", resolve);\n });\n\n // Seed database if provided\n if (options.seedDb) {\n await options.seedDb();\n }\n\n return {\n port,\n result,\n stop: () =>\n new Promise<void>((resolve) => {\n result.io.close();\n result.httpServer.close(() => resolve());\n }),\n connectAs: (userId: string) => connectAsUser(port, userId),\n };\n}\n\n/**\n * Connect to a test server as a specific user.\n */\nexport async function connectAsUser(port: number, userId: string): Promise<TestClient> {\n const socket = ioClient(`http://localhost:${port}`, {\n auth: { userId },\n transports: [\"websocket\"],\n autoConnect: true,\n });\n\n await new Promise<void>((resolve, reject) => {\n const timeout = setTimeout(() => {\n reject(new Error(\"Connection timeout\"));\n }, 5000);\n\n socket.on(\"connect\", () => {\n clearTimeout(timeout);\n resolve();\n });\n\n socket.on(\"connect_error\", (err) => {\n clearTimeout(timeout);\n reject(err);\n });\n });\n\n return {\n socket,\n emit: <TPayload, TResponse>(event: string, payload: TPayload) =>\n emitWithAck<TPayload, TResponse>(socket, event, payload),\n close: () => socket.close(),\n };\n}\n\n/**\n * Emit an event and wait for acknowledgment.\n */\nexport function emitWithAck<TPayload, TResponse>(\n socket: Socket,\n event: string,\n payload: TPayload,\n timeoutMs: number = 5000,\n): Promise<TResponse> {\n return new Promise((resolve, reject) => {\n const timeout = setTimeout(() => {\n reject(new Error(`Timeout waiting for ${event}`));\n }, timeoutMs);\n\n socket.emit(event, payload, (response: ServiceResponse<TResponse>) => {\n clearTimeout(timeout);\n\n if (response.success) {\n resolve(response.data);\n } else {\n reject(new Error(response.error));\n }\n });\n });\n}\n\n/**\n * Wait for a specific socket event.\n */\nexport function waitForEvent<T>(\n socket: Socket,\n event: string,\n timeoutMs: number = 5000,\n): Promise<T> {\n return new Promise((resolve, reject) => {\n const timeout = setTimeout(() => {\n socket.off(event, handler);\n reject(new Error(`Timeout waiting for event ${event}`));\n }, timeoutMs);\n\n const handler = (data: T) => {\n clearTimeout(timeout);\n socket.off(event, handler);\n resolve(data);\n };\n\n socket.on(event, handler);\n });\n}\n"]}
1
+ {"version":3,"sources":["../../src/shared/types.ts","../../src/server/ServiceRegistry.ts","../../src/server/createServer.ts","../../src/server/testing.ts"],"names":["result","SocketIOServer","ioClient"],"mappings":"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","file":"testing.js","sourcesContent":["import type { z } from \"zod\";\n\n// ============================================================================\n// Access Control Types\n// ============================================================================\n\nexport type AccessLevel = \"Public\" | \"Read\" | \"Moderate\" | \"Admin\";\n\nexport type ACE = {\n userId: string;\n level: AccessLevel;\n};\n\nexport type ACL = ACE[];\n\n// ============================================================================\n// Entity and User Interfaces for ACL\n// ============================================================================\n\n/**\n * Interface for entities that support entry-level ACL stored as JSON.\n *\n * Use this pattern when:\n * - You want simple ACL without a separate membership table\n * - Access patterns are straightforward (read/write permissions)\n * - You don't need to query \"all entities user X can access\" efficiently\n *\n * The default `checkEntryACL` in BaseService expects entities to have this shape.\n *\n * @example\n * ```prisma\n * model Document {\n * id String @id @default(cuid())\n * title String\n * acl Json? // Stores ACL array\n * }\n * ```\n *\n * @example\n * ```typescript\n * class DocumentService extends BaseService<Document & ACLEntity, ...> {\n * constructor(prisma: PrismaClient) {\n * super({ serviceName: 'documentService', hasEntryACL: true });\n * this.setDelegate(prisma.document);\n * // Default checkEntryACL will work automatically\n * }\n * }\n * ```\n */\nexport interface ACLEntity {\n id: string;\n acl?: ACL | null;\n}\n\n/**\n * Interface for users with service-level access control.\n *\n * The `serviceAccess` field maps service names to access levels, granting\n * blanket permissions across all entries in that service.\n *\n * @example\n * ```prisma\n * model User {\n * id String @id @default(cuid())\n * serviceAccess Json? // { \"chatService\": \"Admin\", \"documentService\": \"Read\" }\n * }\n * ```\n *\n * @example\n * ```typescript\n * // User with Admin access to chatService can perform any operation\n * // on any chat, regardless of entry-level ACL\n * const user: QuickdrawUser = {\n * id: \"user-123\",\n * serviceAccess: { chatService: \"Admin\" }\n * };\n * ```\n */\nexport interface QuickdrawUser {\n id: string;\n serviceAccess?: Record<string, AccessLevel> | null;\n}\n\n// ============================================================================\n// Service Response Types\n// ============================================================================\n\nexport type ServiceResponse<T = unknown> =\n | { success: true; data: T }\n | { success: false; error: string; code?: number };\n\n// ============================================================================\n// Service Method Definition Types\n// ============================================================================\n\n/**\n * Defines the shape of a service method including its payload and response types.\n * Used to create a type-safe contract between server and client.\n */\nexport interface ServiceMethodDefinition<TPayload = unknown, TResponse = unknown> {\n name: string;\n access: AccessLevel;\n handler: (payload: TPayload, context: ServiceMethodContext) => Promise<TResponse>;\n schema?: z.ZodType<TPayload>;\n resolveEntryId?: (payload: TPayload) => string | null;\n}\n\n/**\n * Context provided to service method handlers\n */\nexport interface ServiceMethodContext {\n userId: string | undefined;\n socketId: string;\n serviceAccess: Record<string, AccessLevel>;\n}\n\n/**\n * Type helper for defining service method maps.\n * Each service should define its methods using this structure.\n *\n * @example\n * ```typescript\n * type ChatServiceMethods = ServiceMethodMap<{\n * createChat: {\n * payload: { title: string };\n * response: { id: string };\n * };\n * updateTitle: {\n * payload: { id: string; title: string };\n * response: { id: string; title: string };\n * };\n * }>;\n * ```\n */\nexport type ServiceMethodMap<T extends Record<string, { payload: unknown; response: unknown }>> = T;\n\n// ============================================================================\n// Service Channel Types (high-frequency, fire-and-forget events)\n// ============================================================================\n\n/**\n * Channels are the high-frequency counterpart to service methods.\n *\n * Where methods are request/response (ack'd, ACL-checked against the database,\n * counted by the global rate limiter), channels are fire-and-forget: no ack,\n * no response, validated and access-checked entirely in memory, and governed\n * by a per-channel token bucket instead of the global limiter. Use them for\n * traffic where per-message overhead matters and losing a message is fine —\n * game input, cursor positions, typing indicators, telemetry.\n *\n * Wire format: channels route as the Socket.io event\n * `channel:<serviceName>:<channelName>` (see {@link channelEventName}).\n * Clients should emit them volatile; servers broadcast tick data back with\n * `emitToRoomVolatile` so backpressured clients drop frames instead of\n * queueing them.\n *\n * Access model (all checks synchronous, zero DB reads):\n * - Authentication is always required — anonymous sockets are dropped silently.\n * - `\"Public\"` / `\"Read\"` access: any authenticated user passes the service gate.\n * - `\"Moderate\"` / `\"Admin\"`: requires that level in the socket's in-memory\n * `serviceAccess`.\n * - Entry-level access is expressed via `requireRoom`: the payload resolves to\n * a room name and the socket must already be in that room. Room membership\n * was ACL-checked at subscribe time, so this inherits full entry ACL\n * semantics without touching the database on the hot path.\n */\nexport interface ServiceChannelDefinition<TPayload = unknown> {\n name: string;\n access: AccessLevel;\n handler: (payload: TPayload, context: ServiceChannelContext) => void;\n /** Required — channel payloads are untrusted high-frequency input. */\n schema: z.ZodType<TPayload>;\n /** Sustained messages per second allowed per socket. */\n ratePerSecond: number;\n /** Bucket capacity — short bursts above ratePerSecond up to this size. */\n burst: number;\n /**\n * Resolve a room the socket must already be a member of (typically the\n * service room joined via subscribe). Return null to skip the check.\n */\n requireRoom?: (payload: TPayload) => string | null;\n}\n\n/**\n * Context provided to channel handlers. Unlike ServiceMethodContext,\n * userId is always present — unauthenticated channel messages are dropped\n * before the handler runs.\n */\nexport interface ServiceChannelContext {\n userId: string;\n socketId: string;\n serviceAccess: Record<string, AccessLevel>;\n}\n\n/**\n * Type helper for defining service channel maps (payload-only — channels\n * have no response).\n *\n * @example\n * ```typescript\n * type GameServiceChannels = ServiceChannelMap<{\n * input: { seq: number; dx: number; dy: number; boost: boolean };\n * }>;\n * ```\n */\nexport type ServiceChannelMap<T extends Record<string, unknown>> = T;\n\n/**\n * The Socket.io event name a channel routes as.\n * Shared by the server registry, the React client, and non-JS clients\n * (e.g. the Godot addon) so the wire contract lives in one place.\n */\nexport function channelEventName(serviceName: string, channelName: string): string {\n return `channel:${serviceName}:${channelName}`;\n}\n\n/**\n * Prefix for all channel events — pass to the rate limiter's\n * `excludePrefixes` so channel traffic bypasses the global request limiter\n * (channels enforce their own per-socket token buckets).\n */\nexport const CHANNEL_EVENT_PREFIX = \"channel:\";\n\n// ============================================================================\n// Admin Method Types (Generic shapes for all services)\n// ============================================================================\n\nexport type AdminListPayload = {\n page?: number;\n pageSize?: number;\n sort?: {\n field?: string;\n direction?: \"asc\" | \"desc\";\n };\n filter?: {\n id?: string;\n ids?: string[];\n createdAfter?: string;\n createdBefore?: string;\n updatedAfter?: string;\n updatedBefore?: string;\n } & Record<string, unknown>;\n};\n\nexport type AdminListResponse<T> = {\n rows: T[];\n page: number;\n pageSize: number;\n total: number;\n};\n\nexport type AdminGetPayload = { id: string };\n\nexport type AdminCreatePayload<TInsert> = { data: Partial<TInsert> };\n\nexport type AdminUpdatePayload<TInsert> = {\n id: string;\n data: Partial<TInsert>;\n};\n\nexport type AdminDeletePayload = { id: string };\nexport type AdminDeleteResponse = { id: string; deleted: true };\n\nexport type AdminSetEntryACLPayload = {\n id: string;\n acl: ACL;\n};\n\nexport type AdminGetSubscribersPayload = { id: string };\nexport type AdminGetSubscribersResponse = {\n id: string;\n subscribers: Array<{ socketId: string; userId?: string }>;\n};\n\nexport type AdminReemitPayload = { id: string };\nexport type AdminReemitResponse = { emitted: boolean };\n\nexport type AdminUnsubscribeAllPayload = { id: string };\nexport type AdminUnsubscribeAllResponse = { id: string; unsubscribed: number };\n\n// ============================================================================\n// Admin Field Configuration Types\n// ============================================================================\n\n/**\n * Supported field types for admin UI generation.\n */\nexport type AdminFieldType =\n | \"string\"\n | \"number\"\n | \"boolean\"\n | \"date\"\n | \"enum\"\n | \"json\"\n | \"relation\";\n\n/**\n * Configuration for a single field in the admin UI.\n * Derived from Zod schemas with optional overrides.\n */\nexport interface AdminFieldConfig {\n /** Field name (property key on the entity) */\n name: string;\n /** Field type for rendering appropriate input */\n type: AdminFieldType;\n /** Human-readable label */\n label: string;\n /** Whether the field is required for creation */\n required: boolean;\n /** Whether the field can be edited (false for id, createdAt, etc.) */\n editable: boolean;\n /** Whether to show this field as a table column */\n showInTable: boolean;\n /** Whether the table can be sorted by this field */\n sortable: boolean;\n /** For enum fields, the allowed values */\n enumValues?: string[];\n /** For relation fields, the related service name */\n relationService?: string;\n}\n\n/**\n * Service metadata for admin UI generation.\n * Returned by the adminMeta method.\n */\nexport interface AdminServiceMeta {\n /** Internal service name (e.g., \"chatService\") */\n serviceName: string;\n /** Human-readable display name (e.g., \"Chats\") */\n displayName: string;\n /** Field configurations derived from schema */\n fields: AdminFieldConfig[];\n}\n\n/**\n * Payload for adminMeta method (no parameters needed)\n */\nexport type AdminMetaPayload = Record<string, never>;\n\n/**\n * Response type for adminMeta method\n */\nexport type AdminMetaResponse = AdminServiceMeta;\n\n// ============================================================================\n// Subscription Types\n// ============================================================================\n\nexport type SubscribePayload = {\n entryId: string;\n requiredLevel?: AccessLevel;\n};\n\nexport type UnsubscribePayload = {\n entryId: string;\n};\n\n// ============================================================================\n// Utility Types\n// ============================================================================\n\n/**\n * Extracts payload type from a service method definition\n */\nexport type ExtractPayload<T> = T extends { payload: infer P } ? P : never;\n\n/**\n * Extracts response type from a service method definition\n */\nexport type ExtractResponse<T> = T extends { response: infer R } ? R : never;\n\n/**\n * Logger interface compatible with Winston and other loggers\n */\nexport interface Logger {\n info(message: string, meta?: Record<string, unknown>): void;\n warn(message: string, meta?: Record<string, unknown>): void;\n error(message: string, meta?: Record<string, unknown>): void;\n debug(message: string, meta?: Record<string, unknown>): void;\n child(options: Record<string, unknown>): Logger;\n}\n\n/**\n * Default console logger implementation\n */\nexport const consoleLogger: Logger = {\n info: (message, meta) => console.log(`[INFO] ${message}`, meta ?? \"\"),\n warn: (message, meta) => console.warn(`[WARN] ${message}`, meta ?? \"\"),\n error: (message, meta) => console.error(`[ERROR] ${message}`, meta ?? \"\"),\n debug: (message, meta) => console.debug(`[DEBUG] ${message}`, meta ?? \"\"),\n child: () => consoleLogger,\n};\n","import type { Server as SocketIOServer } from \"socket.io\";\nimport type {\n ServiceResponse,\n ServiceMethodDefinition,\n ServiceChannelDefinition,\n Logger,\n} from \"../shared/types\";\nimport { consoleLogger, channelEventName } from \"../shared/types\";\nimport type { QuickdrawSocket, BaseServiceInstance, ServiceRegistryInstance } from \"./types\";\n\n/**\n * Channel abuse guard: if a socket's dropped-message count within this window\n * exceeds ratePerSecond × CHANNEL_ABUSE_MULTIPLIER, the socket is disconnected.\n * At 30 msg/s this means sustained sending above ~10× the allowed rate.\n */\nconst CHANNEL_ABUSE_WINDOW_MS = 10_000;\nconst CHANNEL_ABUSE_MULTIPLIER = 100;\n\n/**\n * Central registry for auto-discovering and registering service methods as Socket.io events.\n *\n * Features:\n * - Auto-discovers public methods from registered services\n * - Wires methods to Socket.io events with standardized naming\n * - Handles subscription lifecycle\n * - Provides consistent error handling and logging\n * - Uses a single connection handler for all services (memory efficient)\n *\n * @example\n * ```typescript\n * const registry = new ServiceRegistry(io, { logger });\n * registry.registerService('chatService', chatService);\n * registry.registerService('userService', userService);\n * ```\n */\nexport interface ServiceRegistryOptions {\n logger?: Logger;\n /**\n * Automatic method logging configuration.\n * @default { enabled: true, logPayloads: false, logResponses: false }\n */\n methodLogging?: {\n enabled?: boolean;\n logPayloads?: boolean;\n logResponses?: boolean;\n };\n}\n\nexport class ServiceRegistry implements ServiceRegistryInstance {\n private readonly io: SocketIOServer;\n private readonly services: Map<string, BaseServiceInstance> = new Map();\n private readonly logger: Logger;\n private readonly methodLoggingEnabled: boolean;\n private readonly logPayloads: boolean;\n private readonly logResponses: boolean;\n private connectionHandlerInstalled = false;\n\n // Store method metadata for deferred registration\n private readonly methodRegistry: Map<\n string,\n {\n serviceName: string;\n method: ServiceMethodDefinition<unknown, unknown>;\n service: BaseServiceInstance;\n }\n > = new Map();\n\n // Store channel metadata for deferred registration\n private readonly channelRegistry: Map<\n string,\n {\n serviceName: string;\n channel: ServiceChannelDefinition<unknown>;\n service: BaseServiceInstance;\n }\n > = new Map();\n\n constructor(io: SocketIOServer, options?: ServiceRegistryOptions) {\n this.io = io;\n this.logger =\n options?.logger?.child({ service: \"ServiceRegistry\" }) ??\n consoleLogger.child({ service: \"ServiceRegistry\" });\n\n // Method logging configuration (enabled by default)\n this.methodLoggingEnabled = options?.methodLogging?.enabled ?? true;\n this.logPayloads = options?.methodLogging?.logPayloads ?? false;\n this.logResponses = options?.methodLogging?.logResponses ?? false;\n\n if (this.methodLoggingEnabled) {\n this.logger.info(\"Automatic method logging enabled\", {\n logPayloads: this.logPayloads,\n logResponses: this.logResponses,\n });\n }\n }\n\n /**\n * Install the single connection handler that wires all registered services.\n * This is called lazily on first service registration.\n */\n private installConnectionHandler(): void {\n if (this.connectionHandlerInstalled) return;\n this.connectionHandlerInstalled = true;\n\n this.io.on(\"connection\", (socket) => {\n const quickdrawSocket = socket as QuickdrawSocket;\n this.setupSocketListeners(quickdrawSocket);\n });\n\n this.logger.info(\"Installed single connection handler for all services\");\n }\n\n /**\n * Set up all event listeners for a newly connected socket.\n */\n private setupSocketListeners(socket: QuickdrawSocket): void {\n // Register method listeners for all registered services\n for (const [eventName, { method, service }] of this.methodRegistry) {\n this.registerMethodListener(socket, eventName, method, service);\n }\n\n // Register channel listeners for all registered services\n for (const [eventName, { channel, service }] of this.channelRegistry) {\n this.registerChannelListener(socket, eventName, channel, service);\n }\n\n // Register subscription/unsubscription listeners for all services\n for (const [serviceName, service] of this.services) {\n this.registerSubscriptionListener(socket, `${serviceName}:subscribe`, service);\n this.registerBatchSubscriptionListener(socket, `${serviceName}:batchSubscribe`, service);\n this.registerUnsubscriptionListener(socket, `${serviceName}:unsubscribe`, service);\n }\n }\n\n /**\n * Register a service and auto-discover its public methods.\n */\n public registerService(serviceName: string, service: BaseServiceInstance): void {\n this.services.set(serviceName, service);\n\n // Pass io instance to service for room-based broadcasts\n if (service.setIo) {\n service.setIo(this.io);\n }\n\n this.logger.info(`Registered service: ${serviceName}`);\n\n // Auto-discover and store method metadata\n this.discoverServiceMethods(serviceName, service);\n\n // Auto-discover and store channel metadata\n this.discoverServiceChannels(serviceName, service);\n\n // Ensure connection handler is installed\n this.installConnectionHandler();\n }\n\n /**\n * Discover and store all public methods from a service.\n */\n private discoverServiceMethods(serviceName: string, service: BaseServiceInstance): void {\n const methods = service.getPublicMethods();\n\n for (const method of methods) {\n const eventName = `${serviceName}:${method.name}`;\n this.logger.info(`Registering socket event: ${eventName}`);\n\n // Store method metadata for deferred registration on connection\n this.methodRegistry.set(eventName, {\n serviceName,\n method,\n service,\n });\n }\n }\n\n /**\n * Discover and store all channels from a service.\n */\n private discoverServiceChannels(serviceName: string, service: BaseServiceInstance): void {\n const channels = service.getPublicChannels?.() ?? [];\n\n for (const channel of channels) {\n const eventName = channelEventName(serviceName, channel.name);\n this.logger.info(\n `Registering channel event: ${eventName} (${channel.ratePerSecond}/s, burst ${channel.burst})`,\n );\n\n this.channelRegistry.set(eventName, {\n serviceName,\n channel,\n service,\n });\n }\n }\n\n /**\n * Register a channel as a socket event listener.\n *\n * This is the hot path for high-frequency traffic: no ack, no async access\n * checks, no per-message logging. Order of operations per message:\n * token bucket → schema validation → in-memory access check → handler.\n * Any failure silently drops the message; sustained extreme flooding\n * disconnects the socket.\n */\n private registerChannelListener(\n socket: QuickdrawSocket,\n eventName: string,\n channel: ServiceChannelDefinition<unknown>,\n service: BaseServiceInstance,\n ): void {\n // Per-socket, per-channel token bucket state lives in this closure.\n let tokens = channel.burst;\n let lastRefillAt = Date.now();\n let abuseWindowStart = lastRefillAt;\n let droppedInWindow = 0;\n\n socket.on(eventName, (payload: unknown) => {\n const now = Date.now();\n\n // Refill continuously, capped at burst capacity\n tokens = Math.min(\n channel.burst,\n tokens + ((now - lastRefillAt) / 1000) * channel.ratePerSecond,\n );\n lastRefillAt = now;\n\n if (tokens < 1) {\n if (now - abuseWindowStart > CHANNEL_ABUSE_WINDOW_MS) {\n abuseWindowStart = now;\n droppedInWindow = 0;\n }\n droppedInWindow++;\n if (droppedInWindow > channel.ratePerSecond * CHANNEL_ABUSE_MULTIPLIER) {\n this.logger.warn(\n `Disconnecting socket ${socket.id} for sustained channel flooding on ${eventName}`,\n { userId: socket.userId, droppedInWindow },\n );\n socket.disconnect(true);\n }\n return;\n }\n tokens -= 1;\n\n const parsed = channel.schema.safeParse(payload);\n if (!parsed.success) return;\n\n if (!service.checkChannelAccess?.(channel, socket, parsed.data)) return;\n\n try {\n channel.handler(parsed.data, {\n userId: socket.userId!,\n socketId: socket.id,\n serviceAccess: socket.serviceAccess ?? {},\n });\n } catch (error) {\n this.logger.error(`Channel handler error for ${eventName}`, {\n userId: socket.userId,\n error: error instanceof Error ? error.message : \"Unknown error\",\n });\n }\n });\n }\n\n /**\n * Register a method as a socket event listener.\n */\n private registerMethodListener(\n socket: QuickdrawSocket,\n eventName: string,\n method: ServiceMethodDefinition<unknown, unknown>,\n service: BaseServiceInstance,\n ): void {\n socket.on(\n eventName,\n async (payload: unknown, callback?: (response: ServiceResponse<unknown>) => void) => {\n const startedAt = Date.now();\n const [serviceName, methodName] = eventName.split(\":\");\n const userIdShort = socket.userId?.slice(0, 8) ?? \"anon\";\n\n try {\n // Authentication check\n if (!socket.userId && method.access !== \"Public\") {\n const errorResponse: ServiceResponse<unknown> = {\n success: false,\n error: \"Authentication required\",\n code: 401,\n };\n callback?.(errorResponse);\n return;\n }\n\n // Validate payload with Zod schema if provided\n let validatedPayload = payload;\n if (method.schema) {\n const result = method.schema.safeParse(payload);\n if (!result.success) {\n const errorResponse: ServiceResponse<unknown> = {\n success: false,\n error: `Validation error: ${result.error.message}`,\n code: 400,\n };\n callback?.(errorResponse);\n return;\n }\n validatedPayload = result.data;\n }\n\n // Resolve entry ID for access check\n let entryId: string | undefined;\n if (method.resolveEntryId) {\n entryId = method.resolveEntryId(validatedPayload) ?? undefined;\n } else if (\n validatedPayload &&\n typeof validatedPayload === \"object\" &&\n \"id\" in validatedPayload\n ) {\n const idValue = (validatedPayload as Record<string, unknown>).id;\n if (typeof idValue === \"string\") {\n entryId = idValue;\n }\n }\n\n // Check access\n await service.ensureAccessForMethod(method.access, socket, entryId);\n\n // Log start (if enabled)\n if (this.methodLoggingEnabled) {\n const logContext: Record<string, unknown> = {\n category: \"request_processing\",\n serviceName,\n methodName,\n userId: socket.userId,\n socketId: socket.id,\n };\n if (this.logPayloads) {\n logContext.payload = validatedPayload;\n }\n this.logger.info(\n `User ${userIdShort} ${serviceName}.${methodName} -> start`,\n logContext,\n );\n }\n\n // Execute handler\n const result = await method.handler(validatedPayload, {\n userId: socket.userId,\n socketId: socket.id,\n serviceAccess: socket.serviceAccess ?? {},\n });\n\n // Log success (if enabled)\n if (this.methodLoggingEnabled) {\n const durationMs = Date.now() - startedAt;\n const logContext: Record<string, unknown> = {\n category: \"request_processing\",\n outcome: \"success\",\n durationMs,\n serviceName,\n methodName,\n userId: socket.userId,\n };\n if (this.logResponses) {\n logContext.response = result;\n }\n this.logger.info(\n `User ${userIdShort} ${serviceName}.${methodName} -> success`,\n logContext,\n );\n }\n\n const successResponse: ServiceResponse<unknown> = {\n success: true,\n data: result,\n };\n callback?.(successResponse);\n } catch (error) {\n // Always log errors, even if methodLogging is disabled\n const durationMs = Date.now() - startedAt;\n const logContext: Record<string, unknown> = {\n category: \"request_processing\",\n outcome: \"fail\",\n durationMs,\n serviceName,\n methodName,\n userId: socket.userId,\n socketId: socket.id,\n error: error instanceof Error ? error.message : \"Unknown error\",\n };\n if (this.logPayloads) {\n logContext.payload = payload;\n }\n this.logger.error(`User ${userIdShort} ${serviceName}.${methodName} -> fail`, logContext);\n\n const errorResponse: ServiceResponse<unknown> = {\n success: false,\n error: error instanceof Error ? error.message : \"Internal error\",\n code: 500,\n };\n callback?.(errorResponse);\n }\n },\n );\n }\n\n /**\n * Register subscription listener for a service.\n */\n private registerSubscriptionListener(\n socket: QuickdrawSocket,\n eventName: string,\n service: BaseServiceInstance,\n ): void {\n socket.on(\n eventName,\n async (\n payload: { entryId: string; requiredLevel?: string },\n callback?: (response: ServiceResponse<unknown>) => void,\n ) => {\n try {\n if (!socket.userId) {\n callback?.({\n success: false,\n error: \"Authentication required\",\n code: 401,\n });\n return;\n }\n\n const data = await service.subscribe(\n payload.entryId,\n socket,\n (payload.requiredLevel as \"Read\" | \"Moderate\" | \"Admin\") ?? \"Read\",\n );\n\n if (data === null) {\n callback?.({\n success: false,\n error: \"Access denied or entry not found\",\n code: 403,\n });\n return;\n }\n\n callback?.({ success: true, data });\n } catch (error) {\n this.logger.error(`Error in subscription ${eventName}:`, {\n error: error instanceof Error ? error.message : \"Unknown error\",\n });\n callback?.({\n success: false,\n error: error instanceof Error ? error.message : \"Subscription failed\",\n });\n }\n },\n );\n }\n\n /**\n * Register batch subscription listener for a service.\n * Handles subscribing to multiple entities in a single event.\n */\n private registerBatchSubscriptionListener(\n socket: QuickdrawSocket,\n eventName: string,\n service: BaseServiceInstance,\n ): void {\n socket.on(\n eventName,\n async (\n payload: { entryIds: string[]; requiredLevel?: string },\n callback?: (response: ServiceResponse<Record<string, unknown>>) => void,\n ) => {\n try {\n if (!socket.userId) {\n callback?.({\n success: false,\n error: \"Authentication required\",\n code: 401,\n });\n return;\n }\n\n if (!Array.isArray(payload.entryIds) || payload.entryIds.length === 0) {\n callback?.({\n success: false,\n error: \"entryIds must be a non-empty array\",\n code: 400,\n });\n return;\n }\n\n const data = await service.batchSubscribe(\n payload.entryIds,\n socket,\n (payload.requiredLevel as \"Read\" | \"Moderate\" | \"Admin\") ?? \"Read\",\n );\n\n callback?.({ success: true, data });\n } catch (error) {\n this.logger.error(`Error in batch subscription ${eventName}:`, {\n error: error instanceof Error ? error.message : \"Unknown error\",\n });\n callback?.({\n success: false,\n error: error instanceof Error ? error.message : \"Batch subscription failed\",\n });\n }\n },\n );\n }\n\n /**\n * Register unsubscription listener for a service.\n */\n private registerUnsubscriptionListener(\n socket: QuickdrawSocket,\n eventName: string,\n service: BaseServiceInstance,\n ): void {\n socket.on(\n eventName,\n (\n payload: { entryId: string },\n callback?: (response: ServiceResponse<{ unsubscribed: true; entryId: string }>) => void,\n ) => {\n try {\n service.unsubscribe(payload.entryId, socket);\n callback?.({\n success: true,\n data: { unsubscribed: true, entryId: payload.entryId },\n });\n } catch (error) {\n this.logger.error(`Error in unsubscription ${eventName}:`, {\n error: error instanceof Error ? error.message : \"Unknown error\",\n });\n callback?.({\n success: false,\n error: error instanceof Error ? error.message : \"Unsubscription failed\",\n });\n }\n },\n );\n }\n\n /**\n * Get list of registered service names.\n */\n public getServices(): string[] {\n return Array.from(this.services.keys());\n }\n\n /**\n * Get all service instances for lifecycle management.\n */\n public getServiceInstances(): BaseServiceInstance[] {\n return Array.from(this.services.values());\n }\n}\n","import express from \"express\";\nimport { createServer } from \"http\";\nimport { Server as SocketIOServer } from \"socket.io\";\nimport { consoleLogger } from \"../shared/types\";\nimport type { QuickdrawSocket, QuickdrawServerOptions, QuickdrawServerResult } from \"./types\";\nimport { ServiceRegistry } from \"./ServiceRegistry\";\n\n/**\n * Create a fully configured quickdraw server with one function call.\n *\n * @example\n * ```typescript\n * import { createQuickdrawServer } from '@quickdraw/core/server';\n * import { ChatService, UserService } from './services';\n *\n * const { io, httpServer, registry } = createQuickdrawServer({\n * port: 4000,\n * cors: { origin: 'http://localhost:3000' },\n * services: {\n * chatService: new ChatService(prisma),\n * userService: new UserService(prisma),\n * },\n * auth: {\n * authenticate: async (socket, auth) => {\n * // Verify JWT and return userId\n * const payload = await verifyJWT(auth.token);\n * return payload.userId;\n * },\n * },\n * });\n * ```\n */\nexport function createQuickdrawServer(options: QuickdrawServerOptions): QuickdrawServerResult {\n const logger = options.logger ?? consoleLogger;\n const serverLogger = logger.child({ service: \"QuickdrawServer\" });\n\n // Create Express app with basic middleware\n const app = express();\n app.use(express.json());\n\n // Health check endpoint\n app.get(\"/health\", (_req, res) => {\n res.json({ status: \"ok\", timestamp: new Date().toISOString() });\n });\n\n // Create HTTP server\n const httpServer = createServer(app);\n\n // Create Socket.io server\n const io = new SocketIOServer(httpServer, {\n cors: {\n origin: options.cors?.origin ?? \"*\",\n methods: options.cors?.methods ?? [\"GET\", \"POST\"],\n credentials: options.cors?.credentials ?? true,\n },\n });\n\n // Create service registry with method logging configuration\n const registry = new ServiceRegistry(io, {\n logger,\n methodLogging: options.methodLogging,\n });\n\n // Apply authentication middleware\n io.use(async (socket, next) => {\n const quickdrawSocket = socket as QuickdrawSocket;\n\n try {\n if (options.auth?.authenticate) {\n const auth = socket.handshake.auth as Record<string, unknown>;\n const userId = await options.auth.authenticate(quickdrawSocket, auth);\n quickdrawSocket.userId = userId;\n\n // TODO: Load service access from user record\n // For now, default to empty service access\n quickdrawSocket.serviceAccess = {};\n }\n next();\n } catch (error) {\n serverLogger.error(\"Authentication failed\", {\n error: error instanceof Error ? error.message : \"Unknown error\",\n socketId: socket.id,\n });\n next(new Error(\"Authentication failed\"));\n }\n });\n\n // Register services\n for (const [serviceName, service] of Object.entries(options.services)) {\n registry.registerService(serviceName, service);\n }\n\n // Handle connection lifecycle\n io.on(\"connection\", (socket) => {\n const quickdrawSocket = socket as QuickdrawSocket;\n serverLogger.info(\"Socket connected\", {\n socketId: quickdrawSocket.id,\n userId: quickdrawSocket.userId,\n });\n\n quickdrawSocket.on(\"disconnect\", () => {\n serverLogger.info(\"Socket disconnected\", {\n socketId: quickdrawSocket.id,\n userId: quickdrawSocket.userId,\n });\n\n // Cleanup subscriptions across all services\n for (const service of registry.getServiceInstances()) {\n try {\n service.unsubscribeSocket(quickdrawSocket);\n } catch {\n // Ignore cleanup errors\n }\n }\n });\n });\n\n // Start listening\n httpServer.listen(options.port, \"0.0.0.0\", () => {\n serverLogger.info(`Server listening on port ${options.port}`);\n serverLogger.info(`Registered services: ${registry.getServices().join(\", \")}`);\n });\n\n // Graceful shutdown handler\n const shutdown = async (signal: string) => {\n serverLogger.info(`Received ${signal}, starting graceful shutdown...`);\n\n // Stop accepting new connections\n httpServer.close(() => {\n serverLogger.info(\"HTTP server closed\");\n });\n\n // Close all Socket.io connections gracefully\n const sockets = await io.fetchSockets();\n serverLogger.info(`Closing ${sockets.length} active socket connections...`);\n\n for (const socket of sockets) {\n const quickdrawSocket = socket as unknown as QuickdrawSocket;\n\n // Unsubscribe from all services\n for (const service of registry.getServiceInstances()) {\n try {\n service.unsubscribeSocket(quickdrawSocket);\n } catch {\n // Ignore cleanup errors\n }\n }\n\n // Disconnect socket\n socket.disconnect(true);\n }\n\n // Close Socket.io server\n io.close(() => {\n serverLogger.info(\"Socket.io server closed\");\n process.exit(0);\n });\n\n // Force exit after timeout\n setTimeout(() => {\n serverLogger.error(\"Graceful shutdown timeout, forcing exit\");\n process.exit(1);\n }, 10000); // 10 second timeout\n };\n\n // Register shutdown handlers\n process.on(\"SIGTERM\", () => shutdown(\"SIGTERM\"));\n process.on(\"SIGINT\", () => shutdown(\"SIGINT\"));\n\n return { io, httpServer, registry };\n}\n","import { io as ioClient, Socket } from \"socket.io-client\";\nimport type { ServiceResponse } from \"../shared/types\";\nimport type { BaseServiceInstance, QuickdrawServerResult } from \"./types\";\nimport { createQuickdrawServer } from \"./createServer\";\n\n/**\n * Options for creating a test server.\n */\nexport interface CreateTestServerOptions {\n services: Record<string, BaseServiceInstance>;\n seedDb?: () => Promise<void>;\n}\n\n/**\n * Test server instance with helper methods.\n */\nexport interface TestServer {\n port: number;\n result: QuickdrawServerResult;\n stop: () => Promise<void>;\n connectAs: (userId: string) => Promise<TestClient>;\n}\n\n/**\n * Test client instance with helper methods.\n */\nexport interface TestClient {\n socket: Socket;\n emit: <TPayload, TResponse>(event: string, payload: TPayload) => Promise<TResponse>;\n close: () => void;\n}\n\nlet nextPort = 10000;\n\n/**\n * Get an available port for testing.\n */\nexport function getAvailablePort(): number {\n return nextPort++;\n}\n\n/**\n * Create a test server with dev authentication.\n *\n * @example\n * ```typescript\n * const server = await createTestServer({\n * services: {\n * chatService: new ChatService(testPrisma),\n * },\n * seedDb: async () => {\n * await testPrisma.user.create({ ... });\n * },\n * });\n *\n * const client = await server.connectAs('user-id-123');\n * const result = await client.emit('chatService:createChat', { title: 'Test' });\n *\n * await server.stop();\n * ```\n */\nexport async function createTestServer(options: CreateTestServerOptions): Promise<TestServer> {\n const port = getAvailablePort();\n\n const result = createQuickdrawServer({\n port,\n services: options.services,\n auth: {\n // Dev mode: accept userId directly from auth\n authenticate: async (_socket, auth) => {\n return auth.userId as string | undefined;\n },\n },\n });\n\n // Wait for server to start\n await new Promise<void>((resolve) => {\n result.httpServer.once(\"listening\", resolve);\n });\n\n // Seed database if provided\n if (options.seedDb) {\n await options.seedDb();\n }\n\n return {\n port,\n result,\n stop: () =>\n new Promise<void>((resolve) => {\n result.io.close();\n result.httpServer.close(() => resolve());\n }),\n connectAs: (userId: string) => connectAsUser(port, userId),\n };\n}\n\n/**\n * Connect to a test server as a specific user.\n */\nexport async function connectAsUser(port: number, userId: string): Promise<TestClient> {\n const socket = ioClient(`http://localhost:${port}`, {\n auth: { userId },\n transports: [\"websocket\"],\n autoConnect: true,\n });\n\n await new Promise<void>((resolve, reject) => {\n const timeout = setTimeout(() => {\n reject(new Error(\"Connection timeout\"));\n }, 5000);\n\n socket.on(\"connect\", () => {\n clearTimeout(timeout);\n resolve();\n });\n\n socket.on(\"connect_error\", (err) => {\n clearTimeout(timeout);\n reject(err);\n });\n });\n\n return {\n socket,\n emit: <TPayload, TResponse>(event: string, payload: TPayload) =>\n emitWithAck<TPayload, TResponse>(socket, event, payload),\n close: () => socket.close(),\n };\n}\n\n/**\n * Emit an event and wait for acknowledgment.\n */\nexport function emitWithAck<TPayload, TResponse>(\n socket: Socket,\n event: string,\n payload: TPayload,\n timeoutMs: number = 5000,\n): Promise<TResponse> {\n return new Promise((resolve, reject) => {\n const timeout = setTimeout(() => {\n reject(new Error(`Timeout waiting for ${event}`));\n }, timeoutMs);\n\n socket.emit(event, payload, (response: ServiceResponse<TResponse>) => {\n clearTimeout(timeout);\n\n if (response.success) {\n resolve(response.data);\n } else {\n reject(new Error(response.error));\n }\n });\n });\n}\n\n/**\n * Wait for a specific socket event.\n */\nexport function waitForEvent<T>(\n socket: Socket,\n event: string,\n timeoutMs: number = 5000,\n): Promise<T> {\n return new Promise((resolve, reject) => {\n const timeout = setTimeout(() => {\n socket.off(event, handler);\n reject(new Error(`Timeout waiting for event ${event}`));\n }, timeoutMs);\n\n const handler = (data: T) => {\n clearTimeout(timeout);\n socket.off(event, handler);\n resolve(data);\n };\n\n socket.on(event, handler);\n });\n}\n"]}
@@ -117,6 +117,82 @@ type ServiceMethodMap<T extends Record<string, {
117
117
  payload: unknown;
118
118
  response: unknown;
119
119
  }>> = T;
120
+ /**
121
+ * Channels are the high-frequency counterpart to service methods.
122
+ *
123
+ * Where methods are request/response (ack'd, ACL-checked against the database,
124
+ * counted by the global rate limiter), channels are fire-and-forget: no ack,
125
+ * no response, validated and access-checked entirely in memory, and governed
126
+ * by a per-channel token bucket instead of the global limiter. Use them for
127
+ * traffic where per-message overhead matters and losing a message is fine —
128
+ * game input, cursor positions, typing indicators, telemetry.
129
+ *
130
+ * Wire format: channels route as the Socket.io event
131
+ * `channel:<serviceName>:<channelName>` (see {@link channelEventName}).
132
+ * Clients should emit them volatile; servers broadcast tick data back with
133
+ * `emitToRoomVolatile` so backpressured clients drop frames instead of
134
+ * queueing them.
135
+ *
136
+ * Access model (all checks synchronous, zero DB reads):
137
+ * - Authentication is always required — anonymous sockets are dropped silently.
138
+ * - `"Public"` / `"Read"` access: any authenticated user passes the service gate.
139
+ * - `"Moderate"` / `"Admin"`: requires that level in the socket's in-memory
140
+ * `serviceAccess`.
141
+ * - Entry-level access is expressed via `requireRoom`: the payload resolves to
142
+ * a room name and the socket must already be in that room. Room membership
143
+ * was ACL-checked at subscribe time, so this inherits full entry ACL
144
+ * semantics without touching the database on the hot path.
145
+ */
146
+ interface ServiceChannelDefinition<TPayload = unknown> {
147
+ name: string;
148
+ access: AccessLevel;
149
+ handler: (payload: TPayload, context: ServiceChannelContext) => void;
150
+ /** Required — channel payloads are untrusted high-frequency input. */
151
+ schema: z.ZodType<TPayload>;
152
+ /** Sustained messages per second allowed per socket. */
153
+ ratePerSecond: number;
154
+ /** Bucket capacity — short bursts above ratePerSecond up to this size. */
155
+ burst: number;
156
+ /**
157
+ * Resolve a room the socket must already be a member of (typically the
158
+ * service room joined via subscribe). Return null to skip the check.
159
+ */
160
+ requireRoom?: (payload: TPayload) => string | null;
161
+ }
162
+ /**
163
+ * Context provided to channel handlers. Unlike ServiceMethodContext,
164
+ * userId is always present — unauthenticated channel messages are dropped
165
+ * before the handler runs.
166
+ */
167
+ interface ServiceChannelContext {
168
+ userId: string;
169
+ socketId: string;
170
+ serviceAccess: Record<string, AccessLevel>;
171
+ }
172
+ /**
173
+ * Type helper for defining service channel maps (payload-only — channels
174
+ * have no response).
175
+ *
176
+ * @example
177
+ * ```typescript
178
+ * type GameServiceChannels = ServiceChannelMap<{
179
+ * input: { seq: number; dx: number; dy: number; boost: boolean };
180
+ * }>;
181
+ * ```
182
+ */
183
+ type ServiceChannelMap<T extends Record<string, unknown>> = T;
184
+ /**
185
+ * The Socket.io event name a channel routes as.
186
+ * Shared by the server registry, the React client, and non-JS clients
187
+ * (e.g. the Godot addon) so the wire contract lives in one place.
188
+ */
189
+ declare function channelEventName(serviceName: string, channelName: string): string;
190
+ /**
191
+ * Prefix for all channel events — pass to the rate limiter's
192
+ * `excludePrefixes` so channel traffic bypasses the global request limiter
193
+ * (channels enforce their own per-socket token buckets).
194
+ */
195
+ declare const CHANNEL_EVENT_PREFIX = "channel:";
120
196
  type AdminListPayload = {
121
197
  page?: number;
122
198
  pageSize?: number;
@@ -265,4 +341,4 @@ interface Logger {
265
341
  */
266
342
  declare const consoleLogger: Logger;
267
343
 
268
- export { type ACE, type ACL, type ACLEntity, type AccessLevel, type AdminCreatePayload, type AdminDeletePayload, type AdminDeleteResponse, type AdminFieldConfig, type AdminFieldType, type AdminGetPayload, type AdminGetSubscribersPayload, type AdminGetSubscribersResponse, type AdminListPayload, type AdminListResponse, type AdminMetaPayload, type AdminMetaResponse, type AdminReemitPayload, type AdminReemitResponse, type AdminServiceMeta, type AdminSetEntryACLPayload, type AdminUnsubscribeAllPayload, type AdminUnsubscribeAllResponse, type AdminUpdatePayload, type ExtractPayload, type ExtractResponse, type Logger, type QuickdrawUser, type ServiceMethodContext, type ServiceMethodDefinition, type ServiceMethodMap, type ServiceResponse, type SubscribePayload, type UnsubscribePayload, consoleLogger };
344
+ export { type ACE, type ACL, type ACLEntity, type AccessLevel, type AdminCreatePayload, type AdminDeletePayload, type AdminDeleteResponse, type AdminFieldConfig, type AdminFieldType, type AdminGetPayload, type AdminGetSubscribersPayload, type AdminGetSubscribersResponse, type AdminListPayload, type AdminListResponse, type AdminMetaPayload, type AdminMetaResponse, type AdminReemitPayload, type AdminReemitResponse, type AdminServiceMeta, type AdminSetEntryACLPayload, type AdminUnsubscribeAllPayload, type AdminUnsubscribeAllResponse, type AdminUpdatePayload, CHANNEL_EVENT_PREFIX, type ExtractPayload, type ExtractResponse, type Logger, type QuickdrawUser, type ServiceChannelContext, type ServiceChannelDefinition, type ServiceChannelMap, type ServiceMethodContext, type ServiceMethodDefinition, type ServiceMethodMap, type ServiceResponse, type SubscribePayload, type UnsubscribePayload, channelEventName, consoleLogger };
@@ -1,4 +1,8 @@
1
1
  // src/shared/types.ts
2
+ function channelEventName(serviceName, channelName) {
3
+ return `channel:${serviceName}:${channelName}`;
4
+ }
5
+ var CHANNEL_EVENT_PREFIX = "channel:";
2
6
  var consoleLogger = {
3
7
  info: (message, meta) => console.log(`[INFO] ${message}`, meta ?? ""),
4
8
  warn: (message, meta) => console.warn(`[WARN] ${message}`, meta ?? ""),
@@ -7,6 +11,6 @@ var consoleLogger = {
7
11
  child: () => consoleLogger
8
12
  };
9
13
 
10
- export { consoleLogger };
14
+ export { CHANNEL_EVENT_PREFIX, channelEventName, consoleLogger };
11
15
  //# sourceMappingURL=index.js.map
12
16
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/shared/types.ts"],"names":[],"mappings":";AA0SO,IAAM,aAAA,GAAwB;AAAA,EACnC,IAAA,EAAM,CAAC,OAAA,EAAS,IAAA,KAAS,OAAA,CAAQ,IAAI,CAAA,OAAA,EAAU,OAAO,CAAA,CAAA,EAAI,IAAA,IAAQ,EAAE,CAAA;AAAA,EACpE,IAAA,EAAM,CAAC,OAAA,EAAS,IAAA,KAAS,OAAA,CAAQ,KAAK,CAAA,OAAA,EAAU,OAAO,CAAA,CAAA,EAAI,IAAA,IAAQ,EAAE,CAAA;AAAA,EACrE,KAAA,EAAO,CAAC,OAAA,EAAS,IAAA,KAAS,OAAA,CAAQ,MAAM,CAAA,QAAA,EAAW,OAAO,CAAA,CAAA,EAAI,IAAA,IAAQ,EAAE,CAAA;AAAA,EACxE,KAAA,EAAO,CAAC,OAAA,EAAS,IAAA,KAAS,OAAA,CAAQ,MAAM,CAAA,QAAA,EAAW,OAAO,CAAA,CAAA,EAAI,IAAA,IAAQ,EAAE,CAAA;AAAA,EACxE,OAAO,MAAM;AACf","file":"index.js","sourcesContent":["import type { z } from \"zod\";\n\n// ============================================================================\n// Access Control Types\n// ============================================================================\n\nexport type AccessLevel = \"Public\" | \"Read\" | \"Moderate\" | \"Admin\";\n\nexport type ACE = {\n userId: string;\n level: AccessLevel;\n};\n\nexport type ACL = ACE[];\n\n// ============================================================================\n// Entity and User Interfaces for ACL\n// ============================================================================\n\n/**\n * Interface for entities that support entry-level ACL stored as JSON.\n *\n * Use this pattern when:\n * - You want simple ACL without a separate membership table\n * - Access patterns are straightforward (read/write permissions)\n * - You don't need to query \"all entities user X can access\" efficiently\n *\n * The default `checkEntryACL` in BaseService expects entities to have this shape.\n *\n * @example\n * ```prisma\n * model Document {\n * id String @id @default(cuid())\n * title String\n * acl Json? // Stores ACL array\n * }\n * ```\n *\n * @example\n * ```typescript\n * class DocumentService extends BaseService<Document & ACLEntity, ...> {\n * constructor(prisma: PrismaClient) {\n * super({ serviceName: 'documentService', hasEntryACL: true });\n * this.setDelegate(prisma.document);\n * // Default checkEntryACL will work automatically\n * }\n * }\n * ```\n */\nexport interface ACLEntity {\n id: string;\n acl?: ACL | null;\n}\n\n/**\n * Interface for users with service-level access control.\n *\n * The `serviceAccess` field maps service names to access levels, granting\n * blanket permissions across all entries in that service.\n *\n * @example\n * ```prisma\n * model User {\n * id String @id @default(cuid())\n * serviceAccess Json? // { \"chatService\": \"Admin\", \"documentService\": \"Read\" }\n * }\n * ```\n *\n * @example\n * ```typescript\n * // User with Admin access to chatService can perform any operation\n * // on any chat, regardless of entry-level ACL\n * const user: QuickdrawUser = {\n * id: \"user-123\",\n * serviceAccess: { chatService: \"Admin\" }\n * };\n * ```\n */\nexport interface QuickdrawUser {\n id: string;\n serviceAccess?: Record<string, AccessLevel> | null;\n}\n\n// ============================================================================\n// Service Response Types\n// ============================================================================\n\nexport type ServiceResponse<T = unknown> =\n | { success: true; data: T }\n | { success: false; error: string; code?: number };\n\n// ============================================================================\n// Service Method Definition Types\n// ============================================================================\n\n/**\n * Defines the shape of a service method including its payload and response types.\n * Used to create a type-safe contract between server and client.\n */\nexport interface ServiceMethodDefinition<TPayload = unknown, TResponse = unknown> {\n name: string;\n access: AccessLevel;\n handler: (payload: TPayload, context: ServiceMethodContext) => Promise<TResponse>;\n schema?: z.ZodType<TPayload>;\n resolveEntryId?: (payload: TPayload) => string | null;\n}\n\n/**\n * Context provided to service method handlers\n */\nexport interface ServiceMethodContext {\n userId: string | undefined;\n socketId: string;\n serviceAccess: Record<string, AccessLevel>;\n}\n\n/**\n * Type helper for defining service method maps.\n * Each service should define its methods using this structure.\n *\n * @example\n * ```typescript\n * type ChatServiceMethods = ServiceMethodMap<{\n * createChat: {\n * payload: { title: string };\n * response: { id: string };\n * };\n * updateTitle: {\n * payload: { id: string; title: string };\n * response: { id: string; title: string };\n * };\n * }>;\n * ```\n */\nexport type ServiceMethodMap<T extends Record<string, { payload: unknown; response: unknown }>> = T;\n\n// ============================================================================\n// Admin Method Types (Generic shapes for all services)\n// ============================================================================\n\nexport type AdminListPayload = {\n page?: number;\n pageSize?: number;\n sort?: {\n field?: string;\n direction?: \"asc\" | \"desc\";\n };\n filter?: {\n id?: string;\n ids?: string[];\n createdAfter?: string;\n createdBefore?: string;\n updatedAfter?: string;\n updatedBefore?: string;\n } & Record<string, unknown>;\n};\n\nexport type AdminListResponse<T> = {\n rows: T[];\n page: number;\n pageSize: number;\n total: number;\n};\n\nexport type AdminGetPayload = { id: string };\n\nexport type AdminCreatePayload<TInsert> = { data: Partial<TInsert> };\n\nexport type AdminUpdatePayload<TInsert> = {\n id: string;\n data: Partial<TInsert>;\n};\n\nexport type AdminDeletePayload = { id: string };\nexport type AdminDeleteResponse = { id: string; deleted: true };\n\nexport type AdminSetEntryACLPayload = {\n id: string;\n acl: ACL;\n};\n\nexport type AdminGetSubscribersPayload = { id: string };\nexport type AdminGetSubscribersResponse = {\n id: string;\n subscribers: Array<{ socketId: string; userId?: string }>;\n};\n\nexport type AdminReemitPayload = { id: string };\nexport type AdminReemitResponse = { emitted: boolean };\n\nexport type AdminUnsubscribeAllPayload = { id: string };\nexport type AdminUnsubscribeAllResponse = { id: string; unsubscribed: number };\n\n// ============================================================================\n// Admin Field Configuration Types\n// ============================================================================\n\n/**\n * Supported field types for admin UI generation.\n */\nexport type AdminFieldType =\n | \"string\"\n | \"number\"\n | \"boolean\"\n | \"date\"\n | \"enum\"\n | \"json\"\n | \"relation\";\n\n/**\n * Configuration for a single field in the admin UI.\n * Derived from Zod schemas with optional overrides.\n */\nexport interface AdminFieldConfig {\n /** Field name (property key on the entity) */\n name: string;\n /** Field type for rendering appropriate input */\n type: AdminFieldType;\n /** Human-readable label */\n label: string;\n /** Whether the field is required for creation */\n required: boolean;\n /** Whether the field can be edited (false for id, createdAt, etc.) */\n editable: boolean;\n /** Whether to show this field as a table column */\n showInTable: boolean;\n /** Whether the table can be sorted by this field */\n sortable: boolean;\n /** For enum fields, the allowed values */\n enumValues?: string[];\n /** For relation fields, the related service name */\n relationService?: string;\n}\n\n/**\n * Service metadata for admin UI generation.\n * Returned by the adminMeta method.\n */\nexport interface AdminServiceMeta {\n /** Internal service name (e.g., \"chatService\") */\n serviceName: string;\n /** Human-readable display name (e.g., \"Chats\") */\n displayName: string;\n /** Field configurations derived from schema */\n fields: AdminFieldConfig[];\n}\n\n/**\n * Payload for adminMeta method (no parameters needed)\n */\nexport type AdminMetaPayload = Record<string, never>;\n\n/**\n * Response type for adminMeta method\n */\nexport type AdminMetaResponse = AdminServiceMeta;\n\n// ============================================================================\n// Subscription Types\n// ============================================================================\n\nexport type SubscribePayload = {\n entryId: string;\n requiredLevel?: AccessLevel;\n};\n\nexport type UnsubscribePayload = {\n entryId: string;\n};\n\n// ============================================================================\n// Utility Types\n// ============================================================================\n\n/**\n * Extracts payload type from a service method definition\n */\nexport type ExtractPayload<T> = T extends { payload: infer P } ? P : never;\n\n/**\n * Extracts response type from a service method definition\n */\nexport type ExtractResponse<T> = T extends { response: infer R } ? R : never;\n\n/**\n * Logger interface compatible with Winston and other loggers\n */\nexport interface Logger {\n info(message: string, meta?: Record<string, unknown>): void;\n warn(message: string, meta?: Record<string, unknown>): void;\n error(message: string, meta?: Record<string, unknown>): void;\n debug(message: string, meta?: Record<string, unknown>): void;\n child(options: Record<string, unknown>): Logger;\n}\n\n/**\n * Default console logger implementation\n */\nexport const consoleLogger: Logger = {\n info: (message, meta) => console.log(`[INFO] ${message}`, meta ?? \"\"),\n warn: (message, meta) => console.warn(`[WARN] ${message}`, meta ?? \"\"),\n error: (message, meta) => console.error(`[ERROR] ${message}`, meta ?? \"\"),\n debug: (message, meta) => console.debug(`[DEBUG] ${message}`, meta ?? \"\"),\n child: () => consoleLogger,\n};\n"]}
1
+ {"version":3,"sources":["../../src/shared/types.ts"],"names":[],"mappings":";AAoNO,SAAS,gBAAA,CAAiB,aAAqB,WAAA,EAA6B;AACjF,EAAA,OAAO,CAAA,QAAA,EAAW,WAAW,CAAA,CAAA,EAAI,WAAW,CAAA,CAAA;AAC9C;AAOO,IAAM,oBAAA,GAAuB;AAoK7B,IAAM,aAAA,GAAwB;AAAA,EACnC,IAAA,EAAM,CAAC,OAAA,EAAS,IAAA,KAAS,OAAA,CAAQ,IAAI,CAAA,OAAA,EAAU,OAAO,CAAA,CAAA,EAAI,IAAA,IAAQ,EAAE,CAAA;AAAA,EACpE,IAAA,EAAM,CAAC,OAAA,EAAS,IAAA,KAAS,OAAA,CAAQ,KAAK,CAAA,OAAA,EAAU,OAAO,CAAA,CAAA,EAAI,IAAA,IAAQ,EAAE,CAAA;AAAA,EACrE,KAAA,EAAO,CAAC,OAAA,EAAS,IAAA,KAAS,OAAA,CAAQ,MAAM,CAAA,QAAA,EAAW,OAAO,CAAA,CAAA,EAAI,IAAA,IAAQ,EAAE,CAAA;AAAA,EACxE,KAAA,EAAO,CAAC,OAAA,EAAS,IAAA,KAAS,OAAA,CAAQ,MAAM,CAAA,QAAA,EAAW,OAAO,CAAA,CAAA,EAAI,IAAA,IAAQ,EAAE,CAAA;AAAA,EACxE,OAAO,MAAM;AACf","file":"index.js","sourcesContent":["import type { z } from \"zod\";\n\n// ============================================================================\n// Access Control Types\n// ============================================================================\n\nexport type AccessLevel = \"Public\" | \"Read\" | \"Moderate\" | \"Admin\";\n\nexport type ACE = {\n userId: string;\n level: AccessLevel;\n};\n\nexport type ACL = ACE[];\n\n// ============================================================================\n// Entity and User Interfaces for ACL\n// ============================================================================\n\n/**\n * Interface for entities that support entry-level ACL stored as JSON.\n *\n * Use this pattern when:\n * - You want simple ACL without a separate membership table\n * - Access patterns are straightforward (read/write permissions)\n * - You don't need to query \"all entities user X can access\" efficiently\n *\n * The default `checkEntryACL` in BaseService expects entities to have this shape.\n *\n * @example\n * ```prisma\n * model Document {\n * id String @id @default(cuid())\n * title String\n * acl Json? // Stores ACL array\n * }\n * ```\n *\n * @example\n * ```typescript\n * class DocumentService extends BaseService<Document & ACLEntity, ...> {\n * constructor(prisma: PrismaClient) {\n * super({ serviceName: 'documentService', hasEntryACL: true });\n * this.setDelegate(prisma.document);\n * // Default checkEntryACL will work automatically\n * }\n * }\n * ```\n */\nexport interface ACLEntity {\n id: string;\n acl?: ACL | null;\n}\n\n/**\n * Interface for users with service-level access control.\n *\n * The `serviceAccess` field maps service names to access levels, granting\n * blanket permissions across all entries in that service.\n *\n * @example\n * ```prisma\n * model User {\n * id String @id @default(cuid())\n * serviceAccess Json? // { \"chatService\": \"Admin\", \"documentService\": \"Read\" }\n * }\n * ```\n *\n * @example\n * ```typescript\n * // User with Admin access to chatService can perform any operation\n * // on any chat, regardless of entry-level ACL\n * const user: QuickdrawUser = {\n * id: \"user-123\",\n * serviceAccess: { chatService: \"Admin\" }\n * };\n * ```\n */\nexport interface QuickdrawUser {\n id: string;\n serviceAccess?: Record<string, AccessLevel> | null;\n}\n\n// ============================================================================\n// Service Response Types\n// ============================================================================\n\nexport type ServiceResponse<T = unknown> =\n | { success: true; data: T }\n | { success: false; error: string; code?: number };\n\n// ============================================================================\n// Service Method Definition Types\n// ============================================================================\n\n/**\n * Defines the shape of a service method including its payload and response types.\n * Used to create a type-safe contract between server and client.\n */\nexport interface ServiceMethodDefinition<TPayload = unknown, TResponse = unknown> {\n name: string;\n access: AccessLevel;\n handler: (payload: TPayload, context: ServiceMethodContext) => Promise<TResponse>;\n schema?: z.ZodType<TPayload>;\n resolveEntryId?: (payload: TPayload) => string | null;\n}\n\n/**\n * Context provided to service method handlers\n */\nexport interface ServiceMethodContext {\n userId: string | undefined;\n socketId: string;\n serviceAccess: Record<string, AccessLevel>;\n}\n\n/**\n * Type helper for defining service method maps.\n * Each service should define its methods using this structure.\n *\n * @example\n * ```typescript\n * type ChatServiceMethods = ServiceMethodMap<{\n * createChat: {\n * payload: { title: string };\n * response: { id: string };\n * };\n * updateTitle: {\n * payload: { id: string; title: string };\n * response: { id: string; title: string };\n * };\n * }>;\n * ```\n */\nexport type ServiceMethodMap<T extends Record<string, { payload: unknown; response: unknown }>> = T;\n\n// ============================================================================\n// Service Channel Types (high-frequency, fire-and-forget events)\n// ============================================================================\n\n/**\n * Channels are the high-frequency counterpart to service methods.\n *\n * Where methods are request/response (ack'd, ACL-checked against the database,\n * counted by the global rate limiter), channels are fire-and-forget: no ack,\n * no response, validated and access-checked entirely in memory, and governed\n * by a per-channel token bucket instead of the global limiter. Use them for\n * traffic where per-message overhead matters and losing a message is fine —\n * game input, cursor positions, typing indicators, telemetry.\n *\n * Wire format: channels route as the Socket.io event\n * `channel:<serviceName>:<channelName>` (see {@link channelEventName}).\n * Clients should emit them volatile; servers broadcast tick data back with\n * `emitToRoomVolatile` so backpressured clients drop frames instead of\n * queueing them.\n *\n * Access model (all checks synchronous, zero DB reads):\n * - Authentication is always required — anonymous sockets are dropped silently.\n * - `\"Public\"` / `\"Read\"` access: any authenticated user passes the service gate.\n * - `\"Moderate\"` / `\"Admin\"`: requires that level in the socket's in-memory\n * `serviceAccess`.\n * - Entry-level access is expressed via `requireRoom`: the payload resolves to\n * a room name and the socket must already be in that room. Room membership\n * was ACL-checked at subscribe time, so this inherits full entry ACL\n * semantics without touching the database on the hot path.\n */\nexport interface ServiceChannelDefinition<TPayload = unknown> {\n name: string;\n access: AccessLevel;\n handler: (payload: TPayload, context: ServiceChannelContext) => void;\n /** Required — channel payloads are untrusted high-frequency input. */\n schema: z.ZodType<TPayload>;\n /** Sustained messages per second allowed per socket. */\n ratePerSecond: number;\n /** Bucket capacity — short bursts above ratePerSecond up to this size. */\n burst: number;\n /**\n * Resolve a room the socket must already be a member of (typically the\n * service room joined via subscribe). Return null to skip the check.\n */\n requireRoom?: (payload: TPayload) => string | null;\n}\n\n/**\n * Context provided to channel handlers. Unlike ServiceMethodContext,\n * userId is always present — unauthenticated channel messages are dropped\n * before the handler runs.\n */\nexport interface ServiceChannelContext {\n userId: string;\n socketId: string;\n serviceAccess: Record<string, AccessLevel>;\n}\n\n/**\n * Type helper for defining service channel maps (payload-only — channels\n * have no response).\n *\n * @example\n * ```typescript\n * type GameServiceChannels = ServiceChannelMap<{\n * input: { seq: number; dx: number; dy: number; boost: boolean };\n * }>;\n * ```\n */\nexport type ServiceChannelMap<T extends Record<string, unknown>> = T;\n\n/**\n * The Socket.io event name a channel routes as.\n * Shared by the server registry, the React client, and non-JS clients\n * (e.g. the Godot addon) so the wire contract lives in one place.\n */\nexport function channelEventName(serviceName: string, channelName: string): string {\n return `channel:${serviceName}:${channelName}`;\n}\n\n/**\n * Prefix for all channel events — pass to the rate limiter's\n * `excludePrefixes` so channel traffic bypasses the global request limiter\n * (channels enforce their own per-socket token buckets).\n */\nexport const CHANNEL_EVENT_PREFIX = \"channel:\";\n\n// ============================================================================\n// Admin Method Types (Generic shapes for all services)\n// ============================================================================\n\nexport type AdminListPayload = {\n page?: number;\n pageSize?: number;\n sort?: {\n field?: string;\n direction?: \"asc\" | \"desc\";\n };\n filter?: {\n id?: string;\n ids?: string[];\n createdAfter?: string;\n createdBefore?: string;\n updatedAfter?: string;\n updatedBefore?: string;\n } & Record<string, unknown>;\n};\n\nexport type AdminListResponse<T> = {\n rows: T[];\n page: number;\n pageSize: number;\n total: number;\n};\n\nexport type AdminGetPayload = { id: string };\n\nexport type AdminCreatePayload<TInsert> = { data: Partial<TInsert> };\n\nexport type AdminUpdatePayload<TInsert> = {\n id: string;\n data: Partial<TInsert>;\n};\n\nexport type AdminDeletePayload = { id: string };\nexport type AdminDeleteResponse = { id: string; deleted: true };\n\nexport type AdminSetEntryACLPayload = {\n id: string;\n acl: ACL;\n};\n\nexport type AdminGetSubscribersPayload = { id: string };\nexport type AdminGetSubscribersResponse = {\n id: string;\n subscribers: Array<{ socketId: string; userId?: string }>;\n};\n\nexport type AdminReemitPayload = { id: string };\nexport type AdminReemitResponse = { emitted: boolean };\n\nexport type AdminUnsubscribeAllPayload = { id: string };\nexport type AdminUnsubscribeAllResponse = { id: string; unsubscribed: number };\n\n// ============================================================================\n// Admin Field Configuration Types\n// ============================================================================\n\n/**\n * Supported field types for admin UI generation.\n */\nexport type AdminFieldType =\n | \"string\"\n | \"number\"\n | \"boolean\"\n | \"date\"\n | \"enum\"\n | \"json\"\n | \"relation\";\n\n/**\n * Configuration for a single field in the admin UI.\n * Derived from Zod schemas with optional overrides.\n */\nexport interface AdminFieldConfig {\n /** Field name (property key on the entity) */\n name: string;\n /** Field type for rendering appropriate input */\n type: AdminFieldType;\n /** Human-readable label */\n label: string;\n /** Whether the field is required for creation */\n required: boolean;\n /** Whether the field can be edited (false for id, createdAt, etc.) */\n editable: boolean;\n /** Whether to show this field as a table column */\n showInTable: boolean;\n /** Whether the table can be sorted by this field */\n sortable: boolean;\n /** For enum fields, the allowed values */\n enumValues?: string[];\n /** For relation fields, the related service name */\n relationService?: string;\n}\n\n/**\n * Service metadata for admin UI generation.\n * Returned by the adminMeta method.\n */\nexport interface AdminServiceMeta {\n /** Internal service name (e.g., \"chatService\") */\n serviceName: string;\n /** Human-readable display name (e.g., \"Chats\") */\n displayName: string;\n /** Field configurations derived from schema */\n fields: AdminFieldConfig[];\n}\n\n/**\n * Payload for adminMeta method (no parameters needed)\n */\nexport type AdminMetaPayload = Record<string, never>;\n\n/**\n * Response type for adminMeta method\n */\nexport type AdminMetaResponse = AdminServiceMeta;\n\n// ============================================================================\n// Subscription Types\n// ============================================================================\n\nexport type SubscribePayload = {\n entryId: string;\n requiredLevel?: AccessLevel;\n};\n\nexport type UnsubscribePayload = {\n entryId: string;\n};\n\n// ============================================================================\n// Utility Types\n// ============================================================================\n\n/**\n * Extracts payload type from a service method definition\n */\nexport type ExtractPayload<T> = T extends { payload: infer P } ? P : never;\n\n/**\n * Extracts response type from a service method definition\n */\nexport type ExtractResponse<T> = T extends { response: infer R } ? R : never;\n\n/**\n * Logger interface compatible with Winston and other loggers\n */\nexport interface Logger {\n info(message: string, meta?: Record<string, unknown>): void;\n warn(message: string, meta?: Record<string, unknown>): void;\n error(message: string, meta?: Record<string, unknown>): void;\n debug(message: string, meta?: Record<string, unknown>): void;\n child(options: Record<string, unknown>): Logger;\n}\n\n/**\n * Default console logger implementation\n */\nexport const consoleLogger: Logger = {\n info: (message, meta) => console.log(`[INFO] ${message}`, meta ?? \"\"),\n warn: (message, meta) => console.warn(`[WARN] ${message}`, meta ?? \"\"),\n error: (message, meta) => console.error(`[ERROR] ${message}`, meta ?? \"\"),\n debug: (message, meta) => console.debug(`[DEBUG] ${message}`, meta ?? \"\"),\n child: () => consoleLogger,\n};\n"]}
@@ -1,7 +1,7 @@
1
1
  import { Socket, Server } from 'socket.io';
2
2
  import { Server as Server$1 } from 'http';
3
3
  import { z } from 'zod';
4
- import { AccessLevel, ServiceMethodDefinition, Logger, AdminFieldConfig } from './shared/index.js';
4
+ import { AccessLevel, ServiceMethodDefinition, ServiceChannelDefinition, Logger, AdminFieldConfig } from './shared/index.js';
5
5
 
6
6
  /**
7
7
  * Extended Socket type with authentication properties
@@ -56,6 +56,10 @@ interface BaseServiceInstance {
56
56
  unsubscribeSocket: (socket: QuickdrawSocket) => void;
57
57
  ensureAccessForMethod: (requiredLevel: AccessLevel, socket: QuickdrawSocket, entryId?: string) => Promise<void>;
58
58
  getPublicMethods: () => ServiceMethodDefinition<unknown, unknown>[];
59
+ /** Get channels for registry discovery (services without channels may omit) */
60
+ getPublicChannels?: () => ServiceChannelDefinition<unknown>[];
61
+ /** Synchronous in-memory access check for channel messages */
62
+ checkChannelAccess?: (channel: ServiceChannelDefinition<unknown>, socket: QuickdrawSocket, payload: unknown) => boolean;
59
63
  /** Set Socket.io server instance for room-based broadcasts */
60
64
  setIo?: (io: Server) => void;
61
65
  }
@@ -27,6 +27,25 @@ interface QuickdrawProviderProps {
27
27
  * Required for httpOnly cookie-based authentication.
28
28
  */
29
29
  withCredentials?: boolean;
30
+ /**
31
+ * Custom Socket.io path (default: "/socket.io").
32
+ * Needed when the server sits behind a path-rewriting proxy — e.g.
33
+ * Discord Activities route everything through "/.proxy", so the client
34
+ * must connect with path "/.proxy/api/socket.io".
35
+ */
36
+ socketPath?: string;
37
+ }
38
+ /**
39
+ * Return type for the useChannelSend hook.
40
+ */
41
+ interface UseChannelSendResult<TPayload> {
42
+ /**
43
+ * Send a fire-and-forget channel message. No ack, no response; dropped
44
+ * silently if the socket is not connected (channels tolerate loss by design).
45
+ */
46
+ send: (payload: TPayload) => void;
47
+ /** True when the socket is connected and sends will go out. */
48
+ isReady: boolean;
30
49
  }
31
50
  /**
32
51
  * Options for the useService hook.
@@ -296,4 +315,4 @@ interface QuickdrawSocketContextValue {
296
315
  subscriptionBatcher: SubscriptionBatcher;
297
316
  }
298
317
 
299
- export type { ClientServiceMethodMap as C, QuickdrawSocketContextValue as Q, SubscriptionDataMap as S, UseServiceOptions as U, QuickdrawProviderProps as a, UseServiceResult as b, UseServiceQueryOptions as c, UseServiceQueryResult as d, UseSubscriptionOptions as e, UseSubscriptionResult as f, UseRoomEventsOptions as g };
318
+ export type { ClientServiceMethodMap as C, QuickdrawSocketContextValue as Q, SubscriptionDataMap as S, UseServiceOptions as U, QuickdrawProviderProps as a, UseServiceResult as b, UseServiceQueryOptions as c, UseServiceQueryResult as d, UseSubscriptionOptions as e, UseSubscriptionResult as f, UseRoomEventsOptions as g, UseChannelSendResult as h };