@arcote.tech/arc-host 0.3.3 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/index.d.ts +1 -2
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +1024 -364
  4. package/dist/index.js.map +12 -8
  5. package/dist/src/connection-manager.d.ts +16 -1
  6. package/dist/src/connection-manager.d.ts.map +1 -1
  7. package/dist/src/context-handler.d.ts +3 -5
  8. package/dist/src/context-handler.d.ts.map +1 -1
  9. package/dist/src/create-server.d.ts +36 -0
  10. package/dist/src/create-server.d.ts.map +1 -0
  11. package/dist/src/cron-scheduler.d.ts +30 -0
  12. package/dist/src/cron-scheduler.d.ts.map +1 -0
  13. package/dist/src/event-auth.d.ts +6 -1
  14. package/dist/src/event-auth.d.ts.map +1 -1
  15. package/dist/src/index.d.ts +6 -2
  16. package/dist/src/index.d.ts.map +1 -1
  17. package/dist/src/middleware/http.d.ts +15 -0
  18. package/dist/src/middleware/http.d.ts.map +1 -0
  19. package/dist/src/middleware/index.d.ts +4 -0
  20. package/dist/src/middleware/index.d.ts.map +1 -0
  21. package/dist/src/middleware/types.d.ts +31 -0
  22. package/dist/src/middleware/types.d.ts.map +1 -0
  23. package/dist/src/middleware/ws.d.ts +9 -0
  24. package/dist/src/middleware/ws.d.ts.map +1 -0
  25. package/dist/src/types.d.ts +25 -4
  26. package/dist/src/types.d.ts.map +1 -1
  27. package/index.ts +2 -4
  28. package/package.json +2 -1
  29. package/src/connection-manager.ts +37 -7
  30. package/src/context-handler.ts +22 -23
  31. package/src/create-server.ts +213 -0
  32. package/src/cron-scheduler.ts +124 -0
  33. package/src/event-auth.ts +26 -1
  34. package/src/index.ts +39 -9
  35. package/src/middleware/http.ts +414 -0
  36. package/src/middleware/index.ts +27 -0
  37. package/src/middleware/types.ts +42 -0
  38. package/src/middleware/ws.ts +266 -0
  39. package/src/types.ts +22 -4
  40. package/dist/src/arc-host.d.ts +0 -75
  41. package/dist/src/arc-host.d.ts.map +0 -1
  42. package/src/arc-host.ts +0 -818
