@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.
Files changed (165) 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-WCXHI3FF.js → chunk-QKDFBBUE.js} +26385 -15573
  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-websocket.d.ts +5 -15
  13. package/dist/editable-artifact-workspace-files.d.ts +1 -1
  14. package/dist/http/api-error.d.ts +10 -0
  15. package/dist/http/cors.d.ts +5 -0
  16. package/dist/http/interaction-control-error.d.ts +13 -0
  17. package/dist/http/sse.d.ts +2 -3
  18. package/dist/index.d.ts +5 -2
  19. package/dist/index.js +326 -82
  20. package/dist/index.js.map +1 -1
  21. package/dist/integrations/atlassian.d.ts +17 -0
  22. package/dist/integrations/fiken.d.ts +7 -2
  23. package/dist/integrations/google-drive.d.ts +18 -0
  24. package/dist/integrations/oauth-client.d.ts +22 -3
  25. package/dist/integrations/oauth-profiles.d.ts +195 -0
  26. package/dist/integrations/personal-github-repositories.d.ts +45 -0
  27. package/dist/integrations/personal-github.d.ts +40 -0
  28. package/dist/integrations/pr-review-provider.d.ts +22 -0
  29. package/dist/integrations/provider-oauth.d.ts +8 -0
  30. package/dist/integrations/slack-app-home.d.ts +24 -0
  31. package/dist/integrations/slack-bot.d.ts +149 -9
  32. package/dist/integrations/slack-interactions.d.ts +4 -3
  33. package/dist/integrations/slack-routing.d.ts +120 -0
  34. package/dist/integrations/social-oauth.d.ts +5 -0
  35. package/dist/interaction-frame-proxy.d.ts +68 -0
  36. package/dist/mcp/company-brain-governed-writes.d.ts +26 -0
  37. package/dist/mcp/company-profile-agent-admin.d.ts +44 -0
  38. package/dist/mcp/documents.d.ts +1 -0
  39. package/dist/mcp/remember.d.ts +28 -0
  40. package/dist/mcp/request-abort.d.ts +19 -0
  41. package/dist/mcp/scheduled-task-view.d.ts +4 -4
  42. package/dist/mcp/server.d.ts +15 -2
  43. package/dist/mcp/session-view.d.ts +4 -0
  44. package/dist/mcp/session-wait.d.ts +137 -0
  45. package/dist/routes/automations.d.ts +4 -0
  46. package/dist/routes/billing.d.ts +5 -0
  47. package/dist/routes/browser-sessions.d.ts +4 -1
  48. package/dist/routes/company-brain.d.ts +3 -0
  49. package/dist/routes/company-profile.d.ts +2 -1
  50. package/dist/routes/connection-authorities.d.ts +3 -0
  51. package/dist/routes/personal-github-git-broker.d.ts +29 -0
  52. package/dist/routes/personal-github.d.ts +3 -0
  53. package/dist/routes/pr-review.d.ts +3 -0
  54. package/dist/routes/sessions.d.ts +1 -0
  55. package/dist/routes/user-resource-authorities.d.ts +3 -0
  56. package/dist/routes/workspace-capture.d.ts +10 -2
  57. package/dist/routes/workspace-learning.d.ts +3 -0
  58. package/dist/routes/workspaces.d.ts +19 -0
  59. package/dist/sandbox/channel-a.d.ts +11 -1
  60. package/dist/sandbox/connection-authority.d.ts +3 -0
  61. package/dist/sandbox/enrollment.d.ts +2 -0
  62. package/dist/sandbox/machines.d.ts +2 -2
  63. package/dist/sandbox/metrics-ingestion.d.ts +10 -20
  64. package/dist/sandbox/viewer.d.ts +24 -1
  65. package/dist/sandbox-file-artifacts.d.ts +11 -0
  66. package/dist/scheduled-task-deletion.d.ts +14 -0
  67. package/dist/slack-reaction-files.d.ts +1 -1
  68. package/dist/temporal-schedule-cleanup.d.ts +1 -0
  69. package/package.json +20 -18
  70. package/src/api-websocket.ts +23 -0
  71. package/src/app.ts +153 -23
  72. package/src/auth/managed-auth.ts +25 -3
  73. package/src/codemode.ts +53 -23
  74. package/src/codex-realtime.ts +8 -2
  75. package/src/company-brain-okf.ts +340 -0
  76. package/src/connected-machine-computer-access.ts +33 -0
  77. package/src/connection-authority-owner.ts +61 -0
  78. package/src/connection-ownership.ts +180 -0
  79. package/src/controller-data-plane.ts +47 -0
  80. package/src/editable-artifact-native-kernel.ts +26 -15
  81. package/src/editable-artifact-office-import.ts +28 -3
  82. package/src/editable-artifact-production.ts +13 -7
  83. package/src/editable-artifact-websocket.ts +11 -18
  84. package/src/editable-artifact-workspace-files.ts +31 -6
  85. package/src/http/api-error.ts +28 -0
  86. package/src/http/auth.ts +4 -0
  87. package/src/http/cors.ts +35 -0
  88. package/src/http/interaction-control-error.ts +164 -0
  89. package/src/http/sse.ts +112 -66
  90. package/src/index.ts +46 -6
  91. package/src/integrations/atlassian.ts +146 -35
  92. package/src/integrations/fiken.ts +102 -22
  93. package/src/integrations/google-drive.ts +301 -92
  94. package/src/integrations/oauth-client.ts +272 -143
  95. package/src/integrations/oauth-profiles.ts +477 -0
  96. package/src/integrations/personal-github-repositories.ts +445 -0
  97. package/src/integrations/personal-github.ts +705 -0
  98. package/src/integrations/pr-review-provider.ts +246 -0
  99. package/src/integrations/provider-oauth.ts +121 -5
  100. package/src/integrations/slack-app-home.ts +300 -0
  101. package/src/integrations/slack-bot.ts +337 -57
  102. package/src/integrations/slack-interactions.ts +1265 -208
  103. package/src/integrations/slack-routing.ts +324 -0
  104. package/src/integrations/social-oauth.ts +25 -0
  105. package/src/interaction-frame-proxy.ts +409 -0
  106. package/src/mcp/company-brain-governed-writes.ts +262 -0
  107. package/src/mcp/company-profile-agent-admin.ts +205 -0
  108. package/src/mcp/documents.ts +37 -6
  109. package/src/mcp/files.ts +16 -1
  110. package/src/mcp/remember.ts +181 -0
  111. package/src/mcp/request-abort.ts +44 -0
  112. package/src/mcp/scheduled-task-view.ts +1 -0
  113. package/src/mcp/server.ts +719 -369
  114. package/src/mcp/session-view.ts +54 -0
  115. package/src/mcp/session-wait.ts +554 -0
  116. package/src/model-catalog.ts +20 -12
  117. package/src/routes/api-integrations.ts +4 -0
  118. package/src/routes/api-keys.ts +1 -0
  119. package/src/routes/automations.ts +534 -0
  120. package/src/routes/billing.ts +57 -1
  121. package/src/routes/browser-sessions.ts +329 -79
  122. package/src/routes/channels.ts +17 -1
  123. package/src/routes/codex.ts +50 -0
  124. package/src/routes/company-brain.ts +367 -0
  125. package/src/routes/company-profile.ts +1 -1
  126. package/src/routes/computer-sessions.ts +320 -93
  127. package/src/routes/connection-authorities.ts +139 -0
  128. package/src/routes/connections.ts +144 -34
  129. package/src/routes/documents.ts +221 -3
  130. package/src/routes/editable-artifacts.ts +11 -3
  131. package/src/routes/enrollments.ts +116 -26
  132. package/src/routes/environments.ts +127 -55
  133. package/src/routes/files.ts +72 -11
  134. package/src/routes/install.ts +82 -28
  135. package/src/routes/integration-facets.ts +29 -0
  136. package/src/routes/interaction-resources.ts +20 -2
  137. package/src/routes/machines.ts +275 -13
  138. package/src/routes/organization-memberships.ts +717 -28
  139. package/src/routes/packs.ts +4 -3
  140. package/src/routes/personal-github-git-broker.ts +785 -0
  141. package/src/routes/personal-github.ts +319 -0
  142. package/src/routes/pr-review.ts +531 -0
  143. package/src/routes/rigs.ts +104 -39
  144. package/src/routes/scheduled-tasks.ts +19 -21
  145. package/src/routes/sessions.ts +775 -38
  146. package/src/routes/social.ts +14 -2
  147. package/src/routes/supergrok.ts +23 -4
  148. package/src/routes/transcription-recordings.ts +8 -2
  149. package/src/routes/user-resource-authorities.ts +138 -0
  150. package/src/routes/workspace-artifacts.ts +4 -1
  151. package/src/routes/workspace-capture.ts +21 -0
  152. package/src/routes/workspace-learning.ts +228 -0
  153. package/src/routes/workspaces.ts +59 -6
  154. package/src/sandbox/auth-callout.ts +38 -12
  155. package/src/sandbox/channel-a.ts +242 -19
  156. package/src/sandbox/connection-authority.ts +3 -0
  157. package/src/sandbox/enrollment.ts +15 -3
  158. package/src/sandbox/machines.ts +186 -64
  159. package/src/sandbox/metrics-ingestion.ts +220 -58
  160. package/src/sandbox/viewer.ts +243 -19
  161. package/src/sandbox-file-artifacts.ts +329 -0
  162. package/src/scheduled-task-deletion.ts +127 -0
  163. package/src/slack-reaction-files.ts +16 -5
  164. package/src/temporal-schedule-cleanup.ts +13 -0
  165. 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 { 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,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 objectStorage.headFile(upload.file).catch((error) => {
235
- throw new HTTPException(409, {
236
- message: `uploaded object is not available: ${error instanceof Error ? error.message : String(error)}`,
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
- if (!(await objectStorage.fileExists(file))) {
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
- if (!(await objectStorage.fileExists(file))) {
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 objectStorage.getFileRange(file, {
490
- start: range.start,
491
- end: range.end,
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
  }
@@ -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": { file: "install.sh", contentType: "text/x-shellscript; charset=utf-8" },
48
- "/install.ps1": { file: "install.ps1", contentType: "text/plain; charset=utf-8" },
49
- "/uninstall.sh": { file: "uninstall.sh", contentType: "text/x-shellscript; charset=utf-8" },
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 default-base-URL marker line to
77
- // that origin before serving. The user's OPENGENI_INSTALL_BASE_URL env override
78
- // still wins at run time (the scripts use `:-default`); only the built-in DEFAULT
79
- // changes. The marker lines are kept shape-stable in agent/install/install.{sh,ps1}.
80
- const DEFAULT_BASE_REWRITES: Record<string, (base: string) => { from: string; to: string }> = {
81
- "install.sh": (base) => ({
82
- from: 'OPENGENI_INSTALL_DEFAULT_BASE_URL="https://get.opengeni.ai"',
83
- to: `OPENGENI_INSTALL_DEFAULT_BASE_URL="${base}"`,
84
- }),
85
- "install.ps1": (base) => ({
86
- from: "$OpengeniInstallDefaultBaseUrl = 'https://get.opengeni.ai'",
87
- to: `$OpengeniInstallDefaultBaseUrl = '${base}'`,
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 base URL to this deployment's
92
- // own public origin. A no-op when no public base URL is configured (the public
93
- // archive default stands), when the URL is not absolute http(s) (never serve a
94
- // script with a broken base), or when the file has no marker (script drift — fail
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 rule = DEFAULT_BASE_REWRITES[file]?.(base);
106
- if (!rule || !body.includes(rule.from)) {
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 body.replace(rule.from, rule.to);
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, { status: 302, headers: { location: redirectUrl } });
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, { message: "update manifest asset not found" });
268
+ throw new HTTPException(404, {
269
+ message: "update manifest asset not found",
270
+ });
221
271
  }
222
272
  if (!version) {
223
- throw new HTTPException(404, { message: `${channel} agent channel is not configured` });
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, { message: "invalid version or asset name" });
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,