@opengeni/api-router 0.30.1 → 0.30.3
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.js +1 -1
- package/dist/{chunk-2PQMSRCB.js → chunk-WCXHI3FF.js} +227 -66
- package/dist/chunk-WCXHI3FF.js.map +1 -0
- package/dist/editable-artifact-office-import.d.ts +7 -4
- package/dist/index.js +39 -5
- package/dist/index.js.map +1 -1
- package/dist/integrations/google-drive.d.ts +1 -0
- package/dist/integrations/slack-interactions.d.ts +6 -1
- package/dist/routes/browser-sessions.d.ts +13 -2
- package/dist/routes/files.d.ts +4 -0
- package/package.json +13 -13
- package/src/app.ts +7 -4
- package/src/editable-artifact-office-import.ts +49 -6
- package/src/integrations/google-drive.ts +115 -9
- package/src/integrations/slack-interactions.ts +9 -6
- package/src/mcp/files.ts +7 -2
- package/src/routes/browser-sessions.ts +59 -19
- package/src/routes/connections.ts +1 -0
- package/src/routes/files.ts +80 -16
- package/src/routes/sessions.ts +2 -2
- package/dist/chunk-2PQMSRCB.js.map +0 -1
|
@@ -100,7 +100,7 @@ import {
|
|
|
100
100
|
getBrowserRevisionArtifactAuthority,
|
|
101
101
|
getEnrollment,
|
|
102
102
|
getExternalAuthInteractiveContext,
|
|
103
|
-
|
|
103
|
+
getFilesForSubject,
|
|
104
104
|
getFileUpload,
|
|
105
105
|
getAuthRun,
|
|
106
106
|
getExternalAuthPreparation,
|
|
@@ -145,6 +145,7 @@ import {
|
|
|
145
145
|
SessionAuthorizationDeniedError,
|
|
146
146
|
SessionAuthorizationUnavailableError,
|
|
147
147
|
type ApiRouteDeps,
|
|
148
|
+
type ResolvedSessionAuthorization,
|
|
148
149
|
} from "@opengeni/core";
|
|
149
150
|
import {
|
|
150
151
|
BrowserControlProtocolError,
|
|
@@ -927,7 +928,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
927
928
|
browserSessionId,
|
|
928
929
|
"session.control",
|
|
929
930
|
"browser.action",
|
|
930
|
-
async ({ sessionClient, binding, record }) => {
|
|
931
|
+
async ({ sessionClient, binding, record, sourceAuthorization }) => {
|
|
931
932
|
const command = BrowserActionCommand.parse({
|
|
932
933
|
protocolVersion: BROWSER_CONTROL_PROTOCOL_VERSION,
|
|
933
934
|
operationId: request.operationId,
|
|
@@ -957,27 +958,25 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
957
958
|
message: "object storage is not configured",
|
|
958
959
|
});
|
|
959
960
|
}
|
|
960
|
-
const files = await
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
}
|
|
966
|
-
|
|
967
|
-
throw new HTTPException(409, { message: "workspace file is not ready" });
|
|
968
|
-
}
|
|
961
|
+
const files = await getFilesForSubject(deps.db, {
|
|
962
|
+
accountId: grant.accountId,
|
|
963
|
+
workspaceId,
|
|
964
|
+
subjectId: browserFileAuthoritySubjectId(grant, sourceAuthorization),
|
|
965
|
+
fileIds: workspaceFileIds,
|
|
966
|
+
});
|
|
967
|
+
const ordered = requireAuthorizedBrowserUploadFiles(workspaceFileIds, files);
|
|
969
968
|
const authorities = await Promise.all(
|
|
970
969
|
ordered.map(async (file) => {
|
|
971
970
|
const signed = await deps.objectStorage!.createGetUrl({
|
|
972
|
-
key: file
|
|
971
|
+
key: file.objectKey,
|
|
973
972
|
expiresInSeconds: BROWSER_WORKSPACE_FILE_AUTHORITY_TTL_SECONDS,
|
|
974
973
|
audience: signedUrlAudienceForPlacement(record.session.placement),
|
|
975
974
|
});
|
|
976
975
|
return {
|
|
977
|
-
fileId: file
|
|
978
|
-
safeFilename: browserUploadFilename(file
|
|
979
|
-
sizeBytes: file
|
|
980
|
-
sha256: browserUploadSha256(file
|
|
976
|
+
fileId: file.id,
|
|
977
|
+
safeFilename: browserUploadFilename(file.safeFilename),
|
|
978
|
+
sizeBytes: file.sizeBytes,
|
|
979
|
+
sha256: browserUploadSha256(file.sha256),
|
|
981
980
|
download: {
|
|
982
981
|
url: signed.url,
|
|
983
982
|
expiresAt: signed.expiresAt.toISOString(),
|
|
@@ -2874,6 +2873,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2874
2873
|
record: BrowserSessionControlRecord;
|
|
2875
2874
|
binding: NonNullable<BrowserSessionControlRecord["session"]["controller"]>;
|
|
2876
2875
|
placement: BrowserPlacement;
|
|
2876
|
+
sourceAuthorization: ResolvedSessionAuthorization | null;
|
|
2877
2877
|
}) => Promise<T>,
|
|
2878
2878
|
recoverMissing = true,
|
|
2879
2879
|
): Promise<T> {
|
|
@@ -2883,7 +2883,12 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2883
2883
|
workspaceId,
|
|
2884
2884
|
browserSessionId,
|
|
2885
2885
|
});
|
|
2886
|
-
await authorizeSourceSession(
|
|
2886
|
+
const sourceAuthorization = await authorizeSourceSession(
|
|
2887
|
+
deps,
|
|
2888
|
+
grant,
|
|
2889
|
+
record.sourceSessionId,
|
|
2890
|
+
authorizationOperation,
|
|
2891
|
+
);
|
|
2887
2892
|
if (record.session.lifecycle !== "active" || !record.session.controller) {
|
|
2888
2893
|
throw new BrowserSessionStateError("BrowserSession is not active");
|
|
2889
2894
|
}
|
|
@@ -2923,6 +2928,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2923
2928
|
record,
|
|
2924
2929
|
binding,
|
|
2925
2930
|
placement,
|
|
2931
|
+
sourceAuthorization,
|
|
2926
2932
|
};
|
|
2927
2933
|
let result: T;
|
|
2928
2934
|
try {
|
|
@@ -3792,9 +3798,9 @@ async function authorizeSourceSession(
|
|
|
3792
3798
|
grant: AccessGrant,
|
|
3793
3799
|
sessionId: string,
|
|
3794
3800
|
operation: SessionAuthorizationOperation,
|
|
3795
|
-
): Promise<
|
|
3801
|
+
): Promise<ResolvedSessionAuthorization | null> {
|
|
3796
3802
|
try {
|
|
3797
|
-
await requireSessionAuthorization(deps, grant, {
|
|
3803
|
+
return await requireSessionAuthorization(deps, grant, {
|
|
3798
3804
|
sessionId,
|
|
3799
3805
|
operation,
|
|
3800
3806
|
surface: "http",
|
|
@@ -4285,6 +4291,40 @@ function browserUploadFileIds(action: BrowserActionRequestValue["action"]): stri
|
|
|
4285
4291
|
];
|
|
4286
4292
|
}
|
|
4287
4293
|
|
|
4294
|
+
/** Resolve Drive authority from the same immutable session authorization that
|
|
4295
|
+
* admitted the browser action. Agent-attempt subjects are technical worker
|
|
4296
|
+
* identities; their frozen initiating human is the only personal Drive
|
|
4297
|
+
* principal. Pure service attempts retain null and can read only ordinary
|
|
4298
|
+
* workspace files through the database predicate. */
|
|
4299
|
+
export function browserFileAuthoritySubjectId(
|
|
4300
|
+
grant: AccessGrant,
|
|
4301
|
+
authorization: ResolvedSessionAuthorization | null,
|
|
4302
|
+
): string | null {
|
|
4303
|
+
if (!authorization) return grant.principalKind === "agent_attempt" ? null : grant.subjectId;
|
|
4304
|
+
return authorization.actor.kind === "agent_attempt"
|
|
4305
|
+
? authorization.actor.initiatingHumanSubjectId
|
|
4306
|
+
: authorization.actor.subjectId;
|
|
4307
|
+
}
|
|
4308
|
+
|
|
4309
|
+
/** The batch authority query intentionally omits every unauthorized file. Any
|
|
4310
|
+
* omission therefore fails the whole upload before an object-storage URL is
|
|
4311
|
+
* minted, including mixed ordinary/Drive mappings and partially authorized
|
|
4312
|
+
* batches. */
|
|
4313
|
+
export function requireAuthorizedBrowserUploadFiles(
|
|
4314
|
+
workspaceFileIds: readonly string[],
|
|
4315
|
+
authorizedFiles: readonly FileAsset[],
|
|
4316
|
+
): FileAsset[] {
|
|
4317
|
+
const byId = new Map(authorizedFiles.map((file) => [file.id, file]));
|
|
4318
|
+
return workspaceFileIds.map((fileId) => {
|
|
4319
|
+
const file = byId.get(fileId);
|
|
4320
|
+
if (!file) throw new HTTPException(404, { message: "workspace file not found" });
|
|
4321
|
+
if (file.status !== "ready") {
|
|
4322
|
+
throw new HTTPException(409, { message: "workspace file is not ready" });
|
|
4323
|
+
}
|
|
4324
|
+
return file;
|
|
4325
|
+
});
|
|
4326
|
+
}
|
|
4327
|
+
|
|
4288
4328
|
function browserUploadFilename(value: string): string {
|
|
4289
4329
|
const normalized = value
|
|
4290
4330
|
.trim()
|
|
@@ -557,6 +557,7 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
557
557
|
...(c.req.query("code") ? { code: c.req.query("code") } : {}),
|
|
558
558
|
...(c.req.query("state") ? { state: c.req.query("state") } : {}),
|
|
559
559
|
...(c.req.query("error") ? { error: c.req.query("error") } : {}),
|
|
560
|
+
...(c.req.query("picked_file_ids") ? { pickedFileIds: c.req.query("picked_file_ids") } : {}),
|
|
560
561
|
requestUrl: c.req.url,
|
|
561
562
|
};
|
|
562
563
|
const result = await completeGoogleDriveOAuthCallback(deps, input);
|
package/src/routes/files.ts
CHANGED
|
@@ -24,9 +24,10 @@ import {
|
|
|
24
24
|
getFileUpload,
|
|
25
25
|
getGeneratedImageArtifact,
|
|
26
26
|
getGeneratedVideoArtifact,
|
|
27
|
+
getFilesForSubject,
|
|
27
28
|
getRetainedFileArtifact,
|
|
28
29
|
getRetainedScreenshotArtifact,
|
|
29
|
-
|
|
30
|
+
requireFileForSubject,
|
|
30
31
|
type RetainedFileArtifact,
|
|
31
32
|
type GeneratedImageArtifact,
|
|
32
33
|
type GeneratedVideoArtifact,
|
|
@@ -34,7 +35,11 @@ import {
|
|
|
34
35
|
} from "@opengeni/db";
|
|
35
36
|
import type { Context, Hono } from "hono";
|
|
36
37
|
import { HTTPException } from "hono/http-exception";
|
|
37
|
-
import {
|
|
38
|
+
import {
|
|
39
|
+
requireAccessGrant,
|
|
40
|
+
requireLiveAgentAttemptAuthorization,
|
|
41
|
+
SessionAuthorizationDeniedError,
|
|
42
|
+
} from "@opengeni/core";
|
|
38
43
|
import { recordWorkspaceUsage, requireLimit } from "@opengeni/core";
|
|
39
44
|
import type { ApiRouteDeps } from "@opengeni/core";
|
|
40
45
|
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
@@ -265,8 +270,13 @@ export function registerFileRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
265
270
|
|
|
266
271
|
app.get("/v1/workspaces/:workspaceId/files/:fileId", async (c) => {
|
|
267
272
|
const workspaceId = c.req.param("workspaceId");
|
|
268
|
-
await requireAccessGrant(c, deps, workspaceId, "files:read");
|
|
269
|
-
const file = await
|
|
273
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "files:read");
|
|
274
|
+
const file = await requireFileForSubject(db, {
|
|
275
|
+
accountId: grant.accountId,
|
|
276
|
+
workspaceId,
|
|
277
|
+
subjectId: grant.subjectId,
|
|
278
|
+
fileId: c.req.param("fileId"),
|
|
279
|
+
}).catch(() => null);
|
|
270
280
|
if (!file) {
|
|
271
281
|
throw new HTTPException(404, { message: "file not found" });
|
|
272
282
|
}
|
|
@@ -275,9 +285,14 @@ export function registerFileRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
275
285
|
|
|
276
286
|
app.get("/v1/workspaces/:workspaceId/artifacts/:artifactId", async (c) => {
|
|
277
287
|
const workspaceId = c.req.param("workspaceId");
|
|
278
|
-
await requireAccessGrant(c, deps, workspaceId, "files:read");
|
|
288
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "files:read");
|
|
279
289
|
const artifactId = retainedArtifactId(c.req.param("artifactId"));
|
|
280
|
-
const artifact = await getWorkspaceArtifact(db,
|
|
290
|
+
const artifact = await getWorkspaceArtifact(db, {
|
|
291
|
+
accountId: grant.accountId,
|
|
292
|
+
workspaceId,
|
|
293
|
+
subjectId: await fileAuthoritySubjectIdForGrant(deps, grant),
|
|
294
|
+
artifactId,
|
|
295
|
+
});
|
|
281
296
|
if (!artifact) {
|
|
282
297
|
return c.json(retainedArtifactUnavailable(artifactId, "deleted"), 404);
|
|
283
298
|
}
|
|
@@ -286,9 +301,14 @@ export function registerFileRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
286
301
|
|
|
287
302
|
app.get("/v1/workspaces/:workspaceId/artifacts/:artifactId/content", async (c) => {
|
|
288
303
|
const workspaceId = c.req.param("workspaceId");
|
|
289
|
-
await requireAccessGrant(c, deps, workspaceId, "files:read");
|
|
304
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "files:read");
|
|
290
305
|
const artifactId = retainedArtifactId(c.req.param("artifactId"));
|
|
291
|
-
const artifact = await getWorkspaceArtifact(db,
|
|
306
|
+
const artifact = await getWorkspaceArtifact(db, {
|
|
307
|
+
accountId: grant.accountId,
|
|
308
|
+
workspaceId,
|
|
309
|
+
subjectId: await fileAuthoritySubjectIdForGrant(deps, grant),
|
|
310
|
+
artifactId,
|
|
311
|
+
});
|
|
292
312
|
if (!artifact) {
|
|
293
313
|
return c.json(retainedArtifactUnavailable(artifactId, "deleted"), 404);
|
|
294
314
|
}
|
|
@@ -378,13 +398,18 @@ export function registerFileRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
378
398
|
|
|
379
399
|
app.post("/v1/workspaces/:workspaceId/files/:fileId/download-url", async (c) => {
|
|
380
400
|
const workspaceId = c.req.param("workspaceId");
|
|
381
|
-
await requireAccessGrant(c, deps, workspaceId, "files:read");
|
|
401
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "files:read");
|
|
382
402
|
if (!objectStorage) {
|
|
383
403
|
throw new HTTPException(503, {
|
|
384
404
|
message: "object storage is not configured",
|
|
385
405
|
});
|
|
386
406
|
}
|
|
387
|
-
const file = await
|
|
407
|
+
const file = await requireFileForSubject(db, {
|
|
408
|
+
accountId: grant.accountId,
|
|
409
|
+
workspaceId,
|
|
410
|
+
subjectId: grant.subjectId,
|
|
411
|
+
fileId: c.req.param("fileId"),
|
|
412
|
+
}).catch(() => null);
|
|
388
413
|
if (!file) {
|
|
389
414
|
throw new HTTPException(404, { message: "file not found" });
|
|
390
415
|
}
|
|
@@ -537,14 +562,18 @@ function retainedArtifactMetadata(artifact: RetainedFileArtifact): RetainedArtif
|
|
|
537
562
|
|
|
538
563
|
async function getWorkspaceArtifact(
|
|
539
564
|
db: ApiRouteDeps["db"],
|
|
540
|
-
|
|
541
|
-
|
|
565
|
+
input: {
|
|
566
|
+
accountId: string;
|
|
567
|
+
workspaceId: string;
|
|
568
|
+
subjectId: string | null;
|
|
569
|
+
artifactId: string;
|
|
570
|
+
},
|
|
542
571
|
): Promise<{ file: RetainedFileArtifact["file"]; metadata: RetainedArtifactMetadata } | null> {
|
|
543
|
-
const generated = await getGeneratedImageArtifact(db, workspaceId, artifactId);
|
|
572
|
+
const generated = await getGeneratedImageArtifact(db, input.workspaceId, input.artifactId);
|
|
544
573
|
if (generated) {
|
|
545
574
|
return { file: generated.file, metadata: retainedGeneratedImageMetadata(generated) };
|
|
546
575
|
}
|
|
547
|
-
const generatedVideo = await getGeneratedVideoArtifact(db, workspaceId, artifactId);
|
|
576
|
+
const generatedVideo = await getGeneratedVideoArtifact(db, input.workspaceId, input.artifactId);
|
|
548
577
|
if (generatedVideo && !generatedVideo.artifact.deletedAt) {
|
|
549
578
|
const file = generatedVideoFileAsset(generatedVideo.file);
|
|
550
579
|
return {
|
|
@@ -555,8 +584,43 @@ async function getWorkspaceArtifact(
|
|
|
555
584
|
}),
|
|
556
585
|
};
|
|
557
586
|
}
|
|
558
|
-
const artifact = await getRetainedFileArtifact(db, workspaceId, artifactId);
|
|
559
|
-
|
|
587
|
+
const artifact = await getRetainedFileArtifact(db, input.workspaceId, input.artifactId);
|
|
588
|
+
if (!artifact) return null;
|
|
589
|
+
const [authorizedFile] = await getFilesForSubject(db, {
|
|
590
|
+
accountId: input.accountId,
|
|
591
|
+
workspaceId: input.workspaceId,
|
|
592
|
+
subjectId: input.subjectId,
|
|
593
|
+
fileIds: [artifact.file.id],
|
|
594
|
+
});
|
|
595
|
+
if (!authorizedFile) return null;
|
|
596
|
+
const authorizedArtifact = { ...artifact, file: authorizedFile };
|
|
597
|
+
return {
|
|
598
|
+
file: authorizedFile,
|
|
599
|
+
metadata: retainedArtifactMetadata(authorizedArtifact),
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/** Human and stable service grants already carry their immutable subject.
|
|
604
|
+
* Agent tokens carry only a technical worker subject, so resolve the exact
|
|
605
|
+
* live attempt and use the initiating human frozen on its durable turn. */
|
|
606
|
+
export async function fileAuthoritySubjectIdForGrant(
|
|
607
|
+
deps: Pick<ApiRouteDeps, "db">,
|
|
608
|
+
grant: import("@opengeni/contracts").AccessGrant,
|
|
609
|
+
): Promise<string | null> {
|
|
610
|
+
if (grant.principalKind !== "agent_attempt") return grant.subjectId;
|
|
611
|
+
const sessionId = grant.metadata?.["sessionId"];
|
|
612
|
+
if (typeof sessionId !== "string") {
|
|
613
|
+
throw new HTTPException(404, { message: "file not found" });
|
|
614
|
+
}
|
|
615
|
+
try {
|
|
616
|
+
const actor = await requireLiveAgentAttemptAuthorization(deps.db, grant, sessionId);
|
|
617
|
+
return actor.initiatingHumanSubjectId;
|
|
618
|
+
} catch (error) {
|
|
619
|
+
if (error instanceof SessionAuthorizationDeniedError) {
|
|
620
|
+
throw new HTTPException(404, { message: "file not found", cause: error });
|
|
621
|
+
}
|
|
622
|
+
throw error;
|
|
623
|
+
}
|
|
560
624
|
}
|
|
561
625
|
|
|
562
626
|
function generatedVideoFileAsset(
|
package/src/routes/sessions.ts
CHANGED
|
@@ -2257,7 +2257,7 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
2257
2257
|
const result = await acceptSessionUserMessage(deps, grant, workspaceId, sessionId, {
|
|
2258
2258
|
text: payload.text,
|
|
2259
2259
|
annotations: payload.annotations,
|
|
2260
|
-
|
|
2260
|
+
modelContext: payload.modelContext ?? null,
|
|
2261
2261
|
resources: payload.resources,
|
|
2262
2262
|
model: payload.model ?? null,
|
|
2263
2263
|
reasoningEffort: payload.reasoningEffort ?? null,
|
|
@@ -2300,7 +2300,7 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
2300
2300
|
const { accepted } = await acceptSessionUserMessage(deps, grant, workspaceId, sessionId, {
|
|
2301
2301
|
text: event.payload.text,
|
|
2302
2302
|
annotations: event.payload.annotations,
|
|
2303
|
-
|
|
2303
|
+
modelContext: event.payload.modelContext ?? null,
|
|
2304
2304
|
resources: event.payload.resources ?? [],
|
|
2305
2305
|
model: event.payload.model ?? null,
|
|
2306
2306
|
reasoningEffort: event.payload.reasoningEffort ?? null,
|