@opengeni/api-router 0.26.1 → 0.30.1

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 (87) hide show
  1. package/dist/app.d.ts +1 -0
  2. package/dist/app.js +5 -3
  3. package/dist/auth/canonical-human-session-admission.d.ts +27 -0
  4. package/dist/browser-auth-broker.d.ts +20 -0
  5. package/dist/browser-controller-authority.d.ts +19 -1
  6. package/dist/browser-network-route.d.ts +8 -0
  7. package/dist/{chunk-JIKNR5YL.js → chunk-2PQMSRCB.js} +15518 -6863
  8. package/dist/chunk-2PQMSRCB.js.map +1 -0
  9. package/dist/controller-data-plane.d.ts +12 -0
  10. package/dist/http/auth.d.ts +1 -1
  11. package/dist/http/sse.d.ts +18 -1
  12. package/dist/index.js +1 -1
  13. package/dist/integrations/api-integrations.d.ts +2 -2
  14. package/dist/integrations/fiken.d.ts +233 -0
  15. package/dist/integrations/google-drive.d.ts +6 -6
  16. package/dist/integrations/provider-oauth.d.ts +0 -2
  17. package/dist/integrations/slack-bot.d.ts +171 -1
  18. package/dist/integrations/slack-interactions.d.ts +17 -5
  19. package/dist/interaction-holder-heartbeat.d.ts +17 -0
  20. package/dist/interaction-metrics.d.ts +11 -0
  21. package/dist/mcp/server.d.ts +3 -2
  22. package/dist/model-catalog.d.ts +1 -0
  23. package/dist/routes/canonical-human-identities.d.ts +3 -0
  24. package/dist/routes/channels.d.ts +3 -0
  25. package/dist/routes/integration-facets.d.ts +3 -0
  26. package/dist/routes/interaction-resources.d.ts +5 -0
  27. package/dist/routes/organization-memberships.d.ts +3 -0
  28. package/dist/routes/sessions.d.ts +2 -1
  29. package/dist/routes/slack-task-policy.d.ts +3 -0
  30. package/dist/routes/supergrok.d.ts +3 -0
  31. package/dist/sandbox/channel-a.d.ts +8 -1
  32. package/dist/sandbox/machines.d.ts +2 -2
  33. package/dist/sandbox/viewer.d.ts +3 -1
  34. package/dist/slack-reaction-files.d.ts +27 -0
  35. package/dist/transcription/providers/xai-subscription.d.ts +8 -0
  36. package/dist/xai-realtime.d.ts +26 -0
  37. package/dist/xai-subscription-auth.d.ts +23 -0
  38. package/package.json +17 -16
  39. package/src/app.ts +352 -49
  40. package/src/auth/canonical-human-session-admission.ts +116 -0
  41. package/src/auth/managed-auth.ts +101 -0
  42. package/src/browser-auth-broker.ts +155 -0
  43. package/src/browser-controller-authority.ts +50 -1
  44. package/src/browser-network-route.ts +34 -0
  45. package/src/controller-data-plane.ts +35 -0
  46. package/src/http/auth.ts +27 -9
  47. package/src/http/sse.ts +229 -5
  48. package/src/integrations/api-integrations.ts +57 -42
  49. package/src/integrations/fiken.ts +1230 -0
  50. package/src/integrations/google-drive.ts +31 -26
  51. package/src/integrations/provider-oauth.ts +70 -91
  52. package/src/integrations/slack-bot.ts +638 -9
  53. package/src/integrations/slack-interactions.ts +1583 -73
  54. package/src/interaction-holder-heartbeat.ts +95 -0
  55. package/src/interaction-metrics.ts +159 -0
  56. package/src/mcp/documents.ts +115 -6
  57. package/src/mcp/server.ts +865 -242
  58. package/src/model-catalog.ts +19 -6
  59. package/src/routes/api-integrations.ts +32 -29
  60. package/src/routes/browser-identities.ts +24 -0
  61. package/src/routes/browser-sessions.ts +2122 -183
  62. package/src/routes/canonical-human-identities.ts +156 -0
  63. package/src/routes/channels.ts +90 -0
  64. package/src/routes/computer-sessions.ts +441 -86
  65. package/src/routes/connections.ts +141 -4
  66. package/src/routes/{integration-features.ts → integration-facets.ts} +63 -63
  67. package/src/routes/interaction-resources.ts +595 -0
  68. package/src/routes/organization-memberships.ts +53 -0
  69. package/src/routes/plugins.ts +2 -1
  70. package/src/routes/sessions.ts +419 -43
  71. package/src/routes/skills.ts +92 -10
  72. package/src/routes/slack-task-policy.ts +115 -0
  73. package/src/routes/supergrok.ts +717 -0
  74. package/src/routes/transcription-recordings.ts +9 -2
  75. package/src/routes/transcriptions.ts +2 -1
  76. package/src/routes/video-generation.ts +34 -4
  77. package/src/routes/workspaces.ts +22 -4
  78. package/src/sandbox/channel-a.ts +29 -2
  79. package/src/sandbox/machines.ts +5 -5
  80. package/src/sandbox/viewer.ts +10 -3
  81. package/src/slack-reaction-files.ts +181 -0
  82. package/src/transcription/providers/xai-subscription.ts +110 -0
  83. package/src/transcription/service.ts +17 -2
  84. package/src/xai-realtime.ts +216 -0
  85. package/src/xai-subscription-auth.ts +132 -0
  86. package/dist/chunk-JIKNR5YL.js.map +0 -1
  87. package/dist/routes/integration-features.d.ts +0 -3
