@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.
Files changed (167) hide show
  1. package/dist/api-websocket.d.ts +18 -0
  2. package/dist/app.js +1 -1
  3. package/dist/auth/managed-auth.d.ts +6 -0
  4. package/dist/{chunk-2PQMSRCB.js → chunk-QKDFBBUE.js} +29110 -18137
  5. package/dist/chunk-QKDFBBUE.js.map +1 -0
  6. package/dist/codemode.d.ts +8 -1
  7. package/dist/company-brain-okf.d.ts +24 -0
  8. package/dist/connected-machine-computer-access.d.ts +11 -0
  9. package/dist/connection-authority-owner.d.ts +59 -0
  10. package/dist/connection-ownership.d.ts +47 -0
  11. package/dist/controller-data-plane.d.ts +16 -0
  12. package/dist/editable-artifact-office-import.d.ts +7 -4
  13. package/dist/editable-artifact-websocket.d.ts +5 -15
  14. package/dist/editable-artifact-workspace-files.d.ts +1 -1
  15. package/dist/http/api-error.d.ts +10 -0
  16. package/dist/http/cors.d.ts +5 -0
  17. package/dist/http/interaction-control-error.d.ts +13 -0
  18. package/dist/http/sse.d.ts +2 -3
  19. package/dist/index.d.ts +5 -2
  20. package/dist/index.js +364 -86
  21. package/dist/index.js.map +1 -1
  22. package/dist/integrations/atlassian.d.ts +17 -0
  23. package/dist/integrations/fiken.d.ts +7 -2
  24. package/dist/integrations/google-drive.d.ts +19 -0
  25. package/dist/integrations/oauth-client.d.ts +22 -3
  26. package/dist/integrations/oauth-profiles.d.ts +195 -0
  27. package/dist/integrations/personal-github-repositories.d.ts +45 -0
  28. package/dist/integrations/personal-github.d.ts +40 -0
  29. package/dist/integrations/pr-review-provider.d.ts +22 -0
  30. package/dist/integrations/provider-oauth.d.ts +8 -0
  31. package/dist/integrations/slack-app-home.d.ts +24 -0
  32. package/dist/integrations/slack-bot.d.ts +149 -9
  33. package/dist/integrations/slack-interactions.d.ts +10 -4
  34. package/dist/integrations/slack-routing.d.ts +120 -0
  35. package/dist/integrations/social-oauth.d.ts +5 -0
  36. package/dist/interaction-frame-proxy.d.ts +68 -0
  37. package/dist/mcp/company-brain-governed-writes.d.ts +26 -0
  38. package/dist/mcp/company-profile-agent-admin.d.ts +44 -0
  39. package/dist/mcp/documents.d.ts +1 -0
  40. package/dist/mcp/remember.d.ts +28 -0
  41. package/dist/mcp/request-abort.d.ts +19 -0
  42. package/dist/mcp/scheduled-task-view.d.ts +4 -4
  43. package/dist/mcp/server.d.ts +15 -2
  44. package/dist/mcp/session-view.d.ts +4 -0
  45. package/dist/mcp/session-wait.d.ts +137 -0
  46. package/dist/routes/automations.d.ts +4 -0
  47. package/dist/routes/billing.d.ts +5 -0
  48. package/dist/routes/browser-sessions.d.ts +17 -3
  49. package/dist/routes/company-brain.d.ts +3 -0
  50. package/dist/routes/company-profile.d.ts +2 -1
  51. package/dist/routes/connection-authorities.d.ts +3 -0
  52. package/dist/routes/files.d.ts +4 -0
  53. package/dist/routes/personal-github-git-broker.d.ts +29 -0
  54. package/dist/routes/personal-github.d.ts +3 -0
  55. package/dist/routes/pr-review.d.ts +3 -0
  56. package/dist/routes/sessions.d.ts +1 -0
  57. package/dist/routes/user-resource-authorities.d.ts +3 -0
  58. package/dist/routes/workspace-capture.d.ts +10 -2
  59. package/dist/routes/workspace-learning.d.ts +3 -0
  60. package/dist/routes/workspaces.d.ts +19 -0
  61. package/dist/sandbox/channel-a.d.ts +11 -1
  62. package/dist/sandbox/connection-authority.d.ts +3 -0
  63. package/dist/sandbox/enrollment.d.ts +2 -0
  64. package/dist/sandbox/machines.d.ts +2 -2
  65. package/dist/sandbox/metrics-ingestion.d.ts +10 -20
  66. package/dist/sandbox/viewer.d.ts +24 -1
  67. package/dist/sandbox-file-artifacts.d.ts +11 -0
  68. package/dist/scheduled-task-deletion.d.ts +14 -0
  69. package/dist/slack-reaction-files.d.ts +1 -1
  70. package/dist/temporal-schedule-cleanup.d.ts +1 -0
  71. package/package.json +20 -18
  72. package/src/api-websocket.ts +23 -0
  73. package/src/app.ts +160 -27
  74. package/src/auth/managed-auth.ts +25 -3
  75. package/src/codemode.ts +53 -23
  76. package/src/codex-realtime.ts +8 -2
  77. package/src/company-brain-okf.ts +340 -0
  78. package/src/connected-machine-computer-access.ts +33 -0
  79. package/src/connection-authority-owner.ts +61 -0
  80. package/src/connection-ownership.ts +180 -0
  81. package/src/controller-data-plane.ts +47 -0
  82. package/src/editable-artifact-native-kernel.ts +26 -15
  83. package/src/editable-artifact-office-import.ts +77 -9
  84. package/src/editable-artifact-production.ts +13 -7
  85. package/src/editable-artifact-websocket.ts +11 -18
  86. package/src/editable-artifact-workspace-files.ts +31 -6
  87. package/src/http/api-error.ts +28 -0
  88. package/src/http/auth.ts +4 -0
  89. package/src/http/cors.ts +35 -0
  90. package/src/http/interaction-control-error.ts +164 -0
  91. package/src/http/sse.ts +112 -66
  92. package/src/index.ts +46 -6
  93. package/src/integrations/atlassian.ts +146 -35
  94. package/src/integrations/fiken.ts +102 -22
  95. package/src/integrations/google-drive.ts +416 -101
  96. package/src/integrations/oauth-client.ts +272 -143
  97. package/src/integrations/oauth-profiles.ts +477 -0
  98. package/src/integrations/personal-github-repositories.ts +445 -0
  99. package/src/integrations/personal-github.ts +705 -0
  100. package/src/integrations/pr-review-provider.ts +246 -0
  101. package/src/integrations/provider-oauth.ts +121 -5
  102. package/src/integrations/slack-app-home.ts +300 -0
  103. package/src/integrations/slack-bot.ts +337 -57
  104. package/src/integrations/slack-interactions.ts +1274 -214
  105. package/src/integrations/slack-routing.ts +324 -0
  106. package/src/integrations/social-oauth.ts +25 -0
  107. package/src/interaction-frame-proxy.ts +409 -0
  108. package/src/mcp/company-brain-governed-writes.ts +262 -0
  109. package/src/mcp/company-profile-agent-admin.ts +205 -0
  110. package/src/mcp/documents.ts +37 -6
  111. package/src/mcp/files.ts +22 -2
  112. package/src/mcp/remember.ts +181 -0
  113. package/src/mcp/request-abort.ts +44 -0
  114. package/src/mcp/scheduled-task-view.ts +1 -0
  115. package/src/mcp/server.ts +719 -369
  116. package/src/mcp/session-view.ts +54 -0
  117. package/src/mcp/session-wait.ts +554 -0
  118. package/src/model-catalog.ts +20 -12
  119. package/src/routes/api-integrations.ts +4 -0
  120. package/src/routes/api-keys.ts +1 -0
  121. package/src/routes/automations.ts +534 -0
  122. package/src/routes/billing.ts +57 -1
  123. package/src/routes/browser-sessions.ts +388 -98
  124. package/src/routes/channels.ts +17 -1
  125. package/src/routes/codex.ts +50 -0
  126. package/src/routes/company-brain.ts +367 -0
  127. package/src/routes/company-profile.ts +1 -1
  128. package/src/routes/computer-sessions.ts +320 -93
  129. package/src/routes/connection-authorities.ts +139 -0
  130. package/src/routes/connections.ts +145 -34
  131. package/src/routes/documents.ts +221 -3
  132. package/src/routes/editable-artifacts.ts +11 -3
  133. package/src/routes/enrollments.ts +116 -26
  134. package/src/routes/environments.ts +127 -55
  135. package/src/routes/files.ts +152 -27
  136. package/src/routes/install.ts +82 -28
  137. package/src/routes/integration-facets.ts +29 -0
  138. package/src/routes/interaction-resources.ts +20 -2
  139. package/src/routes/machines.ts +275 -13
  140. package/src/routes/organization-memberships.ts +717 -28
  141. package/src/routes/packs.ts +4 -3
  142. package/src/routes/personal-github-git-broker.ts +785 -0
  143. package/src/routes/personal-github.ts +319 -0
  144. package/src/routes/pr-review.ts +531 -0
  145. package/src/routes/rigs.ts +104 -39
  146. package/src/routes/scheduled-tasks.ts +19 -21
  147. package/src/routes/sessions.ts +777 -40
  148. package/src/routes/social.ts +14 -2
  149. package/src/routes/supergrok.ts +23 -4
  150. package/src/routes/transcription-recordings.ts +8 -2
  151. package/src/routes/user-resource-authorities.ts +138 -0
  152. package/src/routes/workspace-artifacts.ts +4 -1
  153. package/src/routes/workspace-capture.ts +21 -0
  154. package/src/routes/workspace-learning.ts +228 -0
  155. package/src/routes/workspaces.ts +59 -6
  156. package/src/sandbox/auth-callout.ts +38 -12
  157. package/src/sandbox/channel-a.ts +242 -19
  158. package/src/sandbox/connection-authority.ts +3 -0
  159. package/src/sandbox/enrollment.ts +15 -3
  160. package/src/sandbox/machines.ts +186 -64
  161. package/src/sandbox/metrics-ingestion.ts +220 -58
  162. package/src/sandbox/viewer.ts +243 -19
  163. package/src/sandbox-file-artifacts.ts +329 -0
  164. package/src/scheduled-task-deletion.ts +127 -0
  165. package/src/slack-reaction-files.ts +16 -5
  166. package/src/temporal-schedule-cleanup.ts +13 -0
  167. 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 { requireAccessGrant, requireLiteralPermission, requirePermission } from "@opengeni/core";
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(await listVariableSets(db, workspaceId));
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 grant = await requireAccessGrant(c, deps, workspaceId);
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
- if ((await countVariableSets(db, workspaceId)) >= MAX_ENVIRONMENTS_PER_WORKSPACE) {
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, workspaceId, name)) {
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, workspaceId, c.req.param("variableSetId")!));
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 grant = await requireAccessGrant(c, deps, workspaceId);
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
- db,
146
- workspaceId,
147
- c.req.param("variableSetId")!,
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(db, workspaceId, name);
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(db, workspaceId, variableSet.id, {
160
- ...(name !== undefined ? { name } : {}),
161
- ...(payload.description !== undefined ? { description: payload.description } : {}),
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 grant = await requireAccessGrant(c, deps, workspaceId);
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
- db,
178
- workspaceId,
179
- c.req.param("variableSetId")!,
180
- );
181
- const attachedTasks = await countScheduledTasksUsingVariableSet(
182
- db,
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
- const activeSessions = await countActiveSessionsUsingVariableSet(
192
- db,
193
- workspaceId,
194
- variableSet.id,
195
- );
196
- if (activeSessions > 0) {
197
- throw new HTTPException(409, {
198
- message: `variable set is attached to ${activeSessions} active session(s); wait for them to finish or cancel them first`,
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 grant = await requireAccessGrant(c, deps, workspaceId);
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
- db,
219
- workspaceId,
220
- c.req.param("variableSetId")!,
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 grant = await requireAccessGrant(c, deps, workspaceId);
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
- db,
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
- c.req.param("variableSetId")!,
255
- );
256
- const deleted = await deleteVariableSetVariable(db, workspaceId, variableSet.id, name);
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",
@@ -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
- requireFile,
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 { requireAccessGrant } from "@opengeni/core";
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 objectStorage.headFile(upload.file).catch((error) => {
230
- throw new HTTPException(409, {
231
- message: `uploaded object is not available: ${error instanceof Error ? error.message : String(error)}`,
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 requireFile(db, workspaceId, c.req.param("fileId")).catch(() => null);
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, workspaceId, artifactId);
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, workspaceId, artifactId);
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
- if (!(await objectStorage.fileExists(file))) {
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 requireFile(db, workspaceId, c.req.param("fileId")).catch(() => null);
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
- if (!(await objectStorage.fileExists(file))) {
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 objectStorage.getFileRange(file, {
465
- start: range.start,
466
- end: range.end,
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
- workspaceId: string,
541
- artifactId: string,
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
- return artifact ? { file: artifact.file, metadata: retainedArtifactMetadata(artifact) } : null;
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(