@checkstack/backend 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +225 -0
- package/drizzle/0000_loose_yellow_claw.sql +28 -0
- package/drizzle/meta/0000_snapshot.json +187 -0
- package/drizzle/meta/_journal.json +13 -0
- package/drizzle.config.ts +10 -0
- package/package.json +42 -0
- package/src/db.ts +20 -0
- package/src/health-check-plugin-integration.test.ts +93 -0
- package/src/index.ts +419 -0
- package/src/integration/event-bus.integration.test.ts +313 -0
- package/src/logger.ts +65 -0
- package/src/openapi-router.ts +177 -0
- package/src/plugin-lifecycle.test.ts +276 -0
- package/src/plugin-manager/api-router.ts +163 -0
- package/src/plugin-manager/core-services.ts +312 -0
- package/src/plugin-manager/dependency-sorter.ts +103 -0
- package/src/plugin-manager/deregistration-guard.ts +41 -0
- package/src/plugin-manager/extension-points.ts +85 -0
- package/src/plugin-manager/index.ts +13 -0
- package/src/plugin-manager/plugin-admin-router.ts +89 -0
- package/src/plugin-manager/plugin-loader.ts +464 -0
- package/src/plugin-manager/types.ts +14 -0
- package/src/plugin-manager.test.ts +464 -0
- package/src/plugin-manager.ts +431 -0
- package/src/rpc-rest-compat.test.ts +80 -0
- package/src/schema.ts +46 -0
- package/src/services/config-service.test.ts +66 -0
- package/src/services/config-service.ts +322 -0
- package/src/services/event-bus.test.ts +469 -0
- package/src/services/event-bus.ts +317 -0
- package/src/services/health-check-registry.test.ts +101 -0
- package/src/services/health-check-registry.ts +27 -0
- package/src/services/jwt.ts +45 -0
- package/src/services/keystore.test.ts +198 -0
- package/src/services/keystore.ts +136 -0
- package/src/services/plugin-installer.test.ts +90 -0
- package/src/services/plugin-installer.ts +70 -0
- package/src/services/queue-manager.ts +382 -0
- package/src/services/queue-plugin-registry.ts +17 -0
- package/src/services/queue-proxy.ts +182 -0
- package/src/services/service-registry.ts +35 -0
- package/src/test-preload.ts +114 -0
- package/src/utils/plugin-discovery.test.ts +383 -0
- package/src/utils/plugin-discovery.ts +157 -0
- package/src/utils/strip-public-schema.ts +40 -0
- package/tsconfig.json +6 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
import type { Server } from "bun";
|
|
2
|
+
import { Hono } from "hono";
|
|
3
|
+
import { PluginManager } from "./plugin-manager";
|
|
4
|
+
import { logger } from "hono/logger";
|
|
5
|
+
import { migrate } from "drizzle-orm/node-postgres/migrator";
|
|
6
|
+
import { db } from "./db";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import fs from "node:fs";
|
|
9
|
+
import { rootLogger } from "./logger";
|
|
10
|
+
import { coreServices, coreHooks } from "@checkstack/backend-api";
|
|
11
|
+
import { plugins } from "./schema";
|
|
12
|
+
import { eq, and } from "drizzle-orm";
|
|
13
|
+
import { PluginLocalInstaller } from "./services/plugin-installer";
|
|
14
|
+
import { QueuePluginRegistryImpl } from "./services/queue-plugin-registry";
|
|
15
|
+
import { QueueManagerImpl } from "./services/queue-manager";
|
|
16
|
+
import {
|
|
17
|
+
createWebSocketHandler,
|
|
18
|
+
SignalServiceImpl,
|
|
19
|
+
type WebSocketData,
|
|
20
|
+
} from "@checkstack/signal-backend";
|
|
21
|
+
import {
|
|
22
|
+
PLUGIN_INSTALLED,
|
|
23
|
+
PLUGIN_DEREGISTERED,
|
|
24
|
+
} from "@checkstack/signal-common";
|
|
25
|
+
import { createPluginAdminRouter } from "./plugin-manager/plugin-admin-router";
|
|
26
|
+
import {
|
|
27
|
+
pluginMetadata as apiDocsMetadata,
|
|
28
|
+
permissions as apiDocsPermissions,
|
|
29
|
+
} from "@checkstack/api-docs-common";
|
|
30
|
+
import { qualifyPermissionId } from "@checkstack/common";
|
|
31
|
+
|
|
32
|
+
import { cors } from "hono/cors";
|
|
33
|
+
|
|
34
|
+
const app = new Hono();
|
|
35
|
+
const pluginManager = new PluginManager();
|
|
36
|
+
|
|
37
|
+
// WebSocket handler instance (initialized during init)
|
|
38
|
+
let wsHandler: ReturnType<typeof createWebSocketHandler> | undefined;
|
|
39
|
+
|
|
40
|
+
// CORS configuration
|
|
41
|
+
// - In production: uses BASE_URL
|
|
42
|
+
// - In development: allows both backend origin and Vite dev server
|
|
43
|
+
const corsOrigin = process.env.BASE_URL || "http://localhost:3000";
|
|
44
|
+
const corsOrigins = [corsOrigin];
|
|
45
|
+
|
|
46
|
+
// Allow Vite dev server in development
|
|
47
|
+
if (!process.env.BASE_URL || corsOrigin.includes("localhost")) {
|
|
48
|
+
corsOrigins.push("http://localhost:5173");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
app.use(
|
|
52
|
+
"*",
|
|
53
|
+
cors({
|
|
54
|
+
origin: corsOrigins,
|
|
55
|
+
allowHeaders: ["Content-Type", "Authorization"],
|
|
56
|
+
allowMethods: ["POST", "GET", "PUT", "PATCH", "DELETE", "OPTIONS"],
|
|
57
|
+
exposeHeaders: ["Content-Length"],
|
|
58
|
+
maxAge: 600,
|
|
59
|
+
credentials: true,
|
|
60
|
+
})
|
|
61
|
+
);
|
|
62
|
+
app.use("*", logger());
|
|
63
|
+
|
|
64
|
+
// Runtime config endpoint - returns BASE_URL for frontend
|
|
65
|
+
app.get("/api/config", (c) => {
|
|
66
|
+
const baseUrl = process.env.BASE_URL || "http://localhost:3000";
|
|
67
|
+
return c.json({ baseUrl });
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
app.get("/api/plugins", async (c) => {
|
|
71
|
+
// Only return remote plugins that need to be loaded via HTTP
|
|
72
|
+
// Local plugins are bundled and loaded via Vite's glob import
|
|
73
|
+
const enabledPlugins = await db
|
|
74
|
+
.select({
|
|
75
|
+
name: plugins.name,
|
|
76
|
+
path: plugins.path,
|
|
77
|
+
})
|
|
78
|
+
.from(plugins)
|
|
79
|
+
.where(
|
|
80
|
+
and(
|
|
81
|
+
eq(plugins.enabled, true),
|
|
82
|
+
eq(plugins.type, "frontend"),
|
|
83
|
+
eq(plugins.isUninstallable, true) // Only remote plugins
|
|
84
|
+
)
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
return c.json(enabledPlugins);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
app.get("/.well-known/jwks.json", async (c) => {
|
|
91
|
+
const { keyStore } = await import("./services/keystore");
|
|
92
|
+
const jwks = await keyStore.getPublicJWKS();
|
|
93
|
+
return c.json(jwks);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// Production: Serve frontend static files when CHECKSTACK_FRONTEND_DIST is set
|
|
97
|
+
// Must be registered at module load time before Hono's router is built
|
|
98
|
+
const frontendDistPath = process.env.CHECKSTACK_FRONTEND_DIST;
|
|
99
|
+
if (frontendDistPath && fs.existsSync(frontendDistPath)) {
|
|
100
|
+
rootLogger.info(`📦 Serving frontend from: ${frontendDistPath}`);
|
|
101
|
+
|
|
102
|
+
// Serve static assets (JS, CSS, images, etc.)
|
|
103
|
+
app.get("/assets/*", async (c) => {
|
|
104
|
+
const assetPath = c.req.path.replace("/assets/", "");
|
|
105
|
+
const filePath = path.join(frontendDistPath, "assets", assetPath);
|
|
106
|
+
|
|
107
|
+
if (fs.existsSync(filePath)) {
|
|
108
|
+
const file = Bun.file(filePath);
|
|
109
|
+
return new Response(file, {
|
|
110
|
+
headers: { "Content-Type": file.type },
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
return c.notFound();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// Serve vendor scripts (externalized React, react-router-dom, etc.)
|
|
117
|
+
app.get("/vendor/*", async (c) => {
|
|
118
|
+
const vendorPath = c.req.path.replace("/vendor/", "");
|
|
119
|
+
const filePath = path.join(frontendDistPath, "vendor", vendorPath);
|
|
120
|
+
|
|
121
|
+
if (fs.existsSync(filePath)) {
|
|
122
|
+
const file = Bun.file(filePath);
|
|
123
|
+
return new Response(file, {
|
|
124
|
+
headers: { "Content-Type": file.type },
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
return c.notFound();
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// Serve index.html for all non-API routes (SPA fallback)
|
|
131
|
+
app.get("*", async (c, next) => {
|
|
132
|
+
// Skip API and WebSocket routes - let them pass through to actual handlers
|
|
133
|
+
if (c.req.path.startsWith("/api")) {
|
|
134
|
+
return next();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const indexPath = path.join(frontendDistPath, "index.html");
|
|
138
|
+
if (fs.existsSync(indexPath)) {
|
|
139
|
+
const file = Bun.file(indexPath);
|
|
140
|
+
return new Response(file, {
|
|
141
|
+
headers: { "Content-Type": "text/html" },
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
return c.notFound();
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const init = async () => {
|
|
149
|
+
rootLogger.info("🚀 Starting Checkstack Core...");
|
|
150
|
+
|
|
151
|
+
// Register Plugin Installer Service
|
|
152
|
+
const installer = new PluginLocalInstaller(
|
|
153
|
+
path.join(process.cwd(), "runtime_plugins")
|
|
154
|
+
);
|
|
155
|
+
pluginManager.registerService(coreServices.pluginInstaller, installer);
|
|
156
|
+
|
|
157
|
+
// 1. Run Core Migrations
|
|
158
|
+
rootLogger.info("🔄 Running core migrations...");
|
|
159
|
+
try {
|
|
160
|
+
await migrate(db, {
|
|
161
|
+
// Use import.meta.dir to find migrations relative to this file (works in Docker)
|
|
162
|
+
migrationsFolder: path.join(import.meta.dir, "..", "drizzle"),
|
|
163
|
+
});
|
|
164
|
+
rootLogger.info("✅ Core migrations applied.");
|
|
165
|
+
} catch (error) {
|
|
166
|
+
throw new Error("❌ Failed to apply core migrations", {
|
|
167
|
+
cause: error,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// 1.5. Ensure JWKS signing keys exist
|
|
172
|
+
rootLogger.info("🔑 Ensuring JWKS signing keys...");
|
|
173
|
+
const { keyStore } = await import("./services/keystore");
|
|
174
|
+
await keyStore.getSigningKey(); // This triggers generation if missing
|
|
175
|
+
|
|
176
|
+
// 1.6. Create backend-scoped ConfigService for core services
|
|
177
|
+
const { ConfigServiceImpl } = await import("./services/config-service");
|
|
178
|
+
const configService = new ConfigServiceImpl("backend", db);
|
|
179
|
+
|
|
180
|
+
// 1.7. Register Queue Services
|
|
181
|
+
rootLogger.debug("Registering queue services...");
|
|
182
|
+
const queueRegistry = new QueuePluginRegistryImpl();
|
|
183
|
+
const queueManager = new QueueManagerImpl(
|
|
184
|
+
queueRegistry,
|
|
185
|
+
configService,
|
|
186
|
+
rootLogger
|
|
187
|
+
);
|
|
188
|
+
pluginManager.registerService(
|
|
189
|
+
coreServices.queuePluginRegistry,
|
|
190
|
+
queueRegistry
|
|
191
|
+
);
|
|
192
|
+
pluginManager.registerService(coreServices.queueManager, queueManager);
|
|
193
|
+
|
|
194
|
+
// Serve static assets for runtime frontend plugins only
|
|
195
|
+
// Backend plugins don't need public assets - only frontend plugins do
|
|
196
|
+
// e.g. /assets/plugins/my-plugin-frontend/index.js -> runtime_plugins/node_modules/my-plugin-frontend/dist/index.js
|
|
197
|
+
app.use("/assets/plugins/:pluginName/*", async (c, next) => {
|
|
198
|
+
const pluginName = c.req.param("pluginName");
|
|
199
|
+
// Find plugin in DB to get path
|
|
200
|
+
const results = await db
|
|
201
|
+
.select()
|
|
202
|
+
.from(plugins)
|
|
203
|
+
.where(eq(plugins.name, pluginName));
|
|
204
|
+
const plugin = results[0];
|
|
205
|
+
|
|
206
|
+
// Only serve assets for frontend plugins
|
|
207
|
+
if (!plugin || plugin.type !== "frontend") {
|
|
208
|
+
return next();
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// We assume plugins are built into 'dist' folder
|
|
212
|
+
const assetPath = c.req.path.split(`/assets/plugins/${pluginName}/`)[1];
|
|
213
|
+
const filePath = path.join(plugin.path, "dist", assetPath);
|
|
214
|
+
|
|
215
|
+
if (fs.existsSync(filePath)) {
|
|
216
|
+
return c.body(fs.readFileSync(filePath));
|
|
217
|
+
}
|
|
218
|
+
return next();
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// 2. Initialize Signal Service (before plugins so they can use it)
|
|
222
|
+
// SignalService requires EventBus which is a lazy factory depending on QueueManager
|
|
223
|
+
rootLogger.debug("Initializing signal service...");
|
|
224
|
+
const eventBus = await pluginManager.getService(coreServices.eventBus);
|
|
225
|
+
if (!eventBus) {
|
|
226
|
+
throw new Error("EventBus not available - required for SignalService");
|
|
227
|
+
}
|
|
228
|
+
const signalService = new SignalServiceImpl(
|
|
229
|
+
eventBus,
|
|
230
|
+
rootLogger.child({ service: "SignalService" })
|
|
231
|
+
);
|
|
232
|
+
pluginManager.registerService(coreServices.signalService, signalService);
|
|
233
|
+
|
|
234
|
+
// 2.5. Register OpenAPI endpoint BEFORE plugins load
|
|
235
|
+
// Must be registered before /api/:pluginId/* catch-all route
|
|
236
|
+
const authService = await pluginManager.getService(coreServices.auth);
|
|
237
|
+
if (authService) {
|
|
238
|
+
const { createOpenApiHandler } = await import("./openapi-router");
|
|
239
|
+
const baseUrl = process.env.BASE_URL || "http://localhost:3000";
|
|
240
|
+
const openApiHandler = createOpenApiHandler({
|
|
241
|
+
pluginManager,
|
|
242
|
+
authService,
|
|
243
|
+
baseUrl,
|
|
244
|
+
requiredPermission: qualifyPermissionId(
|
|
245
|
+
apiDocsMetadata,
|
|
246
|
+
apiDocsPermissions.apiDocsView
|
|
247
|
+
),
|
|
248
|
+
});
|
|
249
|
+
app.get("/api/openapi.json", async (c) => {
|
|
250
|
+
const response = await openApiHandler(c.req.raw);
|
|
251
|
+
return c.newResponse(response.body, response);
|
|
252
|
+
});
|
|
253
|
+
rootLogger.debug("OpenAPI endpoint registered at /api/openapi.json");
|
|
254
|
+
} else {
|
|
255
|
+
rootLogger.warn(
|
|
256
|
+
"AuthService not available, OpenAPI endpoint will not be registered"
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// 3. Load Plugins
|
|
261
|
+
await pluginManager.loadPlugins(app);
|
|
262
|
+
|
|
263
|
+
// 4. Wire up auth client for permission-based signal filtering
|
|
264
|
+
// This must happen AFTER plugins load so auth-backend is available
|
|
265
|
+
const rpcClient = await pluginManager.getService(coreServices.rpcClient);
|
|
266
|
+
if (rpcClient) {
|
|
267
|
+
const { AuthApi } = await import("@checkstack/auth-common");
|
|
268
|
+
const authClient = rpcClient.forPlugin(AuthApi);
|
|
269
|
+
signalService.setAuthClient(authClient);
|
|
270
|
+
rootLogger.debug(
|
|
271
|
+
"SignalService: Auth client configured for permission filtering"
|
|
272
|
+
);
|
|
273
|
+
} else {
|
|
274
|
+
rootLogger.warn(
|
|
275
|
+
"SignalService: RpcClient not available, sendToAuthorizedUsers will be disabled"
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// 5. Register plugin admin router (core admin endpoints)
|
|
280
|
+
const pluginAdminRouter = createPluginAdminRouter({
|
|
281
|
+
pluginManager,
|
|
282
|
+
installer,
|
|
283
|
+
});
|
|
284
|
+
// Register as core router - available at /api/core/
|
|
285
|
+
pluginManager.registerCoreRouter("core", pluginAdminRouter);
|
|
286
|
+
|
|
287
|
+
// 5. Setup lifecycle listeners for multi-instance coordination
|
|
288
|
+
await pluginManager.setupLifecycleListeners();
|
|
289
|
+
|
|
290
|
+
// 6. Load Queue Configuration AFTER plugins (queue plugins register first)
|
|
291
|
+
rootLogger.info("📋 Loading queue configuration...");
|
|
292
|
+
await queueManager.loadConfiguration();
|
|
293
|
+
|
|
294
|
+
// 7. Start config polling for multi-instance coordination
|
|
295
|
+
queueManager.startPolling(5000);
|
|
296
|
+
|
|
297
|
+
// 9. Setup plugin lifecycle signal broadcasting to frontend
|
|
298
|
+
// Only broadcast for frontend plugins (plugins ending with -frontend)
|
|
299
|
+
await eventBus.subscribe(
|
|
300
|
+
"core",
|
|
301
|
+
coreHooks.pluginInstalled,
|
|
302
|
+
async ({ pluginId }) => {
|
|
303
|
+
// Only signal frontend plugin installations to the frontend
|
|
304
|
+
if (!pluginId.endsWith("-frontend")) {
|
|
305
|
+
rootLogger.debug(
|
|
306
|
+
`Skipping PLUGIN_INSTALLED signal for non-frontend plugin: ${pluginId}`
|
|
307
|
+
);
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
rootLogger.debug(`Broadcasting PLUGIN_INSTALLED signal for: ${pluginId}`);
|
|
311
|
+
await signalService.broadcast(PLUGIN_INSTALLED, { pluginId });
|
|
312
|
+
},
|
|
313
|
+
{ mode: "work-queue", workerGroup: "frontend-signal-installed" }
|
|
314
|
+
);
|
|
315
|
+
await eventBus.subscribe(
|
|
316
|
+
"core",
|
|
317
|
+
coreHooks.pluginDeregistered,
|
|
318
|
+
async ({ pluginId }) => {
|
|
319
|
+
// Only signal frontend plugin deregistrations to the frontend
|
|
320
|
+
if (!pluginId.endsWith("-frontend")) {
|
|
321
|
+
rootLogger.debug(
|
|
322
|
+
`Skipping PLUGIN_DEREGISTERED signal for non-frontend plugin: ${pluginId}`
|
|
323
|
+
);
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
rootLogger.debug(
|
|
327
|
+
`Broadcasting PLUGIN_DEREGISTERED signal for: ${pluginId}`
|
|
328
|
+
);
|
|
329
|
+
await signalService.broadcast(PLUGIN_DEREGISTERED, { pluginId });
|
|
330
|
+
},
|
|
331
|
+
{ mode: "work-queue", workerGroup: "frontend-signal-deregistered" }
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
// 11. Create WebSocket handler for realtime signals
|
|
335
|
+
wsHandler = createWebSocketHandler({
|
|
336
|
+
eventBus,
|
|
337
|
+
logger: rootLogger.child({ service: "WebSocket" }),
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
rootLogger.info("✅ Checkstack Core initialized.");
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
void init();
|
|
344
|
+
|
|
345
|
+
// Custom fetch handler that handles WebSocket upgrades
|
|
346
|
+
const fetch = async (
|
|
347
|
+
req: Request,
|
|
348
|
+
server: Server<WebSocketData>
|
|
349
|
+
): Promise<Response | undefined> => {
|
|
350
|
+
// Set the server reference for WebSocket pub/sub after startup
|
|
351
|
+
if (wsHandler && !server.upgrade) {
|
|
352
|
+
// Server doesn't support WebSocket upgrade (shouldn't happen with Bun)
|
|
353
|
+
return app.fetch(req, server);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Give the WebSocket handler the server reference if needed
|
|
357
|
+
wsHandler?.setServer(server);
|
|
358
|
+
|
|
359
|
+
const url = new URL(req.url);
|
|
360
|
+
|
|
361
|
+
// Handle WebSocket upgrade for signals
|
|
362
|
+
if (url.pathname === "/api/signals/ws") {
|
|
363
|
+
// Try to authenticate, but allow anonymous connections for broadcast signals
|
|
364
|
+
const authService = await pluginManager.getService(coreServices.auth);
|
|
365
|
+
let userId: string | undefined;
|
|
366
|
+
|
|
367
|
+
if (authService) {
|
|
368
|
+
const user = await authService.authenticate(req);
|
|
369
|
+
// Only RealUser (type: 'user') can have a private channel
|
|
370
|
+
if (user?.type === "user") {
|
|
371
|
+
userId = user.id;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const success = server.upgrade(req, {
|
|
376
|
+
data: {
|
|
377
|
+
userId, // undefined for anonymous, set for authenticated users
|
|
378
|
+
createdAt: Date.now(),
|
|
379
|
+
},
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
return success
|
|
383
|
+
? undefined
|
|
384
|
+
: new Response("WebSocket upgrade failed", { status: 500 });
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Handle regular HTTP requests with Hono
|
|
388
|
+
return app.fetch(req, server);
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
export default {
|
|
392
|
+
port: 3000,
|
|
393
|
+
fetch,
|
|
394
|
+
websocket: {
|
|
395
|
+
// Type template for ws.data
|
|
396
|
+
data: {} as WebSocketData,
|
|
397
|
+
|
|
398
|
+
open(ws: import("bun").ServerWebSocket<WebSocketData>) {
|
|
399
|
+
wsHandler?.websocket.open(ws);
|
|
400
|
+
},
|
|
401
|
+
|
|
402
|
+
message(
|
|
403
|
+
ws: import("bun").ServerWebSocket<WebSocketData>,
|
|
404
|
+
message: string | Buffer
|
|
405
|
+
) {
|
|
406
|
+
wsHandler?.websocket.message(ws, message);
|
|
407
|
+
},
|
|
408
|
+
|
|
409
|
+
close(
|
|
410
|
+
ws: import("bun").ServerWebSocket<WebSocketData>,
|
|
411
|
+
code: number,
|
|
412
|
+
reason: string
|
|
413
|
+
) {
|
|
414
|
+
wsHandler?.websocket.close(ws, code, reason);
|
|
415
|
+
},
|
|
416
|
+
},
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
export { jwtService } from "./services/jwt";
|