@opengeni/api-router 0.22.2 → 0.26.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/dist/app.d.ts +2 -2
- package/dist/app.js +1 -1
- package/dist/auth/managed-auth.d.ts +0 -30
- package/dist/browser-controller-authority.d.ts +43 -0
- package/dist/browser-state-authority.d.ts +27 -0
- package/dist/{chunk-HWXJW5C7.js → chunk-JIKNR5YL.js} +27993 -14546
- package/dist/chunk-JIKNR5YL.js.map +1 -0
- package/dist/codemode.d.ts +23 -0
- package/dist/editable-artifact-live-hints.d.ts +11 -0
- package/dist/editable-artifact-native-kernel.d.ts +37 -0
- package/dist/editable-artifact-office-import.d.ts +22 -0
- package/dist/editable-artifact-production.d.ts +29 -0
- package/dist/editable-artifact-websocket.d.ts +49 -0
- package/dist/editable-artifact-workspace-files.d.ts +23 -0
- package/dist/github-browser-flow.d.ts +6 -0
- package/dist/http/cors.d.ts +1 -0
- package/dist/http/sse.d.ts +2 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.js +1824 -30
- package/dist/index.js.map +1 -1
- package/dist/integrations/api-integrations.d.ts +24 -0
- package/dist/integrations/atlassian.d.ts +176 -0
- package/dist/integrations/github-skill-source.d.ts +5 -0
- package/dist/integrations/google-drive.d.ts +85 -0
- package/dist/integrations/oauth-client.d.ts +30 -1
- package/dist/integrations/provider-oauth.d.ts +19 -0
- package/dist/integrations/slack-bot.d.ts +4 -0
- package/dist/integrations/slack-interactions.d.ts +14 -2
- package/dist/integrations/social-api.d.ts +2 -1
- package/dist/mcp/editable-artifact-query-schema.d.ts +4 -0
- package/dist/mcp/editable-artifacts.d.ts +13 -0
- package/dist/mcp/receipts.d.ts +28 -0
- package/dist/mcp/scheduled-task-view.d.ts +518 -0
- package/dist/mcp/server.d.ts +14 -3
- package/dist/memory-slack-delivery.d.ts +9 -0
- package/dist/routes/api-integrations.d.ts +8 -0
- package/dist/routes/browser-identities.d.ts +5 -0
- package/dist/routes/browser-sessions.d.ts +6 -0
- package/dist/routes/company-profile.d.ts +3 -0
- package/dist/routes/computer-sessions.d.ts +6 -0
- package/dist/routes/editable-artifacts.d.ts +44 -0
- package/dist/routes/integration-features.d.ts +3 -0
- package/dist/routes/memory-slack-publications.d.ts +6 -0
- package/dist/routes/plugins.d.ts +8 -0
- package/dist/routes/sessions.d.ts +17 -2
- package/dist/routes/skills.d.ts +6 -0
- package/dist/routes/video-generation.d.ts +3 -0
- package/dist/sandbox/auth-callout.d.ts +2 -0
- package/dist/sandbox/channel-a.d.ts +59 -2
- package/dist/sandbox/metrics-ingestion.d.ts +6 -1
- package/dist/sandbox/viewer.d.ts +4 -2
- package/dist/temporal-schedule-cleanup.d.ts +26 -0
- package/package.json +19 -14
- package/src/app.ts +277 -41
- package/src/auth/managed-auth.ts +0 -16
- package/src/browser-controller-authority.ts +137 -0
- package/src/browser-state-authority.ts +236 -0
- package/src/codemode.ts +186 -0
- package/src/editable-artifact-live-hints.ts +64 -0
- package/src/editable-artifact-native-kernel.ts +659 -0
- package/src/editable-artifact-office-import.ts +230 -0
- package/src/editable-artifact-production.ts +419 -0
- package/src/editable-artifact-websocket.ts +311 -0
- package/src/editable-artifact-workspace-files.ts +186 -0
- package/src/github-browser-flow.ts +35 -6
- package/src/http/auth.ts +2 -0
- package/src/http/cors.ts +3 -0
- package/src/http/sse.ts +101 -6
- package/src/index.ts +147 -23
- package/src/integrations/api-integrations.ts +350 -0
- package/src/integrations/atlassian.ts +1621 -0
- package/src/integrations/github-skill-source.ts +142 -0
- package/src/integrations/google-drive.ts +1000 -64
- package/src/integrations/oauth-client.ts +159 -89
- package/src/integrations/provider-oauth.ts +777 -0
- package/src/integrations/slack-bot.ts +31 -2
- package/src/integrations/slack-interactions.ts +610 -42
- package/src/integrations/social-api.ts +11 -0
- package/src/mcp/documents.ts +74 -26
- package/src/mcp/editable-artifact-query-schema.ts +236 -0
- package/src/mcp/editable-artifacts.ts +448 -0
- package/src/mcp/receipts.ts +95 -0
- package/src/mcp/scheduled-task-view.ts +642 -0
- package/src/mcp/server.ts +1718 -310
- package/src/memory-slack-delivery.ts +209 -0
- package/src/observability.ts +3 -3
- package/src/routes/api-integrations.ts +407 -0
- package/src/routes/api-keys.ts +7 -1
- package/src/routes/browser-identities.ts +136 -0
- package/src/routes/browser-sessions.ts +2543 -0
- package/src/routes/codex.ts +7 -4
- package/src/routes/company-profile.ts +255 -0
- package/src/routes/computer-sessions.ts +1247 -0
- package/src/routes/connections.ts +358 -102
- package/src/routes/documents.ts +22 -3
- package/src/routes/editable-artifacts.ts +1159 -0
- package/src/routes/enrollments.ts +54 -12
- package/src/routes/environments.ts +60 -11
- package/src/routes/files.ts +277 -65
- package/src/routes/github.ts +18 -2
- package/src/routes/install.ts +38 -2
- package/src/routes/integration-features.ts +258 -0
- package/src/routes/machines.ts +1 -1
- package/src/routes/memory-slack-publications.ts +216 -0
- package/src/routes/packs.ts +437 -7
- package/src/routes/plugins.ts +751 -0
- package/src/routes/rigs.ts +77 -20
- package/src/routes/scheduled-tasks.ts +94 -42
- package/src/routes/sessions.ts +475 -234
- package/src/routes/skills.ts +174 -0
- package/src/routes/transcription-recordings.ts +65 -33
- package/src/routes/video-generation.ts +132 -0
- package/src/routes/workspaces.ts +46 -24
- package/src/sandbox/auth-callout.ts +16 -4
- package/src/sandbox/channel-a.ts +809 -85
- package/src/sandbox/enrollment.ts +13 -3
- package/src/sandbox/machines.ts +1 -1
- package/src/sandbox/metrics-ingestion.ts +121 -3
- package/src/sandbox/rematerialize.ts +35 -47
- package/src/sandbox/viewer.ts +58 -29
- package/src/temporal-schedule-cleanup.ts +135 -0
- package/dist/chunk-HWXJW5C7.js.map +0 -1
- package/dist/mcp/toolspace.d.ts +0 -62
- package/src/mcp/toolspace.ts +0 -1186
package/src/routes/files.ts
CHANGED
|
@@ -8,6 +8,10 @@ import {
|
|
|
8
8
|
RETAINED_OUTPUT_MAX_PAGE_BYTES,
|
|
9
9
|
RetainedArtifactMetadataSchema,
|
|
10
10
|
retainedArtifactReferenceFromFile,
|
|
11
|
+
retainedGeneratedImageReferenceFromFile,
|
|
12
|
+
retainedGeneratedVideoReferenceFromFile,
|
|
13
|
+
retainedScreenshotReferenceFromFile,
|
|
14
|
+
VideoArtifactPlaybackSource,
|
|
11
15
|
resolveRetainedOutputRange,
|
|
12
16
|
type RetainedArtifactMetadata,
|
|
13
17
|
type RetainedOutputUnavailableReason,
|
|
@@ -18,11 +22,17 @@ import {
|
|
|
18
22
|
completeFileUpload,
|
|
19
23
|
createFileUpload,
|
|
20
24
|
getFileUpload,
|
|
25
|
+
getGeneratedImageArtifact,
|
|
26
|
+
getGeneratedVideoArtifact,
|
|
21
27
|
getRetainedFileArtifact,
|
|
28
|
+
getRetainedScreenshotArtifact,
|
|
22
29
|
requireFile,
|
|
23
30
|
type RetainedFileArtifact,
|
|
31
|
+
type GeneratedImageArtifact,
|
|
32
|
+
type GeneratedVideoArtifact,
|
|
33
|
+
type RetainedScreenshotArtifact,
|
|
24
34
|
} from "@opengeni/db";
|
|
25
|
-
import type { Hono } from "hono";
|
|
35
|
+
import type { Context, Hono } from "hono";
|
|
26
36
|
import { HTTPException } from "hono/http-exception";
|
|
27
37
|
import { requireAccessGrant } from "@opengeni/core";
|
|
28
38
|
import { recordWorkspaceUsage, requireLimit } from "@opengeni/core";
|
|
@@ -36,7 +46,9 @@ export function registerFileRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
36
46
|
app.all("/v1/workspaces/:workspaceId/mcp/files", async (c) => {
|
|
37
47
|
const workspaceId = c.req.param("workspaceId");
|
|
38
48
|
const grant = await requireAccessGrant(c, deps, workspaceId, "files:read");
|
|
39
|
-
const transport = new WebStandardStreamableHTTPServerTransport({
|
|
49
|
+
const transport = new WebStandardStreamableHTTPServerTransport({
|
|
50
|
+
enableJsonResponse: true,
|
|
51
|
+
});
|
|
40
52
|
const server = buildFilesMcpServer(deps, grant);
|
|
41
53
|
await server.connect(transport);
|
|
42
54
|
return await transport.handleRequest(c.req.raw);
|
|
@@ -46,7 +58,9 @@ export function registerFileRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
46
58
|
const workspaceId = c.req.param("workspaceId");
|
|
47
59
|
const grant = await requireAccessGrant(c, deps, workspaceId, "files:upload");
|
|
48
60
|
if (!objectStorage) {
|
|
49
|
-
throw new HTTPException(503, {
|
|
61
|
+
throw new HTTPException(503, {
|
|
62
|
+
message: "object storage is not configured",
|
|
63
|
+
});
|
|
50
64
|
}
|
|
51
65
|
const payload = CreateFileUploadRequest.parse(await c.req.json());
|
|
52
66
|
await requireLimit(deps, {
|
|
@@ -98,7 +112,9 @@ export function registerFileRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
98
112
|
const workspaceId = c.req.param("workspaceId");
|
|
99
113
|
const grant = await requireAccessGrant(c, deps, workspaceId, "files:upload");
|
|
100
114
|
if (!objectStorage) {
|
|
101
|
-
throw new HTTPException(503, {
|
|
115
|
+
throw new HTTPException(503, {
|
|
116
|
+
message: "object storage is not configured",
|
|
117
|
+
});
|
|
102
118
|
}
|
|
103
119
|
const upload = await getFileUpload(db, workspaceId, c.req.param("uploadId"));
|
|
104
120
|
if (!upload) {
|
|
@@ -186,7 +202,9 @@ export function registerFileRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
186
202
|
terminalStatus,
|
|
187
203
|
});
|
|
188
204
|
if (!settled) {
|
|
189
|
-
throw new HTTPException(409, {
|
|
205
|
+
throw new HTTPException(409, {
|
|
206
|
+
message: "file upload cleanup claim was superseded",
|
|
207
|
+
});
|
|
190
208
|
}
|
|
191
209
|
throw new HTTPException(status, { message });
|
|
192
210
|
};
|
|
@@ -259,91 +277,112 @@ export function registerFileRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
259
277
|
const workspaceId = c.req.param("workspaceId");
|
|
260
278
|
await requireAccessGrant(c, deps, workspaceId, "files:read");
|
|
261
279
|
const artifactId = retainedArtifactId(c.req.param("artifactId"));
|
|
262
|
-
const artifact = await
|
|
280
|
+
const artifact = await getWorkspaceArtifact(db, workspaceId, artifactId);
|
|
263
281
|
if (!artifact) {
|
|
264
282
|
return c.json(retainedArtifactUnavailable(artifactId, "deleted"), 404);
|
|
265
283
|
}
|
|
266
|
-
return c.json(
|
|
284
|
+
return c.json(artifact.metadata);
|
|
267
285
|
});
|
|
268
286
|
|
|
269
287
|
app.get("/v1/workspaces/:workspaceId/artifacts/:artifactId/content", async (c) => {
|
|
270
288
|
const workspaceId = c.req.param("workspaceId");
|
|
271
289
|
await requireAccessGrant(c, deps, workspaceId, "files:read");
|
|
272
290
|
const artifactId = retainedArtifactId(c.req.param("artifactId"));
|
|
273
|
-
const artifact = await
|
|
291
|
+
const artifact = await getWorkspaceArtifact(db, workspaceId, artifactId);
|
|
274
292
|
if (!artifact) {
|
|
275
293
|
return c.json(retainedArtifactUnavailable(artifactId, "deleted"), 404);
|
|
276
294
|
}
|
|
277
295
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
296
|
+
return await serveRetainedArtifactContent(
|
|
297
|
+
c,
|
|
298
|
+
objectStorage,
|
|
299
|
+
artifactId,
|
|
300
|
+
artifact.file,
|
|
301
|
+
artifact.metadata,
|
|
302
|
+
);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
app.post("/v1/workspaces/:workspaceId/artifacts/:artifactId/playback-source", async (c) => {
|
|
306
|
+
const workspaceId = c.req.param("workspaceId");
|
|
307
|
+
await requireAccessGrant(c, deps, workspaceId, "files:read");
|
|
282
308
|
if (!objectStorage) {
|
|
283
|
-
|
|
309
|
+
throw new HTTPException(503, { message: "object storage is not configured" });
|
|
284
310
|
}
|
|
285
|
-
|
|
286
|
-
const
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
metadata.originalBytes,
|
|
290
|
-
rangeHeader ? RETAINED_OUTPUT_MAX_PAGE_BYTES : RETAINED_OUTPUT_DEFAULT_PAGE_BYTES,
|
|
291
|
-
);
|
|
292
|
-
if (range.kind === "invalid") {
|
|
293
|
-
return c.json(
|
|
294
|
-
{
|
|
295
|
-
message: "invalid retained artifact byte range",
|
|
296
|
-
reason: range.reason,
|
|
297
|
-
maxRangeBytes: RETAINED_OUTPUT_MAX_PAGE_BYTES,
|
|
298
|
-
},
|
|
299
|
-
400,
|
|
300
|
-
);
|
|
311
|
+
const artifactId = retainedArtifactId(c.req.param("artifactId"));
|
|
312
|
+
const retained = await getGeneratedVideoArtifact(db, workspaceId, artifactId);
|
|
313
|
+
if (!retained || retained.artifact.deletedAt) {
|
|
314
|
+
throw new HTTPException(404, { message: "generated video not found" });
|
|
301
315
|
}
|
|
302
|
-
if (
|
|
303
|
-
|
|
304
|
-
{ message: "retained artifact byte range is not satisfiable", reason: range.reason },
|
|
305
|
-
416,
|
|
306
|
-
{
|
|
307
|
-
"Accept-Ranges": "bytes",
|
|
308
|
-
"Content-Range": range.contentRange,
|
|
309
|
-
"Cache-Control": "private, no-store",
|
|
310
|
-
},
|
|
311
|
-
);
|
|
316
|
+
if (retained.file.status !== "ready") {
|
|
317
|
+
throw new HTTPException(409, { message: `generated video is ${retained.file.status}` });
|
|
312
318
|
}
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
"
|
|
316
|
-
"Cache-Control": "private, no-store",
|
|
317
|
-
"Content-Length": String(range.length),
|
|
318
|
-
"Content-Type": metadata.contentType,
|
|
319
|
-
"X-Content-Type-Options": "nosniff",
|
|
320
|
-
...(range.contentRange ? { "Content-Range": range.contentRange } : {}),
|
|
321
|
-
};
|
|
322
|
-
if (range.kind === "empty") {
|
|
323
|
-
if (!(await objectStorage.fileExists(artifact.file))) {
|
|
324
|
-
return c.json(retainedArtifactUnavailable(artifactId, "missing_storage"), 410);
|
|
325
|
-
}
|
|
326
|
-
return c.body(null, 200, headers);
|
|
319
|
+
const file = generatedVideoFileAsset(retained.file);
|
|
320
|
+
if (!(await objectStorage.fileExists(file))) {
|
|
321
|
+
throw new HTTPException(410, { message: "generated video bytes are unavailable" });
|
|
327
322
|
}
|
|
323
|
+
const signed = await objectStorage.createGetUrl({ key: file.objectKey });
|
|
324
|
+
return c.json(
|
|
325
|
+
VideoArtifactPlaybackSource.parse({
|
|
326
|
+
schemaVersion: 1,
|
|
327
|
+
artifactId,
|
|
328
|
+
url: signed.url,
|
|
329
|
+
expiresAt: signed.expiresAt.toISOString(),
|
|
330
|
+
contentType: "video/mp4",
|
|
331
|
+
sizeBytes: file.sizeBytes,
|
|
332
|
+
sha256: file.sha256,
|
|
333
|
+
acceptRanges: "bytes",
|
|
334
|
+
}),
|
|
335
|
+
);
|
|
336
|
+
});
|
|
328
337
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
+
app.get("/v1/workspaces/:workspaceId/sessions/:sessionId/artifacts/:artifactId", async (c) => {
|
|
339
|
+
const workspaceId = c.req.param("workspaceId");
|
|
340
|
+
await requireAccessGrant(c, deps, workspaceId, "files:read");
|
|
341
|
+
const artifactId = retainedArtifactId(c.req.param("artifactId"));
|
|
342
|
+
const artifact = await getRetainedScreenshotArtifact(
|
|
343
|
+
db,
|
|
344
|
+
workspaceId,
|
|
345
|
+
c.req.param("sessionId"),
|
|
346
|
+
artifactId,
|
|
347
|
+
);
|
|
348
|
+
if (!artifact) {
|
|
349
|
+
return c.json(retainedArtifactUnavailable(artifactId, "deleted"), 404);
|
|
338
350
|
}
|
|
339
|
-
return c.
|
|
351
|
+
return c.json(retainedScreenshotMetadata(artifact));
|
|
340
352
|
});
|
|
341
353
|
|
|
354
|
+
app.get(
|
|
355
|
+
"/v1/workspaces/:workspaceId/sessions/:sessionId/artifacts/:artifactId/content",
|
|
356
|
+
async (c) => {
|
|
357
|
+
const workspaceId = c.req.param("workspaceId");
|
|
358
|
+
await requireAccessGrant(c, deps, workspaceId, "files:read");
|
|
359
|
+
const artifactId = retainedArtifactId(c.req.param("artifactId"));
|
|
360
|
+
const artifact = await getRetainedScreenshotArtifact(
|
|
361
|
+
db,
|
|
362
|
+
workspaceId,
|
|
363
|
+
c.req.param("sessionId"),
|
|
364
|
+
artifactId,
|
|
365
|
+
);
|
|
366
|
+
if (!artifact) {
|
|
367
|
+
return c.json(retainedArtifactUnavailable(artifactId, "deleted"), 404);
|
|
368
|
+
}
|
|
369
|
+
return await serveRetainedArtifactContent(
|
|
370
|
+
c,
|
|
371
|
+
objectStorage,
|
|
372
|
+
artifactId,
|
|
373
|
+
artifact.file,
|
|
374
|
+
retainedScreenshotMetadata(artifact),
|
|
375
|
+
);
|
|
376
|
+
},
|
|
377
|
+
);
|
|
378
|
+
|
|
342
379
|
app.post("/v1/workspaces/:workspaceId/files/:fileId/download-url", async (c) => {
|
|
343
380
|
const workspaceId = c.req.param("workspaceId");
|
|
344
381
|
await requireAccessGrant(c, deps, workspaceId, "files:read");
|
|
345
382
|
if (!objectStorage) {
|
|
346
|
-
throw new HTTPException(503, {
|
|
383
|
+
throw new HTTPException(503, {
|
|
384
|
+
message: "object storage is not configured",
|
|
385
|
+
});
|
|
347
386
|
}
|
|
348
387
|
const file = await requireFile(db, workspaceId, c.req.param("fileId")).catch(() => null);
|
|
349
388
|
if (!file) {
|
|
@@ -362,6 +401,81 @@ export function registerFileRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
362
401
|
});
|
|
363
402
|
}
|
|
364
403
|
|
|
404
|
+
async function serveRetainedArtifactContent(
|
|
405
|
+
c: Context,
|
|
406
|
+
objectStorage: ApiRouteDeps["objectStorage"],
|
|
407
|
+
artifactId: string,
|
|
408
|
+
file: RetainedFileArtifact["file"],
|
|
409
|
+
metadata: RetainedArtifactMetadata,
|
|
410
|
+
) {
|
|
411
|
+
if (!metadata.available) {
|
|
412
|
+
return c.json(metadata, retainedArtifactUnavailableStatus(metadata.reason));
|
|
413
|
+
}
|
|
414
|
+
if (!objectStorage) {
|
|
415
|
+
return c.json(retainedArtifactUnavailable(artifactId, "missing_storage"), 503);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const rangeHeader = c.req.header("range");
|
|
419
|
+
const range = resolveRetainedOutputRange(
|
|
420
|
+
rangeHeader,
|
|
421
|
+
metadata.originalBytes,
|
|
422
|
+
rangeHeader ? RETAINED_OUTPUT_MAX_PAGE_BYTES : RETAINED_OUTPUT_DEFAULT_PAGE_BYTES,
|
|
423
|
+
);
|
|
424
|
+
if (range.kind === "invalid") {
|
|
425
|
+
return c.json(
|
|
426
|
+
{
|
|
427
|
+
message: "invalid retained artifact byte range",
|
|
428
|
+
reason: range.reason,
|
|
429
|
+
maxRangeBytes: RETAINED_OUTPUT_MAX_PAGE_BYTES,
|
|
430
|
+
},
|
|
431
|
+
400,
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
if (range.kind === "unsatisfiable") {
|
|
435
|
+
return c.json(
|
|
436
|
+
{
|
|
437
|
+
message: "retained artifact byte range is not satisfiable",
|
|
438
|
+
reason: range.reason,
|
|
439
|
+
},
|
|
440
|
+
416,
|
|
441
|
+
{
|
|
442
|
+
"Accept-Ranges": "bytes",
|
|
443
|
+
"Content-Range": range.contentRange,
|
|
444
|
+
"Cache-Control": "private, no-store",
|
|
445
|
+
},
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const headers = {
|
|
450
|
+
"Accept-Ranges": range.acceptRanges,
|
|
451
|
+
"Cache-Control": "private, no-store",
|
|
452
|
+
"Content-Length": String(range.length),
|
|
453
|
+
"Content-Type": metadata.contentType,
|
|
454
|
+
"X-Content-Type-Options": "nosniff",
|
|
455
|
+
...(range.contentRange ? { "Content-Range": range.contentRange } : {}),
|
|
456
|
+
};
|
|
457
|
+
if (range.kind === "empty") {
|
|
458
|
+
if (!(await objectStorage.fileExists(file))) {
|
|
459
|
+
return c.json(retainedArtifactUnavailable(artifactId, "missing_storage"), 410);
|
|
460
|
+
}
|
|
461
|
+
return c.body(null, 200, headers);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const bytes = await objectStorage.getFileRange(file, {
|
|
465
|
+
start: range.start,
|
|
466
|
+
end: range.end,
|
|
467
|
+
});
|
|
468
|
+
if (!bytes) {
|
|
469
|
+
return c.json(retainedArtifactUnavailable(artifactId, "missing_storage"), 410);
|
|
470
|
+
}
|
|
471
|
+
if (bytes.byteLength !== range.length) {
|
|
472
|
+
throw new HTTPException(502, {
|
|
473
|
+
message: "object storage returned an invalid byte range",
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
return c.body(new Uint8Array(bytes), range.status, headers);
|
|
477
|
+
}
|
|
478
|
+
|
|
365
479
|
export function sanitizeFilename(filename: string): string {
|
|
366
480
|
const trimmed = filename.trim().replace(/[/\\]/g, "_");
|
|
367
481
|
const safe = trimmed
|
|
@@ -388,7 +502,11 @@ function retainedArtifactUnavailable(
|
|
|
388
502
|
artifactId: string,
|
|
389
503
|
reason: RetainedOutputUnavailableReason,
|
|
390
504
|
): RetainedArtifactMetadata {
|
|
391
|
-
return RetainedArtifactMetadataSchema.parse({
|
|
505
|
+
return RetainedArtifactMetadataSchema.parse({
|
|
506
|
+
available: false,
|
|
507
|
+
artifactId,
|
|
508
|
+
reason,
|
|
509
|
+
});
|
|
392
510
|
}
|
|
393
511
|
|
|
394
512
|
function retainedArtifactMetadata(artifact: RetainedFileArtifact): RetainedArtifactMetadata {
|
|
@@ -417,6 +535,97 @@ function retainedArtifactMetadata(artifact: RetainedFileArtifact): RetainedArtif
|
|
|
417
535
|
return retainedArtifactUnavailable(file.id, "unsupported");
|
|
418
536
|
}
|
|
419
537
|
|
|
538
|
+
async function getWorkspaceArtifact(
|
|
539
|
+
db: ApiRouteDeps["db"],
|
|
540
|
+
workspaceId: string,
|
|
541
|
+
artifactId: string,
|
|
542
|
+
): Promise<{ file: RetainedFileArtifact["file"]; metadata: RetainedArtifactMetadata } | null> {
|
|
543
|
+
const generated = await getGeneratedImageArtifact(db, workspaceId, artifactId);
|
|
544
|
+
if (generated) {
|
|
545
|
+
return { file: generated.file, metadata: retainedGeneratedImageMetadata(generated) };
|
|
546
|
+
}
|
|
547
|
+
const generatedVideo = await getGeneratedVideoArtifact(db, workspaceId, artifactId);
|
|
548
|
+
if (generatedVideo && !generatedVideo.artifact.deletedAt) {
|
|
549
|
+
const file = generatedVideoFileAsset(generatedVideo.file);
|
|
550
|
+
return {
|
|
551
|
+
file,
|
|
552
|
+
metadata: retainedGeneratedVideoMetadata({
|
|
553
|
+
artifact: generatedVideo.artifact,
|
|
554
|
+
file,
|
|
555
|
+
}),
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
const artifact = await getRetainedFileArtifact(db, workspaceId, artifactId);
|
|
559
|
+
return artifact ? { file: artifact.file, metadata: retainedArtifactMetadata(artifact) } : null;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function generatedVideoFileAsset(
|
|
563
|
+
file: NonNullable<Awaited<ReturnType<typeof getGeneratedVideoArtifact>>>["file"],
|
|
564
|
+
): RetainedFileArtifact["file"] {
|
|
565
|
+
return FileAsset.parse({
|
|
566
|
+
...file,
|
|
567
|
+
createdAt: file.createdAt.toISOString(),
|
|
568
|
+
updatedAt: file.updatedAt.toISOString(),
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function retainedGeneratedVideoMetadata(artifact: {
|
|
573
|
+
artifact: GeneratedVideoArtifact;
|
|
574
|
+
file: RetainedFileArtifact["file"];
|
|
575
|
+
}): RetainedArtifactMetadata {
|
|
576
|
+
const reference = retainedGeneratedVideoReferenceFromFile({
|
|
577
|
+
...artifact.file,
|
|
578
|
+
id: artifact.artifact.id,
|
|
579
|
+
width: artifact.artifact.width,
|
|
580
|
+
height: artifact.artifact.height,
|
|
581
|
+
updatedAt: artifact.artifact.readyAt.toISOString(),
|
|
582
|
+
});
|
|
583
|
+
return reference ?? retainedArtifactUnavailable(artifact.artifact.id, "failed");
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function retainedGeneratedImageMetadata(
|
|
587
|
+
artifact: GeneratedImageArtifact,
|
|
588
|
+
): RetainedArtifactMetadata {
|
|
589
|
+
if (artifact.status === "ready") {
|
|
590
|
+
const reference = retainedGeneratedImageReferenceFromFile({
|
|
591
|
+
...artifact.file,
|
|
592
|
+
width: artifact.width,
|
|
593
|
+
height: artifact.height,
|
|
594
|
+
});
|
|
595
|
+
if (reference) return reference;
|
|
596
|
+
}
|
|
597
|
+
return retainedArtifactUnavailable(artifact.artifactId, "pending");
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function retainedScreenshotMetadata(
|
|
601
|
+
artifact: RetainedScreenshotArtifact,
|
|
602
|
+
): RetainedArtifactMetadata {
|
|
603
|
+
if (
|
|
604
|
+
artifact.status === "ready" &&
|
|
605
|
+
artifact.sessionId &&
|
|
606
|
+
artifact.retentionExpiresAt.getTime() > Date.now()
|
|
607
|
+
) {
|
|
608
|
+
const reference = retainedScreenshotReferenceFromFile({
|
|
609
|
+
...artifact.file,
|
|
610
|
+
sessionId: artifact.sessionId,
|
|
611
|
+
width: artifact.width,
|
|
612
|
+
height: artifact.height,
|
|
613
|
+
expiresAt: artifact.retentionExpiresAt.toISOString(),
|
|
614
|
+
});
|
|
615
|
+
if (reference) return reference;
|
|
616
|
+
}
|
|
617
|
+
if (artifact.status === "deleted") {
|
|
618
|
+
return retainedArtifactUnavailable(artifact.artifactId, "deleted");
|
|
619
|
+
}
|
|
620
|
+
if (artifact.status === "expired" || artifact.retentionExpiresAt.getTime() <= Date.now()) {
|
|
621
|
+
return retainedArtifactUnavailable(artifact.artifactId, "expired");
|
|
622
|
+
}
|
|
623
|
+
if (artifact.status === "pending" || artifact.status === "reconciling") {
|
|
624
|
+
return retainedArtifactUnavailable(artifact.artifactId, "pending");
|
|
625
|
+
}
|
|
626
|
+
return retainedArtifactUnavailable(artifact.artifactId, "failed");
|
|
627
|
+
}
|
|
628
|
+
|
|
420
629
|
function retainedArtifactUnavailableStatus(
|
|
421
630
|
reason: RetainedOutputUnavailableReason,
|
|
422
631
|
): 404 | 409 | 410 | 422 {
|
|
@@ -429,6 +638,9 @@ function retainedArtifactUnavailableStatus(
|
|
|
429
638
|
case "unsupported":
|
|
430
639
|
case "not_retained":
|
|
431
640
|
case "storage_write_failed":
|
|
641
|
+
case "quota_exceeded":
|
|
642
|
+
case "invalid_content":
|
|
643
|
+
case "oversized":
|
|
432
644
|
return 422;
|
|
433
645
|
case "pending":
|
|
434
646
|
case "failed":
|
package/src/routes/github.ts
CHANGED
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
githubBrowserBaseUrl,
|
|
39
39
|
githubBrowserGrantClaims,
|
|
40
40
|
githubBrowserGrantFromState,
|
|
41
|
+
githubSessionReturnPath,
|
|
41
42
|
} from "../github-browser-flow";
|
|
42
43
|
import {
|
|
43
44
|
githubBindingStatus,
|
|
@@ -64,12 +65,14 @@ export function registerGitHubRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
64
65
|
const status = githubBindingStatus(missing.length === 0, installations);
|
|
65
66
|
const setupMode = settings.productAccessMode === "managed" ? "platform" : "operator";
|
|
66
67
|
const canManage = hasPermission(grant.permissions, "github:manage");
|
|
68
|
+
const returnPath = githubSessionReturnPath(c.req.query("returnPath"), grant.workspaceId);
|
|
67
69
|
const connectState =
|
|
68
70
|
missing.length === 0 && slug && canManage
|
|
69
71
|
? createSignedState(githubStateSecret, {
|
|
70
72
|
accountId: grant.accountId,
|
|
71
73
|
workspaceId: grant.workspaceId,
|
|
72
74
|
intent: "installation_authority",
|
|
75
|
+
...(returnPath ? { returnPath } : {}),
|
|
73
76
|
...githubBrowserGrantClaims(settings, grant),
|
|
74
77
|
})
|
|
75
78
|
: null;
|
|
@@ -517,7 +520,12 @@ export function registerGitHubRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
517
520
|
return c.html(
|
|
518
521
|
githubSetupSuccessHtml(
|
|
519
522
|
proof.installation.accountLogin ?? `installation ${installationId}`,
|
|
520
|
-
openGeniReturnUrl(
|
|
523
|
+
openGeniReturnUrl(
|
|
524
|
+
settings,
|
|
525
|
+
c,
|
|
526
|
+
grant.workspaceId,
|
|
527
|
+
typeof statePayload.returnPath === "string" ? statePayload.returnPath : null,
|
|
528
|
+
),
|
|
521
529
|
),
|
|
522
530
|
);
|
|
523
531
|
});
|
|
@@ -879,8 +887,16 @@ function openGeniReturnUrl(
|
|
|
879
887
|
settings: ApiRouteDeps["settings"],
|
|
880
888
|
c: Context,
|
|
881
889
|
workspaceId: string,
|
|
890
|
+
returnPath: string | null = null,
|
|
882
891
|
): string {
|
|
883
|
-
const
|
|
892
|
+
const baseUrl = openGeniBaseUrl(settings, c) || new URL(c.req.url).origin;
|
|
893
|
+
const safeReturnPath = githubSessionReturnPath(returnPath, workspaceId);
|
|
894
|
+
const url = safeReturnPath ? new URL(safeReturnPath, `${baseUrl}/`) : new URL(baseUrl);
|
|
895
|
+
if (safeReturnPath) {
|
|
896
|
+
url.searchParams.delete("capability_auth");
|
|
897
|
+
url.searchParams.set("github", "connected");
|
|
898
|
+
return url.toString();
|
|
899
|
+
}
|
|
884
900
|
url.searchParams.set("workspaceId", workspaceId);
|
|
885
901
|
return url.toString();
|
|
886
902
|
}
|
package/src/routes/install.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
1
2
|
import { readFile, stat } from "node:fs/promises";
|
|
2
3
|
import type { Hono } from "hono";
|
|
3
4
|
import { HTTPException } from "hono/http-exception";
|
|
@@ -27,7 +28,12 @@ import type { ApiRouteDeps } from "@opengeni/core";
|
|
|
27
28
|
// The committed install artifacts, resolved relative to this module so the API
|
|
28
29
|
// (run from source under /app via bun) locates the sibling agent/install/ dir at
|
|
29
30
|
// runtime. apps/api/src/routes -> ../../../../agent/install.
|
|
30
|
-
const INSTALL_DIR =
|
|
31
|
+
const INSTALL_DIR =
|
|
32
|
+
[
|
|
33
|
+
new URL("./assets/agent-install/", import.meta.url),
|
|
34
|
+
new URL("../../../../agent/install/", import.meta.url),
|
|
35
|
+
].find((candidate) => existsSync(candidate)) ??
|
|
36
|
+
new URL("../../../../agent/install/", import.meta.url);
|
|
31
37
|
|
|
32
38
|
// The baked release-binary dir (a sibling of the committed scripts). The build's
|
|
33
39
|
// signing step writes the per-SHA Linux musl binaries + their `.sha256`/`.minisig`
|
|
@@ -200,6 +206,31 @@ export function registerInstallRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
200
206
|
return serveAsset(asset, `${releasesBase}/download/${stableAgentTag}/${asset}`);
|
|
201
207
|
});
|
|
202
208
|
|
|
209
|
+
// Signed self-update channel manifests. Each deployment exposes its own
|
|
210
|
+
// immutable promotion pointer, so an enrolled machine never depends on the
|
|
211
|
+
// public get.opengeni.ai hostname. The manifest and signature are ordinary
|
|
212
|
+
// assets on the selected immutable agent release.
|
|
213
|
+
for (const [channel, version] of [
|
|
214
|
+
["stable", deps.settings.agentStableVersion],
|
|
215
|
+
["beta", deps.settings.agentBetaVersion],
|
|
216
|
+
] as const) {
|
|
217
|
+
app.get(`/agent/${channel}/:manifestAsset`, (c) => {
|
|
218
|
+
const manifestAsset = c.req.param("manifestAsset");
|
|
219
|
+
if (manifestAsset !== "manifest.json" && manifestAsset !== "manifest.json.minisig") {
|
|
220
|
+
throw new HTTPException(404, { message: "update manifest asset not found" });
|
|
221
|
+
}
|
|
222
|
+
if (!version) {
|
|
223
|
+
throw new HTTPException(404, { message: `${channel} agent channel is not configured` });
|
|
224
|
+
}
|
|
225
|
+
return new Response(null, {
|
|
226
|
+
status: 302,
|
|
227
|
+
headers: {
|
|
228
|
+
location: `${releasesBase}/download/agent-v${version}/${manifestAsset}`,
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
203
234
|
// The version segment is the literal `v<ver>` (e.g. `v1.2.3`) — Hono cannot bind
|
|
204
235
|
// a param glued to a literal prefix, so the whole segment is the param and the
|
|
205
236
|
// `v` prefix is validated/stripped here. The release tag is `agent-v<ver>`.
|
|
@@ -220,5 +251,10 @@ export function registerInstallRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
220
251
|
export const installExactPaths: ReadonlySet<string> = new Set(Object.keys(TEXT_ASSETS));
|
|
221
252
|
|
|
222
253
|
export function isInstallRedirectPath(path: string): boolean {
|
|
223
|
-
return
|
|
254
|
+
return (
|
|
255
|
+
path.startsWith("/agent/latest/") ||
|
|
256
|
+
path.startsWith("/agent/v") ||
|
|
257
|
+
path.startsWith("/agent/stable/") ||
|
|
258
|
+
path.startsWith("/agent/beta/")
|
|
259
|
+
);
|
|
224
260
|
}
|