package/src/arc-host.ts DELETED
@@ -1,818 +0,0 @@
1
- import type { ArcRouteAny } from "@arcote.tech/arc";
2
- import { liveQuery } from "@arcote.tech/arc";
3
- import type { Server, ServerWebSocket } from "bun";
4
- import jwt from "jsonwebtoken";
5
- import { ConnectionManager } from "./connection-manager";
6
- import { ContextHandler } from "./context-handler";
7
- import { filterEventsForToken } from "./event-auth";
8
- import type {
9
- ArcHostConfig,
10
- ClientToHostMessage,
11
- HostToClientMessage,
12
- TokenPayload,
13
- } from "./types";
14
-
15
- type WebSocketData = {
16
- clientId: string;
17
- token: TokenPayload | null;
18
- rawToken: string | null;
19
- };
20
-
21
- /**
22
- * Active SSE stream connection
23
- */
24
- interface StreamConnection {
25
- id: string;
26
- controller: ReadableStreamDefaultController<Uint8Array>;
27
- unsubscribe: () => void;
28
- }
29
-
30
- /**
31
- * Arc Host - WebSocket server for real-time event sync
32
- */
33
- export class ArcHost {
34
- private server!: Server<WebSocketData>;
35
- private connectionManager = new ConnectionManager();
36
- private contextHandler!: ContextHandler;
37
- private streamConnections = new Map<string, StreamConnection>();
38
- private jwtSecret: string;
39
- private port: number;
40
- private streamIdCounter = 0;
41
-
42
- constructor(private config: ArcHostConfig) {
43
- this.jwtSecret =
44
- config.jwtSecret ||
45
- process.env.JWT_SECRET ||
46
- "arc-host-secret-change-in-production";
47
- this.port = config.port || 5005;
48
- }
49
-
50
- /**
51
- * Start the host server
52
- */
53
- async start(): Promise<void> {
54
- // Initialize context handler
55
- const dbAdapter = this.config.dbAdapterFactory(this.config.context);
56
- this.contextHandler = new ContextHandler(this.config.context, dbAdapter);
57
- await this.contextHandler.init();
58
-
59
- this.setupServer();
60
- }
61
-
62
- /**
63
- * Verify JWT token
64
- * Supports both standard JWT (jsonwebtoken) and Arc's custom JWT format
65
- */
66
- private verifyToken(token: string): TokenPayload | null {
67
- try {
68
- // Try standard JWT first
69
- const decoded = jwt.verify(token, this.jwtSecret) as any;
70
-
71
- // Handle Arc's custom JWT format (tokenName -> tokenType)
72
- if (decoded.tokenName && !decoded.tokenType) {
73
- return {
74
- tokenType: decoded.tokenName,
75
- params: decoded.params || {},
76
- iat: decoded.iat,
77
- exp: decoded.exp,
78
- };
79
- }
80
-
81
- return decoded as TokenPayload;
82
- } catch {
83
- // Try Arc's custom JWT format (base64 encoded, custom signature)
84
- try {
85
- const parts = token.split(".");
86
- if (parts.length !== 3) return null;
87
-
88
- const payload = JSON.parse(atob(parts[1]));
89
-
90
- // TODO: Verify signature with context's token secret
91
- // For now, just decode and trust (signature verification should be added)
92
-
93
- return {
94
- tokenType: payload.tokenName,
95
- params: payload.params || {},
96
- iat: payload.iat,
97
- exp: payload.exp,
98
- };
99
- } catch {
100
- return null;
101
- }
102
- }
103
- }
104
-
105
- /**
106
- * Handle WebSocket message
107
- */
108
- private async handleMessage(
109
- ws: ServerWebSocket<WebSocketData>,
110
- message: ClientToHostMessage,
111
- ): Promise<void> {
112
- const client = this.connectionManager.getClientByWs(ws);
113
- if (!client) {
114
- this.sendError(ws, "Client not found");
115
- return;
116
- }
117
-
118
- switch (message.type) {
119
- case "sync-events":
120
- await this.handleSyncEvents(client.id, message, client.token);
121
- break;
122
-
123
- case "request-sync":
124
- await this.handleRequestSync(client.id, message, client.token);
125
- break;
126
-
127
- case "execute-command":
128
- await this.handleExecuteCommand(client.id, message, client.rawToken);
129
- break;
130
-
131
- default:
132
- this.sendError(ws, `Unknown message type: ${(message as any).type}`);
133
- }
134
- }
135
-
136
- /**
137
- * Handle sync-events message
138
- */
139
- private async handleSyncEvents(
140
- clientId: string,
141
- message: Extract<ClientToHostMessage, { type: "sync-events" }>,
142
- token: TokenPayload | null,
143
- ): Promise<void> {
144
- // Persist events
145
- const persistedEvents = await this.contextHandler.persistEvents(
146
- message.events,
147
- clientId,
148
- token,
149
- );
150
-
151
- if (persistedEvents.length === 0) {
152
- return;
153
- }
154
-
155
- // Broadcast to other authorized clients
156
- const clients = this.connectionManager.getAllClients();
157
-
158
- for (const client of clients) {
159
- if (client.id === clientId) continue;
160
-
161
- // Filter events for this client's token
162
- const authorizedEvents = filterEventsForToken(
163
- client.token,
164
- persistedEvents,
165
- this.contextHandler.getEventDefinitions(),
166
- );
167
-
168
- if (authorizedEvents.length > 0) {
169
- this.connectionManager.sendToClient(client.id, {
170
- type: "events",
171
- events: authorizedEvents,
172
- });
173
- }
174
- }
175
-
176
- // Update sender's last synced ID
177
- const lastEvent = persistedEvents[persistedEvents.length - 1];
178
- this.connectionManager.updateLastSyncedEventId(clientId, lastEvent.hostId);
179
-
180
- // Confirm sync to sender
181
- this.connectionManager.sendToClient(clientId, {
182
- type: "sync-complete",
183
- lastHostEventId: lastEvent.hostId,
184
- });
185
- }
186
-
187
- /**
188
- * Handle request-sync message (initial sync or catch-up)
189
- */
190
- private async handleRequestSync(
191
- clientId: string,
192
- message: Extract<ClientToHostMessage, { type: "request-sync" }>,
193
- token: TokenPayload | null,
194
- ): Promise<void> {
195
- const events = await this.contextHandler.getEventsSince(
196
- message.lastHostEventId,
197
- token,
198
- );
199
-
200
- this.connectionManager.sendToClient(clientId, {
201
- type: "events",
202
- events,
203
- });
204
-
205
- if (events.length > 0) {
206
- const lastEvent = events[events.length - 1];
207
- this.connectionManager.updateLastSyncedEventId(
208
- clientId,
209
- lastEvent.hostId,
210
- );
211
-
212
- this.connectionManager.sendToClient(clientId, {
213
- type: "sync-complete",
214
- lastHostEventId: lastEvent.hostId,
215
- });
216
- } else {
217
- this.connectionManager.sendToClient(clientId, {
218
- type: "sync-complete",
219
- lastHostEventId: message.lastHostEventId || "",
220
- });
221
- }
222
- }
223
-
224
- /**
225
- * Handle execute-command message
226
- */
227
- private async handleExecuteCommand(
228
- clientId: string,
229
- message: Extract<ClientToHostMessage, { type: "execute-command" }>,
230
- rawToken: string | null,
231
- ): Promise<void> {
232
- try {
233
- const result = await this.contextHandler.executeCommand(
234
- message.commandName,
235
- message.params,
236
- rawToken,
237
- );
238
-
239
- this.connectionManager.sendToClient(clientId, {
240
- type: "command-result",
241
- requestId: message.requestId,
242
- result,
243
- });
244
- } catch (error) {
245
- this.connectionManager.sendToClient(clientId, {
246
- type: "command-result",
247
- requestId: message.requestId,
248
- error: (error as Error).message,
249
- });
250
- }
251
- }
252
-
253
- /**
254
- * Send error to WebSocket
255
- */
256
- private sendError(ws: ServerWebSocket<WebSocketData>, message: string): void {
257
- ws.send(JSON.stringify({ type: "error", message } as HostToClientMessage));
258
- }
259
-
260
- /**
261
- * Setup Bun server
262
- */
263
- private setupServer(): void {
264
- const self = this;
265
-
266
- this.server = Bun.serve<WebSocketData>({
267
- port: this.port,
268
- idleTimeout: 255,
269
-
270
- fetch(req, server) {
271
- const url = new URL(req.url);
272
-
273
- // CORS headers
274
- const corsHeaders = {
275
- "Access-Control-Allow-Origin": "*",
276
- "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
277
- "Access-Control-Allow-Headers": "Content-Type, Authorization",
278
- };
279
-
280
- // Handle preflight
281
- if (req.method === "OPTIONS") {
282
- return new Response(null, { headers: corsHeaders });
283
- }
284
-
285
- // Extract token
286
- const authHeader = req.headers.get("Authorization");
287
- const token =
288
- authHeader?.replace("Bearer ", "") || url.searchParams.get("token");
289
-
290
- // Verify token
291
- let tokenPayload: TokenPayload | null = null;
292
- if (token) {
293
- tokenPayload = self.verifyToken(token);
294
- }
295
-
296
- // WebSocket upgrade for /ws
297
- if (
298
- url.pathname === "/ws" &&
299
- req.headers.get("Upgrade") === "websocket"
300
- ) {
301
- // Pass token to WebSocket data
302
- if (
303
- server.upgrade(req, {
304
- data: {
305
- clientId: "",
306
- token: tokenPayload,
307
- rawToken: token,
308
- },
309
- })
310
- ) {
311
- // Connection will be set up in websocket.open
312
- return undefined;
313
- }
314
- return new Response("WebSocket upgrade failed", {
315
- status: 500,
316
- headers: corsHeaders,
317
- });
318
- }
319
-
320
- // Health check
321
- if (url.pathname === "/health") {
322
- return new Response(
323
- JSON.stringify({
324
- status: "ok",
325
- clients: self.connectionManager.clientCount,
326
- }),
327
- {
328
- headers: { ...corsHeaders, "Content-Type": "application/json" },
329
- },
330
- );
331
- }
332
-
333
- // HTTP command endpoint: POST /command/:commandName
334
- if (url.pathname.startsWith("/command/") && req.method === "POST") {
335
- return self.handleHttpCommand(req, url, corsHeaders, token);
336
- }
337
-
338
- // HTTP query endpoint: POST /query/:viewName
339
- if (url.pathname.startsWith("/query/") && req.method === "POST") {
340
- return self.handleHttpQuery(
341
- req,
342
- url,
343
- tokenPayload,
344
- corsHeaders,
345
- token,
346
- );
347
- }
348
-
349
- // SSE stream endpoint: GET /stream/:viewName
350
- if (url.pathname.startsWith("/stream/") && req.method === "GET") {
351
- return self.handleHttpStream(
352
- req,
353
- url,
354
- tokenPayload,
355
- corsHeaders,
356
- token,
357
- );
358
- }
359
-
360
- // HTTP event sync endpoint: POST /sync/events
361
- if (url.pathname === "/sync/events" && req.method === "POST") {
362
- return self.handleHttpEventSync(req, tokenPayload, corsHeaders);
363
- }
364
-
365
- // Route endpoints: /route/*
366
- if (url.pathname.startsWith("/route/")) {
367
- return self.handleHttpRoute(
368
- req,
369
- url,
370
- tokenPayload,
371
- corsHeaders,
372
- token,
373
- );
374
- }
375
-
376
- return new Response("Not Found", { status: 404, headers: corsHeaders });
377
- },
378
-
379
- websocket: {
380
- open(ws) {
381
- // Get token from upgrade data
382
- const tokenPayload = ws.data?.token || null;
383
- const rawToken = ws.data?.rawToken || null;
384
-
385
- // Add client to connection manager
386
- const client = self.connectionManager.addClient(
387
- ws,
388
- tokenPayload,
389
- rawToken,
390
- );
391
-
392
- console.log(`Client connected: ${client.id}`);
393
- },
394
-
395
- message(ws, messageStr) {
396
- try {
397
- const message = JSON.parse(
398
- messageStr as string,
399
- ) as ClientToHostMessage;
400
- self.handleMessage(ws, message);
401
- } catch (error) {
402
- console.error("Failed to parse message:", error);
403
- self.sendError(ws, "Invalid message format");
404
- }
405
- },
406
-
407
- close(ws) {
408
- const client = self.connectionManager.getClientByWs(ws);
409
- if (client) {
410
- console.log(`Client disconnected: ${client.id}`);
411
- self.connectionManager.removeClient(client.id);
412
- }
413
- },
414
- },
415
- });
416
-
417
- console.log(`✅ Arc Host running on http://localhost:${this.port}`);
418
- }
419
-
420
- /**
421
- * Handle HTTP command request
422
- */
423
- private async handleHttpCommand(
424
- req: Request,
425
- url: URL,
426
- corsHeaders: Record<string, string>,
427
- rawToken: string | null,
428
- ): Promise<Response> {
429
- const commandName = url.pathname.split("/command/")[1];
430
- if (!commandName) {
431
- return new Response("Invalid command path", {
432
- status: 400,
433
- headers: corsHeaders,
434
- });
435
- }
436
-
437
- try {
438
- const params = await req.json();
439
- const result = await this.contextHandler.executeCommand(
440
- commandName,
441
- params,
442
- rawToken,
443
- );
444
-
445
- return new Response(JSON.stringify(result ?? { success: true }), {
446
- headers: { ...corsHeaders, "Content-Type": "application/json" },
447
- });
448
- } catch (error) {
449
- console.error(`[ARC HTTP] Command '${commandName}' error:`, error);
450
- if (error instanceof Error && error.stack) {
451
- console.error(`[ARC HTTP] Stack trace:`, error.stack);
452
- }
453
- return new Response(JSON.stringify({ error: (error as Error).message }), {
454
- status: 500,
455
- headers: { ...corsHeaders, "Content-Type": "application/json" },
456
- });
457
- }
458
- }
459
-
460
- /**
461
- * Handle HTTP query request
462
- * Uses view.queryContext() to apply protections consistently with liveQuery
463
- */
464
- private async handleHttpQuery(
465
- req: Request,
466
- url: URL,
467
- _token: TokenPayload | null,
468
- corsHeaders: Record<string, string>,
469
- rawToken: string | null,
470
- ): Promise<Response> {
471
- const viewName = url.pathname.split("/query/")[1];
472
- if (!viewName) {
473
- return new Response("Invalid query path", {
474
- status: 400,
475
- headers: corsHeaders,
476
- });
477
- }
478
-
479
- try {
480
- const params = await req.json();
481
-
482
- // Get view element
483
- const viewElement = this.contextHandler
484
- .getModel()
485
- .context.get(viewName) as any;
486
-
487
- if (!viewElement || !viewElement.queryContext) {
488
- return new Response(JSON.stringify({ error: "View not found" }), {
489
- status: 404,
490
- headers: { ...corsHeaders, "Content-Type": "application/json" },
491
- });
492
- }
493
-
494
- // Set auth token so view.queryContext() can apply protections
495
- this.contextHandler.setAuthToken(rawToken);
496
-
497
- // Use view's queryContext which applies protections automatically
498
- const model = this.contextHandler.getModel();
499
- const adapters = model.getAdapters();
500
- const queryCtx = viewElement.queryContext(adapters);
501
-
502
- // Execute query through view's queryContext (protections applied)
503
- const result = await queryCtx.find(params);
504
-
505
- return new Response(JSON.stringify(result), {
506
- headers: { ...corsHeaders, "Content-Type": "application/json" },
507
- });
508
- } catch (error) {
509
- return new Response(JSON.stringify({ error: (error as Error).message }), {
510
- status: 500,
511
- headers: { ...corsHeaders, "Content-Type": "application/json" },
512
- });
513
- }
514
- }
515
-
516
- /**
517
- * Handle HTTP stream (SSE) request for live queries
518
- */
519
- private handleHttpStream(
520
- _req: Request,
521
- url: URL,
522
- token: TokenPayload | null,
523
- corsHeaders: Record<string, string>,
524
- rawToken: string | null,
525
- ): Response {
526
- const viewName = url.pathname.split("/stream/")[1];
527
- if (!viewName) {
528
- return new Response("Invalid stream path", {
529
- status: 400,
530
- headers: corsHeaders,
531
- });
532
- }
533
-
534
- // Parse query options from URL params
535
- const findOptions: any = {};
536
- const whereParam = url.searchParams.get("where");
537
- if (whereParam) {
538
- try {
539
- findOptions.where = JSON.parse(whereParam);
540
- } catch {
541
- return new Response("Invalid 'where' parameter", {
542
- status: 400,
543
- headers: corsHeaders,
544
- });
545
- }
546
- }
547
-
548
- const orderByParam = url.searchParams.get("orderBy");
549
- if (orderByParam) {
550
- try {
551
- findOptions.orderBy = JSON.parse(orderByParam);
552
- } catch {
553
- return new Response("Invalid 'orderBy' parameter", {
554
- status: 400,
555
- headers: corsHeaders,
556
- });
557
- }
558
- }
559
-
560
- const limitParam = url.searchParams.get("limit");
561
- if (limitParam) {
562
- findOptions.limit = parseInt(limitParam, 10);
563
- }
564
-
565
- // Create SSE stream
566
- const streamId = `stream_${++this.streamIdCounter}_${Date.now()}`;
567
- const self = this;
568
-
569
- const stream = new ReadableStream<Uint8Array>({
570
- start(controller) {
571
- // Set up live query
572
- const model = self.contextHandler.getModel();
573
-
574
- // Set the token on the auth adapter so view protection is applied
575
- self.contextHandler.setAuthToken(rawToken);
576
-
577
- const { unsubscribe } = liveQuery(
578
- model,
579
- async (q: any) => {
580
- const view = q[viewName];
581
- if (!view) {
582
- throw new Error(`View '${viewName}' not found`);
583
- }
584
- return view.find(findOptions);
585
- },
586
- (data: any[]) => {
587
- // Send SSE event
588
- const event = `data: ${JSON.stringify({ type: "data", data })}\n\n`;
589
- try {
590
- controller.enqueue(new TextEncoder().encode(event));
591
- } catch {
592
- // Stream closed
593
- unsubscribe();
594
- }
595
- },
596
- );
597
-
598
- // Store connection for cleanup
599
- self.streamConnections.set(streamId, {
600
- id: streamId,
601
- controller,
602
- unsubscribe,
603
- });
604
-
605
- // Send initial connection event
606
- const connectEvent = `data: ${JSON.stringify({ type: "connected", streamId })}\n\n`;
607
- controller.enqueue(new TextEncoder().encode(connectEvent));
608
- },
609
-
610
- cancel() {
611
- // Clean up on client disconnect
612
- const conn = self.streamConnections.get(streamId);
613
- if (conn) {
614
- conn.unsubscribe();
615
- self.streamConnections.delete(streamId);
616
- }
617
- },
618
- });
619
-
620
- return new Response(stream, {
621
- headers: {
622
- ...corsHeaders,
623
- "Content-Type": "text/event-stream",
624
- "Cache-Control": "no-cache",
625
- Connection: "keep-alive",
626
- },
627
- });
628
- }
629
-
630
- /**
631
- * Handle HTTP event sync request
632
- */
633
- private async handleHttpEventSync(
634
- req: Request,
635
- token: TokenPayload | null,
636
- corsHeaders: Record<string, string>,
637
- ): Promise<Response> {
638
- try {
639
- const body = await req.json();
640
- const events = body.events || [];
641
-
642
- // Persist events via context handler
643
- const persistedEvents = await this.contextHandler.persistEvents(
644
- events.map((e: any) => ({
645
- localId: e.localId,
646
- type: e.type,
647
- payload: e.payload,
648
- createdAt: e.createdAt,
649
- })),
650
- "http-sync",
651
- token,
652
- );
653
-
654
- return new Response(
655
- JSON.stringify({
656
- success: true,
657
- syncedIds: persistedEvents.map((e) => e.localId),
658
- }),
659
- {
660
- headers: { ...corsHeaders, "Content-Type": "application/json" },
661
- },
662
- );
663
- } catch (error) {
664
- return new Response(
665
- JSON.stringify({
666
- success: false,
667
- error: (error as Error).message,
668
- }),
669
- {
670
- status: 500,
671
- headers: { ...corsHeaders, "Content-Type": "application/json" },
672
- },
673
- );
674
- }
675
- }
676
-
677
- /**
678
- * Handle HTTP route request
679
- * Matches path against registered routes and executes handler
680
- */
681
- private async handleHttpRoute(
682
- req: Request,
683
- url: URL,
684
- tokenPayload: TokenPayload | null,
685
- corsHeaders: Record<string, string>,
686
- rawToken: string | null,
687
- ): Promise<Response> {
688
- const method = req.method as "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
689
-
690
- // Find matching route in context
691
- const context = this.contextHandler.getModel().context;
692
- let matchedRoute: ArcRouteAny | null = null;
693
- let routeParams: Record<string, string> = {};
694
-
695
- for (const element of context.elements) {
696
- // Check if element is a route (has matchesPath method)
697
- if (element && typeof (element as any).matchesPath === "function") {
698
- const route = element as ArcRouteAny;
699
- const match = route.matchesPath(url.pathname);
700
- if (match.matches) {
701
- matchedRoute = route;
702
- routeParams = match.params;
703
- break;
704
- }
705
- }
706
- }
707
-
708
- if (!matchedRoute) {
709
- return new Response(JSON.stringify({ error: "Route not found" }), {
710
- status: 404,
711
- headers: { ...corsHeaders, "Content-Type": "application/json" },
712
- });
713
- }
714
-
715
- // Get handler for this method
716
- const handler = matchedRoute.getHandler(method);
717
- if (!handler) {
718
- return new Response(
719
- JSON.stringify({ error: `Method ${method} not allowed` }),
720
- {
721
- status: 405,
722
- headers: { ...corsHeaders, "Content-Type": "application/json" },
723
- },
724
- );
725
- }
726
-
727
- // Check protection
728
- if (!matchedRoute.isPublic && matchedRoute.hasProtections) {
729
- if (!tokenPayload) {
730
- return new Response(JSON.stringify({ error: "Unauthorized" }), {
731
- status: 401,
732
- headers: { ...corsHeaders, "Content-Type": "application/json" },
733
- });
734
- }
735
-
736
- // Set auth token for protection check
737
- this.contextHandler.setAuthToken(rawToken);
738
-
739
- // Check if token type matches any protection
740
- let isAuthorized = false;
741
- for (const protection of matchedRoute.protections) {
742
- if (protection.token.name === tokenPayload.tokenType) {
743
- // Create a mock token instance for the check
744
- const mockTokenInstance = {
745
- params: tokenPayload.params,
746
- getTokenDefinition: () => protection.token,
747
- };
748
- const allowed = await protection.check(mockTokenInstance as any);
749
- if (allowed) {
750
- isAuthorized = true;
751
- break;
752
- }
753
- }
754
- }
755
-
756
- if (!isAuthorized) {
757
- return new Response(JSON.stringify({ error: "Forbidden" }), {
758
- status: 403,
759
- headers: { ...corsHeaders, "Content-Type": "application/json" },
760
- });
761
- }
762
- } else if (!matchedRoute.isPublic && !matchedRoute.hasProtections) {
763
- // Route is not public and has no protections - require any valid token
764
- if (!tokenPayload) {
765
- return new Response(JSON.stringify({ error: "Unauthorized" }), {
766
- status: 401,
767
- headers: { ...corsHeaders, "Content-Type": "application/json" },
768
- });
769
- }
770
- }
771
-
772
- // Build route context
773
- this.contextHandler.setAuthToken(rawToken);
774
- const model = this.contextHandler.getModel();
775
- const adapters = model.getAdapters();
776
-
777
- const authParams = tokenPayload
778
- ? { params: tokenPayload.params, tokenName: tokenPayload.tokenType }
779
- : undefined;
780
-
781
- const routeContext = matchedRoute.buildContext(adapters, authParams);
782
-
783
- try {
784
- // Execute handler
785
- const response = await handler(routeContext, req, routeParams, url);
786
-
787
- // Add CORS headers to response
788
- const newHeaders = new Headers(response.headers);
789
- for (const [key, value] of Object.entries(corsHeaders)) {
790
- newHeaders.set(key, value);
791
- }
792
-
793
- return new Response(response.body, {
794
- status: response.status,
795
- statusText: response.statusText,
796
- headers: newHeaders,
797
- });
798
- } catch (error) {
799
- return new Response(JSON.stringify({ error: (error as Error).message }), {
800
- status: 500,
801
- headers: { ...corsHeaders, "Content-Type": "application/json" },
802
- });
803
- }
804
- }
805
-
806
- /**
807
- * Stop the server
808
- */
809
- stop(): void {
810
- // Clean up all stream connections
811
- for (const conn of this.streamConnections.values()) {
812
- conn.unsubscribe();
813
- }
814
- this.streamConnections.clear();
815
-
816
- this.server?.stop();
817
- }
818
- }