@@ -362,12 +362,18 @@ export function registerResumableTranscriptionRoutes(app: Hono, deps: ApiRouteDe
362
362
  if (
363
363
  !deps.objectStorage ||
364
364
  !service ||
365
- !(await service.available({ workspaceId: authority.workspaceId }))
365
+ !(await service.available({
366
+ workspaceId: authority.workspaceId,
367
+ subjectId: authority.subjectId,
368
+ }))
366
369
  ) {
367
370
  return c.json({ code: "unavailable" }, 503);
368
371
  }
369
372
  const selectedProvider = service.selectProvider
370
- ? await service.selectProvider({ workspaceId: authority.workspaceId })
373
+ ? await service.selectProvider({
374
+ workspaceId: authority.workspaceId,
375
+ subjectId: authority.subjectId,
376
+ })
371
377
  : "host";
372
378
  if (!selectedProvider) return c.json({ code: "unavailable" }, 503);
373
379
  attemptId = correlationId(c);
@@ -418,6 +424,7 @@ export function registerResumableTranscriptionRoutes(app: Hono, deps: ApiRouteDe
418
424
  const result = await service.transcribe({
419
425
  workspaceId: authority.workspaceId,
420
426
  accountId: authority.accountId,
427
+ subjectId: authority.subjectId,
421
428
  audio: stored.bytes,
422
429
  mimeType: "audio/wav",
423
430
  durationSeconds: claim.segment.durationMilliseconds / 1_000,
@@ -18,7 +18,7 @@ export function registerTranscriptionRoutes(app: Hono, deps: ApiRouteDeps): void
18
18
  return c.json({ code: "policy_blocked" }, 403);
19
19
  }
20
20
  const service = deps.transcription;
21
- if (!service || !(await service.available())) {
21
+ if (!service || !(await service.available({ workspaceId, subjectId: grant.subjectId }))) {
22
22
  return c.json({ code: "unavailable" }, 503);
23
23
  }
24
24
  try {
@@ -26,6 +26,7 @@ export function registerTranscriptionRoutes(app: Hono, deps: ApiRouteDeps): void
26
26
  const result = await service.transcribe({
27
27
  workspaceId,
28
28
  accountId: grant.accountId,
29
+ subjectId: grant.subjectId,
29
30
  audio: body.audio,
30
31
  mimeType: body.mimeType,
31
32
  durationSeconds: body.durationSeconds,
@@ -10,10 +10,12 @@ import {
10
10
  getWorkspaceVideoGenerationPolicy,
11
11
  updateWorkspaceVideoGenerationPolicy,
12
12
  VideoGenerationConflictError,
13
+ workspaceXaiSubscriptionActive,
13
14
  } from "@opengeni/db";
14
15
  import {
15
16
  requireAccessGrant,
16
17
  VIDEO_GENERATION_MODEL_CATALOG,
18
+ videoGenerationModelSupportsFundingSource,
17
19
  videoGenerationCapabilitiesForPolicy,
18
20
  type ApiRouteDeps,
19
21
  } from "@opengeni/core";
@@ -23,14 +25,16 @@ import { HTTPException } from "hono/http-exception";
23
25
  export function registerVideoGenerationRoutes(app: Hono, deps: ApiRouteDeps): void {
24
26
  app.get("/v1/workspaces/:workspaceId/video-generation", async (c) => {
25
27
  const workspaceId = c.req.param("workspaceId");
26
- await requireAccessGrant(c, deps, workspaceId, "workspace:read");
27
- const [policy, connection] = await Promise.all([
28
+ const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:read");
29
+ const [policy, connection, supergrokConfigured] = await Promise.all([
28
30
  getWorkspaceVideoGenerationPolicy(deps.db, workspaceId),
29
31
  getWorkspaceVercelAiGatewayConnectionMetadata(deps.db, workspaceId),
32
+ workspaceXaiSubscriptionActive(deps.db, deps.settings, workspaceId, grant.subjectId),
30
33
  ]);
31
34
  const fundingOptions = videoGenerationFundingOptions({
32
35
  managedConfigured: managedVideoGenerationConfigured(deps),
33
36
  workspaceGatewayConfigured: connection !== null,
37
+ supergrokConfigured,
34
38
  });
35
39
  const selectedFunding = fundingOptions.find((option) => option.source === policy.fundingSource);
36
40
  const capabilities =
@@ -56,10 +60,14 @@ export function registerVideoGenerationRoutes(app: Hono, deps: ApiRouteDeps): vo
56
60
  const workspaceId = c.req.param("workspaceId");
57
61
  const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
58
62
  const payload = UpdateVideoGenerationPolicyRequest.parse(await c.req.json());
59
- const connection = await getWorkspaceVercelAiGatewayConnectionMetadata(deps.db, workspaceId);
63
+ const [connection, supergrokConfigured] = await Promise.all([
64
+ getWorkspaceVercelAiGatewayConnectionMetadata(deps.db, workspaceId),
65
+ workspaceXaiSubscriptionActive(deps.db, deps.settings, workspaceId, grant.subjectId),
66
+ ]);
60
67
  const fundingOptions = videoGenerationFundingOptions({
61
68
  managedConfigured: managedVideoGenerationConfigured(deps),
62
69
  workspaceGatewayConfigured: connection !== null,
70
+ supergrokConfigured,
63
71
  });
64
72
  const selectedFunding = fundingOptions.find(
65
73
  (option) => option.source === payload.fundingSource,
@@ -69,6 +77,15 @@ export function registerVideoGenerationRoutes(app: Hono, deps: ApiRouteDeps): vo
69
77
  message: selectedFunding?.unavailableReason ?? "Video generation funding is unavailable",
70
78
  });
71
79
  }
80
+ if (
81
+ payload.enabledModelIds.some(
82
+ (modelId) => !videoGenerationModelSupportsFundingSource(modelId, payload.fundingSource),
83
+ )
84
+ ) {
85
+ throw new HTTPException(422, {
86
+ message: "The selected video model is unavailable for this funding source",
87
+ });
88
+ }
72
89
  try {
73
90
  const policy = await updateWorkspaceVideoGenerationPolicy(deps.db, {
74
91
  accountId: grant.accountId,
@@ -96,7 +113,10 @@ export function registerVideoGenerationRoutes(app: Hono, deps: ApiRouteDeps): vo
96
113
  workspaceId,
97
114
  c.req.param("operationId"),
98
115
  );
99
- if (!summary) throw new HTTPException(404, { message: "video generation operation not found" });
116
+ if (!summary)
117
+ throw new HTTPException(404, {
118
+ message: "video generation operation not found",
119
+ });
100
120
  return c.json(VideoGenerationOperationSummary.parse(summary));
101
121
  });
102
122
  }
@@ -108,6 +128,7 @@ function managedVideoGenerationConfigured(deps: ApiRouteDeps): boolean {
108
128
  function videoGenerationFundingOptions(input: {
109
129
  managedConfigured: boolean;
110
130
  workspaceGatewayConfigured: boolean;
131
+ supergrokConfigured: boolean;
111
132
  }) {
112
133
  return [
113
134
  {
@@ -119,6 +140,15 @@ function videoGenerationFundingOptions(input: {
119
140
  ? null
120
141
  : "OpenGeni-managed video generation is not configured.",
121
142
  },
143
+ {
144
+ source: "supergrok_subscription" as const,
145
+ label: "SuperGrok",
146
+ description: "Uses your connected SuperGrok subscription.",
147
+ available: input.supergrokConfigured,
148
+ unavailableReason: input.supergrokConfigured
149
+ ? null
150
+ : "Connect an eligible SuperGrok account first.",
151
+ },
122
152
  {
123
153
  source: "workspace_gateway" as const,
124
154
  label: "Your Gateway",
@@ -35,6 +35,7 @@ import {
35
35
  updateWorkspaceSettings,
36
36
  upsertWorkspaceModelPolicy,
37
37
  workspaceCodexSubscriptionActive,
38
+ workspaceXaiSubscriptionActive,
38
39
  workspaceVercelAiGatewayConnectionActive,
39
40
  } from "@opengeni/db";
40
41
  import { boundWorkspaceControlHttpPage } from "@opengeni/events";
@@ -55,6 +56,7 @@ import { processTemporalScheduleCleanupClaims } from "../temporal-schedule-clean
55
56
  import {
56
57
  AI_GATEWAY_REALTIME_MODELS,
57
58
  CODEX_REALTIME_MODEL_ID,
59
+ SUPERGROK_REALTIME_MODEL_ID,
58
60
  canonicalizeConfiguredModelId,
59
61
  type Settings,
60
62
  } from "@opengeni/config";
@@ -165,10 +167,16 @@ export function registerWorkspaceRoutes(app: Hono, deps: ApiRouteDeps): void {
165
167
  // so bearer/federated definitions intentionally fail closed as not ready.
166
168
  app.get("/v1/workspaces/:workspaceId/model-catalog", async (c) => {
167
169
  const workspaceId = c.req.param("workspaceId");
168
- await requireAccessGrant(c, deps, workspaceId, "workspace:read");
169
- const [policy, codexSubscriptionActive, workspaceGatewayConnectionActive] = await Promise.all([
170
+ const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:read");
171
+ const [
172
+ policy,
173
+ codexSubscriptionActive,
174
+ xaiSubscriptionActive,
175
+ workspaceGatewayConnectionActive,
176
+ ] = await Promise.all([
170
177
  getWorkspaceModelPolicy(deps.db, workspaceId),
171
178
  workspaceCodexSubscriptionActive(deps.db, deps.settings, workspaceId),
179
+ workspaceXaiSubscriptionActive(deps.db, deps.settings, workspaceId, grant.subjectId),
172
180
  workspaceVercelAiGatewayConnectionActive(deps.db, workspaceId),
173
181
  ]);
174
182
  c.header("cache-control", "private, no-store");
@@ -178,6 +186,7 @@ export function registerWorkspaceRoutes(app: Hono, deps: ApiRouteDeps): void {
178
186
  settings: deps.settings,
179
187
  policy,
180
188
  codexSubscriptionActive,
189
+ xaiSubscriptionActive,
181
190
  workspaceGatewayConnectionActive,
182
191
  }),
183
192
  ),
@@ -186,9 +195,10 @@ export function registerWorkspaceRoutes(app: Hono, deps: ApiRouteDeps): void {
186
195
 
187
196
  app.get("/v1/workspaces/:workspaceId/realtime-model-catalog", async (c) => {
188
197
  const workspaceId = c.req.param("workspaceId");
189
- await requireAccessGrant(c, deps, workspaceId, "workspace:read");
190
- const [codexConnected, workspaceGatewayConnected] = await Promise.all([
198
+ const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:read");
199
+ const [codexConnected, supergrokConnected, workspaceGatewayConnected] = await Promise.all([
191
200
  workspaceCodexSubscriptionActive(deps.db, deps.settings, workspaceId),
201
+ workspaceXaiSubscriptionActive(deps.db, deps.settings, workspaceId, grant.subjectId),
192
202
  workspaceVercelAiGatewayConnectionActive(deps.db, workspaceId),
193
203
  ]);
194
204
  const availability = (
@@ -220,6 +230,14 @@ export function registerWorkspaceRoutes(app: Hono, deps: ApiRouteDeps): void {
220
230
  ...availability(codexConnected, "Connect Codex to use this voice model"),
221
231
  recommended: false,
222
232
  },
233
+ {
234
+ id: SUPERGROK_REALTIME_MODEL_ID,
235
+ label: "Grok Voice Think Fast 2.0",
236
+ provider: "Connected SuperGrok" as const,
237
+ description: "Direct SuperGrok speech-to-speech",
238
+ ...availability(supergrokConnected, "Connect SuperGrok to use this voice model"),
239
+ recommended: false,
240
+ },
223
241
  ...gatewayModels.map((model) => ({
224
242
  id: model.workspaceModelId,
225
243
  label: model.label,
@@ -64,6 +64,8 @@ import {
64
64
  ChannelAUnsupportedError,
65
65
  ChannelAUnavailableError,
66
66
  ChannelAValidationError,
67
+ BrowserControlRequestError,
68
+ BrowserControlTransportError,
67
69
  codemodeTokenFileFromEnvironment,
68
70
  withCodemodeTokenSession,
69
71
  withRunCredentialsSession,
@@ -104,11 +106,14 @@ export type ChannelAOperation =
104
106
  | "browser.suspend"
105
107
  | "browser.end"
106
108
  | "browser.read"
109
+ | "browser.action"
107
110
  | "browser.control"
111
+ | "browser.download.save"
108
112
  | "browser.attach"
109
113
  | "computer.create"
110
114
  | "computer.end"
111
115
  | "computer.read"
116
+ | "computer.action"
112
117
  | "computer.control"
113
118
  | "computer.attach";
114
119
 
@@ -122,6 +127,11 @@ export type ChannelAContext = {
122
127
  waitSignal?: AbortSignal | undefined;
123
128
  /** Bounded route identity for metrics and safe operator diagnostics. */
124
129
  operation?: ChannelAOperation | undefined;
130
+ /** The callback is an interaction-controller read or an exactly-once action.
131
+ * A controller transport failure may therefore rebuild the exact fenced
132
+ * provider handle and replay the request. Tab/lifecycle mutations that lack
133
+ * a controller operation id must never opt into this recovery. */
134
+ retryControllerTransport?: boolean | undefined;
125
135
  };
126
136
 
127
137
  export type ChannelAOperationFailureReason =
@@ -371,6 +381,8 @@ type ChannelAReadRecoveryOptions = {
371
381
  maxFreshHandleRetries?: 1 | 2;
372
382
  /** Never start another provider attempt after the originating request ends. */
373
383
  waitSignal?: AbortSignal | undefined;
384
+ /** Additional callback-specific failure that is safe to replay. */
385
+ retryableError?: ((error: unknown) => boolean) | undefined;
374
386
  };
375
387
 
376
388
  /** Retry a side-effect-free Channel-A read only after the caller has discarded
@@ -391,7 +403,9 @@ export async function runChannelAReadWithFreshHandleRetry<T>(
391
403
  try {
392
404
  return await run();
393
405
  } catch (error) {
394
- if (!(error instanceof ChannelAUnavailableError) || retries >= maxFreshHandleRetries) {
406
+ const retryable =
407
+ error instanceof ChannelAUnavailableError || options.retryableError?.(error) === true;
408
+ if (!retryable || retries >= maxFreshHandleRetries) {
395
409
  throw error;
396
410
  }
397
411
  options.waitSignal?.throwIfAborted();
@@ -405,7 +419,17 @@ export function shouldEvictChannelAHandleAfterError(
405
419
  error: unknown,
406
420
  cacheKind: EstablishedHandleCacheKind,
407
421
  ): boolean {
408
- return error instanceof ChannelAUnavailableError && cacheKind === "read";
422
+ return (
423
+ cacheKind === "read" &&
424
+ (error instanceof ChannelAUnavailableError || isRetryableControllerTransport(error))
425
+ );
426
+ }
427
+
428
+ function isRetryableControllerTransport(error: unknown): boolean {
429
+ return (
430
+ error instanceof BrowserControlTransportError ||
431
+ (error instanceof BrowserControlRequestError && error.retryable)
432
+ );
409
433
  }
410
434
 
411
435
  function evictEstablishedHandle(key: string, cacheKind: EstablishedHandleCacheKind): void {
@@ -941,6 +965,9 @@ async function withChannelAOperation<T>(
941
965
  {
942
966
  maxFreshHandleRetries: session.sandboxBackend === "modal" ? 2 : 1,
943
967
  ...(ctx.waitSignal ? { waitSignal: ctx.waitSignal } : {}),
968
+ ...(ctx.retryControllerTransport
969
+ ? { retryableError: isRetryableControllerTransport }
970
+ : {}),
944
971
  },
945
972
  )
946
973
  : await runProviderOperation();
@@ -156,8 +156,8 @@ function machineStateFor(
156
156
 
157
157
  /**
158
158
  * Build the Machines dashboard response for a workspace. When `sessionId` is
159
- * supplied (an in-session view) the session's synthetic Modal group box is
160
- * prepended (`isSessionGroup:true`) and the active-sandbox pointer is echoed;
159
+ * supplied (an in-session view) the session's synthetic home group box is
160
+ * prepended when one exists (`isSessionGroup:true`) and the active-sandbox pointer is echoed;
161
161
  * without it (the pure workspace dashboard) `activeSandboxId` is null and only
162
162
  * the enrolled machines are listed.
163
163
  */
@@ -184,9 +184,9 @@ export async function listMachines(
184
184
 
185
185
  const machines: MachineView[] = [];
186
186
 
187
- // The session's own Modal group box (synthetic): the default/home sandbox a
188
- // null active pointer routes to. Only present in an in-session view.
189
- if (session) {
187
+ // The session's own group box (synthetic): the default/home sandbox a null
188
+ // active pointer routes to. A backend:none session has no home box.
189
+ if (session && session.sandboxBackend !== "none") {
190
190
  const groupActive = activeSandboxId === null;
191
191
  const groupLease = await readLease(db, workspaceId, session.sandboxGroupId);
192
192
  machines.push(
@@ -891,8 +891,11 @@ export type TerminalStreamMint = {
891
891
  token: string;
892
892
  expiresAt: string;
893
893
  leaseEpoch: number;
894
+ transport: "pty-ws" | "relay-pty";
894
895
  };
895
896
 
897
+ type SelfhostedStreamMint = Omit<TerminalStreamMint, "transport">;
898
+
896
899
  export type MintTerminalStreamInput = {
897
900
  accountId: string;
898
901
  workspaceId: string;
@@ -950,7 +953,7 @@ export async function mintTerminalStream(
950
953
  if (session.activeSandboxId) {
951
954
  const active = await getSandbox(db, workspaceId, session.activeSandboxId);
952
955
  if (active?.kind === "selfhosted") {
953
- return await tryMintActiveSelfhostedStream(
956
+ const stream = await tryMintActiveSelfhostedStream(
954
957
  services,
955
958
  {
956
959
  session,
@@ -961,6 +964,7 @@ export async function mintTerminalStream(
961
964
  },
962
965
  input.resolveSelfhostedSession,
963
966
  );
967
+ return stream ? { ...stream, transport: "relay-pty" } : null;
964
968
  }
965
969
  // A Modal swap target (or unknown) falls through to the existing group-box path
966
970
  // (unchanged — Modal swap-target streaming is out of scope for this fix).
@@ -1052,6 +1056,7 @@ export async function mintTerminalStream(
1052
1056
  token: exposed.token,
1053
1057
  expiresAt: exposed.expiresAt,
1054
1058
  leaseEpoch: lease.leaseEpoch,
1059
+ transport: "pty-ws",
1055
1060
  };
1056
1061
  } catch {
1057
1062
  // Any other failure degrades the terminal pty-ws cell to the sse-events
@@ -1107,7 +1112,7 @@ async function tryMintActiveSelfhostedStream(
1107
1112
  resolveSelfhostedSession?: (
1108
1113
  sandbox: SandboxRecord,
1109
1114
  ) => Promise<{ resolveExposedPort?: (port: number) => Promise<unknown> }>,
1110
- ): Promise<TerminalStreamMint | null> {
1115
+ ): Promise<SelfhostedStreamMint | null> {
1111
1116
  const { settings, bus } = services;
1112
1117
  const { session, workspaceId, port, sandbox } = input;
1113
1118
  if (!sandbox.enrollmentId) {
@@ -1128,6 +1133,8 @@ async function tryMintActiveSelfhostedStream(
1128
1133
  controlRpcFactory: () => controlRpc(bus),
1129
1134
  agentId: sandbox.enrollmentId,
1130
1135
  epoch: session.activeEpoch,
1136
+ // Stream authority rotates; terminal process identity does not.
1137
+ terminalScopeId: session.id,
1131
1138
  });
1132
1139
  shSession = await client.resume({ agentId: sandbox.enrollmentId });
1133
1140
  }
@@ -1185,7 +1192,7 @@ export type MintSelfhostedStreamInput = {
1185
1192
  export async function mintSelfhostedStream(
1186
1193
  services: ViewerServices,
1187
1194
  input: MintSelfhostedStreamInput,
1188
- ): Promise<TerminalStreamMint | null> {
1195
+ ): Promise<SelfhostedStreamMint | null> {
1189
1196
  const { settings } = services;
1190
1197
  const secret = resolveStreamTokenSecret(settings);
1191
1198
  if (!secret) {
@@ -0,0 +1,181 @@
1
+ import { createHash } from "node:crypto";
2
+ import type { FileResourceRef } from "@opengeni/contracts";
3
+ import { completeFileUpload, prepareGeneratedWorkspaceFile, type Database } from "@opengeni/db";
4
+ import type { ObjectHead, ObjectStorage } from "@opengeni/storage";
5
+ import type { DownloadedSlackReactionImage } from "./integrations/slack-bot";
6
+
7
+ const IMPORT_IDENTITY_VERSION = "slack-reaction-image-v1";
8
+ const UPLOAD_INTENT_TTL_MS = 60 * 60_000;
9
+ const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u;
10
+
11
+ export type SlackReactionImageImportSource = Readonly<{
12
+ accountId: string;
13
+ workspaceId: string;
14
+ connectionId: string;
15
+ slackTeamId: string;
16
+ slackChannelId: string;
17
+ slackMessageTs: string;
18
+ }>;
19
+
20
+ export type ImportedSlackReactionImage = Readonly<{
21
+ resource: FileResourceRef;
22
+ filename: string;
23
+ contentType: "image/png" | "image/jpeg" | "image/webp";
24
+ sizeBytes: number;
25
+ }>;
26
+
27
+ export async function importSlackReactionImage(
28
+ dependencies: Readonly<{
29
+ db: Database;
30
+ objectStorage: ObjectStorage;
31
+ now?: () => Date;
32
+ prepareFile?: typeof prepareGeneratedWorkspaceFile;
33
+ completeFile?: typeof completeFileUpload;
34
+ }>,
35
+ source: SlackReactionImageImportSource,
36
+ image: DownloadedSlackReactionImage,
37
+ ordinal: number,
38
+ ): Promise<ImportedSlackReactionImage> {
39
+ if (!Number.isSafeInteger(ordinal) || ordinal < 1 || ordinal > 4) {
40
+ throw new RangeError("Slack reaction attachment ordinal is invalid");
41
+ }
42
+ const storage = dependencies.objectStorage;
43
+ if (!storage.headObject) {
44
+ throw new Error("Object storage lacks immutable object verification support");
45
+ }
46
+ if (image.bytes.byteLength < 1 || image.bytes.byteLength > 4 * 1024 * 1024) {
47
+ throw new Error("Slack reaction image size is invalid");
48
+ }
49
+ const sha256 = createHash("sha256").update(image.bytes).digest("hex");
50
+ const identity = slackReactionImageIdentity(source, image.fileId);
51
+ const fileId = deterministicUuid(`${identity}:file`);
52
+ const uploadId = deterministicUuid(`${identity}:upload`);
53
+ const safeFilename = safeSlackImageFilename(image.filename, image.contentType);
54
+ const objectKey = `workspaces/${source.workspaceId}/files/${fileId}/slack-reactions/${safeFilename}`;
55
+ const filename = safeFilename;
56
+ const prepared = await (dependencies.prepareFile ?? prepareGeneratedWorkspaceFile)(
57
+ dependencies.db,
58
+ {
59
+ accountId: source.accountId,
60
+ workspaceId: source.workspaceId,
61
+ fileId,
62
+ uploadId,
63
+ filename,
64
+ safeFilename,
65
+ contentType: image.contentType,
66
+ sizeBytes: image.bytes.byteLength,
67
+ sha256,
68
+ bucket: storage.bucket,
69
+ objectKey,
70
+ expiresAt: new Date((dependencies.now?.() ?? new Date()).getTime() + UPLOAD_INTENT_TTL_MS),
71
+ },
72
+ );
73
+
74
+ let file = prepared.file;
75
+ if (file.status !== "ready") {
76
+ const existing = await storage.headObject(objectKey);
77
+ if (existing) {
78
+ assertStoredSlackImage(existing, image, sha256);
79
+ } else if (storage.putObjectIfAbsent) {
80
+ await storage.putObjectIfAbsent({
81
+ key: objectKey,
82
+ contentType: image.contentType,
83
+ body: image.bytes,
84
+ sha256,
85
+ });
86
+ assertStoredSlackImage(await storage.headObject(objectKey), image, sha256);
87
+ } else {
88
+ await storage.putObject({
89
+ key: objectKey,
90
+ contentType: image.contentType,
91
+ body: image.bytes,
92
+ sha256,
93
+ });
94
+ assertStoredSlackImage(await storage.headObject(objectKey), image, sha256);
95
+ }
96
+ file = await (dependencies.completeFile ?? completeFileUpload)(
97
+ dependencies.db,
98
+ source.workspaceId,
99
+ uploadId,
100
+ );
101
+ } else {
102
+ assertStoredSlackImage(await storage.headObject(objectKey), image, sha256);
103
+ }
104
+
105
+ if (
106
+ file.id !== fileId ||
107
+ file.status !== "ready" ||
108
+ file.filename !== filename ||
109
+ file.safeFilename !== safeFilename ||
110
+ file.contentType !== image.contentType ||
111
+ file.sizeBytes !== image.bytes.byteLength ||
112
+ file.sha256 !== sha256
113
+ ) {
114
+ throw new Error("Imported Slack image differs from its immutable workspace file");
115
+ }
116
+ const mountPath = `attachments/slack/${String(ordinal).padStart(2, "0")}-${safeFilename}`;
117
+ return Object.freeze({
118
+ resource: Object.freeze({ kind: "file", fileId, mountPath }),
119
+ filename,
120
+ contentType: image.contentType,
121
+ sizeBytes: image.bytes.byteLength,
122
+ });
123
+ }
124
+
125
+ export function slackReactionImageIdentity(
126
+ source: SlackReactionImageImportSource,
127
+ slackFileId: string,
128
+ ): string {
129
+ return [
130
+ IMPORT_IDENTITY_VERSION,
131
+ source.workspaceId,
132
+ source.connectionId,
133
+ source.slackTeamId,
134
+ source.slackChannelId,
135
+ source.slackMessageTs,
136
+ slackFileId,
137
+ ].join(":");
138
+ }
139
+
140
+ export function safeSlackImageFilename(
141
+ value: string,
142
+ contentType: "image/png" | "image/jpeg" | "image/webp",
143
+ ): string {
144
+ const extension =
145
+ contentType === "image/png" ? ".png" : contentType === "image/jpeg" ? ".jpg" : ".webp";
146
+ const normalized = value
147
+ .normalize("NFKC")
148
+ .replace(/[\\/:*?"<>|\u0000-\u001f\u007f]+/gu, "-")
149
+ .replace(/\s+/gu, " ")
150
+ .trim();
151
+ const withoutKnownExtension = normalized.replace(/\.(?:png|jpe?g|webp)$/iu, "");
152
+ const stem = withoutKnownExtension.replace(/^[. -]+|[. ]+$/gu, "").slice(0, 180) || "slack-image";
153
+ return `${stem}${extension}`;
154
+ }
155
+
156
+ function assertStoredSlackImage(
157
+ head: ObjectHead | null,
158
+ image: DownloadedSlackReactionImage,
159
+ sha256: string,
160
+ ): void {
161
+ if (
162
+ !head ||
163
+ head.ContentLength !== image.bytes.byteLength ||
164
+ head.ContentType !== image.contentType ||
165
+ head.Metadata?.sha256 !== sha256 ||
166
+ typeof head.VersionToken !== "string" ||
167
+ head.VersionToken.length < 1
168
+ ) {
169
+ throw new Error("Stored Slack image differs from its immutable import");
170
+ }
171
+ }
172
+
173
+ function deterministicUuid(seed: string): string {
174
+ const bytes = createHash("sha256").update(seed, "utf8").digest().subarray(0, 16);
175
+ bytes[6] = ((bytes[6] ?? 0) & 0x0f) | 0x50;
176
+ bytes[8] = ((bytes[8] ?? 0) & 0x3f) | 0x80;
177
+ const hex = bytes.toString("hex");
178
+ const value = `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
179
+ if (!UUID_PATTERN.test(value)) throw new Error("Slack reaction file identity is invalid");
180
+ return value;
181
+ }
@@ -0,0 +1,110 @@
1
+ import { type Settings } from "@opengeni/config";
2
+ import {
3
+ type TranscriptionAvailabilityContext,
4
+ type TranscriptionProvider,
5
+ TranscriptionServiceError,
6
+ } from "@opengeni/core";
7
+ import { workspaceXaiSubscriptionActive, type Database } from "@opengeni/db";
8
+ import {
9
+ XAI_CLIENT_MODE,
10
+ XAI_CLIENT_VERSION,
11
+ XAI_PUBLIC_API_BASE_URL,
12
+ type XaiFetch,
13
+ } from "@opengeni/xai-subscription";
14
+ import { buildXaiSubscriptionAuthorization } from "../../xai-subscription-auth";
15
+ import { fetchError, responseError } from "./openai";
16
+
17
+ const TRANSCRIBE_URL = `${XAI_PUBLIC_API_BASE_URL}/stt`;
18
+
19
+ export function createXaiSubscriptionTranscriptionProvider(input: {
20
+ settings: Settings;
21
+ db: Database;
22
+ fetch?: typeof fetch;
23
+ }): TranscriptionProvider {
24
+ const fetchImpl = input.fetch ?? fetch;
25
+ return {
26
+ id: "supergrok-subscription",
27
+ supportsServerDeadline: true,
28
+ experimental: true,
29
+ async available(context?: TranscriptionAvailabilityContext) {
30
+ if (!context?.workspaceId || !context.subjectId) {
31
+ return input.settings.supergrokSubscriptionEnabled;
32
+ }
33
+ return await workspaceXaiSubscriptionActive(
34
+ input.db,
35
+ input.settings,
36
+ context.workspaceId,
37
+ context.subjectId,
38
+ );
39
+ },
40
+ async transcribe({
41
+ audio,
42
+ mimeType,
43
+ filename,
44
+ workspaceId,
45
+ accountId,
46
+ subjectId,
47
+ requestId,
48
+ signal,
49
+ }) {
50
+ let auth: Awaited<ReturnType<typeof buildXaiSubscriptionAuthorization>>;
51
+ try {
52
+ auth = await buildXaiSubscriptionAuthorization({
53
+ db: input.db,
54
+ settings: input.settings,
55
+ accountId,
56
+ workspaceId,
57
+ subjectId,
58
+ shardKey: requestId,
59
+ sessionId: requestId,
60
+ ...(input.fetch ? { fetch: input.fetch as XaiFetch } : {}),
61
+ });
62
+ } catch {
63
+ throw new TranscriptionServiceError({
64
+ code: "unavailable",
65
+ message: "Transcription is unavailable.",
66
+ });
67
+ }
68
+ const request = async (accessToken: string) => {
69
+ const form = new FormData();
70
+ form.append(
71
+ "file",
72
+ new Blob([Uint8Array.from(audio).buffer], { type: mimeType }),
73
+ filename,
74
+ );
75
+ return await fetchImpl(TRANSCRIBE_URL, {
76
+ method: "POST",
77
+ headers: {
78
+ Authorization: `Bearer ${accessToken}`,
79
+ "User-Agent": `opengeni/${XAI_CLIENT_VERSION}`,
80
+ "x-grok-client-version": XAI_CLIENT_VERSION,
81
+ "x-grok-client-identifier": "opengeni",
82
+ "x-grok-client-mode": XAI_CLIENT_MODE,
83
+ "x-grok-session-id": requestId,
84
+ },
85
+ body: form,
86
+ ...(signal ? { signal } : {}),
87
+ });
88
+ };
89
+ let response: Response;
90
+ try {
91
+ let token = await auth.context.getToken();
92
+ response = await request(token.accessToken);
93
+ if (response.status === 401) {
94
+ await response.body?.cancel().catch(() => undefined);
95
+ token = await auth.context.refresh();
96
+ response = await request(token.accessToken);
97
+ }
98
+ } catch (error) {
99
+ throw fetchError(error);
100
+ }
101
+ if (!response.ok) throw responseError(response.status);
102
+ const body = (await response.json().catch(() => null)) as Record<string, unknown> | null;
103
+ if (!body || typeof body.text !== "string") throw responseError(502);
104
+ return {
105
+ text: body.text,
106
+ languages: typeof body.language === "string" && body.language ? [body.language] : [],
107
+ };
108
+ },
109
+ };
110
+ }