@l4yercak3/cli 1.1.11 → 1.2.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.
@@ -0,0 +1,639 @@
1
+ /**
2
+ * CLI APPLICATIONS API
3
+ *
4
+ * HTTP handlers for CLI application registration and management.
5
+ * Uses CLI session token authentication (Bearer token).
6
+ *
7
+ * Endpoints:
8
+ * - POST /api/v1/cli/applications - Register new application
9
+ * - GET /api/v1/cli/applications - List all applications
10
+ * - GET /api/v1/cli/applications/by-path?hash={hash} - Find by project path
11
+ * - GET /api/v1/cli/applications/:id - Get application details
12
+ * - PATCH /api/v1/cli/applications/:id - Update application
13
+ */
14
+
15
+ import { httpAction } from "../../_generated/server";
16
+ import { internal } from "../../_generated/api";
17
+ import { getCorsHeaders, handleOptionsRequest } from "./corsHeaders";
18
+ import type { Id } from "../../_generated/dataModel";
19
+
20
+ // ============================================================================
21
+ // HELPER: Verify CLI Token
22
+ // ============================================================================
23
+
24
+ async function verifyCliToken(
25
+ ctx: { runQuery: (fn: any, args: any) => Promise<any> },
26
+ authHeader: string | null
27
+ ): Promise<{
28
+ userId: Id<"users">;
29
+ email: string;
30
+ organizationId: Id<"organizations">;
31
+ } | null> {
32
+ if (!authHeader || !authHeader.startsWith("Bearer ")) {
33
+ return null;
34
+ }
35
+
36
+ const token = authHeader.substring(7);
37
+
38
+ // Validate CLI session
39
+ const session = await ctx.runQuery(internal.api.v1.cliApplicationsInternal.validateCliTokenInternal, {
40
+ token,
41
+ });
42
+
43
+ return session;
44
+ }
45
+
46
+ // ============================================================================
47
+ // POST /api/v1/cli/applications - Register Application
48
+ // ============================================================================
49
+
50
+ export const registerApplication = httpAction(async (ctx, request) => {
51
+ const origin = request.headers.get("origin");
52
+ const corsHeaders = getCorsHeaders(origin);
53
+
54
+ try {
55
+ // Verify CLI token
56
+ const authContext = await verifyCliToken(ctx, request.headers.get("Authorization"));
57
+ if (!authContext) {
58
+ return new Response(
59
+ JSON.stringify({ error: "Invalid or expired CLI session", code: "INVALID_SESSION" }),
60
+ { status: 401, headers: { "Content-Type": "application/json", ...corsHeaders } }
61
+ );
62
+ }
63
+
64
+ // Parse request body
65
+ const body = await request.json();
66
+ const {
67
+ organizationId,
68
+ name,
69
+ description,
70
+ source,
71
+ connection,
72
+ modelMappings,
73
+ } = body;
74
+
75
+ // Validate required fields
76
+ if (!name || !source || !connection) {
77
+ return new Response(
78
+ JSON.stringify({ error: "Missing required fields: name, source, connection", code: "VALIDATION_ERROR" }),
79
+ { status: 400, headers: { "Content-Type": "application/json", ...corsHeaders } }
80
+ );
81
+ }
82
+
83
+ // Use provided organizationId or default from session
84
+ const targetOrgId = organizationId || authContext.organizationId;
85
+
86
+ // Verify user has access to the organization
87
+ const hasAccess = await ctx.runQuery(internal.api.v1.cliApplicationsInternal.checkOrgAccessInternal, {
88
+ userId: authContext.userId,
89
+ organizationId: targetOrgId,
90
+ });
91
+
92
+ if (!hasAccess) {
93
+ return new Response(
94
+ JSON.stringify({ error: "Not authorized to access this organization", code: "UNAUTHORIZED" }),
95
+ { status: 403, headers: { "Content-Type": "application/json", ...corsHeaders } }
96
+ );
97
+ }
98
+
99
+ // Register application
100
+ const result = await ctx.runMutation(internal.applicationOntology.registerApplicationInternal, {
101
+ organizationId: targetOrgId,
102
+ name,
103
+ description,
104
+ source: {
105
+ type: source.type || "cli",
106
+ projectPathHash: source.projectPathHash,
107
+ cliVersion: source.cliVersion,
108
+ framework: source.framework,
109
+ frameworkVersion: source.frameworkVersion,
110
+ hasTypeScript: source.hasTypeScript,
111
+ routerType: source.routerType,
112
+ },
113
+ connection: {
114
+ features: connection.features || [],
115
+ hasFrontendDatabase: connection.hasFrontendDatabase,
116
+ frontendDatabaseType: connection.frontendDatabaseType,
117
+ },
118
+ modelMappings: modelMappings?.map((m: any) => ({
119
+ localModel: m.localModel,
120
+ layerCakeType: m.layerCakeType,
121
+ syncDirection: m.syncDirection || "none",
122
+ confidence: m.confidence || 0,
123
+ isAutoDetected: m.isAutoDetected || false,
124
+ })),
125
+ });
126
+
127
+ // If this was an existing application, return 200 with existing flag
128
+ if (result.existingApplication) {
129
+ return new Response(
130
+ JSON.stringify({
131
+ success: true,
132
+ applicationId: result.applicationId,
133
+ existingApplication: true,
134
+ message: "Application already registered for this project",
135
+ }),
136
+ { status: 200, headers: { "Content-Type": "application/json", ...corsHeaders } }
137
+ );
138
+ }
139
+
140
+ // Generate API key for the new application
141
+ const apiKeyResult = await ctx.runAction(internal.api.v1.cliAuth.generateCliApiKeyInternal, {
142
+ organizationId: targetOrgId,
143
+ userId: authContext.userId,
144
+ name: `${name} API Key`,
145
+ scopes: ["*"],
146
+ });
147
+
148
+ // Link API key to application
149
+ await ctx.runMutation(internal.api.v1.cliApplicationsInternal.linkApiKeyToApplication, {
150
+ applicationId: result.applicationId,
151
+ apiKeyId: apiKeyResult.id,
152
+ });
153
+
154
+ return new Response(
155
+ JSON.stringify({
156
+ success: true,
157
+ applicationId: result.applicationId,
158
+ existingApplication: false,
159
+ apiKey: {
160
+ id: apiKeyResult.id,
161
+ key: apiKeyResult.key,
162
+ prefix: apiKeyResult.key.substring(0, 12) + "...",
163
+ },
164
+ backendUrl: result.backendUrl,
165
+ }),
166
+ { status: 201, headers: { "Content-Type": "application/json", ...corsHeaders } }
167
+ );
168
+ } catch (error) {
169
+ console.error("[CLI Applications] Register error:", error);
170
+ return new Response(
171
+ JSON.stringify({ error: "Internal server error", code: "INTERNAL_ERROR" }),
172
+ { status: 500, headers: { "Content-Type": "application/json", ...corsHeaders } }
173
+ );
174
+ }
175
+ });
176
+
177
+ // ============================================================================
178
+ // GET /api/v1/cli/applications - List Applications
179
+ // ============================================================================
180
+
181
+ export const listApplications = httpAction(async (ctx, request) => {
182
+ const origin = request.headers.get("origin");
183
+ const corsHeaders = getCorsHeaders(origin);
184
+
185
+ try {
186
+ // Verify CLI token
187
+ const authContext = await verifyCliToken(ctx, request.headers.get("Authorization"));
188
+ if (!authContext) {
189
+ return new Response(
190
+ JSON.stringify({ error: "Invalid or expired CLI session", code: "INVALID_SESSION" }),
191
+ { status: 401, headers: { "Content-Type": "application/json", ...corsHeaders } }
192
+ );
193
+ }
194
+
195
+ // Parse query params
196
+ const url = new URL(request.url);
197
+ const organizationId = url.searchParams.get("organizationId") || authContext.organizationId;
198
+ const status = url.searchParams.get("status") || undefined;
199
+ const limit = Math.min(parseInt(url.searchParams.get("limit") || "50"), 100);
200
+ const offset = parseInt(url.searchParams.get("offset") || "0");
201
+
202
+ // Verify user has access to the organization
203
+ const hasAccess = await ctx.runQuery(internal.api.v1.cliApplicationsInternal.checkOrgAccessInternal, {
204
+ userId: authContext.userId,
205
+ organizationId: organizationId as Id<"organizations">,
206
+ });
207
+
208
+ if (!hasAccess) {
209
+ return new Response(
210
+ JSON.stringify({ error: "Not authorized to access this organization", code: "UNAUTHORIZED" }),
211
+ { status: 403, headers: { "Content-Type": "application/json", ...corsHeaders } }
212
+ );
213
+ }
214
+
215
+ // List applications
216
+ const result = await ctx.runQuery(internal.applicationOntology.listApplicationsInternal, {
217
+ organizationId: organizationId as Id<"organizations">,
218
+ status,
219
+ limit,
220
+ offset,
221
+ });
222
+
223
+ return new Response(
224
+ JSON.stringify({
225
+ success: true,
226
+ applications: result.applications.map((app: any) => ({
227
+ id: app._id,
228
+ name: app.name,
229
+ description: app.description,
230
+ status: app.status,
231
+ framework: app.customProperties?.source?.framework,
232
+ features: app.customProperties?.connection?.features || [],
233
+ registeredAt: app.customProperties?.cli?.registeredAt,
234
+ lastActivityAt: app.customProperties?.cli?.lastActivityAt,
235
+ })),
236
+ total: result.total,
237
+ hasMore: result.hasMore,
238
+ }),
239
+ { status: 200, headers: { "Content-Type": "application/json", ...corsHeaders } }
240
+ );
241
+ } catch (error) {
242
+ console.error("[CLI Applications] List error:", error);
243
+ return new Response(
244
+ JSON.stringify({ error: "Internal server error", code: "INTERNAL_ERROR" }),
245
+ { status: 500, headers: { "Content-Type": "application/json", ...corsHeaders } }
246
+ );
247
+ }
248
+ });
249
+
250
+ // ============================================================================
251
+ // GET /api/v1/cli/applications/by-path - Find by Path Hash
252
+ // ============================================================================
253
+
254
+ export const getApplicationByPath = httpAction(async (ctx, request) => {
255
+ const origin = request.headers.get("origin");
256
+ const corsHeaders = getCorsHeaders(origin);
257
+
258
+ try {
259
+ // Verify CLI token
260
+ const authContext = await verifyCliToken(ctx, request.headers.get("Authorization"));
261
+ if (!authContext) {
262
+ return new Response(
263
+ JSON.stringify({ error: "Invalid or expired CLI session", code: "INVALID_SESSION" }),
264
+ { status: 401, headers: { "Content-Type": "application/json", ...corsHeaders } }
265
+ );
266
+ }
267
+
268
+ // Parse query params
269
+ const url = new URL(request.url);
270
+ const hash = url.searchParams.get("hash");
271
+ const organizationId = url.searchParams.get("organizationId") || authContext.organizationId;
272
+
273
+ if (!hash) {
274
+ return new Response(
275
+ JSON.stringify({ error: "Missing required parameter: hash", code: "VALIDATION_ERROR" }),
276
+ { status: 400, headers: { "Content-Type": "application/json", ...corsHeaders } }
277
+ );
278
+ }
279
+
280
+ // Verify user has access to the organization
281
+ const hasAccess = await ctx.runQuery(internal.api.v1.cliApplicationsInternal.checkOrgAccessInternal, {
282
+ userId: authContext.userId,
283
+ organizationId: organizationId as Id<"organizations">,
284
+ });
285
+
286
+ if (!hasAccess) {
287
+ return new Response(
288
+ JSON.stringify({ error: "Not authorized to access this organization", code: "UNAUTHORIZED" }),
289
+ { status: 403, headers: { "Content-Type": "application/json", ...corsHeaders } }
290
+ );
291
+ }
292
+
293
+ // Find application
294
+ const app = await ctx.runQuery(internal.applicationOntology.getApplicationByPathHashInternal, {
295
+ organizationId: organizationId as Id<"organizations">,
296
+ projectPathHash: hash,
297
+ });
298
+
299
+ if (!app) {
300
+ return new Response(
301
+ JSON.stringify({ found: false }),
302
+ { status: 200, headers: { "Content-Type": "application/json", ...corsHeaders } }
303
+ );
304
+ }
305
+
306
+ return new Response(
307
+ JSON.stringify({
308
+ found: true,
309
+ application: app,
310
+ }),
311
+ { status: 200, headers: { "Content-Type": "application/json", ...corsHeaders } }
312
+ );
313
+ } catch (error) {
314
+ console.error("[CLI Applications] Get by path error:", error);
315
+ return new Response(
316
+ JSON.stringify({ error: "Internal server error", code: "INTERNAL_ERROR" }),
317
+ { status: 500, headers: { "Content-Type": "application/json", ...corsHeaders } }
318
+ );
319
+ }
320
+ });
321
+
322
+ // ============================================================================
323
+ // GET /api/v1/cli/applications/:id - Get Application Details
324
+ // ============================================================================
325
+
326
+ export const getApplication = httpAction(async (ctx, request) => {
327
+ const origin = request.headers.get("origin");
328
+ const corsHeaders = getCorsHeaders(origin);
329
+
330
+ try {
331
+ // Verify CLI token
332
+ const authContext = await verifyCliToken(ctx, request.headers.get("Authorization"));
333
+ if (!authContext) {
334
+ return new Response(
335
+ JSON.stringify({ error: "Invalid or expired CLI session", code: "INVALID_SESSION" }),
336
+ { status: 401, headers: { "Content-Type": "application/json", ...corsHeaders } }
337
+ );
338
+ }
339
+
340
+ // Extract application ID from URL
341
+ const url = new URL(request.url);
342
+ const pathParts = url.pathname.split("/");
343
+ const applicationId = pathParts[pathParts.length - 1];
344
+
345
+ // Skip handling for special paths that should be handled by other routes
346
+ if (!applicationId || applicationId === "by-path" || applicationId === "applications") {
347
+ return new Response(
348
+ JSON.stringify({ error: "Application ID required", code: "VALIDATION_ERROR" }),
349
+ { status: 400, headers: { "Content-Type": "application/json", ...corsHeaders } }
350
+ );
351
+ }
352
+
353
+ // Get application
354
+ const app = await ctx.runQuery(internal.applicationOntology.getApplicationInternal, {
355
+ applicationId: applicationId as Id<"objects">,
356
+ organizationId: authContext.organizationId,
357
+ });
358
+
359
+ if (!app) {
360
+ return new Response(
361
+ JSON.stringify({ error: "Application not found", code: "NOT_FOUND" }),
362
+ { status: 404, headers: { "Content-Type": "application/json", ...corsHeaders } }
363
+ );
364
+ }
365
+
366
+ // Verify user has access to the application's organization
367
+ const hasAccess = await ctx.runQuery(internal.api.v1.cliApplicationsInternal.checkOrgAccessInternal, {
368
+ userId: authContext.userId,
369
+ organizationId: app.organizationId,
370
+ });
371
+
372
+ if (!hasAccess) {
373
+ return new Response(
374
+ JSON.stringify({ error: "Not authorized to access this application", code: "UNAUTHORIZED" }),
375
+ { status: 403, headers: { "Content-Type": "application/json", ...corsHeaders } }
376
+ );
377
+ }
378
+
379
+ const props = app.customProperties as any;
380
+
381
+ return new Response(
382
+ JSON.stringify({
383
+ success: true,
384
+ application: {
385
+ id: app._id,
386
+ name: app.name,
387
+ description: app.description,
388
+ status: app.status,
389
+ source: props?.source,
390
+ connection: {
391
+ ...props?.connection,
392
+ apiKeyPrefix: props?.connection?.apiKeyId ? "***" : undefined,
393
+ },
394
+ modelMappings: props?.modelMappings,
395
+ deployment: props?.deployment,
396
+ sync: props?.sync,
397
+ cli: props?.cli,
398
+ createdAt: app.createdAt,
399
+ updatedAt: app.updatedAt,
400
+ },
401
+ }),
402
+ { status: 200, headers: { "Content-Type": "application/json", ...corsHeaders } }
403
+ );
404
+ } catch (error) {
405
+ console.error("[CLI Applications] Get error:", error);
406
+ return new Response(
407
+ JSON.stringify({ error: "Internal server error", code: "INTERNAL_ERROR" }),
408
+ { status: 500, headers: { "Content-Type": "application/json", ...corsHeaders } }
409
+ );
410
+ }
411
+ });
412
+
413
+ // ============================================================================
414
+ // PATCH /api/v1/cli/applications/:id - Update Application
415
+ // ============================================================================
416
+
417
+ export const updateApplication = httpAction(async (ctx, request) => {
418
+ const origin = request.headers.get("origin");
419
+ const corsHeaders = getCorsHeaders(origin);
420
+
421
+ try {
422
+ // Verify CLI token
423
+ const authContext = await verifyCliToken(ctx, request.headers.get("Authorization"));
424
+ if (!authContext) {
425
+ return new Response(
426
+ JSON.stringify({ error: "Invalid or expired CLI session", code: "INVALID_SESSION" }),
427
+ { status: 401, headers: { "Content-Type": "application/json", ...corsHeaders } }
428
+ );
429
+ }
430
+
431
+ // Extract application ID from URL
432
+ const url = new URL(request.url);
433
+ const pathParts = url.pathname.split("/");
434
+ const applicationId = pathParts[pathParts.length - 1];
435
+
436
+ if (!applicationId) {
437
+ return new Response(
438
+ JSON.stringify({ error: "Application ID required", code: "VALIDATION_ERROR" }),
439
+ { status: 400, headers: { "Content-Type": "application/json", ...corsHeaders } }
440
+ );
441
+ }
442
+
443
+ // Get application to verify ownership
444
+ const app = await ctx.runQuery(internal.applicationOntology.getApplicationInternal, {
445
+ applicationId: applicationId as Id<"objects">,
446
+ organizationId: authContext.organizationId,
447
+ });
448
+
449
+ if (!app) {
450
+ return new Response(
451
+ JSON.stringify({ error: "Application not found", code: "NOT_FOUND" }),
452
+ { status: 404, headers: { "Content-Type": "application/json", ...corsHeaders } }
453
+ );
454
+ }
455
+
456
+ // Verify user has access
457
+ const hasAccess = await ctx.runQuery(internal.api.v1.cliApplicationsInternal.checkOrgAccessInternal, {
458
+ userId: authContext.userId,
459
+ organizationId: app.organizationId,
460
+ });
461
+
462
+ if (!hasAccess) {
463
+ return new Response(
464
+ JSON.stringify({ error: "Not authorized to update this application", code: "UNAUTHORIZED" }),
465
+ { status: 403, headers: { "Content-Type": "application/json", ...corsHeaders } }
466
+ );
467
+ }
468
+
469
+ // Parse request body
470
+ const body = await request.json();
471
+
472
+ // Update application
473
+ await ctx.runMutation(internal.applicationOntology.updateApplicationInternal, {
474
+ applicationId: applicationId as Id<"objects">,
475
+ organizationId: app.organizationId,
476
+ name: body.name,
477
+ description: body.description,
478
+ status: body.status,
479
+ connection: body.connection,
480
+ deployment: body.deployment,
481
+ modelMappings: body.modelMappings,
482
+ });
483
+
484
+ return new Response(
485
+ JSON.stringify({ success: true }),
486
+ { status: 200, headers: { "Content-Type": "application/json", ...corsHeaders } }
487
+ );
488
+ } catch (error) {
489
+ console.error("[CLI Applications] Update error:", error);
490
+ return new Response(
491
+ JSON.stringify({ error: "Internal server error", code: "INTERNAL_ERROR" }),
492
+ { status: 500, headers: { "Content-Type": "application/json", ...corsHeaders } }
493
+ );
494
+ }
495
+ });
496
+
497
+ // ============================================================================
498
+ // POST /api/v1/cli/applications/:id/sync - Sync Application
499
+ // ============================================================================
500
+
501
+ export const syncApplication = httpAction(async (ctx, request) => {
502
+ const origin = request.headers.get("origin");
503
+ const corsHeaders = getCorsHeaders(origin);
504
+
505
+ try {
506
+ // Verify CLI token
507
+ const authContext = await verifyCliToken(ctx, request.headers.get("Authorization"));
508
+ if (!authContext) {
509
+ return new Response(
510
+ JSON.stringify({ error: "Invalid or expired CLI session", code: "INVALID_SESSION" }),
511
+ { status: 401, headers: { "Content-Type": "application/json", ...corsHeaders } }
512
+ );
513
+ }
514
+
515
+ // Extract application ID from URL
516
+ const url = new URL(request.url);
517
+ const pathParts = url.pathname.split("/");
518
+ // URL: /api/v1/cli/applications/:id/sync
519
+ const syncIndex = pathParts.indexOf("sync");
520
+ const applicationId = syncIndex > 0 ? pathParts[syncIndex - 1] : null;
521
+
522
+ if (!applicationId) {
523
+ return new Response(
524
+ JSON.stringify({ error: "Application ID required", code: "VALIDATION_ERROR" }),
525
+ { status: 400, headers: { "Content-Type": "application/json", ...corsHeaders } }
526
+ );
527
+ }
528
+
529
+ // Get application to verify ownership
530
+ const app = await ctx.runQuery(internal.applicationOntology.getApplicationInternal, {
531
+ applicationId: applicationId as Id<"objects">,
532
+ organizationId: authContext.organizationId,
533
+ });
534
+
535
+ if (!app) {
536
+ return new Response(
537
+ JSON.stringify({ error: "Application not found", code: "NOT_FOUND" }),
538
+ { status: 404, headers: { "Content-Type": "application/json", ...corsHeaders } }
539
+ );
540
+ }
541
+
542
+ // Verify user has access
543
+ const hasAccess = await ctx.runQuery(internal.api.v1.cliApplicationsInternal.checkOrgAccessInternal, {
544
+ userId: authContext.userId,
545
+ organizationId: app.organizationId,
546
+ });
547
+
548
+ if (!hasAccess) {
549
+ return new Response(
550
+ JSON.stringify({ error: "Not authorized to sync this application", code: "UNAUTHORIZED" }),
551
+ { status: 403, headers: { "Content-Type": "application/json", ...corsHeaders } }
552
+ );
553
+ }
554
+
555
+ // Parse request body
556
+ const body = await request.json();
557
+ const {
558
+ direction = "bidirectional",
559
+ models,
560
+ dryRun = false,
561
+ // Sync results (if CLI is reporting completed sync)
562
+ results,
563
+ } = body;
564
+
565
+ // If CLI is reporting sync results
566
+ if (results) {
567
+ // Record sync event
568
+ await ctx.runMutation(internal.api.v1.cliApplicationsInternal.recordSyncEvent, {
569
+ applicationId: applicationId as Id<"objects">,
570
+ direction: results.direction || direction,
571
+ status: results.status || "success",
572
+ recordsProcessed: results.recordsProcessed || 0,
573
+ recordsCreated: results.recordsCreated || 0,
574
+ recordsUpdated: results.recordsUpdated || 0,
575
+ errors: results.errors || 0,
576
+ });
577
+
578
+ return new Response(
579
+ JSON.stringify({
580
+ success: true,
581
+ message: "Sync results recorded",
582
+ syncId: `sync_${Date.now()}`,
583
+ }),
584
+ { status: 200, headers: { "Content-Type": "application/json", ...corsHeaders } }
585
+ );
586
+ }
587
+
588
+ // Otherwise, this is a sync request - return what should be synced
589
+ // For now, return sync configuration (CLI will execute the actual sync)
590
+ const props = app.customProperties as any;
591
+ const modelMappings = props?.modelMappings || [];
592
+ const features = props?.connection?.features || [];
593
+
594
+ // Filter mappings by requested models if specified
595
+ const filteredMappings = models
596
+ ? modelMappings.filter((m: any) => models.includes(m.localModel))
597
+ : modelMappings;
598
+
599
+ return new Response(
600
+ JSON.stringify({
601
+ success: true,
602
+ syncId: `sync_${Date.now()}`,
603
+ dryRun,
604
+ direction,
605
+ application: {
606
+ id: app._id,
607
+ name: app.name,
608
+ features,
609
+ },
610
+ modelMappings: filteredMappings,
611
+ // Sync instructions for CLI
612
+ instructions: {
613
+ backendUrl: props?.connection?.backendUrl,
614
+ endpoints: features.map((feature: string) => ({
615
+ feature,
616
+ push: `/api/v1/${feature}/bulk`,
617
+ pull: `/api/v1/${feature}`,
618
+ })),
619
+ },
620
+ }),
621
+ { status: 200, headers: { "Content-Type": "application/json", ...corsHeaders } }
622
+ );
623
+ } catch (error) {
624
+ console.error("[CLI Applications] Sync error:", error);
625
+ return new Response(
626
+ JSON.stringify({ error: "Internal server error", code: "INTERNAL_ERROR" }),
627
+ { status: 500, headers: { "Content-Type": "application/json", ...corsHeaders } }
628
+ );
629
+ }
630
+ });
631
+
632
+ // ============================================================================
633
+ // OPTIONS Handler (CORS)
634
+ // ============================================================================
635
+
636
+ export const handleOptions = httpAction(async (ctx, request) => {
637
+ const origin = request.headers.get("origin");
638
+ return handleOptionsRequest(origin);
639
+ });