@opengeni/api-router 0.21.14 → 0.23.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 +1 -0
- package/dist/app.js +1 -1
- package/dist/auth/managed-auth.d.ts +0 -30
- package/dist/{chunk-R5PDSH2A.js → chunk-T4T2PGU4.js} +3989 -1356
- package/dist/chunk-T4T2PGU4.js.map +1 -0
- package/dist/http/sse.d.ts +2 -0
- package/dist/index.js +29 -9
- package/dist/index.js.map +1 -1
- package/dist/integrations/oauth-client.d.ts +8 -0
- package/dist/integrations/slack-interactions.d.ts +7 -1
- package/dist/mcp/receipts.d.ts +28 -0
- package/dist/mcp/scheduled-task-view.d.ts +350 -0
- package/dist/mcp/toolspace.d.ts +9 -0
- package/dist/routes/transcription-recordings.d.ts +3 -0
- package/dist/sandbox/auth-callout.d.ts +2 -0
- package/dist/sandbox/channel-a.d.ts +5 -1
- package/dist/transcription/segmenter.d.ts +10 -0
- package/dist/transcription/service.d.ts +5 -0
- package/package.json +12 -12
- package/src/app.ts +39 -6
- package/src/auth/managed-auth.ts +0 -16
- package/src/http/sse.ts +101 -6
- package/src/index.ts +28 -3
- package/src/integrations/oauth-client.ts +36 -56
- package/src/integrations/slack-interactions.ts +123 -15
- package/src/mcp/documents.ts +42 -25
- package/src/mcp/receipts.ts +95 -0
- package/src/mcp/scheduled-task-view.ts +608 -0
- package/src/mcp/server.ts +812 -182
- package/src/mcp/toolspace.ts +75 -71
- package/src/observability.ts +3 -3
- package/src/routes/api-keys.ts +7 -1
- package/src/routes/codex.ts +7 -4
- package/src/routes/connections.ts +74 -3
- package/src/routes/enrollments.ts +54 -12
- package/src/routes/environments.ts +60 -11
- package/src/routes/files.ts +175 -65
- package/src/routes/install.ts +31 -1
- package/src/routes/machines.ts +1 -1
- package/src/routes/scheduled-tasks.ts +39 -14
- package/src/routes/sessions.ts +77 -12
- package/src/routes/transcription-recordings.ts +754 -0
- package/src/routes/transcriptions.ts +2 -0
- package/src/sandbox/auth-callout.ts +16 -4
- package/src/sandbox/channel-a.ts +124 -7
- package/src/sandbox/enrollment.ts +13 -3
- package/src/sandbox/machines.ts +1 -1
- package/src/sandbox/viewer.ts +29 -20
- package/src/transcription/providers/azure-openai.ts +4 -3
- package/src/transcription/providers/codex-subscription.ts +4 -1
- package/src/transcription/providers/openai.ts +7 -2
- package/src/transcription/segmenter.ts +260 -0
- package/src/transcription/service.ts +111 -10
- package/dist/chunk-R5PDSH2A.js.map +0 -1
package/src/routes/sessions.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
EditSessionQueueItemRequest,
|
|
13
13
|
EndSessionRealtimeRequest,
|
|
14
14
|
FsDeleteRequest,
|
|
15
|
+
FsListBatchRequest,
|
|
15
16
|
FsListRequest,
|
|
16
17
|
FsMkdirRequest,
|
|
17
18
|
FsMoveRequest,
|
|
@@ -19,6 +20,7 @@ import {
|
|
|
19
20
|
FsWriteRequest,
|
|
20
21
|
HumanInputRequestStatus,
|
|
21
22
|
GitDiffRequest,
|
|
23
|
+
GitReadBatchRequest,
|
|
22
24
|
GitLogRequest,
|
|
23
25
|
GitShowRequest,
|
|
24
26
|
GitStatusRequest,
|
|
@@ -148,6 +150,7 @@ import { HTTPException } from "hono/http-exception";
|
|
|
148
150
|
import type { ContentfulStatusCode } from "hono/utils/http-status";
|
|
149
151
|
import {
|
|
150
152
|
requireAccessGrant,
|
|
153
|
+
requirePermission,
|
|
151
154
|
requireSessionAuthorization,
|
|
152
155
|
requireSessionAuthorizationListScope,
|
|
153
156
|
SESSION_AUTHORIZATION_DEFAULT_REAUTHORIZE_MS,
|
|
@@ -333,6 +336,13 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
333
336
|
const authorizeSessionHttp: MiddlewareHandler = async (c, next) => {
|
|
334
337
|
const workspaceId = c.req.param("workspaceId") ?? "";
|
|
335
338
|
const sessionId = c.req.param("sessionId") ?? "";
|
|
339
|
+
// Reject malformed route identifiers before the authorization resolver
|
|
340
|
+
// reaches UUID-typed persistence queries. Besides avoiding a needless DB
|
|
341
|
+
// round trip, this preserves the session surface's non-enumerating 404
|
|
342
|
+
// contract instead of leaking a driver-level 500.
|
|
343
|
+
if (!z.string().uuid().safeParse(sessionId).success) {
|
|
344
|
+
throw new HTTPException(404, { message: "session not found" });
|
|
345
|
+
}
|
|
336
346
|
const operation = sessionAuthorizationOperationForHttp(
|
|
337
347
|
c.req.method,
|
|
338
348
|
new URL(c.req.url).pathname,
|
|
@@ -1342,11 +1352,12 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
1342
1352
|
if (event) {
|
|
1343
1353
|
try {
|
|
1344
1354
|
await bus.publish(workspaceId, sessionId, [event]);
|
|
1345
|
-
} catch
|
|
1346
|
-
console.warn(
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1355
|
+
} catch {
|
|
1356
|
+
console.warn("[api] cleared-goal live publish failed; durable event reconciles on replay", {
|
|
1357
|
+
errorClass: "EventPublishOperationError",
|
|
1358
|
+
errorCode: "cleared_goal_live_publish_failed",
|
|
1359
|
+
origin: "api",
|
|
1360
|
+
});
|
|
1350
1361
|
}
|
|
1351
1362
|
}
|
|
1352
1363
|
return c.body(null, 204);
|
|
@@ -2190,6 +2201,16 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
2190
2201
|
: {}),
|
|
2191
2202
|
});
|
|
2192
2203
|
|
|
2204
|
+
const repositoryRoots = [
|
|
2205
|
+
...new Set(
|
|
2206
|
+
session.resources.flatMap((resource) =>
|
|
2207
|
+
resource.kind === "repository" && typeof resource.mountPath === "string"
|
|
2208
|
+
? [resource.mountPath.replace(/^\/+|\/+$/g, "")]
|
|
2209
|
+
: [],
|
|
2210
|
+
),
|
|
2211
|
+
),
|
|
2212
|
+
].filter(Boolean);
|
|
2213
|
+
|
|
2193
2214
|
// SWAP-CASE desktop transport (BOTH directions): negotiateCapabilities keyed on
|
|
2194
2215
|
// the HOME backend, but the pixel plane actually runs on the ACTIVE sandbox — and
|
|
2195
2216
|
// the two backends use DIFFERENT wire transports. The advertised transport MUST
|
|
@@ -2209,7 +2230,13 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
2209
2230
|
// active sandbox kind is "selfhosted") — EXACTLY mintDesktopStream's routing. When
|
|
2210
2231
|
// the desktop is available we set the transport from the ACTIVE sandbox in one
|
|
2211
2232
|
// place (resolveActiveDesktopTransport), covering BOTH swap directions.
|
|
2212
|
-
let responseCapabilities =
|
|
2233
|
+
let responseCapabilities = {
|
|
2234
|
+
...capabilities,
|
|
2235
|
+
Git: {
|
|
2236
|
+
...capabilities.Git,
|
|
2237
|
+
repos: capabilities.Git.available ? repositoryRoots : [],
|
|
2238
|
+
},
|
|
2239
|
+
};
|
|
2213
2240
|
if (capabilities.DesktopStream.transport !== null) {
|
|
2214
2241
|
const activeSandbox = session.activeSandboxId
|
|
2215
2242
|
? await getSandbox(db, workspaceId, session.activeSandboxId)
|
|
@@ -2219,7 +2246,7 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
2219
2246
|
settings.sandboxDesktopInteractive !== false,
|
|
2220
2247
|
);
|
|
2221
2248
|
responseCapabilities = {
|
|
2222
|
-
...
|
|
2249
|
+
...responseCapabilities,
|
|
2223
2250
|
DesktopStream: { ...capabilities.DesktopStream, ...wire },
|
|
2224
2251
|
};
|
|
2225
2252
|
}
|
|
@@ -2273,7 +2300,10 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
2273
2300
|
// when cold).
|
|
2274
2301
|
app.post("/v1/workspaces/:workspaceId/sessions/:sessionId/viewers", async (c) => {
|
|
2275
2302
|
const workspaceId = c.req.param("workspaceId");
|
|
2276
|
-
|
|
2303
|
+
// Authenticate and bind the workspace before parsing. The requested plane
|
|
2304
|
+
// determines the narrower permission below: terminal-only holders must not
|
|
2305
|
+
// require the strictly broader un-redacted Desktop permission.
|
|
2306
|
+
const grant = await requireAccessGrant(c, deps, workspaceId);
|
|
2277
2307
|
assertOwnershipEnabled();
|
|
2278
2308
|
const sessionId = c.req.param("sessionId");
|
|
2279
2309
|
const session = await getSession(db, workspaceId, sessionId);
|
|
@@ -2297,6 +2327,7 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
2297
2327
|
// read-only sse-events firehose forever ("read only"), and with the desktop tier
|
|
2298
2328
|
// off by default there was no consent flow to ever clear the gate.
|
|
2299
2329
|
const wantDesktop = parsed.data.desktop ?? false;
|
|
2330
|
+
requirePermission(grant, wantDesktop ? "stream:view" : "terminal:attach");
|
|
2300
2331
|
const { shared } = await resolveSharedExposure(workspaceId, session);
|
|
2301
2332
|
if (wantDesktop) {
|
|
2302
2333
|
const ack = await getStreamAcknowledgment(db, {
|
|
@@ -2443,12 +2474,13 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
2443
2474
|
});
|
|
2444
2475
|
|
|
2445
2476
|
// POST .../viewers/:viewerId/heartbeat — refresh the holder TTL (epoch-fenced).
|
|
2446
|
-
//
|
|
2477
|
+
// A holder can represent the terminal-only plane, so lifecycle control uses
|
|
2478
|
+
// terminal:attach. Desktop callers continue to pass via workspace:admin.
|
|
2447
2479
|
app.post(
|
|
2448
2480
|
"/v1/workspaces/:workspaceId/sessions/:sessionId/viewers/:viewerId/heartbeat",
|
|
2449
2481
|
async (c) => {
|
|
2450
2482
|
const workspaceId = c.req.param("workspaceId");
|
|
2451
|
-
const grant = await requireAccessGrant(c, deps, workspaceId, "
|
|
2483
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "terminal:attach");
|
|
2452
2484
|
assertOwnershipEnabled();
|
|
2453
2485
|
const sessionId = c.req.param("sessionId");
|
|
2454
2486
|
const session = await getSession(db, workspaceId, sessionId);
|
|
@@ -2478,7 +2510,7 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
2478
2510
|
// DELETE .../viewers/:viewerId — release the holder (idempotent).
|
|
2479
2511
|
app.delete("/v1/workspaces/:workspaceId/sessions/:sessionId/viewers/:viewerId", async (c) => {
|
|
2480
2512
|
const workspaceId = c.req.param("workspaceId");
|
|
2481
|
-
const grant = await requireAccessGrant(c, deps, workspaceId, "
|
|
2513
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "terminal:attach");
|
|
2482
2514
|
assertOwnershipEnabled();
|
|
2483
2515
|
const sessionId = c.req.param("sessionId");
|
|
2484
2516
|
const session = await getSession(db, workspaceId, sessionId);
|
|
@@ -2596,6 +2628,19 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
2596
2628
|
return c.json(out);
|
|
2597
2629
|
});
|
|
2598
2630
|
|
|
2631
|
+
app.post("/v1/workspaces/:workspaceId/sessions/:sessionId/fs/list-batch", async (c) => {
|
|
2632
|
+
const ctx = await channelAPreamble(c, "files:read");
|
|
2633
|
+
const req = await parseChannelABody(c, FsListBatchRequest);
|
|
2634
|
+
const out = await withChannelA(channelAServices, ctx, async ({ service }) => ({
|
|
2635
|
+
results: await Promise.all(
|
|
2636
|
+
req.requests.map((request) =>
|
|
2637
|
+
Promise.resolve().then(async () => await service.fsList(request)),
|
|
2638
|
+
),
|
|
2639
|
+
),
|
|
2640
|
+
}));
|
|
2641
|
+
return c.json(out);
|
|
2642
|
+
});
|
|
2643
|
+
|
|
2599
2644
|
app.post("/v1/workspaces/:workspaceId/sessions/:sessionId/fs/read", async (c) => {
|
|
2600
2645
|
const ctx = await channelAPreamble(c, "files:read");
|
|
2601
2646
|
const req = await parseChannelABody(c, FsReadRequest);
|
|
@@ -2646,6 +2691,26 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
2646
2691
|
return c.json(out);
|
|
2647
2692
|
});
|
|
2648
2693
|
|
|
2694
|
+
app.post("/v1/workspaces/:workspaceId/sessions/:sessionId/git/read-batch", async (c) => {
|
|
2695
|
+
const ctx = await channelAPreamble(c, "files:read");
|
|
2696
|
+
const req = await parseChannelABody(c, GitReadBatchRequest);
|
|
2697
|
+
const out = await withChannelA(channelAServices, ctx, async ({ service }) => ({
|
|
2698
|
+
results: await Promise.all(
|
|
2699
|
+
req.requests.map(async (request) => {
|
|
2700
|
+
const diffRequest = request.diff;
|
|
2701
|
+
const [status, diff] = await Promise.all([
|
|
2702
|
+
Promise.resolve().then(async () => await service.gitStatus(request.status)),
|
|
2703
|
+
diffRequest
|
|
2704
|
+
? Promise.resolve().then(async () => await service.gitDiff(diffRequest))
|
|
2705
|
+
: Promise.resolve(undefined),
|
|
2706
|
+
]);
|
|
2707
|
+
return { status, ...(diff ? { diff } : {}) };
|
|
2708
|
+
}),
|
|
2709
|
+
),
|
|
2710
|
+
}));
|
|
2711
|
+
return c.json(out);
|
|
2712
|
+
});
|
|
2713
|
+
|
|
2649
2714
|
app.post("/v1/workspaces/:workspaceId/sessions/:sessionId/git/log", async (c) => {
|
|
2650
2715
|
const ctx = await channelAPreamble(c, "files:read");
|
|
2651
2716
|
const req = await parseChannelABody(c, GitLogRequest);
|
|
@@ -3101,7 +3166,7 @@ export function sessionAuthorizationOperationForHttp(
|
|
|
3101
3166
|
if (suffix.startsWith("/viewers/") && ["POST", "DELETE"].includes(verb)) {
|
|
3102
3167
|
return "session.viewer.control";
|
|
3103
3168
|
}
|
|
3104
|
-
if (suffix === "/fs/list" || suffix === "/fs/read") {
|
|
3169
|
+
if (suffix === "/fs/list" || suffix === "/fs/list-batch" || suffix === "/fs/read") {
|
|
3105
3170
|
return verb === "POST" ? "session.files.read" : null;
|
|
3106
3171
|
}
|
|
3107
3172
|
if (["/fs/write", "/fs/delete", "/fs/move", "/fs/mkdir"].includes(suffix)) {
|