@opengeni/api-router 0.30.1 → 2.1.0-canary.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.
- package/dist/api-websocket.d.ts +18 -0
- package/dist/app.js +1 -1
- package/dist/auth/managed-auth.d.ts +6 -0
- package/dist/{chunk-2PQMSRCB.js → chunk-QKDFBBUE.js} +29110 -18137
- package/dist/chunk-QKDFBBUE.js.map +1 -0
- package/dist/codemode.d.ts +8 -1
- package/dist/company-brain-okf.d.ts +24 -0
- package/dist/connected-machine-computer-access.d.ts +11 -0
- package/dist/connection-authority-owner.d.ts +59 -0
- package/dist/connection-ownership.d.ts +47 -0
- package/dist/controller-data-plane.d.ts +16 -0
- package/dist/editable-artifact-office-import.d.ts +7 -4
- package/dist/editable-artifact-websocket.d.ts +5 -15
- package/dist/editable-artifact-workspace-files.d.ts +1 -1
- package/dist/http/api-error.d.ts +10 -0
- package/dist/http/cors.d.ts +5 -0
- package/dist/http/interaction-control-error.d.ts +13 -0
- package/dist/http/sse.d.ts +2 -3
- package/dist/index.d.ts +5 -2
- package/dist/index.js +364 -86
- package/dist/index.js.map +1 -1
- package/dist/integrations/atlassian.d.ts +17 -0
- package/dist/integrations/fiken.d.ts +7 -2
- package/dist/integrations/google-drive.d.ts +19 -0
- package/dist/integrations/oauth-client.d.ts +22 -3
- package/dist/integrations/oauth-profiles.d.ts +195 -0
- package/dist/integrations/personal-github-repositories.d.ts +45 -0
- package/dist/integrations/personal-github.d.ts +40 -0
- package/dist/integrations/pr-review-provider.d.ts +22 -0
- package/dist/integrations/provider-oauth.d.ts +8 -0
- package/dist/integrations/slack-app-home.d.ts +24 -0
- package/dist/integrations/slack-bot.d.ts +149 -9
- package/dist/integrations/slack-interactions.d.ts +10 -4
- package/dist/integrations/slack-routing.d.ts +120 -0
- package/dist/integrations/social-oauth.d.ts +5 -0
- package/dist/interaction-frame-proxy.d.ts +68 -0
- package/dist/mcp/company-brain-governed-writes.d.ts +26 -0
- package/dist/mcp/company-profile-agent-admin.d.ts +44 -0
- package/dist/mcp/documents.d.ts +1 -0
- package/dist/mcp/remember.d.ts +28 -0
- package/dist/mcp/request-abort.d.ts +19 -0
- package/dist/mcp/scheduled-task-view.d.ts +4 -4
- package/dist/mcp/server.d.ts +15 -2
- package/dist/mcp/session-view.d.ts +4 -0
- package/dist/mcp/session-wait.d.ts +137 -0
- package/dist/routes/automations.d.ts +4 -0
- package/dist/routes/billing.d.ts +5 -0
- package/dist/routes/browser-sessions.d.ts +17 -3
- package/dist/routes/company-brain.d.ts +3 -0
- package/dist/routes/company-profile.d.ts +2 -1
- package/dist/routes/connection-authorities.d.ts +3 -0
- package/dist/routes/files.d.ts +4 -0
- package/dist/routes/personal-github-git-broker.d.ts +29 -0
- package/dist/routes/personal-github.d.ts +3 -0
- package/dist/routes/pr-review.d.ts +3 -0
- package/dist/routes/sessions.d.ts +1 -0
- package/dist/routes/user-resource-authorities.d.ts +3 -0
- package/dist/routes/workspace-capture.d.ts +10 -2
- package/dist/routes/workspace-learning.d.ts +3 -0
- package/dist/routes/workspaces.d.ts +19 -0
- package/dist/sandbox/channel-a.d.ts +11 -1
- package/dist/sandbox/connection-authority.d.ts +3 -0
- package/dist/sandbox/enrollment.d.ts +2 -0
- package/dist/sandbox/machines.d.ts +2 -2
- package/dist/sandbox/metrics-ingestion.d.ts +10 -20
- package/dist/sandbox/viewer.d.ts +24 -1
- package/dist/sandbox-file-artifacts.d.ts +11 -0
- package/dist/scheduled-task-deletion.d.ts +14 -0
- package/dist/slack-reaction-files.d.ts +1 -1
- package/dist/temporal-schedule-cleanup.d.ts +1 -0
- package/package.json +20 -18
- package/src/api-websocket.ts +23 -0
- package/src/app.ts +160 -27
- package/src/auth/managed-auth.ts +25 -3
- package/src/codemode.ts +53 -23
- package/src/codex-realtime.ts +8 -2
- package/src/company-brain-okf.ts +340 -0
- package/src/connected-machine-computer-access.ts +33 -0
- package/src/connection-authority-owner.ts +61 -0
- package/src/connection-ownership.ts +180 -0
- package/src/controller-data-plane.ts +47 -0
- package/src/editable-artifact-native-kernel.ts +26 -15
- package/src/editable-artifact-office-import.ts +77 -9
- package/src/editable-artifact-production.ts +13 -7
- package/src/editable-artifact-websocket.ts +11 -18
- package/src/editable-artifact-workspace-files.ts +31 -6
- package/src/http/api-error.ts +28 -0
- package/src/http/auth.ts +4 -0
- package/src/http/cors.ts +35 -0
- package/src/http/interaction-control-error.ts +164 -0
- package/src/http/sse.ts +112 -66
- package/src/index.ts +46 -6
- package/src/integrations/atlassian.ts +146 -35
- package/src/integrations/fiken.ts +102 -22
- package/src/integrations/google-drive.ts +416 -101
- package/src/integrations/oauth-client.ts +272 -143
- package/src/integrations/oauth-profiles.ts +477 -0
- package/src/integrations/personal-github-repositories.ts +445 -0
- package/src/integrations/personal-github.ts +705 -0
- package/src/integrations/pr-review-provider.ts +246 -0
- package/src/integrations/provider-oauth.ts +121 -5
- package/src/integrations/slack-app-home.ts +300 -0
- package/src/integrations/slack-bot.ts +337 -57
- package/src/integrations/slack-interactions.ts +1274 -214
- package/src/integrations/slack-routing.ts +324 -0
- package/src/integrations/social-oauth.ts +25 -0
- package/src/interaction-frame-proxy.ts +409 -0
- package/src/mcp/company-brain-governed-writes.ts +262 -0
- package/src/mcp/company-profile-agent-admin.ts +205 -0
- package/src/mcp/documents.ts +37 -6
- package/src/mcp/files.ts +22 -2
- package/src/mcp/remember.ts +181 -0
- package/src/mcp/request-abort.ts +44 -0
- package/src/mcp/scheduled-task-view.ts +1 -0
- package/src/mcp/server.ts +719 -369
- package/src/mcp/session-view.ts +54 -0
- package/src/mcp/session-wait.ts +554 -0
- package/src/model-catalog.ts +20 -12
- package/src/routes/api-integrations.ts +4 -0
- package/src/routes/api-keys.ts +1 -0
- package/src/routes/automations.ts +534 -0
- package/src/routes/billing.ts +57 -1
- package/src/routes/browser-sessions.ts +388 -98
- package/src/routes/channels.ts +17 -1
- package/src/routes/codex.ts +50 -0
- package/src/routes/company-brain.ts +367 -0
- package/src/routes/company-profile.ts +1 -1
- package/src/routes/computer-sessions.ts +320 -93
- package/src/routes/connection-authorities.ts +139 -0
- package/src/routes/connections.ts +145 -34
- package/src/routes/documents.ts +221 -3
- package/src/routes/editable-artifacts.ts +11 -3
- package/src/routes/enrollments.ts +116 -26
- package/src/routes/environments.ts +127 -55
- package/src/routes/files.ts +152 -27
- package/src/routes/install.ts +82 -28
- package/src/routes/integration-facets.ts +29 -0
- package/src/routes/interaction-resources.ts +20 -2
- package/src/routes/machines.ts +275 -13
- package/src/routes/organization-memberships.ts +717 -28
- package/src/routes/packs.ts +4 -3
- package/src/routes/personal-github-git-broker.ts +785 -0
- package/src/routes/personal-github.ts +319 -0
- package/src/routes/pr-review.ts +531 -0
- package/src/routes/rigs.ts +104 -39
- package/src/routes/scheduled-tasks.ts +19 -21
- package/src/routes/sessions.ts +777 -40
- package/src/routes/social.ts +14 -2
- package/src/routes/supergrok.ts +23 -4
- package/src/routes/transcription-recordings.ts +8 -2
- package/src/routes/user-resource-authorities.ts +138 -0
- package/src/routes/workspace-artifacts.ts +4 -1
- package/src/routes/workspace-capture.ts +21 -0
- package/src/routes/workspace-learning.ts +228 -0
- package/src/routes/workspaces.ts +59 -6
- package/src/sandbox/auth-callout.ts +38 -12
- package/src/sandbox/channel-a.ts +242 -19
- package/src/sandbox/connection-authority.ts +3 -0
- package/src/sandbox/enrollment.ts +15 -3
- package/src/sandbox/machines.ts +186 -64
- package/src/sandbox/metrics-ingestion.ts +220 -58
- package/src/sandbox/viewer.ts +243 -19
- package/src/sandbox-file-artifacts.ts +329 -0
- package/src/scheduled-task-deletion.ts +127 -0
- package/src/slack-reaction-files.ts +16 -5
- package/src/temporal-schedule-cleanup.ts +13 -0
- package/dist/chunk-2PQMSRCB.js.map +0 -1
|
@@ -5,8 +5,6 @@ import {
|
|
|
5
5
|
VariableSetVariableName,
|
|
6
6
|
} from "@opengeni/contracts";
|
|
7
7
|
import {
|
|
8
|
-
countActiveSessionsUsingVariableSet,
|
|
9
|
-
countScheduledTasksUsingVariableSet,
|
|
10
8
|
countVariableSets,
|
|
11
9
|
createVariableSet,
|
|
12
10
|
decryptVariableSetValue,
|
|
@@ -18,10 +16,16 @@ import {
|
|
|
18
16
|
readVariableSetSecretAtomically,
|
|
19
17
|
setVariableSetVariable,
|
|
20
18
|
updateVariableSet,
|
|
19
|
+
VariableSetAttachedError,
|
|
21
20
|
} from "@opengeni/db";
|
|
22
21
|
import type { Hono } from "hono";
|
|
23
22
|
import { HTTPException } from "hono/http-exception";
|
|
24
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
requireAccessGrant,
|
|
25
|
+
requireAccessGrantAuthorization,
|
|
26
|
+
requireLiteralPermission,
|
|
27
|
+
requirePermission,
|
|
28
|
+
} from "@opengeni/core";
|
|
25
29
|
import type { ApiRouteDeps } from "@opengeni/core";
|
|
26
30
|
import {
|
|
27
31
|
assertAllowedVariableSetVariableName,
|
|
@@ -45,15 +49,35 @@ export function registerVariableSetRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
45
49
|
const grant = await requireAccessGrant(c, deps, workspaceId);
|
|
46
50
|
requirePermission(grant, "variable-sets:list");
|
|
47
51
|
requirePermission(grant, "secrets:list");
|
|
48
|
-
return c.json(
|
|
52
|
+
return c.json(
|
|
53
|
+
await listVariableSets(db, {
|
|
54
|
+
accountId: grant.accountId,
|
|
55
|
+
workspaceId,
|
|
56
|
+
subjectId: grant.subjectId,
|
|
57
|
+
}),
|
|
58
|
+
);
|
|
49
59
|
});
|
|
50
60
|
|
|
51
61
|
app.post(`${prefix}`, async (c) => {
|
|
52
62
|
const workspaceId = c.req.param("workspaceId")!;
|
|
53
|
-
const
|
|
63
|
+
const authorization = await requireAccessGrantAuthorization(c, deps, workspaceId);
|
|
64
|
+
const grant = authorization.grant;
|
|
54
65
|
requirePermission(grant, "variable-sets:write");
|
|
55
66
|
const key = requireVariableSetEncryption(settings);
|
|
56
67
|
const payload = CreateVariableSetRequest.parse(await c.req.json());
|
|
68
|
+
if (prefix.endsWith("/environments") && payload.scope !== "workspace") {
|
|
69
|
+
throw new HTTPException(422, {
|
|
70
|
+
message: "the legacy environments route only creates workspace-scoped variable sets",
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
if (
|
|
74
|
+
payload.scope === "organization" &&
|
|
75
|
+
authorization.accountGrant?.permissions.includes("account:admin") !== true
|
|
76
|
+
) {
|
|
77
|
+
throw new HTTPException(403, {
|
|
78
|
+
message: "missing permission: account:admin",
|
|
79
|
+
});
|
|
80
|
+
}
|
|
57
81
|
if (payload.variables.length > 0) {
|
|
58
82
|
requirePermission(grant, "secrets:write");
|
|
59
83
|
}
|
|
@@ -73,12 +97,17 @@ export function registerVariableSetRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
73
97
|
}
|
|
74
98
|
variableNames.add(variable.name);
|
|
75
99
|
}
|
|
76
|
-
|
|
100
|
+
const access = {
|
|
101
|
+
accountId: grant.accountId,
|
|
102
|
+
workspaceId,
|
|
103
|
+
subjectId: grant.subjectId,
|
|
104
|
+
};
|
|
105
|
+
if ((await countVariableSets(db, access, payload.scope)) >= MAX_ENVIRONMENTS_PER_WORKSPACE) {
|
|
77
106
|
throw new HTTPException(422, {
|
|
78
107
|
message: `a workspace supports at most ${MAX_ENVIRONMENTS_PER_WORKSPACE} variable sets`,
|
|
79
108
|
});
|
|
80
109
|
}
|
|
81
|
-
if (await getVariableSetByName(db,
|
|
110
|
+
if (await getVariableSetByName(db, access, name, payload.scope)) {
|
|
82
111
|
throw new HTTPException(409, {
|
|
83
112
|
message: `variable set name is already in use: ${name}`,
|
|
84
113
|
});
|
|
@@ -88,6 +117,9 @@ export function registerVariableSetRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
88
117
|
const created = await createVariableSet(db, {
|
|
89
118
|
accountId: grant.accountId,
|
|
90
119
|
workspaceId,
|
|
120
|
+
scope: payload.scope,
|
|
121
|
+
subjectId: grant.subjectId,
|
|
122
|
+
allowOrganization: payload.scope === "organization",
|
|
91
123
|
name,
|
|
92
124
|
description: payload.description ?? null,
|
|
93
125
|
variables: payload.variables.map((variable) => ({
|
|
@@ -108,7 +140,7 @@ export function registerVariableSetRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
108
140
|
const grant = await requireAccessGrant(c, deps, workspaceId);
|
|
109
141
|
requirePermission(grant, "variable-sets:read");
|
|
110
142
|
requirePermission(grant, "secrets:list");
|
|
111
|
-
return c.json(await requireVariableSetForApi(db,
|
|
143
|
+
return c.json(await requireVariableSetForApi(db, grant, c.req.param("variableSetId")!));
|
|
112
144
|
});
|
|
113
145
|
|
|
114
146
|
if (prefix.endsWith("/variable-sets")) {
|
|
@@ -139,27 +171,47 @@ export function registerVariableSetRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
139
171
|
|
|
140
172
|
app.patch(`${prefix}/:variableSetId`, async (c) => {
|
|
141
173
|
const workspaceId = c.req.param("workspaceId")!;
|
|
142
|
-
const
|
|
174
|
+
const authorization = await requireAccessGrantAuthorization(c, deps, workspaceId);
|
|
175
|
+
const grant = authorization.grant;
|
|
143
176
|
requirePermission(grant, "variable-sets:write");
|
|
144
|
-
const variableSet = await requireVariableSetForApi(
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
)
|
|
177
|
+
const variableSet = await requireVariableSetForApi(db, grant, c.req.param("variableSetId")!);
|
|
178
|
+
const allowOrganization =
|
|
179
|
+
variableSet.scope === "organization" &&
|
|
180
|
+
authorization.accountGrant?.permissions.includes("account:admin") === true;
|
|
181
|
+
if (variableSet.scope === "organization" && !allowOrganization) {
|
|
182
|
+
throw new HTTPException(403, {
|
|
183
|
+
message: "missing permission: account:admin",
|
|
184
|
+
});
|
|
185
|
+
}
|
|
149
186
|
const payload = UpdateVariableSetRequest.parse(await c.req.json());
|
|
150
187
|
const name = payload.name !== undefined ? trimmedVariableSetName(payload.name) : undefined;
|
|
151
188
|
if (name !== undefined && name !== variableSet.name) {
|
|
152
|
-
const existing = await getVariableSetByName(
|
|
189
|
+
const existing = await getVariableSetByName(
|
|
190
|
+
db,
|
|
191
|
+
{
|
|
192
|
+
accountId: grant.accountId,
|
|
193
|
+
workspaceId,
|
|
194
|
+
subjectId: grant.subjectId,
|
|
195
|
+
},
|
|
196
|
+
name,
|
|
197
|
+
variableSet.scope,
|
|
198
|
+
);
|
|
153
199
|
if (existing && existing.id !== variableSet.id) {
|
|
154
200
|
throw new HTTPException(409, {
|
|
155
201
|
message: `variable set name is already in use: ${name}`,
|
|
156
202
|
});
|
|
157
203
|
}
|
|
158
204
|
}
|
|
159
|
-
const updated = await updateVariableSet(
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
205
|
+
const updated = await updateVariableSet(
|
|
206
|
+
db,
|
|
207
|
+
{ accountId: grant.accountId, workspaceId, subjectId: grant.subjectId },
|
|
208
|
+
variableSet.id,
|
|
209
|
+
{
|
|
210
|
+
...(name !== undefined ? { name } : {}),
|
|
211
|
+
...(payload.description !== undefined ? { description: payload.description } : {}),
|
|
212
|
+
allowOrganization,
|
|
213
|
+
},
|
|
214
|
+
);
|
|
163
215
|
await recordVariableSetAuditEvent(db, {
|
|
164
216
|
grant,
|
|
165
217
|
action: "variable_set.updated",
|
|
@@ -170,35 +222,36 @@ export function registerVariableSetRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
170
222
|
|
|
171
223
|
app.delete(`${prefix}/:variableSetId`, async (c) => {
|
|
172
224
|
const workspaceId = c.req.param("workspaceId")!;
|
|
173
|
-
const
|
|
225
|
+
const authorization = await requireAccessGrantAuthorization(c, deps, workspaceId);
|
|
226
|
+
const grant = authorization.grant;
|
|
174
227
|
requirePermission(grant, "variable-sets:write");
|
|
175
228
|
requirePermission(grant, "secrets:write");
|
|
176
|
-
const variableSet = await requireVariableSetForApi(
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
)
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
workspaceId,
|
|
184
|
-
variableSet.id,
|
|
185
|
-
);
|
|
186
|
-
if (attachedTasks > 0) {
|
|
187
|
-
throw new HTTPException(409, {
|
|
188
|
-
message: `variable set is attached to ${attachedTasks} scheduled task(s); detach first`,
|
|
229
|
+
const variableSet = await requireVariableSetForApi(db, grant, c.req.param("variableSetId")!);
|
|
230
|
+
const allowOrganization =
|
|
231
|
+
variableSet.scope === "organization" &&
|
|
232
|
+
authorization.accountGrant?.permissions.includes("account:admin") === true;
|
|
233
|
+
if (variableSet.scope === "organization" && !allowOrganization) {
|
|
234
|
+
throw new HTTPException(403, {
|
|
235
|
+
message: "missing permission: account:admin",
|
|
189
236
|
});
|
|
190
237
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
238
|
+
try {
|
|
239
|
+
await deleteVariableSet(
|
|
240
|
+
db,
|
|
241
|
+
{
|
|
242
|
+
accountId: grant.accountId,
|
|
243
|
+
workspaceId,
|
|
244
|
+
subjectId: grant.subjectId,
|
|
245
|
+
},
|
|
246
|
+
variableSet.id,
|
|
247
|
+
{ allowOrganization },
|
|
248
|
+
);
|
|
249
|
+
} catch (error) {
|
|
250
|
+
if (error instanceof VariableSetAttachedError) {
|
|
251
|
+
throw new HTTPException(409, { message: error.message });
|
|
252
|
+
}
|
|
253
|
+
throw error;
|
|
200
254
|
}
|
|
201
|
-
await deleteVariableSet(db, workspaceId, variableSet.id);
|
|
202
255
|
await recordVariableSetAuditEvent(db, {
|
|
203
256
|
grant,
|
|
204
257
|
action: "variable_set.deleted",
|
|
@@ -209,16 +262,21 @@ export function registerVariableSetRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
209
262
|
|
|
210
263
|
app.put(`${prefix}/:variableSetId/variables/:name`, async (c) => {
|
|
211
264
|
const workspaceId = c.req.param("workspaceId")!;
|
|
212
|
-
const
|
|
265
|
+
const authorization = await requireAccessGrantAuthorization(c, deps, workspaceId);
|
|
266
|
+
const grant = authorization.grant;
|
|
213
267
|
requirePermission(grant, "variable-sets:write");
|
|
214
268
|
requirePermission(grant, "secrets:write");
|
|
215
269
|
const key = requireVariableSetEncryption(settings);
|
|
216
270
|
const name = parseVariableName(c.req.param("name")!);
|
|
217
|
-
const variableSet = await requireVariableSetForApi(
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
)
|
|
271
|
+
const variableSet = await requireVariableSetForApi(db, grant, c.req.param("variableSetId")!);
|
|
272
|
+
const allowOrganization =
|
|
273
|
+
variableSet.scope === "organization" &&
|
|
274
|
+
authorization.accountGrant?.permissions.includes("account:admin") === true;
|
|
275
|
+
if (variableSet.scope === "organization" && !allowOrganization) {
|
|
276
|
+
throw new HTTPException(403, {
|
|
277
|
+
message: "missing permission: account:admin",
|
|
278
|
+
});
|
|
279
|
+
}
|
|
222
280
|
const payload = SetVariableSetVariableRequest.parse(await c.req.json());
|
|
223
281
|
const exists = variableSet.variables.some((variable) => variable.name === name);
|
|
224
282
|
if (!exists && variableSet.variables.length >= MAX_VARIABLES_PER_ENVIRONMENT) {
|
|
@@ -229,9 +287,11 @@ export function registerVariableSetRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
229
287
|
const metadata = await setVariableSetVariable(db, {
|
|
230
288
|
accountId: grant.accountId,
|
|
231
289
|
workspaceId,
|
|
290
|
+
subjectId: grant.subjectId,
|
|
232
291
|
variableSetId: variableSet.id,
|
|
233
292
|
name,
|
|
234
293
|
valueEncrypted: encryptVariableSetValue(key, payload.value),
|
|
294
|
+
allowOrganization,
|
|
235
295
|
});
|
|
236
296
|
await recordVariableSetAuditEvent(db, {
|
|
237
297
|
grant,
|
|
@@ -244,16 +304,28 @@ export function registerVariableSetRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
244
304
|
|
|
245
305
|
app.delete(`${prefix}/:variableSetId/variables/:name`, async (c) => {
|
|
246
306
|
const workspaceId = c.req.param("workspaceId")!;
|
|
247
|
-
const
|
|
307
|
+
const authorization = await requireAccessGrantAuthorization(c, deps, workspaceId);
|
|
308
|
+
const grant = authorization.grant;
|
|
248
309
|
requirePermission(grant, "variable-sets:write");
|
|
249
310
|
requirePermission(grant, "secrets:write");
|
|
250
311
|
const name = parseVariableName(c.req.param("name")!);
|
|
251
|
-
const variableSet = await requireVariableSetForApi(
|
|
252
|
-
|
|
312
|
+
const variableSet = await requireVariableSetForApi(db, grant, c.req.param("variableSetId")!);
|
|
313
|
+
const allowOrganization =
|
|
314
|
+
variableSet.scope === "organization" &&
|
|
315
|
+
authorization.accountGrant?.permissions.includes("account:admin") === true;
|
|
316
|
+
if (variableSet.scope === "organization" && !allowOrganization) {
|
|
317
|
+
throw new HTTPException(403, {
|
|
318
|
+
message: "missing permission: account:admin",
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
const deleted = await deleteVariableSetVariable(db, {
|
|
322
|
+
accountId: grant.accountId,
|
|
253
323
|
workspaceId,
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
324
|
+
subjectId: grant.subjectId,
|
|
325
|
+
variableSetId: variableSet.id,
|
|
326
|
+
name,
|
|
327
|
+
allowOrganization,
|
|
328
|
+
});
|
|
257
329
|
if (!deleted) {
|
|
258
330
|
throw new HTTPException(404, {
|
|
259
331
|
message: "variable set variable not found",
|
package/src/routes/files.ts
CHANGED
|
@@ -23,10 +23,12 @@ import {
|
|
|
23
23
|
createFileUpload,
|
|
24
24
|
getFileUpload,
|
|
25
25
|
getGeneratedImageArtifact,
|
|
26
|
+
recordAuditEvent,
|
|
26
27
|
getGeneratedVideoArtifact,
|
|
28
|
+
getFilesForSubject,
|
|
27
29
|
getRetainedFileArtifact,
|
|
28
30
|
getRetainedScreenshotArtifact,
|
|
29
|
-
|
|
31
|
+
requireFileForSubject,
|
|
30
32
|
type RetainedFileArtifact,
|
|
31
33
|
type GeneratedImageArtifact,
|
|
32
34
|
type GeneratedVideoArtifact,
|
|
@@ -34,9 +36,14 @@ import {
|
|
|
34
36
|
} from "@opengeni/db";
|
|
35
37
|
import type { Context, Hono } from "hono";
|
|
36
38
|
import { HTTPException } from "hono/http-exception";
|
|
37
|
-
import {
|
|
39
|
+
import {
|
|
40
|
+
requireAccessGrant,
|
|
41
|
+
requireLiveAgentAttemptAuthorization,
|
|
42
|
+
SessionAuthorizationDeniedError,
|
|
43
|
+
} from "@opengeni/core";
|
|
38
44
|
import { recordWorkspaceUsage, requireLimit } from "@opengeni/core";
|
|
39
45
|
import type { ApiRouteDeps } from "@opengeni/core";
|
|
46
|
+
import { retryWhileMissing } from "@opengeni/storage";
|
|
40
47
|
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
41
48
|
import { buildFilesMcpServer } from "../mcp/files";
|
|
42
49
|
|
|
@@ -95,6 +102,23 @@ export function registerFileRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
95
102
|
objectKey,
|
|
96
103
|
expiresAt: signed.expiresAt,
|
|
97
104
|
});
|
|
105
|
+
// Every principal-facing signed URL issuance is a
|
|
106
|
+
// metadata-only audit fact. Awaited before the URL leaves the platform:
|
|
107
|
+
// no recorded fact, no bearer capability. Never the URL or object key.
|
|
108
|
+
await recordAuditEvent(db, {
|
|
109
|
+
accountId: grant.accountId,
|
|
110
|
+
workspaceId,
|
|
111
|
+
subjectId: grant.subjectId,
|
|
112
|
+
action: "file.signed_upload.issued",
|
|
113
|
+
targetType: "workspace_file",
|
|
114
|
+
targetId: fileId,
|
|
115
|
+
metadata: {
|
|
116
|
+
fileId,
|
|
117
|
+
contentType: payload.contentType,
|
|
118
|
+
sizeBytes: payload.sizeBytes,
|
|
119
|
+
expiresAt: signed.expiresAt.toISOString(),
|
|
120
|
+
},
|
|
121
|
+
});
|
|
98
122
|
return c.json(
|
|
99
123
|
CreateFileUploadResponse.parse({
|
|
100
124
|
fileId: upload.file.id,
|
|
@@ -226,11 +250,13 @@ export function registerFileRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
226
250
|
const file = await rejectAndCleanObject(409, "file upload has expired", "expired");
|
|
227
251
|
return c.json(CompleteFileUploadResponse.parse({ file }));
|
|
228
252
|
}
|
|
229
|
-
const head = await
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
});
|
|
253
|
+
const head = await retryWhileMissing(async () => {
|
|
254
|
+
if (!(await objectStorage.fileExists(upload.file))) return null;
|
|
255
|
+
return await objectStorage.headFile(upload.file);
|
|
233
256
|
});
|
|
257
|
+
if (!head) {
|
|
258
|
+
throw new HTTPException(409, { message: "uploaded object is not available" });
|
|
259
|
+
}
|
|
234
260
|
if (Number(head.ContentLength ?? -1) !== upload.file.sizeBytes) {
|
|
235
261
|
const file = await rejectAndCleanObject(
|
|
236
262
|
422,
|
|
@@ -265,8 +291,13 @@ export function registerFileRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
265
291
|
|
|
266
292
|
app.get("/v1/workspaces/:workspaceId/files/:fileId", async (c) => {
|
|
267
293
|
const workspaceId = c.req.param("workspaceId");
|
|
268
|
-
await requireAccessGrant(c, deps, workspaceId, "files:read");
|
|
269
|
-
const file = await
|
|
294
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "files:read");
|
|
295
|
+
const file = await requireFileForSubject(db, {
|
|
296
|
+
accountId: grant.accountId,
|
|
297
|
+
workspaceId,
|
|
298
|
+
subjectId: grant.subjectId,
|
|
299
|
+
fileId: c.req.param("fileId"),
|
|
300
|
+
}).catch(() => null);
|
|
270
301
|
if (!file) {
|
|
271
302
|
throw new HTTPException(404, { message: "file not found" });
|
|
272
303
|
}
|
|
@@ -275,9 +306,14 @@ export function registerFileRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
275
306
|
|
|
276
307
|
app.get("/v1/workspaces/:workspaceId/artifacts/:artifactId", async (c) => {
|
|
277
308
|
const workspaceId = c.req.param("workspaceId");
|
|
278
|
-
await requireAccessGrant(c, deps, workspaceId, "files:read");
|
|
309
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "files:read");
|
|
279
310
|
const artifactId = retainedArtifactId(c.req.param("artifactId"));
|
|
280
|
-
const artifact = await getWorkspaceArtifact(db,
|
|
311
|
+
const artifact = await getWorkspaceArtifact(db, {
|
|
312
|
+
accountId: grant.accountId,
|
|
313
|
+
workspaceId,
|
|
314
|
+
subjectId: await fileAuthoritySubjectIdForGrant(deps, grant),
|
|
315
|
+
artifactId,
|
|
316
|
+
});
|
|
281
317
|
if (!artifact) {
|
|
282
318
|
return c.json(retainedArtifactUnavailable(artifactId, "deleted"), 404);
|
|
283
319
|
}
|
|
@@ -286,9 +322,14 @@ export function registerFileRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
286
322
|
|
|
287
323
|
app.get("/v1/workspaces/:workspaceId/artifacts/:artifactId/content", async (c) => {
|
|
288
324
|
const workspaceId = c.req.param("workspaceId");
|
|
289
|
-
await requireAccessGrant(c, deps, workspaceId, "files:read");
|
|
325
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "files:read");
|
|
290
326
|
const artifactId = retainedArtifactId(c.req.param("artifactId"));
|
|
291
|
-
const artifact = await getWorkspaceArtifact(db,
|
|
327
|
+
const artifact = await getWorkspaceArtifact(db, {
|
|
328
|
+
accountId: grant.accountId,
|
|
329
|
+
workspaceId,
|
|
330
|
+
subjectId: await fileAuthoritySubjectIdForGrant(deps, grant),
|
|
331
|
+
artifactId,
|
|
332
|
+
});
|
|
292
333
|
if (!artifact) {
|
|
293
334
|
return c.json(retainedArtifactUnavailable(artifactId, "deleted"), 404);
|
|
294
335
|
}
|
|
@@ -304,7 +345,7 @@ export function registerFileRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
304
345
|
|
|
305
346
|
app.post("/v1/workspaces/:workspaceId/artifacts/:artifactId/playback-source", async (c) => {
|
|
306
347
|
const workspaceId = c.req.param("workspaceId");
|
|
307
|
-
await requireAccessGrant(c, deps, workspaceId, "files:read");
|
|
348
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "files:read");
|
|
308
349
|
if (!objectStorage) {
|
|
309
350
|
throw new HTTPException(503, { message: "object storage is not configured" });
|
|
310
351
|
}
|
|
@@ -317,10 +358,26 @@ export function registerFileRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
317
358
|
throw new HTTPException(409, { message: `generated video is ${retained.file.status}` });
|
|
318
359
|
}
|
|
319
360
|
const file = generatedVideoFileAsset(retained.file);
|
|
320
|
-
|
|
361
|
+
const videoPresent = await retryWhileMissing(async () =>
|
|
362
|
+
(await objectStorage.fileExists(file)) ? true : null,
|
|
363
|
+
);
|
|
364
|
+
if (!videoPresent) {
|
|
321
365
|
throw new HTTPException(410, { message: "generated video bytes are unavailable" });
|
|
322
366
|
}
|
|
323
367
|
const signed = await objectStorage.createGetUrl({ key: file.objectKey });
|
|
368
|
+
await recordAuditEvent(db, {
|
|
369
|
+
accountId: grant.accountId,
|
|
370
|
+
workspaceId,
|
|
371
|
+
subjectId: grant.subjectId,
|
|
372
|
+
action: "file.signed_url.issued",
|
|
373
|
+
targetType: "retained_artifact",
|
|
374
|
+
targetId: artifactId,
|
|
375
|
+
metadata: {
|
|
376
|
+
artifactId,
|
|
377
|
+
kind: "video_playback",
|
|
378
|
+
expiresAt: signed.expiresAt.toISOString(),
|
|
379
|
+
},
|
|
380
|
+
});
|
|
324
381
|
return c.json(
|
|
325
382
|
VideoArtifactPlaybackSource.parse({
|
|
326
383
|
schemaVersion: 1,
|
|
@@ -378,20 +435,44 @@ export function registerFileRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
378
435
|
|
|
379
436
|
app.post("/v1/workspaces/:workspaceId/files/:fileId/download-url", async (c) => {
|
|
380
437
|
const workspaceId = c.req.param("workspaceId");
|
|
381
|
-
await requireAccessGrant(c, deps, workspaceId, "files:read");
|
|
438
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "files:read");
|
|
382
439
|
if (!objectStorage) {
|
|
383
440
|
throw new HTTPException(503, {
|
|
384
441
|
message: "object storage is not configured",
|
|
385
442
|
});
|
|
386
443
|
}
|
|
387
|
-
const file = await
|
|
444
|
+
const file = await requireFileForSubject(db, {
|
|
445
|
+
accountId: grant.accountId,
|
|
446
|
+
workspaceId,
|
|
447
|
+
subjectId: grant.subjectId,
|
|
448
|
+
fileId: c.req.param("fileId"),
|
|
449
|
+
}).catch(() => null);
|
|
388
450
|
if (!file) {
|
|
389
451
|
throw new HTTPException(404, { message: "file not found" });
|
|
390
452
|
}
|
|
391
453
|
if (file.status !== "ready") {
|
|
392
454
|
throw new HTTPException(409, { message: `file is ${file.status}` });
|
|
393
455
|
}
|
|
456
|
+
const present = await retryWhileMissing(async () =>
|
|
457
|
+
(await objectStorage.fileExists(file)) ? true : null,
|
|
458
|
+
);
|
|
459
|
+
if (!present) {
|
|
460
|
+
throw new HTTPException(410, { message: "file bytes are unavailable" });
|
|
461
|
+
}
|
|
394
462
|
const signed = await objectStorage.createGetUrl({ key: file.objectKey });
|
|
463
|
+
await recordAuditEvent(db, {
|
|
464
|
+
accountId: grant.accountId,
|
|
465
|
+
workspaceId,
|
|
466
|
+
subjectId: grant.subjectId,
|
|
467
|
+
action: "file.signed_url.issued",
|
|
468
|
+
targetType: "workspace_file",
|
|
469
|
+
targetId: file.id,
|
|
470
|
+
metadata: {
|
|
471
|
+
fileId: file.id,
|
|
472
|
+
kind: "download",
|
|
473
|
+
expiresAt: signed.expiresAt.toISOString(),
|
|
474
|
+
},
|
|
475
|
+
});
|
|
395
476
|
return c.json(
|
|
396
477
|
FileDownloadUrlResponse.parse({
|
|
397
478
|
url: signed.url,
|
|
@@ -455,16 +536,21 @@ async function serveRetainedArtifactContent(
|
|
|
455
536
|
...(range.contentRange ? { "Content-Range": range.contentRange } : {}),
|
|
456
537
|
};
|
|
457
538
|
if (range.kind === "empty") {
|
|
458
|
-
|
|
539
|
+
const present = await retryWhileMissing(async () =>
|
|
540
|
+
(await objectStorage.fileExists(file)) ? true : null,
|
|
541
|
+
);
|
|
542
|
+
if (!present) {
|
|
459
543
|
return c.json(retainedArtifactUnavailable(artifactId, "missing_storage"), 410);
|
|
460
544
|
}
|
|
461
545
|
return c.body(null, 200, headers);
|
|
462
546
|
}
|
|
463
547
|
|
|
464
|
-
const bytes = await
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
548
|
+
const bytes = await retryWhileMissing(async () =>
|
|
549
|
+
objectStorage.getFileRange(file, {
|
|
550
|
+
start: range.start,
|
|
551
|
+
end: range.end,
|
|
552
|
+
}),
|
|
553
|
+
);
|
|
468
554
|
if (!bytes) {
|
|
469
555
|
return c.json(retainedArtifactUnavailable(artifactId, "missing_storage"), 410);
|
|
470
556
|
}
|
|
@@ -537,14 +623,18 @@ function retainedArtifactMetadata(artifact: RetainedFileArtifact): RetainedArtif
|
|
|
537
623
|
|
|
538
624
|
async function getWorkspaceArtifact(
|
|
539
625
|
db: ApiRouteDeps["db"],
|
|
540
|
-
|
|
541
|
-
|
|
626
|
+
input: {
|
|
627
|
+
accountId: string;
|
|
628
|
+
workspaceId: string;
|
|
629
|
+
subjectId: string | null;
|
|
630
|
+
artifactId: string;
|
|
631
|
+
},
|
|
542
632
|
): Promise<{ file: RetainedFileArtifact["file"]; metadata: RetainedArtifactMetadata } | null> {
|
|
543
|
-
const generated = await getGeneratedImageArtifact(db, workspaceId, artifactId);
|
|
633
|
+
const generated = await getGeneratedImageArtifact(db, input.workspaceId, input.artifactId);
|
|
544
634
|
if (generated) {
|
|
545
635
|
return { file: generated.file, metadata: retainedGeneratedImageMetadata(generated) };
|
|
546
636
|
}
|
|
547
|
-
const generatedVideo = await getGeneratedVideoArtifact(db, workspaceId, artifactId);
|
|
637
|
+
const generatedVideo = await getGeneratedVideoArtifact(db, input.workspaceId, input.artifactId);
|
|
548
638
|
if (generatedVideo && !generatedVideo.artifact.deletedAt) {
|
|
549
639
|
const file = generatedVideoFileAsset(generatedVideo.file);
|
|
550
640
|
return {
|
|
@@ -555,8 +645,43 @@ async function getWorkspaceArtifact(
|
|
|
555
645
|
}),
|
|
556
646
|
};
|
|
557
647
|
}
|
|
558
|
-
const artifact = await getRetainedFileArtifact(db, workspaceId, artifactId);
|
|
559
|
-
|
|
648
|
+
const artifact = await getRetainedFileArtifact(db, input.workspaceId, input.artifactId);
|
|
649
|
+
if (!artifact) return null;
|
|
650
|
+
const [authorizedFile] = await getFilesForSubject(db, {
|
|
651
|
+
accountId: input.accountId,
|
|
652
|
+
workspaceId: input.workspaceId,
|
|
653
|
+
subjectId: input.subjectId,
|
|
654
|
+
fileIds: [artifact.file.id],
|
|
655
|
+
});
|
|
656
|
+
if (!authorizedFile) return null;
|
|
657
|
+
const authorizedArtifact = { ...artifact, file: authorizedFile };
|
|
658
|
+
return {
|
|
659
|
+
file: authorizedFile,
|
|
660
|
+
metadata: retainedArtifactMetadata(authorizedArtifact),
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/** Human and stable service grants already carry their immutable subject.
|
|
665
|
+
* Agent tokens carry only a technical worker subject, so resolve the exact
|
|
666
|
+
* live attempt and use the initiating human frozen on its durable turn. */
|
|
667
|
+
export async function fileAuthoritySubjectIdForGrant(
|
|
668
|
+
deps: Pick<ApiRouteDeps, "db">,
|
|
669
|
+
grant: import("@opengeni/contracts").AccessGrant,
|
|
670
|
+
): Promise<string | null> {
|
|
671
|
+
if (grant.principalKind !== "agent_attempt") return grant.subjectId;
|
|
672
|
+
const sessionId = grant.metadata?.["sessionId"];
|
|
673
|
+
if (typeof sessionId !== "string") {
|
|
674
|
+
throw new HTTPException(404, { message: "file not found" });
|
|
675
|
+
}
|
|
676
|
+
try {
|
|
677
|
+
const actor = await requireLiveAgentAttemptAuthorization(deps.db, grant, sessionId);
|
|
678
|
+
return actor.initiatingHumanSubjectId;
|
|
679
|
+
} catch (error) {
|
|
680
|
+
if (error instanceof SessionAuthorizationDeniedError) {
|
|
681
|
+
throw new HTTPException(404, { message: "file not found", cause: error });
|
|
682
|
+
}
|
|
683
|
+
throw error;
|
|
684
|
+
}
|
|
560
685
|
}
|
|
561
686
|
|
|
562
687
|
function generatedVideoFileAsset(
|