@opengeni/api-router 0.30.3 → 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-WCXHI3FF.js → chunk-QKDFBBUE.js} +26385 -15573
- 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-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 +326 -82
- 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 +18 -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 +4 -3
- 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 +4 -1
- 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/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 +153 -23
- 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 +28 -3
- 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 +301 -92
- 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 +1265 -208
- 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 +16 -1
- 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 +329 -79
- 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 +144 -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 +72 -11
- 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 +775 -38
- 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-WCXHI3FF.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,6 +23,7 @@ import {
|
|
|
23
23
|
createFileUpload,
|
|
24
24
|
getFileUpload,
|
|
25
25
|
getGeneratedImageArtifact,
|
|
26
|
+
recordAuditEvent,
|
|
26
27
|
getGeneratedVideoArtifact,
|
|
27
28
|
getFilesForSubject,
|
|
28
29
|
getRetainedFileArtifact,
|
|
@@ -42,6 +43,7 @@ import {
|
|
|
42
43
|
} from "@opengeni/core";
|
|
43
44
|
import { recordWorkspaceUsage, requireLimit } from "@opengeni/core";
|
|
44
45
|
import type { ApiRouteDeps } from "@opengeni/core";
|
|
46
|
+
import { retryWhileMissing } from "@opengeni/storage";
|
|
45
47
|
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
46
48
|
import { buildFilesMcpServer } from "../mcp/files";
|
|
47
49
|
|
|
@@ -100,6 +102,23 @@ export function registerFileRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
100
102
|
objectKey,
|
|
101
103
|
expiresAt: signed.expiresAt,
|
|
102
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
|
+
});
|
|
103
122
|
return c.json(
|
|
104
123
|
CreateFileUploadResponse.parse({
|
|
105
124
|
fileId: upload.file.id,
|
|
@@ -231,11 +250,13 @@ export function registerFileRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
231
250
|
const file = await rejectAndCleanObject(409, "file upload has expired", "expired");
|
|
232
251
|
return c.json(CompleteFileUploadResponse.parse({ file }));
|
|
233
252
|
}
|
|
234
|
-
const head = await
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
});
|
|
253
|
+
const head = await retryWhileMissing(async () => {
|
|
254
|
+
if (!(await objectStorage.fileExists(upload.file))) return null;
|
|
255
|
+
return await objectStorage.headFile(upload.file);
|
|
238
256
|
});
|
|
257
|
+
if (!head) {
|
|
258
|
+
throw new HTTPException(409, { message: "uploaded object is not available" });
|
|
259
|
+
}
|
|
239
260
|
if (Number(head.ContentLength ?? -1) !== upload.file.sizeBytes) {
|
|
240
261
|
const file = await rejectAndCleanObject(
|
|
241
262
|
422,
|
|
@@ -324,7 +345,7 @@ export function registerFileRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
324
345
|
|
|
325
346
|
app.post("/v1/workspaces/:workspaceId/artifacts/:artifactId/playback-source", async (c) => {
|
|
326
347
|
const workspaceId = c.req.param("workspaceId");
|
|
327
|
-
await requireAccessGrant(c, deps, workspaceId, "files:read");
|
|
348
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "files:read");
|
|
328
349
|
if (!objectStorage) {
|
|
329
350
|
throw new HTTPException(503, { message: "object storage is not configured" });
|
|
330
351
|
}
|
|
@@ -337,10 +358,26 @@ export function registerFileRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
337
358
|
throw new HTTPException(409, { message: `generated video is ${retained.file.status}` });
|
|
338
359
|
}
|
|
339
360
|
const file = generatedVideoFileAsset(retained.file);
|
|
340
|
-
|
|
361
|
+
const videoPresent = await retryWhileMissing(async () =>
|
|
362
|
+
(await objectStorage.fileExists(file)) ? true : null,
|
|
363
|
+
);
|
|
364
|
+
if (!videoPresent) {
|
|
341
365
|
throw new HTTPException(410, { message: "generated video bytes are unavailable" });
|
|
342
366
|
}
|
|
343
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
|
+
});
|
|
344
381
|
return c.json(
|
|
345
382
|
VideoArtifactPlaybackSource.parse({
|
|
346
383
|
schemaVersion: 1,
|
|
@@ -416,7 +453,26 @@ export function registerFileRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
416
453
|
if (file.status !== "ready") {
|
|
417
454
|
throw new HTTPException(409, { message: `file is ${file.status}` });
|
|
418
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
|
+
}
|
|
419
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
|
+
});
|
|
420
476
|
return c.json(
|
|
421
477
|
FileDownloadUrlResponse.parse({
|
|
422
478
|
url: signed.url,
|
|
@@ -480,16 +536,21 @@ async function serveRetainedArtifactContent(
|
|
|
480
536
|
...(range.contentRange ? { "Content-Range": range.contentRange } : {}),
|
|
481
537
|
};
|
|
482
538
|
if (range.kind === "empty") {
|
|
483
|
-
|
|
539
|
+
const present = await retryWhileMissing(async () =>
|
|
540
|
+
(await objectStorage.fileExists(file)) ? true : null,
|
|
541
|
+
);
|
|
542
|
+
if (!present) {
|
|
484
543
|
return c.json(retainedArtifactUnavailable(artifactId, "missing_storage"), 410);
|
|
485
544
|
}
|
|
486
545
|
return c.body(null, 200, headers);
|
|
487
546
|
}
|
|
488
547
|
|
|
489
|
-
const bytes = await
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
548
|
+
const bytes = await retryWhileMissing(async () =>
|
|
549
|
+
objectStorage.getFileRange(file, {
|
|
550
|
+
start: range.start,
|
|
551
|
+
end: range.end,
|
|
552
|
+
}),
|
|
553
|
+
);
|
|
493
554
|
if (!bytes) {
|
|
494
555
|
return c.json(retainedArtifactUnavailable(artifactId, "missing_storage"), 410);
|
|
495
556
|
}
|
package/src/routes/install.ts
CHANGED
|
@@ -44,15 +44,34 @@ const BAKED_DIR = new URL("baked/", INSTALL_DIR);
|
|
|
44
44
|
// The static text artifacts served verbatim, with their content types. Each is
|
|
45
45
|
// read once at first request and memoized (committed files; immutable per deploy).
|
|
46
46
|
const TEXT_ASSETS: Record<string, { file: string; contentType: string }> = {
|
|
47
|
-
"/install.sh": {
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
"/install.sh": {
|
|
48
|
+
file: "install.sh",
|
|
49
|
+
contentType: "text/x-shellscript; charset=utf-8",
|
|
50
|
+
},
|
|
51
|
+
"/install.ps1": {
|
|
52
|
+
file: "install.ps1",
|
|
53
|
+
contentType: "text/plain; charset=utf-8",
|
|
54
|
+
},
|
|
55
|
+
"/uninstall.sh": {
|
|
56
|
+
file: "uninstall.sh",
|
|
57
|
+
contentType: "text/x-shellscript; charset=utf-8",
|
|
58
|
+
},
|
|
50
59
|
"/opengeni-agent-minisign.pub": {
|
|
51
60
|
file: "opengeni-agent-minisign.pub",
|
|
52
61
|
contentType: "text/plain; charset=utf-8",
|
|
53
62
|
},
|
|
54
63
|
};
|
|
55
64
|
|
|
65
|
+
// Non-executable resources needed to assemble a complete platform bundle. These
|
|
66
|
+
// stay committed beside the installer rather than depending on a release/CDN so
|
|
67
|
+
// an exact-SHA local or private deployment remains self-contained.
|
|
68
|
+
const COMMITTED_BINARY_ASSETS: Record<string, { file: string; contentType: string }> = {
|
|
69
|
+
"OpenGeni-Agent.icns": {
|
|
70
|
+
file: "OpenGeni-Agent.icns",
|
|
71
|
+
contentType: "image/icns",
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
|
|
56
75
|
const assetCache = new Map<string, string>();
|
|
57
76
|
|
|
58
77
|
async function loadAsset(file: string): Promise<string> {
|
|
@@ -73,26 +92,40 @@ async function loadAsset(file: string): Promise<string> {
|
|
|
73
92
|
// `curl https://<this-host>/install.sh | sh` installs the exact agent that
|
|
74
93
|
// matches the API it enrolls against, with NO dependency on a public CDN (which a
|
|
75
94
|
// private/air-gapped deploy may not even resolve). When a public base URL is
|
|
76
|
-
// configured we therefore rewrite each script's
|
|
77
|
-
// that origin before serving. The user's
|
|
78
|
-
// still
|
|
79
|
-
//
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
95
|
+
// configured we therefore rewrite each script's release-asset AND connection-API
|
|
96
|
+
// default markers to that origin before serving. The user's
|
|
97
|
+
// OPENGENI_INSTALL_BASE_URL / OPENGENI_API_URL overrides still win at run time;
|
|
98
|
+
// only the built-in defaults change. The marker lines are kept shape-stable in
|
|
99
|
+
// agent/install/install.{sh,ps1}.
|
|
100
|
+
type DefaultRewrite = { from: string; to: string };
|
|
101
|
+
|
|
102
|
+
const DEFAULT_BASE_REWRITES: Record<string, (base: string) => DefaultRewrite[]> = {
|
|
103
|
+
"install.sh": (base) => [
|
|
104
|
+
{
|
|
105
|
+
from: 'OPENGENI_INSTALL_DEFAULT_BASE_URL="https://get.opengeni.ai"',
|
|
106
|
+
to: `OPENGENI_INSTALL_DEFAULT_BASE_URL="${base}"`,
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
from: 'OPENGENI_API_DEFAULT_URL="https://app.opengeni.ai"',
|
|
110
|
+
to: `OPENGENI_API_DEFAULT_URL="${base}"`,
|
|
111
|
+
},
|
|
112
|
+
],
|
|
113
|
+
"install.ps1": (base) => [
|
|
114
|
+
{
|
|
115
|
+
from: "$OpengeniInstallDefaultBaseUrl = 'https://get.opengeni.ai'",
|
|
116
|
+
to: `$OpengeniInstallDefaultBaseUrl = '${base}'`,
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
from: "$OpengeniApiDefaultUrl = 'https://app.opengeni.ai'",
|
|
120
|
+
to: `$OpengeniApiDefaultUrl = '${base}'`,
|
|
121
|
+
},
|
|
122
|
+
],
|
|
89
123
|
};
|
|
90
124
|
|
|
91
|
-
// Rewrite a served script's default release-asset
|
|
92
|
-
// own public origin. A no-op when no public base URL is configured
|
|
93
|
-
//
|
|
94
|
-
//
|
|
95
|
-
// safe to serving it verbatim rather than crashing the install surface).
|
|
125
|
+
// Rewrite a served script's default release-asset and connection-API URLs to this
|
|
126
|
+
// deployment's own public origin. A no-op when no public base URL is configured,
|
|
127
|
+
// when the URL is not absolute http(s), or when either marker is absent (script
|
|
128
|
+
// drift — fail safe to serving it verbatim rather than splitting the two origins).
|
|
96
129
|
function rewriteDefaultBaseUrl(
|
|
97
130
|
file: string,
|
|
98
131
|
body: string,
|
|
@@ -102,11 +135,11 @@ function rewriteDefaultBaseUrl(
|
|
|
102
135
|
return body;
|
|
103
136
|
}
|
|
104
137
|
const base = publicBaseUrl.replace(/\/+$/, "");
|
|
105
|
-
const
|
|
106
|
-
if (!
|
|
138
|
+
const rules = DEFAULT_BASE_REWRITES[file]?.(base);
|
|
139
|
+
if (!rules || rules.some((rule) => !body.includes(rule.from))) {
|
|
107
140
|
return body;
|
|
108
141
|
}
|
|
109
|
-
return
|
|
142
|
+
return rules.reduce((rewritten, rule) => rewritten.replace(rule.from, rule.to), body);
|
|
110
143
|
}
|
|
111
144
|
|
|
112
145
|
// The content type for a baked release asset. The binary is an
|
|
@@ -173,6 +206,18 @@ export function registerInstallRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
173
206
|
// un-baked asset). The baked path makes the agent the API enrolls against
|
|
174
207
|
// identical to the API that serves it — no version skew, no extra hop.
|
|
175
208
|
async function serveAsset(asset: string, redirectUrl: string): Promise<Response> {
|
|
209
|
+
const committed = COMMITTED_BINARY_ASSETS[asset];
|
|
210
|
+
if (committed) {
|
|
211
|
+
const buf = await readFile(new URL(committed.file, INSTALL_DIR));
|
|
212
|
+
return new Response(buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength), {
|
|
213
|
+
status: 200,
|
|
214
|
+
headers: {
|
|
215
|
+
"content-type": committed.contentType,
|
|
216
|
+
"cache-control": "public, max-age=3600",
|
|
217
|
+
"x-opengeni-agent-source": "committed",
|
|
218
|
+
},
|
|
219
|
+
});
|
|
220
|
+
}
|
|
176
221
|
const baked = await readBaked(asset);
|
|
177
222
|
if (baked !== null) {
|
|
178
223
|
return new Response(baked, {
|
|
@@ -189,7 +234,10 @@ export function registerInstallRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
189
234
|
}
|
|
190
235
|
// Not baked here → the GitHub Release is the source of truth (the public
|
|
191
236
|
// archive + the documented install.sh fallback). 302 so the client refetches.
|
|
192
|
-
return new Response(null, {
|
|
237
|
+
return new Response(null, {
|
|
238
|
+
status: 302,
|
|
239
|
+
headers: { location: redirectUrl },
|
|
240
|
+
});
|
|
193
241
|
}
|
|
194
242
|
|
|
195
243
|
// `latest` → the BAKED binary if present, else the operator-selected immutable
|
|
@@ -217,10 +265,14 @@ export function registerInstallRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
217
265
|
app.get(`/agent/${channel}/:manifestAsset`, (c) => {
|
|
218
266
|
const manifestAsset = c.req.param("manifestAsset");
|
|
219
267
|
if (manifestAsset !== "manifest.json" && manifestAsset !== "manifest.json.minisig") {
|
|
220
|
-
throw new HTTPException(404, {
|
|
268
|
+
throw new HTTPException(404, {
|
|
269
|
+
message: "update manifest asset not found",
|
|
270
|
+
});
|
|
221
271
|
}
|
|
222
272
|
if (!version) {
|
|
223
|
-
throw new HTTPException(404, {
|
|
273
|
+
throw new HTTPException(404, {
|
|
274
|
+
message: `${channel} agent channel is not configured`,
|
|
275
|
+
});
|
|
224
276
|
}
|
|
225
277
|
return new Response(null, {
|
|
226
278
|
status: 302,
|
|
@@ -240,7 +292,9 @@ export function registerInstallRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
240
292
|
// `/agent/latest/<asset>` is handled by the more specific route above; any
|
|
241
293
|
// other version segment must be the `v<ver>` shape.
|
|
242
294
|
if (!VERSION_SEG.test(versionSeg) || !ASSET_NAME.test(asset)) {
|
|
243
|
-
throw new HTTPException(400, {
|
|
295
|
+
throw new HTTPException(400, {
|
|
296
|
+
message: "invalid version or asset name",
|
|
297
|
+
});
|
|
244
298
|
}
|
|
245
299
|
return serveAsset(asset, `${releasesBase}/download/agent-${versionSeg}/${asset}`);
|
|
246
300
|
});
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
} from "@opengeni/core";
|
|
14
14
|
import {
|
|
15
15
|
configureIntegrationFacet,
|
|
16
|
+
integrationFacetConfigureRequestDigest,
|
|
16
17
|
IntegrationFacetBindingOwnershipConflictError,
|
|
17
18
|
IntegrationFacetBindingVersionConflictError,
|
|
18
19
|
IntegrationFacetBindingVersionRequiredError,
|
|
@@ -22,6 +23,7 @@ import {
|
|
|
22
23
|
IntegrationFacetOperationIdempotencyError,
|
|
23
24
|
listIntegrationInstanceFacets,
|
|
24
25
|
removeIntegrationFacet,
|
|
26
|
+
replayCompletedIntegrationFacetOperation,
|
|
25
27
|
setIntegrationFacetLifecycle,
|
|
26
28
|
} from "@opengeni/db";
|
|
27
29
|
import type { Hono } from "hono";
|
|
@@ -126,6 +128,33 @@ export function registerIntegrationFacetRoutes(app: Hono, deps: ApiRouteDeps): v
|
|
|
126
128
|
const instanceKey = decoded(c.req.param("instanceKey"));
|
|
127
129
|
const facetKey = decoded(c.req.param("facetKey"));
|
|
128
130
|
try {
|
|
131
|
+
const replayed =
|
|
132
|
+
await replayCompletedIntegrationFacetOperation<IntegrationFacetMutationResult>(deps.db, {
|
|
133
|
+
accountId: grant.accountId,
|
|
134
|
+
workspaceId,
|
|
135
|
+
subjectId: grant.subjectId,
|
|
136
|
+
capabilityId,
|
|
137
|
+
instanceKey,
|
|
138
|
+
facetKey,
|
|
139
|
+
idempotencyKey: payload.idempotencyKey,
|
|
140
|
+
kind: "configure",
|
|
141
|
+
expectedRequestDigest: integrationFacetConfigureRequestDigest({
|
|
142
|
+
capabilityId,
|
|
143
|
+
instanceKey,
|
|
144
|
+
facetKey,
|
|
145
|
+
displayName: payload.displayName,
|
|
146
|
+
config: payload.config,
|
|
147
|
+
...(payload.expectedVersion !== undefined
|
|
148
|
+
? { expectedVersion: payload.expectedVersion }
|
|
149
|
+
: {}),
|
|
150
|
+
}),
|
|
151
|
+
});
|
|
152
|
+
if (replayed) {
|
|
153
|
+
return c.json(
|
|
154
|
+
IntegrationFacetMutationResult.parse(replayed),
|
|
155
|
+
payload.expectedVersion === undefined ? 201 : 200,
|
|
156
|
+
);
|
|
157
|
+
}
|
|
129
158
|
const instance = await listIntegrationInstanceFacets(
|
|
130
159
|
deps.db,
|
|
131
160
|
workspaceId,
|