@l4yercak3/cli 1.1.12 → 1.2.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.
- package/bin/cli.js +6 -0
- package/docs/mcp_server/MCP_SERVER_ARCHITECTURE.md +1481 -0
- package/docs/mcp_server/applicationOntology.ts +817 -0
- package/docs/mcp_server/cliApplications.ts +639 -0
- package/docs/mcp_server/crmOntology.ts +1063 -0
- package/docs/mcp_server/eventOntology.ts +1183 -0
- package/docs/mcp_server/formsOntology.ts +1401 -0
- package/docs/mcp_server/ontologySchemas.ts +185 -0
- package/docs/mcp_server/schema.ts +250 -0
- package/package.json +5 -2
- package/src/commands/login.js +0 -6
- package/src/commands/mcp-server.js +32 -0
- package/src/commands/spread.js +54 -1
- package/src/detectors/expo-detector.js +163 -0
- package/src/detectors/registry.js +2 -0
- package/src/mcp/auth.js +127 -0
- package/src/mcp/registry/domains/applications.js +516 -0
- package/src/mcp/registry/domains/codegen.js +894 -0
- package/src/mcp/registry/domains/core.js +326 -0
- package/src/mcp/registry/domains/crm.js +591 -0
- package/src/mcp/registry/domains/events.js +649 -0
- package/src/mcp/registry/domains/forms.js +696 -0
- package/src/mcp/registry/index.js +162 -0
- package/src/mcp/server.js +116 -0
|
@@ -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
|
+
});
|