@opengeni/api-router 0.21.11 → 0.22.2

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.
@@ -13,14 +13,15 @@ import {
13
13
  OPENGENI_API_CONTRACT_REVISION,
14
14
  OPENGENI_CORRELATION_HEADER,
15
15
  resolveWorkspaceMemoryEnabled,
16
- VOICE_INPUT_ACCEPTED_MIME_TYPES as VOICE_INPUT_ACCEPTED_MIME_TYPES2
16
+ VOICE_INPUT_ACCEPTED_MIME_TYPES as VOICE_INPUT_ACCEPTED_MIME_TYPES2,
17
+ TRANSCRIPTION_RECORDING_PROVIDER_SEGMENT_SECONDS as TRANSCRIPTION_RECORDING_PROVIDER_SEGMENT_SECONDS2
17
18
  } from "@opengeni/contracts";
18
19
  import {
19
20
  createDocumentServices,
20
21
  getDocument as getDocument2,
21
22
  indexDocumentNow
22
23
  } from "@opengeni/documents";
23
- import { dbSql, getWorkspace as getWorkspace5, rlsContextForWorkspace } from "@opengeni/db";
24
+ import { dbSql, getWorkspace as getWorkspace6, rlsContextForWorkspace } from "@opengeni/db";
24
25
  import { createObservability } from "@opengeni/observability";
25
26
  import { createObjectStorage } from "@opengeni/storage";
26
27
  import { WebStandardStreamableHTTPServerTransport as WebStandardStreamableHTTPServerTransport3 } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
@@ -49,7 +50,7 @@ var ApiHttpError = class extends HTTPException {
49
50
  import {
50
51
  CodexCompactionV2ProviderLockedError,
51
52
  hasPermission as hasPermission15,
52
- requireAccessGrant as requireAccessGrant24,
53
+ requireAccessGrant as requireAccessGrant25,
53
54
  requirePermission,
54
55
  requireSessionAuthorization as requireSessionAuthorization3,
55
56
  SessionAuthorizationDeniedError as SessionAuthorizationDeniedError2,
@@ -347,7 +348,6 @@ import {
347
348
  getPreferenceRegistryFullContent,
348
349
  getVariableSet,
349
350
  getVariableSetByName,
350
- areGitHubRepositoriesAllowedForWorkspace,
351
351
  listScheduledTaskRuns,
352
352
  listScheduledTasks,
353
353
  listSessionEventPage,
@@ -386,7 +386,6 @@ import {
386
386
  import { appendAndPublishEvents as appendAndPublishEvents2, publishDurableSessionEvents } from "@opengeni/events";
387
387
  import {
388
388
  createSignedState as createSignedState3,
389
- createGitHubAppInstallationToken,
390
389
  GitHubAppConfigurationError,
391
390
  githubAppMissingSettings
392
391
  } from "@opengeni/github";
@@ -6253,7 +6252,6 @@ var FIRST_PARTY_TOOL_AUTHORIZATION = {
6253
6252
  variable_set_set_variable: { allOf: ["variable-sets:manage"] },
6254
6253
  environment_set_variable: { allOf: ["variable-sets:manage"] },
6255
6254
  github_connect_link: { allOf: ["github:use"] },
6256
- github_token: { sessionRequired: true, allOf: ["github:use"] },
6257
6255
  github_repositories_list: { allOf: ["github:use"] },
6258
6256
  social_connections_list: { allOf: ["connections:read"] },
6259
6257
  social_posts_recent: { allOf: ["connections:read"] },
@@ -6411,9 +6409,6 @@ function buildOpenGeniMcpServer(deps, grant, options = {}) {
6411
6409
  registerVariableSetTools(server, deps, grant, can, json);
6412
6410
  if (can("github:use")) {
6413
6411
  registerGitHubConnectTool(server, deps, grant, options, json);
6414
- if (sessionId !== null) {
6415
- registerGitHubTokenTool(server, deps, grant, sessionId, json);
6416
- }
6417
6412
  }
6418
6413
  if (!toolspaceMode || can("github:use")) {
6419
6414
  server.registerTool(
@@ -7631,7 +7626,7 @@ function registerFleetTools(server, deps, grant, sessionId, json) {
7631
7626
  server.registerTool(
7632
7627
  "sandboxes_list",
7633
7628
  {
7634
- description: "List the sandboxes this session can run on: its own session sandbox plus enrolled selfhosted machines. `liveness` is conservative: online requires observed provider existence and verified workspace readiness. Provider, lease, route, archive, restore, workspace, lease epoch, and route epoch are also reported separately. Use an entry `id` as an attach/swap/run_on target.",
7629
+ description: "List the sandboxes this session can run on: its own session sandbox plus enrolled selfhosted machines. `liveness` is conservative: online requires observed provider existence and verified workspace readiness. An idle session-home sandbox may report offline/cold/draining and still wake or restore on the next ordinary sandbox operation; never infer that shell/files are unavailable from list liveness alone\u2014only a typed operation/attach failure proves that. Provider, lease, route, archive, restore, workspace, lease epoch, and route epoch are also reported separately. Use an entry `id` as an attach/swap/run_on target.",
7635
7630
  inputSchema: {}
7636
7631
  },
7637
7632
  async () => json(await listFleet(services, await fleetContext()))
@@ -8508,50 +8503,6 @@ function registerGitHubConnectTool(server, deps, grant, options, json) {
8508
8503
  }
8509
8504
  );
8510
8505
  }
8511
- function registerGitHubTokenTool(server, deps, grant, sessionId, json) {
8512
- server.registerTool(
8513
- "github_token",
8514
- {
8515
- description: "Mint a fresh short-lived GitHub token for this session's repositories. Write it to $OPENGENI_GIT_TOKEN_FILE (default $HOME/.opengeni/git-token) to refresh git auth before the current token expires.",
8516
- inputSchema: {}
8517
- },
8518
- async () => {
8519
- const session = await requireSession(deps.db, grant.workspaceId, sessionId);
8520
- const selected = (session.resources ?? []).flatMap((resource) => {
8521
- if (resource.kind !== "repository") {
8522
- return [];
8523
- }
8524
- const installationId2 = resource.githubInstallationId;
8525
- const repositoryId = resource.githubRepositoryId;
8526
- return typeof installationId2 === "number" && installationId2 > 0 && typeof repositoryId === "number" && repositoryId > 0 ? [{ installationId: installationId2, repositoryId }] : [];
8527
- });
8528
- if (selected.length === 0) {
8529
- throw new Error("this session has no GitHub App repository resources to mint a token for");
8530
- }
8531
- const installationId = selected[0].installationId;
8532
- if (selected.some((item) => item.installationId !== installationId)) {
8533
- throw new Error("GitHub App repository resources must belong to one installation");
8534
- }
8535
- const repositoryIds = selected.map((item) => item.repositoryId);
8536
- if (!await areGitHubRepositoriesAllowedForWorkspace(
8537
- deps.db,
8538
- grant.workspaceId,
8539
- installationId,
8540
- repositoryIds
8541
- )) {
8542
- throw new Error("this workspace no longer authorizes the session's GitHub repositories");
8543
- }
8544
- const token = await createGitHubAppInstallationToken(deps.settings, {
8545
- installationId,
8546
- repositoryIds
8547
- });
8548
- return json({
8549
- token,
8550
- tokenFile: "$OPENGENI_GIT_TOKEN_FILE (default $HOME/.opengeni/git-token)"
8551
- });
8552
- }
8553
- );
8554
- }
8555
8506
  function requireVariableSetsUseForMcpAttachment(grant, variableSetId) {
8556
8507
  if (variableSetId !== void 0 && !hasPermission4(grant.permissions, "variable-sets:use")) {
8557
8508
  throw new Error("missing permission: variable-sets:use");
@@ -8937,6 +8888,8 @@ import {
8937
8888
  boundedParallelMap,
8938
8889
  cancelMcpResponseBody,
8939
8890
  guardedMcpFetch,
8891
+ mcpJsonRpcErrorPayloadForRequest,
8892
+ mcpRequestReplayInfo,
8940
8893
  mcpSerializedSizeBytes
8941
8894
  } from "@opengeni/runtime/mcp-network";
8942
8895
  import { Buffer as Buffer4 } from "buffer";
@@ -8948,8 +8901,12 @@ var TOOLSPACE_AUTH_NEEDED_ERROR = {
8948
8901
  code: 40101,
8949
8902
  message: "Authentication required - a connection link was posted to the session."
8950
8903
  };
8904
+ var TOOLSPACE_TOOL_OUTCOME_UNCERTAIN_ERROR = {
8905
+ code: 40102,
8906
+ message: "Tool outcome uncertain: the provider returned 401 after receiving the request. OpenGeni did not replay this call. Do not retry automatically; verify provider state before any new attempt."
8907
+ };
8951
8908
  var TOOLSPACE_NO_ACTIVE_TURN_MESSAGE = "no active turn - toolspace calls require an in-flight turn";
8952
- var FIRST_PARTY_PROXY_IDS = /* @__PURE__ */ new Set(["files", "docs"]);
8909
+ var FIRST_PARTY_PROXY_IDS = /* @__PURE__ */ new Set(["files", "docs", "codex_apps"]);
8953
8910
  var TOOLSPACE_TOOL_LIST_TTL_MS = 3e4;
8954
8911
  var TOOLSPACE_TOOL_LIST_CACHE_MAX_ENTRIES = 2e3;
8955
8912
  var TOOLSPACE_TOOL_LIST_CACHE_MAX_BYTES = 64 * 1024 * 1024;
@@ -9462,6 +9419,9 @@ async function callRemoteTool(deps, server, toolName, args) {
9462
9419
  if (isToolspaceAuthNeededError(error)) {
9463
9420
  return mcpError(TOOLSPACE_AUTH_NEEDED_ERROR.message);
9464
9421
  }
9422
+ if (isToolspaceOutcomeUncertainError(error)) {
9423
+ return mcpError(TOOLSPACE_TOOL_OUTCOME_UNCERTAIN_ERROR.message);
9424
+ }
9465
9425
  deps.observability?.warn("toolspace upstream tool call failed", {
9466
9426
  serverId: server.config.id,
9467
9427
  toolName,
@@ -9588,7 +9548,7 @@ function connectionBrokerFetch(baseFetch, input) {
9588
9548
  ownerHasWorkspaceMembership: delegatedOwnerHasMembership
9589
9549
  });
9590
9550
  return async (requestInput, init) => {
9591
- const request = await mcpRequestInfo(requestInput, init);
9551
+ const request = await mcpRequestReplayInfo(requestInput, init);
9592
9552
  const destinationUrl = mcpRequestDestinationUrl(requestInput);
9593
9553
  const resolverSubjectId = connectionRef.subjectScope !== "subject" && hostCredentialPort ? input.grant.subjectId : void 0;
9594
9554
  const resolve = async (forceRefresh) => {
@@ -9613,10 +9573,22 @@ function connectionBrokerFetch(baseFetch, input) {
9613
9573
  );
9614
9574
  if (response.status === 401) {
9615
9575
  await cancelMcpResponseBody(response);
9616
- const refreshed = await resolve(true);
9576
+ let refreshed;
9577
+ try {
9578
+ refreshed = await resolve(true);
9579
+ } catch {
9580
+ refreshed = authNeededFromStatus(input.config, first, "refresh_failed");
9581
+ }
9617
9582
  if (refreshed.status === "auth_needed") {
9583
+ if (!request.replaySafeAfter401) {
9584
+ await publishToolspaceAuthNeeded(input, request, refreshed);
9585
+ return toolspaceMcpOutcomeUncertainResponse(request);
9586
+ }
9618
9587
  return await authNeededFetchResponse(input, request, refreshed);
9619
9588
  }
9589
+ if (!request.replaySafeAfter401) {
9590
+ return toolspaceMcpOutcomeUncertainResponse(request);
9591
+ }
9620
9592
  const retry = await baseFetch(
9621
9593
  fetchInputForAttempt(requestInput),
9622
9594
  withConnectionHeaders(requestInput, init, refreshed.headers)
@@ -9664,6 +9636,13 @@ function authNeededFromStatus(config, first, reason) {
9664
9636
  };
9665
9637
  }
9666
9638
  async function authNeededFetchResponse(input, request, auth) {
9639
+ await publishToolspaceAuthNeeded(input, request, auth);
9640
+ if (request.method === "tools/call") {
9641
+ return toolspaceMcpErrorResponse(request.id, TOOLSPACE_AUTH_NEEDED_ERROR);
9642
+ }
9643
+ return new Response("Authentication required for MCP server connection", { status: 401 });
9644
+ }
9645
+ async function publishToolspaceAuthNeeded(input, request, auth) {
9667
9646
  await appendAndPublishEvents3(
9668
9647
  input.deps.db,
9669
9648
  input.deps.bus,
@@ -9689,42 +9668,30 @@ async function authNeededFetchResponse(input, request, auth) {
9689
9668
  }
9690
9669
  ]
9691
9670
  ).catch(() => void 0);
9692
- if (request.method === "tools/call") {
9693
- return new Response(
9694
- JSON.stringify({
9695
- jsonrpc: "2.0",
9696
- id: request.id ?? null,
9697
- error: {
9698
- code: TOOLSPACE_AUTH_NEEDED_ERROR.code,
9699
- message: TOOLSPACE_AUTH_NEEDED_ERROR.message
9700
- }
9701
- }),
9702
- {
9703
- status: 200,
9704
- headers: { "content-type": "application/json" }
9705
- }
9706
- );
9707
- }
9708
- return new Response("Authentication required for MCP server connection", { status: 401 });
9709
9671
  }
9710
- async function mcpRequestInfo(input, init) {
9711
- const body2 = typeof init?.body === "string" ? init.body : input instanceof Request && (init?.method ?? input.method).toUpperCase() === "POST" ? await input.clone().text().catch(() => "") : "";
9712
- if (!body2) {
9713
- return {};
9714
- }
9715
- try {
9716
- const parsed = JSON.parse(body2);
9717
- const method = typeof parsed.method === "string" ? parsed.method : void 0;
9718
- const id = typeof parsed.id === "string" || typeof parsed.id === "number" || parsed.id === null ? parsed.id : void 0;
9719
- const toolName = method === "tools/call" && typeof parsed.params?.name === "string" ? parsed.params.name : void 0;
9720
- return {
9721
- ...method ? { method } : {},
9722
- ...id !== void 0 ? { id } : {},
9723
- ...toolName ? { toolName } : {}
9724
- };
9725
- } catch {
9726
- return {};
9727
- }
9672
+ function toolspaceMcpErrorResponse(id, error) {
9673
+ return new Response(
9674
+ JSON.stringify({
9675
+ jsonrpc: "2.0",
9676
+ id: id ?? null,
9677
+ error
9678
+ }),
9679
+ {
9680
+ status: 200,
9681
+ headers: { "content-type": "application/json" }
9682
+ }
9683
+ );
9684
+ }
9685
+ function toolspaceMcpOutcomeUncertainResponse(request) {
9686
+ return new Response(
9687
+ JSON.stringify(
9688
+ mcpJsonRpcErrorPayloadForRequest(request, TOOLSPACE_TOOL_OUTCOME_UNCERTAIN_ERROR)
9689
+ ),
9690
+ {
9691
+ status: 200,
9692
+ headers: { "content-type": "application/json" }
9693
+ }
9694
+ );
9728
9695
  }
9729
9696
  function withConnectionHeaders(input, init, authHeaders) {
9730
9697
  const headers = new Headers(
@@ -9745,6 +9712,13 @@ function isToolspaceAuthNeededError(error) {
9745
9712
  const code = error.code;
9746
9713
  return code === TOOLSPACE_AUTH_NEEDED_ERROR.code && (error.message === TOOLSPACE_AUTH_NEEDED_ERROR.message || error.message === `MCP error ${TOOLSPACE_AUTH_NEEDED_ERROR.code}: ${TOOLSPACE_AUTH_NEEDED_ERROR.message}`);
9747
9714
  }
9715
+ function isToolspaceOutcomeUncertainError(error) {
9716
+ if (!(error instanceof Error)) {
9717
+ return false;
9718
+ }
9719
+ const code = error.code;
9720
+ return code === TOOLSPACE_TOOL_OUTCOME_UNCERTAIN_ERROR.code && (error.message === TOOLSPACE_TOOL_OUTCOME_UNCERTAIN_ERROR.message || error.message === `MCP error ${TOOLSPACE_TOOL_OUTCOME_UNCERTAIN_ERROR.code}: ${TOOLSPACE_TOOL_OUTCOME_UNCERTAIN_ERROR.message}`);
9721
+ }
9748
9722
 
9749
9723
  // src/app.ts
9750
9724
  import { boundedMcpRequest, McpPayloadTooLargeError as McpPayloadTooLargeError2 } from "@opengeni/runtime/mcp-network";
@@ -10166,6 +10140,8 @@ import {
10166
10140
  buildCodexTokenResolver,
10167
10141
  claimCodexResetRedemption,
10168
10142
  completeCodexResetRedemption,
10143
+ clearCodexAppsCredential,
10144
+ designateCodexAppsCredential,
10169
10145
  disconnectAllCodexAccounts,
10170
10146
  disconnectCodexAccount,
10171
10147
  encryptEnvironmentValue as encryptEnvironmentValue3,
@@ -10175,6 +10151,7 @@ import {
10175
10151
  fenceCodexResetRedemptionSend,
10176
10152
  getCodexResetRedemptionAttempt,
10177
10153
  getCodexCredentialStatus,
10154
+ getCodexAppsSettings,
10178
10155
  getCodexRotationSettings,
10179
10156
  listPendingCodexCapacityWakeTargets,
10180
10157
  listCodexAccountStatuses,
@@ -10255,7 +10232,7 @@ async function verifyCodexRedemptionConfirmation(secret, token, now = Date.now()
10255
10232
 
10256
10233
  // src/routes/codex.ts
10257
10234
  var CODEX_PROVIDER_LABEL = "Codex subscription \xB7 no credits";
10258
- function codexAccountJson(row) {
10235
+ function codexAccountJson(row, options = {}) {
10259
10236
  return {
10260
10237
  id: row.id,
10261
10238
  chatgptAccountId: row.chatgptAccountId,
@@ -10284,7 +10261,9 @@ function codexAccountJson(row) {
10284
10261
  resetCreditAvailableCount: row.resetCreditAvailableCount,
10285
10262
  resetCreditsCheckedAt: row.resetCreditsCheckedAt,
10286
10263
  // P3 rotation cooldown: when set and in the future, this account is cooling-down.
10287
- exhaustedUntil: row.exhaustedUntil
10264
+ exhaustedUntil: row.exhaustedUntil,
10265
+ appsDesignated: options.appsCredentialId === row.id,
10266
+ canEnableApps: options.appsCredentialId === null && options.canManageApps === true && options.humanSubjectId !== null && options.humanSubjectId !== void 0 && row.connectedBySubjectId === options.humanSubjectId && row.status === "active"
10288
10267
  };
10289
10268
  }
10290
10269
  function codexUsageJson(payload) {
@@ -10378,6 +10357,23 @@ async function requireRedemptionHuman(c, deps, workspaceId) {
10378
10357
  }
10379
10358
  return { human, accountId: grant.accountId };
10380
10359
  }
10360
+ async function requireCodexAppsHuman(c, deps, workspaceId) {
10361
+ if (c.req.header("authorization")) {
10362
+ throw new HTTPException10(403, {
10363
+ message: "authorization bearer is not allowed for Codex Apps designation"
10364
+ });
10365
+ }
10366
+ requireSameOriginBrowserMutation(c, deps);
10367
+ const human = await managedCookieHuman(c, deps);
10368
+ if (!human) {
10369
+ throw new HTTPException10(401, { message: "managed browser session required" });
10370
+ }
10371
+ const grant = await requireAccessGrant2(c, deps, workspaceId, "connections:write");
10372
+ if (grant.subjectId !== human.subjectId) {
10373
+ throw new HTTPException10(403, { message: "managed browser identity mismatch" });
10374
+ }
10375
+ return { human, accountId: grant.accountId };
10376
+ }
10381
10377
  function cachedUsage(row) {
10382
10378
  const fiveHour = buildCodexUsageWindowFromCache(
10383
10379
  row.primaryUsedPercent,
@@ -10733,15 +10729,32 @@ function registerCodexRoutes(app, deps) {
10733
10729
  });
10734
10730
  app.get("/v1/workspaces/:workspaceId/codex/accounts", async (c) => {
10735
10731
  const workspaceId = c.req.param("workspaceId");
10736
- await requireAccessGrant2(c, deps, workspaceId, "workspace:read");
10737
- const [accounts, rotation] = await Promise.all([
10732
+ const grant = await requireAccessGrant2(c, deps, workspaceId, "workspace:read");
10733
+ const [accounts, rotation, apps, human] = await Promise.all([
10738
10734
  listCodexAccountStatuses(db, workspaceId),
10739
- getCodexRotationSettings(db, workspaceId)
10735
+ getCodexRotationSettings(db, workspaceId),
10736
+ getCodexAppsSettings(db, workspaceId),
10737
+ managedCookieHuman(c, deps)
10740
10738
  ]);
10741
10739
  const activeAccountId = rotation?.activeCredentialId ?? null;
10740
+ const humanSubjectId = human?.subjectId === grant.subjectId ? human.subjectId : null;
10741
+ const canManageApps = humanSubjectId !== null && hasPermission6(grant.permissions, "connections:write");
10742
10742
  return c.json({
10743
- accounts: accounts.map(codexAccountJson),
10743
+ accounts: accounts.map(
10744
+ (account) => codexAccountJson(account, {
10745
+ appsCredentialId: apps.credentialId,
10746
+ canManageApps: settings.codexConnectedAppsEnabled && canManageApps,
10747
+ humanSubjectId
10748
+ })
10749
+ ),
10744
10750
  activeAccountId,
10751
+ apps: {
10752
+ available: settings.codexConnectedAppsEnabled,
10753
+ credentialId: apps.credentialId,
10754
+ version: apps.version,
10755
+ designatedAt: apps.designatedAt,
10756
+ canDisable: canManageApps && apps.credentialId !== null
10757
+ },
10745
10758
  settings: {
10746
10759
  rotationEnabled: rotation?.rotationEnabled ?? false,
10747
10760
  // sharded-rotation policy: rotation-enabled always behaves as sticky-sharded; report the
@@ -10751,6 +10764,72 @@ function registerCodexRoutes(app, deps) {
10751
10764
  }
10752
10765
  });
10753
10766
  });
10767
+ app.post("/v1/workspaces/:workspaceId/codex/apps", async (c) => {
10768
+ const workspaceId = c.req.param("workspaceId");
10769
+ if (!settings.codexConnectedAppsEnabled) {
10770
+ throw new HTTPException10(409, { message: "Codex Apps is disabled for this deployment" });
10771
+ }
10772
+ const { human, accountId } = await requireCodexAppsHuman(c, deps, workspaceId);
10773
+ const parsed = z.object({
10774
+ accountId: z.string().uuid(),
10775
+ expectedVersion: z.number().int().nonnegative()
10776
+ }).safeParse(await c.req.json().catch(() => null));
10777
+ if (!parsed.success) {
10778
+ throw new HTTPException10(400, { message: "accountId and expectedVersion are required" });
10779
+ }
10780
+ const result = await designateCodexAppsCredential(db, {
10781
+ accountId,
10782
+ workspaceId,
10783
+ credentialId: parsed.data.accountId,
10784
+ subjectId: human.subjectId,
10785
+ expectedVersion: parsed.data.expectedVersion
10786
+ });
10787
+ if (result.kind === "not_found") {
10788
+ throw new HTTPException10(404, { message: "codex account not found" });
10789
+ }
10790
+ if (result.kind === "not_owner") {
10791
+ throw new HTTPException10(403, {
10792
+ message: "only the managed human who connected this subscription may designate it"
10793
+ });
10794
+ }
10795
+ if (result.kind === "forbidden") {
10796
+ throw new HTTPException10(403, { message: "missing permission: connections:write" });
10797
+ }
10798
+ if (result.kind === "unavailable") {
10799
+ throw new HTTPException10(409, { message: "codex account requires relogin" });
10800
+ }
10801
+ const response = {
10802
+ credentialId: result.credentialId,
10803
+ version: result.version,
10804
+ designatedAt: result.designatedAt,
10805
+ changed: result.kind === "updated"
10806
+ };
10807
+ return result.kind === "updated" ? c.json(response) : c.json(response, 409);
10808
+ });
10809
+ app.delete("/v1/workspaces/:workspaceId/codex/apps", async (c) => {
10810
+ const workspaceId = c.req.param("workspaceId");
10811
+ const { human, accountId } = await requireCodexAppsHuman(c, deps, workspaceId);
10812
+ const parsed = z.object({ expectedVersion: z.number().int().nonnegative() }).safeParse(await c.req.json().catch(() => null));
10813
+ if (!parsed.success) {
10814
+ throw new HTTPException10(400, { message: "expectedVersion is required" });
10815
+ }
10816
+ const result = await clearCodexAppsCredential(db, {
10817
+ accountId,
10818
+ workspaceId,
10819
+ subjectId: human.subjectId,
10820
+ expectedVersion: parsed.data.expectedVersion
10821
+ });
10822
+ if (result.kind === "forbidden") {
10823
+ throw new HTTPException10(403, { message: "missing permission: connections:write" });
10824
+ }
10825
+ const response = {
10826
+ credentialId: result.credentialId,
10827
+ version: result.version,
10828
+ designatedAt: result.designatedAt,
10829
+ changed: result.kind === "updated"
10830
+ };
10831
+ return result.kind === "conflict" ? c.json(response, 409) : c.json(response);
10832
+ });
10754
10833
  app.post("/v1/workspaces/:workspaceId/codex/accounts/:accountId/activate", async (c) => {
10755
10834
  const workspaceId = c.req.param("workspaceId");
10756
10835
  await requireAccessGrant2(c, deps, workspaceId, "workspace:admin");
@@ -10859,13 +10938,13 @@ function registerCodexRoutes(app, deps) {
10859
10938
  });
10860
10939
  app.delete("/v1/workspaces/:workspaceId/codex/accounts/:accountId", async (c) => {
10861
10940
  const workspaceId = c.req.param("workspaceId");
10862
- await requireAccessGrant2(c, deps, workspaceId, "workspace:admin");
10941
+ const grant = await requireAccessGrant2(c, deps, workspaceId, "workspace:admin");
10863
10942
  const accountId = c.req.param("accountId");
10864
10943
  const mutation = await withCodexCapacityMutation(
10865
10944
  db,
10866
10945
  { workspaceId, reason: "codex_credential_disconnected" },
10867
10946
  async (tx) => {
10868
- const result2 = await disconnectCodexAccount(tx, workspaceId, accountId);
10947
+ const result2 = await disconnectCodexAccount(tx, workspaceId, accountId, grant.subjectId);
10869
10948
  return { result: result2, changed: result2.removed };
10870
10949
  }
10871
10950
  );
@@ -10880,12 +10959,12 @@ function registerCodexRoutes(app, deps) {
10880
10959
  });
10881
10960
  app.delete("/v1/workspaces/:workspaceId/codex", async (c) => {
10882
10961
  const workspaceId = c.req.param("workspaceId");
10883
- await requireAccessGrant2(c, deps, workspaceId, "workspace:admin");
10962
+ const grant = await requireAccessGrant2(c, deps, workspaceId, "workspace:admin");
10884
10963
  const mutation = await withCodexCapacityMutation(
10885
10964
  db,
10886
10965
  { workspaceId, reason: "codex_credentials_disconnected" },
10887
10966
  async (tx) => {
10888
- const result2 = await disconnectAllCodexAccounts(tx, workspaceId);
10967
+ const result2 = await disconnectAllCodexAccounts(tx, workspaceId, grant.subjectId);
10889
10968
  return { result: result2, changed: result2.removed > 0 };
10890
10969
  }
10891
10970
  );
@@ -23868,17 +23947,871 @@ function registerInsightsRoutes(app, deps) {
23868
23947
 
23869
23948
  // src/routes/transcriptions.ts
23870
23949
  import {
23871
- resolveWorkspaceVoiceInputEnabled
23950
+ resolveWorkspaceVoiceInputEnabled as resolveWorkspaceVoiceInputEnabled2
23951
+ } from "@opengeni/contracts";
23952
+ import { requireAccessGrant as requireAccessGrant23, TranscriptionServiceError as TranscriptionServiceError2 } from "@opengeni/core";
23953
+ import { getWorkspace as getWorkspace4 } from "@opengeni/db";
23954
+
23955
+ // src/routes/transcription-recordings.ts
23956
+ import { createHash as createHash11 } from "crypto";
23957
+ import {
23958
+ CreateTranscriptionRecordingRequest,
23959
+ FinalizeTranscriptionRecordingRequest,
23960
+ resolveWorkspaceVoiceInputEnabled,
23961
+ TRANSCRIPTION_RECORDING_PROVIDER_SEGMENT_SECONDS,
23962
+ TRANSCRIPTION_RECORDING_RECOVERY_RETRY_AFTER_MILLISECONDS
23872
23963
  } from "@opengeni/contracts";
23873
- import { requireAccessGrant as requireAccessGrant22, TranscriptionServiceError } from "@opengeni/core";
23964
+ import {
23965
+ isAcceptedMimeType,
23966
+ normalizeMimeType,
23967
+ requireAccessGrant as requireAccessGrant22,
23968
+ TRANSCRIPTION_PROVIDER_REQUEST_TIMEOUT_MILLISECONDS,
23969
+ TranscriptionServiceError
23970
+ } from "@opengeni/core";
23971
+ import {
23972
+ claimNextTranscriptionRecordingSegment,
23973
+ claimTranscriptionRecordingAssembly,
23974
+ completeTranscriptionRecordingAssembly,
23975
+ completeTranscriptionRecordingChunk,
23976
+ completeTranscriptionRecordingSegment,
23977
+ completeTranscriptionRecordingSegmentPreparation,
23978
+ createTranscriptionRecording,
23979
+ discardTranscriptionRecording,
23980
+ failTranscriptionRecordingAssembly,
23981
+ failTranscriptionRecordingSegment,
23982
+ getTranscriptionRecording,
23983
+ listTranscriptionRecordings,
23984
+ listTranscriptionRecordingChunks,
23985
+ markTranscriptionRecordingObjectCleaned,
23986
+ markTranscriptionRecordingObjectsCleaned,
23987
+ reserveTranscriptionRecordingChunk,
23988
+ reserveTranscriptionRecordingSegment,
23989
+ startTranscriptionRecordingSegmentProviderCall,
23990
+ transcriptionRecordingObjectKeys,
23991
+ TranscriptionRecordingConflictError,
23992
+ TranscriptionRecordingNotFoundError,
23993
+ TranscriptionRecordingStateError
23994
+ } from "@opengeni/db";
23874
23995
  import { getWorkspace as getWorkspace3 } from "@opengeni/db";
23996
+
23997
+ // src/transcription/segmenter.ts
23998
+ import { spawn } from "child_process";
23999
+ import { createWriteStream } from "fs";
24000
+ import { mkdtemp, readFile as readFile2, readdir, rm } from "fs/promises";
24001
+ import { tmpdir } from "os";
24002
+ import { join } from "path";
24003
+ import { once } from "events";
24004
+ var STDERR_MAX_BYTES = 64 * 1024;
24005
+ var TranscriptionSegmenterError = class extends Error {
24006
+ constructor(message, code, retryable) {
24007
+ super(message);
24008
+ this.code = code;
24009
+ this.retryable = retryable;
24010
+ }
24011
+ name = "TranscriptionSegmenterError";
24012
+ };
24013
+ function createFfmpegTranscriptionSegmenter(input) {
24014
+ let availability = null;
24015
+ return {
24016
+ available() {
24017
+ availability ??= commandSucceeds(input.ffmpegPath, ["-version"]);
24018
+ return availability;
24019
+ },
24020
+ async *segment(request) {
24021
+ if (request.signal?.aborted) {
24022
+ throw new TranscriptionSegmenterError(
24023
+ "Audio segmentation was cancelled",
24024
+ "cancelled",
24025
+ true
24026
+ );
24027
+ }
24028
+ if (!Number.isSafeInteger(request.providerSegmentSeconds) || request.providerSegmentSeconds <= 0 || !Number.isSafeInteger(request.totalDurationMilliseconds) || request.totalDurationMilliseconds <= 0) {
24029
+ throw new TranscriptionSegmenterError(
24030
+ "Audio segmentation bounds are invalid",
24031
+ "invalid_audio",
24032
+ false
24033
+ );
24034
+ }
24035
+ if (Math.ceil(request.totalDurationMilliseconds / (request.providerSegmentSeconds * 1e3)) > 1e3) {
24036
+ throw new TranscriptionSegmenterError(
24037
+ "Audio exceeds the bounded segment projection",
24038
+ "too_large",
24039
+ false
24040
+ );
24041
+ }
24042
+ const directory = await mkdtemp(join(tmpdir(), "opengeni-transcription-"));
24043
+ const inputPath = join(
24044
+ directory,
24045
+ `recording.${extensionForMimeType(request.sourceMimeType)}`
24046
+ );
24047
+ const outputPattern = join(directory, "segment-%06d.wav");
24048
+ try {
24049
+ await writeChunks(inputPath, request.chunks, request.signal);
24050
+ const result = await runCommand(
24051
+ input.ffmpegPath,
24052
+ [
24053
+ "-nostdin",
24054
+ "-hide_banner",
24055
+ "-loglevel",
24056
+ "error",
24057
+ "-y",
24058
+ "-i",
24059
+ inputPath,
24060
+ "-map",
24061
+ "0:a:0",
24062
+ "-vn",
24063
+ "-ac",
24064
+ "1",
24065
+ "-ar",
24066
+ "16000",
24067
+ "-c:a",
24068
+ "pcm_s16le",
24069
+ "-f",
24070
+ "segment",
24071
+ "-segment_time",
24072
+ String(request.providerSegmentSeconds),
24073
+ "-reset_timestamps",
24074
+ "1",
24075
+ outputPattern
24076
+ ],
24077
+ request.signal
24078
+ );
24079
+ if (result.cancelled) {
24080
+ throw new TranscriptionSegmenterError(
24081
+ "Audio segmentation was cancelled",
24082
+ "cancelled",
24083
+ true
24084
+ );
24085
+ }
24086
+ if (result.spawnError) {
24087
+ throw new TranscriptionSegmenterError(
24088
+ "Audio segmentation is unavailable",
24089
+ "unavailable",
24090
+ true
24091
+ );
24092
+ }
24093
+ if (result.exitCode !== 0) {
24094
+ throw new TranscriptionSegmenterError(
24095
+ result.stderr || "Audio could not be decoded",
24096
+ "invalid_audio",
24097
+ false
24098
+ );
24099
+ }
24100
+ const files = (await readdir(directory)).filter((file) => /^segment-[0-9]{6}\.wav$/.test(file)).sort();
24101
+ if (files.length === 0 || files.length > 1e3) {
24102
+ throw new TranscriptionSegmenterError(
24103
+ "Audio did not produce a bounded segment set",
24104
+ "invalid_audio",
24105
+ false
24106
+ );
24107
+ }
24108
+ const segmentMilliseconds = request.providerSegmentSeconds * 1e3;
24109
+ for (let segmentNumber = 0; segmentNumber < files.length; segmentNumber += 1) {
24110
+ if (request.signal?.aborted) {
24111
+ throw new TranscriptionSegmenterError(
24112
+ "Audio segmentation was cancelled",
24113
+ "cancelled",
24114
+ true
24115
+ );
24116
+ }
24117
+ const bytes = new Uint8Array(await readFile2(join(directory, files[segmentNumber])));
24118
+ if (bytes.byteLength === 0) {
24119
+ throw new TranscriptionSegmenterError(
24120
+ "Audio produced an empty segment",
24121
+ "invalid_audio",
24122
+ false
24123
+ );
24124
+ }
24125
+ const startMilliseconds = segmentNumber * segmentMilliseconds;
24126
+ const remaining = request.totalDurationMilliseconds - startMilliseconds;
24127
+ if (remaining <= 0) {
24128
+ throw new TranscriptionSegmenterError(
24129
+ "Audio segment count exceeds the declared duration",
24130
+ "invalid_audio",
24131
+ false
24132
+ );
24133
+ }
24134
+ yield {
24135
+ segmentNumber,
24136
+ startMilliseconds,
24137
+ durationMilliseconds: Math.min(segmentMilliseconds, remaining),
24138
+ mimeType: "audio/wav",
24139
+ bytes
24140
+ };
24141
+ }
24142
+ } catch (error) {
24143
+ if (error instanceof TranscriptionSegmenterError) throw error;
24144
+ throw new TranscriptionSegmenterError(
24145
+ error instanceof Error ? error.message : "Audio segmentation failed",
24146
+ request.signal?.aborted ? "cancelled" : "unknown",
24147
+ true
24148
+ );
24149
+ } finally {
24150
+ await rm(directory, { recursive: true, force: true }).catch(() => void 0);
24151
+ }
24152
+ }
24153
+ };
24154
+ }
24155
+ async function writeChunks(path, chunks, signal) {
24156
+ const stream = createWriteStream(path, { flags: "wx" });
24157
+ try {
24158
+ for await (const chunk of chunks) {
24159
+ if (signal?.aborted) {
24160
+ throw new TranscriptionSegmenterError(
24161
+ "Audio segmentation was cancelled",
24162
+ "cancelled",
24163
+ true
24164
+ );
24165
+ }
24166
+ if (!stream.write(chunk)) await once(stream, "drain");
24167
+ }
24168
+ stream.end();
24169
+ await once(stream, "close");
24170
+ } catch (error) {
24171
+ stream.destroy();
24172
+ throw error;
24173
+ }
24174
+ }
24175
+ function extensionForMimeType(mimeType) {
24176
+ switch (mimeType.trim().toLowerCase().split(";", 1)[0]) {
24177
+ case "audio/mp4":
24178
+ case "audio/m4a":
24179
+ return "mp4";
24180
+ case "audio/ogg":
24181
+ return "ogg";
24182
+ case "audio/mpeg":
24183
+ case "audio/mp3":
24184
+ return "mp3";
24185
+ case "audio/wav":
24186
+ case "audio/x-wav":
24187
+ return "wav";
24188
+ case "audio/webm":
24189
+ default:
24190
+ return "webm";
24191
+ }
24192
+ }
24193
+ async function commandSucceeds(command, args) {
24194
+ const result = await runCommand(command, args);
24195
+ return !result.spawnError && result.exitCode === 0;
24196
+ }
24197
+ async function runCommand(command, args, signal) {
24198
+ return await new Promise((resolve) => {
24199
+ const child = spawn(command, args, { stdio: ["ignore", "ignore", "pipe"] });
24200
+ let stderr = Buffer.alloc(0);
24201
+ let spawnError = false;
24202
+ let settled = false;
24203
+ const onAbort = () => child.kill("SIGKILL");
24204
+ signal?.addEventListener("abort", onAbort, { once: true });
24205
+ if (signal?.aborted) onAbort();
24206
+ child.stderr.on("data", (chunk) => {
24207
+ if (stderr.byteLength >= STDERR_MAX_BYTES) return;
24208
+ const remaining = STDERR_MAX_BYTES - stderr.byteLength;
24209
+ stderr = Buffer.concat([stderr, Buffer.from(chunk).subarray(0, remaining)]);
24210
+ });
24211
+ child.once("error", () => {
24212
+ spawnError = true;
24213
+ });
24214
+ child.once("close", (exitCode) => {
24215
+ if (settled) return;
24216
+ settled = true;
24217
+ signal?.removeEventListener("abort", onAbort);
24218
+ resolve({
24219
+ exitCode,
24220
+ stderr: stderr.toString("utf8").trim(),
24221
+ spawnError,
24222
+ cancelled: signal?.aborted ?? false
24223
+ });
24224
+ });
24225
+ });
24226
+ }
24227
+
24228
+ // src/routes/transcription-recordings.ts
24229
+ var CHUNK_SHA256_HEADER = "x-opengeni-chunk-sha256";
24230
+ var CHUNK_START_HEADER = "x-opengeni-chunk-start-milliseconds";
24231
+ var CHUNK_DURATION_HEADER = "x-opengeni-chunk-duration-milliseconds";
24232
+ var PROCESSING_LEASE_MILLISECONDS = 15 * 60 * 1e3;
24233
+ var RecordingProcessingError = class extends Error {
24234
+ constructor(message, code, retryable) {
24235
+ super(message);
24236
+ this.code = code;
24237
+ this.retryable = retryable;
24238
+ }
24239
+ name = "RecordingProcessingError";
24240
+ };
24241
+ function registerResumableTranscriptionRoutes(app, deps) {
24242
+ app.get("/v1/workspaces/:workspaceId/transcription-recordings", async (c) => {
24243
+ try {
24244
+ const authority = await requireRecordingAuthority(c, deps, false);
24245
+ return c.json({
24246
+ recordings: await listTranscriptionRecordings(deps.db, {
24247
+ workspaceId: authority.workspaceId,
24248
+ subjectId: authority.subjectId
24249
+ })
24250
+ });
24251
+ } catch (error) {
24252
+ return routeError(c, error);
24253
+ }
24254
+ });
24255
+ app.post("/v1/workspaces/:workspaceId/transcription-recordings", async (c) => {
24256
+ try {
24257
+ const authority = await requireRecordingAuthority(c, deps, true);
24258
+ if (!await resumableAvailable(deps, authority.workspaceId)) {
24259
+ return c.json({ code: "unavailable" }, 503);
24260
+ }
24261
+ const parsed = CreateTranscriptionRecordingRequest.safeParse(await jsonBody(c));
24262
+ if (!parsed.success) return c.json({ code: "invalid_request" }, 400);
24263
+ const mimeType = normalizeMimeType(parsed.data.mimeType);
24264
+ if (!isAcceptedMimeType(mimeType, deps.transcription.limits().acceptedMimeTypes)) {
24265
+ return c.json({ code: "not_supported" }, 415);
24266
+ }
24267
+ const recording = await createTranscriptionRecording(deps.db, {
24268
+ ...authority,
24269
+ recordingId: parsed.data.recordingId,
24270
+ mimeType,
24271
+ expiresAt: new Date(Date.now() + deps.settings.voiceInputResumableRetentionSeconds * 1e3)
24272
+ });
24273
+ return c.json(recording, 201);
24274
+ } catch (error) {
24275
+ return routeError(c, error);
24276
+ }
24277
+ });
24278
+ app.get("/v1/workspaces/:workspaceId/transcription-recordings/:recordingId", async (c) => {
24279
+ try {
24280
+ const authority = await requireRecordingAuthority(c, deps, false);
24281
+ const response = await getTranscriptionRecording(deps.db, {
24282
+ ...authority,
24283
+ recordingId: uuidParam(c, "recordingId")
24284
+ });
24285
+ return c.json(withRecoveryRetryHint(await cleanupTerminalObjects(deps, authority, response)));
24286
+ } catch (error) {
24287
+ return routeError(c, error);
24288
+ }
24289
+ });
24290
+ app.put(
24291
+ "/v1/workspaces/:workspaceId/transcription-recordings/:recordingId/chunks/:chunkNumber",
24292
+ async (c) => {
24293
+ try {
24294
+ const authority = await requireRecordingAuthority(c, deps, true);
24295
+ if (!deps.objectStorage || !deps.settings.voiceInputResumableEnabled) {
24296
+ return c.json({ code: "unavailable" }, 503);
24297
+ }
24298
+ const chunkNumber = nonnegativeInteger(c.req.param("chunkNumber"));
24299
+ const startMilliseconds = headerInteger(c, CHUNK_START_HEADER, true);
24300
+ const durationMilliseconds = headerInteger(c, CHUNK_DURATION_HEADER, true);
24301
+ const declaredSha256 = c.req.header(CHUNK_SHA256_HEADER)?.trim().toLowerCase() ?? "";
24302
+ if (!/^[0-9a-f]{64}$/.test(declaredSha256)) {
24303
+ return c.json({ code: "invalid_request" }, 400);
24304
+ }
24305
+ const body2 = await readBoundedBody(
24306
+ c.req.raw,
24307
+ deps.settings.voiceInputResumableMaxChunkSizeBytes
24308
+ );
24309
+ const sha256 = sha256Hex2(body2);
24310
+ if (sha256 !== declaredSha256) {
24311
+ return c.json({ code: "conflict" }, 409);
24312
+ }
24313
+ const existing = await getTranscriptionRecording(deps.db, {
24314
+ ...authority,
24315
+ recordingId: uuidParam(c, "recordingId")
24316
+ });
24317
+ if (normalizeMimeType(c.req.header("content-type") ?? "") !== normalizeMimeType(existing.recording.mimeType)) {
24318
+ return c.json({ code: "not_supported" }, 415);
24319
+ }
24320
+ const reservation = await reserveTranscriptionRecordingChunk(deps.db, {
24321
+ ...authority,
24322
+ recordingId: existing.recording.id,
24323
+ chunkNumber,
24324
+ byteLength: body2.byteLength,
24325
+ sha256,
24326
+ startMilliseconds,
24327
+ durationMilliseconds,
24328
+ maxTotalBytes: deps.settings.voiceInputResumableMaxSizeBytes,
24329
+ maxDurationMilliseconds: deps.settings.voiceInputResumableMaxDurationSeconds * 1e3
24330
+ });
24331
+ if (!reservation.deduplicated) {
24332
+ try {
24333
+ await deps.objectStorage.putObject({
24334
+ key: reservation.chunk.objectKey,
24335
+ contentType: existing.recording.mimeType,
24336
+ body: body2,
24337
+ sha256
24338
+ });
24339
+ } catch {
24340
+ throw new RecordingProcessingError("Chunk upload failed", "network", true);
24341
+ }
24342
+ }
24343
+ const completed = await completeTranscriptionRecordingChunk(deps.db, {
24344
+ workspaceId: authority.workspaceId,
24345
+ subjectId: authority.subjectId,
24346
+ recordingId: existing.recording.id,
24347
+ chunkNumber
24348
+ });
24349
+ const response = {
24350
+ recording: completed.recording.recording,
24351
+ chunk: {
24352
+ chunkNumber: completed.chunk.chunkNumber,
24353
+ byteLength: completed.chunk.byteLength,
24354
+ sha256: completed.chunk.sha256,
24355
+ startMilliseconds: completed.chunk.startMilliseconds,
24356
+ durationMilliseconds: completed.chunk.durationMilliseconds,
24357
+ deduplicated: reservation.deduplicated || completed.deduplicated
24358
+ }
24359
+ };
24360
+ return c.json(response);
24361
+ } catch (error) {
24362
+ return routeError(c, error);
24363
+ }
24364
+ }
24365
+ );
24366
+ app.post(
24367
+ "/v1/workspaces/:workspaceId/transcription-recordings/:recordingId/finalize",
24368
+ async (c) => {
24369
+ const owner = correlationId(c);
24370
+ let authority = null;
24371
+ let generation = 0;
24372
+ try {
24373
+ authority = await requireRecordingAuthority(c, deps, true);
24374
+ if (!await resumableAvailable(deps, authority.workspaceId)) {
24375
+ return c.json({ code: "unavailable" }, 503);
24376
+ }
24377
+ const parsed = FinalizeTranscriptionRecordingRequest.safeParse(await jsonBody(c));
24378
+ if (!parsed.success) return c.json({ code: "invalid_request" }, 400);
24379
+ const recordingId = uuidParam(c, "recordingId");
24380
+ const claim = await claimTranscriptionRecordingAssembly(deps.db, {
24381
+ workspaceId: authority.workspaceId,
24382
+ subjectId: authority.subjectId,
24383
+ recordingId,
24384
+ owner,
24385
+ ...parsed.data,
24386
+ staleBefore: new Date(Date.now() - PROCESSING_LEASE_MILLISECONDS)
24387
+ });
24388
+ generation = claim.generation;
24389
+ if (!claim.claimed) {
24390
+ const response = withRecoveryRetryHint(claim.recording);
24391
+ return c.json(response, claim.recording.recording.state === "segmenting" ? 202 : 200);
24392
+ }
24393
+ for (const key of claim.staleObjectKeys) {
24394
+ try {
24395
+ await deps.objectStorage.deleteObject(key);
24396
+ await markTranscriptionRecordingObjectCleaned(deps.db, {
24397
+ workspaceId: authority.workspaceId,
24398
+ subjectId: authority.subjectId,
24399
+ recordingId,
24400
+ objectKey: key
24401
+ });
24402
+ } catch {
24403
+ }
24404
+ }
24405
+ const chunks = await listTranscriptionRecordingChunks(deps.db, {
24406
+ workspaceId: authority.workspaceId,
24407
+ subjectId: authority.subjectId,
24408
+ recordingId
24409
+ });
24410
+ const providerMaxSegmentSeconds = deps.transcription.limits().maxDurationSeconds;
24411
+ const minimumBoundedSegmentSeconds = Math.ceil(
24412
+ claim.recording.recording.totalDurationMilliseconds / 1e3 / 1e3
24413
+ );
24414
+ if (providerMaxSegmentSeconds < minimumBoundedSegmentSeconds) {
24415
+ throw new RecordingProcessingError(
24416
+ "Recording cannot fit the bounded provider segment projection",
24417
+ "too_large",
24418
+ false
24419
+ );
24420
+ }
24421
+ const providerSegmentSeconds = Math.min(
24422
+ TRANSCRIPTION_RECORDING_PROVIDER_SEGMENT_SECONDS,
24423
+ providerMaxSegmentSeconds
24424
+ );
24425
+ for await (const segment of deps.transcriptionSegmenter.segment({
24426
+ sourceMimeType: claim.recording.recording.mimeType,
24427
+ totalDurationMilliseconds: claim.recording.recording.totalDurationMilliseconds,
24428
+ providerSegmentSeconds,
24429
+ chunks: verifiedChunkBytes(deps, chunks, c.req.raw.signal),
24430
+ signal: c.req.raw.signal
24431
+ })) {
24432
+ const sha256 = sha256Hex2(segment.bytes);
24433
+ const reservation = await reserveTranscriptionRecordingSegment(deps.db, {
24434
+ ...authority,
24435
+ recordingId,
24436
+ owner,
24437
+ generation,
24438
+ segmentNumber: segment.segmentNumber,
24439
+ byteLength: segment.bytes.byteLength,
24440
+ sha256,
24441
+ startMilliseconds: segment.startMilliseconds,
24442
+ durationMilliseconds: segment.durationMilliseconds
24443
+ });
24444
+ try {
24445
+ await deps.objectStorage.putObject({
24446
+ key: reservation.objectKey,
24447
+ contentType: segment.mimeType,
24448
+ body: segment.bytes,
24449
+ sha256
24450
+ });
24451
+ } catch {
24452
+ throw new RecordingProcessingError("Segment upload failed", "network", true);
24453
+ }
24454
+ await completeTranscriptionRecordingSegmentPreparation(deps.db, {
24455
+ workspaceId: authority.workspaceId,
24456
+ subjectId: authority.subjectId,
24457
+ recordingId,
24458
+ owner,
24459
+ generation,
24460
+ segmentNumber: segment.segmentNumber
24461
+ });
24462
+ }
24463
+ return c.json(
24464
+ await completeTranscriptionRecordingAssembly(deps.db, {
24465
+ workspaceId: authority.workspaceId,
24466
+ subjectId: authority.subjectId,
24467
+ recordingId,
24468
+ owner,
24469
+ generation
24470
+ })
24471
+ );
24472
+ } catch (error) {
24473
+ if (authority && generation > 0) {
24474
+ const failure = processingFailure(error);
24475
+ const persisted = await failTranscriptionRecordingAssembly(deps.db, {
24476
+ workspaceId: authority.workspaceId,
24477
+ subjectId: authority.subjectId,
24478
+ recordingId: uuidParam(c, "recordingId"),
24479
+ owner,
24480
+ generation,
24481
+ errorCode: failure.code,
24482
+ retryable: failure.retryable
24483
+ }).catch(() => null);
24484
+ if (persisted) return c.json(persisted);
24485
+ }
24486
+ return routeError(c, error);
24487
+ }
24488
+ }
24489
+ );
24490
+ app.post(
24491
+ "/v1/workspaces/:workspaceId/transcription-recordings/:recordingId/process-next",
24492
+ async (c) => {
24493
+ let authority = null;
24494
+ let attemptId = null;
24495
+ let segmentNumber = null;
24496
+ try {
24497
+ authority = await requireRecordingAuthority(c, deps, true);
24498
+ const service = deps.transcription;
24499
+ if (!deps.objectStorage || !service || !await service.available({ workspaceId: authority.workspaceId })) {
24500
+ return c.json({ code: "unavailable" }, 503);
24501
+ }
24502
+ const selectedProvider = service.selectProvider ? await service.selectProvider({ workspaceId: authority.workspaceId }) : "host";
24503
+ if (!selectedProvider) return c.json({ code: "unavailable" }, 503);
24504
+ attemptId = correlationId(c);
24505
+ const claim = await claimNextTranscriptionRecordingSegment(deps.db, {
24506
+ workspaceId: authority.workspaceId,
24507
+ subjectId: authority.subjectId,
24508
+ recordingId: uuidParam(c, "recordingId"),
24509
+ attemptId,
24510
+ providerId: selectedProvider,
24511
+ staleBefore: new Date(Date.now() - PROCESSING_LEASE_MILLISECONDS),
24512
+ providerDeadlineAt: new Date(Date.now() + PROCESSING_LEASE_MILLISECONDS)
24513
+ });
24514
+ if (!claim.claimed || !claim.segment) {
24515
+ const cleaned = await cleanupTerminalObjects(deps, authority, claim.recording);
24516
+ return c.json(
24517
+ withRecoveryRetryHint(cleaned),
24518
+ cleaned.recording.state === "transcribing" ? 202 : 200
24519
+ );
24520
+ }
24521
+ segmentNumber = claim.segment.segmentNumber;
24522
+ const stored = await deps.objectStorage?.getObjectBytes(claim.segment.objectKey);
24523
+ if (!stored) {
24524
+ throw new RecordingProcessingError("Provider segment is missing", "invalid_audio", false);
24525
+ }
24526
+ if (stored.bytes.byteLength !== claim.segment.byteLength || sha256Hex2(stored.bytes) !== claim.segment.sha256) {
24527
+ throw new RecordingProcessingError(
24528
+ "Provider segment failed integrity verification",
24529
+ "invalid_audio",
24530
+ false
24531
+ );
24532
+ }
24533
+ const providerStartedAt = /* @__PURE__ */ new Date();
24534
+ const providerDeadlineAt = new Date(
24535
+ providerStartedAt.getTime() + TRANSCRIPTION_PROVIDER_REQUEST_TIMEOUT_MILLISECONDS
24536
+ );
24537
+ await startTranscriptionRecordingSegmentProviderCall(deps.db, {
24538
+ workspaceId: authority.workspaceId,
24539
+ subjectId: authority.subjectId,
24540
+ recordingId: uuidParam(c, "recordingId"),
24541
+ segmentNumber,
24542
+ attemptId,
24543
+ providerStartedAt,
24544
+ providerDeadlineAt
24545
+ });
24546
+ const result = await service.transcribe({
24547
+ workspaceId: authority.workspaceId,
24548
+ accountId: authority.accountId,
24549
+ audio: stored.bytes,
24550
+ mimeType: "audio/wav",
24551
+ durationSeconds: claim.segment.durationMilliseconds / 1e3,
24552
+ requestId: attemptId,
24553
+ providerDeadlineAt,
24554
+ ...claim.segment.providerId && claim.segment.providerId !== "host" ? { providerId: claim.segment.providerId } : {}
24555
+ });
24556
+ const completed = await completeTranscriptionRecordingSegment(deps.db, {
24557
+ workspaceId: authority.workspaceId,
24558
+ subjectId: authority.subjectId,
24559
+ recordingId: uuidParam(c, "recordingId"),
24560
+ segmentNumber,
24561
+ attemptId,
24562
+ text: result.text,
24563
+ languages: result.languages,
24564
+ providerId: claim.segment.providerId ?? result.providerId
24565
+ });
24566
+ return c.json(await cleanupTerminalObjects(deps, authority, completed));
24567
+ } catch (error) {
24568
+ if (authority && attemptId && segmentNumber !== null) {
24569
+ const failure = processingFailure(error);
24570
+ const persisted = await failTranscriptionRecordingSegment(deps.db, {
24571
+ workspaceId: authority.workspaceId,
24572
+ subjectId: authority.subjectId,
24573
+ recordingId: uuidParam(c, "recordingId"),
24574
+ segmentNumber,
24575
+ attemptId,
24576
+ errorCode: failure.code,
24577
+ retryable: failure.retryable
24578
+ }).catch(() => null);
24579
+ if (persisted) return c.json(persisted);
24580
+ }
24581
+ return routeError(c, error);
24582
+ }
24583
+ }
24584
+ );
24585
+ app.delete("/v1/workspaces/:workspaceId/transcription-recordings/:recordingId", async (c) => {
24586
+ try {
24587
+ const authority = await requireRecordingAuthority(c, deps, false);
24588
+ const discarded = await discardTranscriptionRecording(deps.db, {
24589
+ workspaceId: authority.workspaceId,
24590
+ subjectId: authority.subjectId,
24591
+ recordingId: uuidParam(c, "recordingId")
24592
+ });
24593
+ return c.json(await cleanupTerminalObjects(deps, authority, discarded));
24594
+ } catch (error) {
24595
+ return routeError(c, error);
24596
+ }
24597
+ });
24598
+ }
24599
+ function withRecoveryRetryHint(response) {
24600
+ if (response.recording.state === "segmenting" || response.recording.state === "transcribing") {
24601
+ return {
24602
+ ...response,
24603
+ retryAfterMilliseconds: TRANSCRIPTION_RECORDING_RECOVERY_RETRY_AFTER_MILLISECONDS
24604
+ };
24605
+ }
24606
+ return response;
24607
+ }
24608
+ async function requireRecordingAuthority(c, deps, requirePolicy) {
24609
+ const workspaceId = c.req.param("workspaceId");
24610
+ if (!workspaceId) {
24611
+ throw new TranscriptionRecordingNotFoundError("Workspace not found");
24612
+ }
24613
+ const grant = await requireAccessGrant22(c, deps, workspaceId, "sessions:create");
24614
+ if (requirePolicy) {
24615
+ const workspace = await getWorkspace3(deps.db, workspaceId);
24616
+ if (!workspace) throw new TranscriptionRecordingNotFoundError("Workspace not found");
24617
+ if (resolveWorkspaceVoiceInputEnabled(workspace.settings) === false) {
24618
+ throw new RecordingProcessingError("Voice input is disabled", "policy_blocked", false);
24619
+ }
24620
+ }
24621
+ return {
24622
+ accountId: grant.accountId,
24623
+ workspaceId,
24624
+ subjectId: grant.subjectId
24625
+ };
24626
+ }
24627
+ async function resumableAvailable(deps, workspaceId) {
24628
+ return Boolean(
24629
+ deps.settings.voiceInputResumableEnabled && deps.objectStorage && deps.transcription && deps.transcriptionSegmenter && await deps.transcription.available({ workspaceId }) && await deps.transcriptionSegmenter.available()
24630
+ );
24631
+ }
24632
+ async function cleanupTerminalObjects(deps, authority, response) {
24633
+ if (!deps.objectStorage || response.recording.objectsCleaned || response.recording.state !== "complete" && response.recording.state !== "discarded" && !(response.recording.state === "failed" && !response.recording.retryable)) {
24634
+ return response;
24635
+ }
24636
+ try {
24637
+ const keys = await transcriptionRecordingObjectKeys(deps.db, {
24638
+ workspaceId: authority.workspaceId,
24639
+ subjectId: authority.subjectId,
24640
+ recordingId: response.recording.id
24641
+ });
24642
+ let current = response;
24643
+ for (const key of keys) {
24644
+ await deps.objectStorage.deleteObject(key);
24645
+ current = await markTranscriptionRecordingObjectCleaned(deps.db, {
24646
+ workspaceId: authority.workspaceId,
24647
+ subjectId: authority.subjectId,
24648
+ recordingId: response.recording.id,
24649
+ objectKey: key
24650
+ });
24651
+ }
24652
+ if (current.recording.objectsCleaned) return current;
24653
+ return await markTranscriptionRecordingObjectsCleaned(deps.db, {
24654
+ workspaceId: authority.workspaceId,
24655
+ subjectId: authority.subjectId,
24656
+ recordingId: response.recording.id
24657
+ });
24658
+ } catch {
24659
+ return response;
24660
+ }
24661
+ }
24662
+ async function* verifiedChunkBytes(deps, chunks, signal) {
24663
+ for (const chunk of chunks) {
24664
+ if (signal.aborted) {
24665
+ throw new RecordingProcessingError("Audio assembly was cancelled", "cancelled", true);
24666
+ }
24667
+ let stored;
24668
+ try {
24669
+ stored = await deps.objectStorage.getObjectBytes(chunk.objectKey);
24670
+ } catch {
24671
+ throw new RecordingProcessingError("Chunk download failed", "network", true);
24672
+ }
24673
+ if (!stored) {
24674
+ throw new RecordingProcessingError("Chunk is missing", "invalid_audio", false);
24675
+ }
24676
+ if (stored.bytes.byteLength !== chunk.byteLength || sha256Hex2(stored.bytes) !== chunk.sha256) {
24677
+ throw new RecordingProcessingError(
24678
+ "Chunk integrity verification failed",
24679
+ "invalid_audio",
24680
+ false
24681
+ );
24682
+ }
24683
+ yield stored.bytes;
24684
+ }
24685
+ }
24686
+ function processingFailure(error) {
24687
+ if (error instanceof RecordingProcessingError) {
24688
+ return { code: error.code, retryable: error.retryable };
24689
+ }
24690
+ if (error instanceof TranscriptionSegmenterError) {
24691
+ return { code: error.code, retryable: error.retryable };
24692
+ }
24693
+ if (error instanceof TranscriptionServiceError) {
24694
+ return {
24695
+ code: error.code,
24696
+ retryable: error.retryable || error.code === "cancelled" || error.code === "network" || error.code === "timeout" || error.code === "unavailable" || error.code === "provider"
24697
+ };
24698
+ }
24699
+ if (error instanceof DOMException && error.name === "AbortError") {
24700
+ return { code: "cancelled", retryable: true };
24701
+ }
24702
+ return { code: "unknown", retryable: true };
24703
+ }
24704
+ function routeError(c, error) {
24705
+ if (error instanceof TranscriptionRecordingNotFoundError) {
24706
+ return c.json({ code: "not_found" }, 404);
24707
+ }
24708
+ if (error instanceof TranscriptionRecordingConflictError) {
24709
+ return c.json({ code: "conflict" }, 409);
24710
+ }
24711
+ if (error instanceof TranscriptionRecordingStateError) {
24712
+ return c.json({ code: "invalid_state" }, 409);
24713
+ }
24714
+ if (error instanceof RecordingProcessingError) {
24715
+ const status = error.code === "policy_blocked" ? 403 : error.code === "not_supported" ? 415 : error.code === "too_large" ? 413 : error.code === "invalid_audio" ? 400 : error.code === "unavailable" ? 503 : 502;
24716
+ return c.json({ code: error.code }, status);
24717
+ }
24718
+ return c.json({ code: "unknown" }, 500);
24719
+ }
24720
+ async function jsonBody(c) {
24721
+ try {
24722
+ return await c.req.json();
24723
+ } catch {
24724
+ return null;
24725
+ }
24726
+ }
24727
+ function correlationId(c) {
24728
+ const value = c.req.header("x-opengeni-correlation-id")?.trim();
24729
+ return value && /^[A-Za-z0-9._:-]{1,128}$/.test(value) ? valueAsUuid(value) : crypto.randomUUID();
24730
+ }
24731
+ function valueAsUuid(value) {
24732
+ if (/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value)) {
24733
+ return value;
24734
+ }
24735
+ const hex = createHash11("sha256").update(value).digest("hex").slice(0, 32);
24736
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-4${hex.slice(13, 16)}-a${hex.slice(17, 20)}-${hex.slice(20, 32)}`;
24737
+ }
24738
+ function nonnegativeInteger(value) {
24739
+ if (!/^(0|[1-9][0-9]*)$/.test(value)) {
24740
+ throw new RecordingProcessingError("Invalid integer", "invalid_audio", false);
24741
+ }
24742
+ const parsed = Number(value);
24743
+ if (!Number.isSafeInteger(parsed)) {
24744
+ throw new RecordingProcessingError("Invalid integer", "invalid_audio", false);
24745
+ }
24746
+ return parsed;
24747
+ }
24748
+ function uuidParam(c, name) {
24749
+ const value = c.req.param(name);
24750
+ if (!value || !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value)) {
24751
+ throw new TranscriptionRecordingNotFoundError("Recording not found");
24752
+ }
24753
+ return value;
24754
+ }
24755
+ function headerInteger(c, name, allowZero) {
24756
+ const raw = c.req.header(name) ?? "";
24757
+ const value = nonnegativeInteger(raw);
24758
+ if (!allowZero && value === 0) {
24759
+ throw new RecordingProcessingError("Invalid integer", "invalid_audio", false);
24760
+ }
24761
+ return value;
24762
+ }
24763
+ async function readBoundedBody(request, maxBytes) {
24764
+ const contentLength = Number(request.headers.get("content-length"));
24765
+ if (Number.isFinite(contentLength) && contentLength > maxBytes) {
24766
+ throw new RecordingProcessingError("Chunk is too large", "too_large", false);
24767
+ }
24768
+ if (!request.body) {
24769
+ throw new RecordingProcessingError("Chunk is required", "invalid_audio", false);
24770
+ }
24771
+ const reader = request.body.getReader();
24772
+ const chunks = [];
24773
+ let total = 0;
24774
+ try {
24775
+ for (; ; ) {
24776
+ if (request.signal.aborted) {
24777
+ throw new RecordingProcessingError("Chunk upload was cancelled", "cancelled", true);
24778
+ }
24779
+ const next = await reader.read();
24780
+ if (next.done) break;
24781
+ total += next.value.byteLength;
24782
+ if (total > maxBytes) {
24783
+ await reader.cancel();
24784
+ throw new RecordingProcessingError("Chunk is too large", "too_large", false);
24785
+ }
24786
+ chunks.push(next.value);
24787
+ }
24788
+ } finally {
24789
+ reader.releaseLock();
24790
+ }
24791
+ if (total === 0) {
24792
+ throw new RecordingProcessingError("Chunk is required", "invalid_audio", false);
24793
+ }
24794
+ const body2 = new Uint8Array(total);
24795
+ let offset = 0;
24796
+ for (const chunk of chunks) {
24797
+ body2.set(chunk, offset);
24798
+ offset += chunk.byteLength;
24799
+ }
24800
+ return body2;
24801
+ }
24802
+ function sha256Hex2(bytes) {
24803
+ return createHash11("sha256").update(bytes).digest("hex");
24804
+ }
24805
+
24806
+ // src/routes/transcriptions.ts
23875
24807
  function registerTranscriptionRoutes(app, deps) {
24808
+ registerResumableTranscriptionRoutes(app, deps);
23876
24809
  app.post("/v1/workspaces/:workspaceId/transcriptions", async (c) => {
23877
24810
  const workspaceId = c.req.param("workspaceId");
23878
- const grant = await requireAccessGrant22(c, deps, workspaceId, "sessions:create");
23879
- const workspace = await getWorkspace3(deps.db, workspaceId);
24811
+ const grant = await requireAccessGrant23(c, deps, workspaceId, "sessions:create");
24812
+ const workspace = await getWorkspace4(deps.db, workspaceId);
23880
24813
  if (!workspace) return c.json({ code: "not_found" }, 404);
23881
- if (resolveWorkspaceVoiceInputEnabled(workspace.settings) === false) {
24814
+ if (resolveWorkspaceVoiceInputEnabled2(workspace.settings) === false) {
23882
24815
  return c.json({ code: "policy_blocked" }, 403);
23883
24816
  }
23884
24817
  const service = deps.transcription;
@@ -23902,7 +24835,7 @@ function registerTranscriptionRoutes(app, deps) {
23902
24835
  };
23903
24836
  return c.json(response);
23904
24837
  } catch (error) {
23905
- if (error instanceof TranscriptionServiceError) {
24838
+ if (error instanceof TranscriptionServiceError2) {
23906
24839
  return c.json({ code: error.code }, error.status);
23907
24840
  }
23908
24841
  if (isAbort(error)) return c.json({ code: "cancelled" }, 499);
@@ -23913,7 +24846,7 @@ function registerTranscriptionRoutes(app, deps) {
23913
24846
  async function audioRequest(request, maxSizeBytes) {
23914
24847
  const contentLength = Number(request.headers.get("content-length"));
23915
24848
  if (Number.isFinite(contentLength) && contentLength > maxSizeBytes + 64 * 1024) {
23916
- throw new TranscriptionServiceError({
24849
+ throw new TranscriptionServiceError2({
23917
24850
  code: "too_large",
23918
24851
  message: "Audio is too large."
23919
24852
  });
@@ -23923,13 +24856,13 @@ async function audioRequest(request, maxSizeBytes) {
23923
24856
  const form = await request.formData();
23924
24857
  const audio = form.get("audio");
23925
24858
  if (!(audio instanceof File)) {
23926
- throw new TranscriptionServiceError({
24859
+ throw new TranscriptionServiceError2({
23927
24860
  code: "invalid_audio",
23928
24861
  message: "Audio file is required."
23929
24862
  });
23930
24863
  }
23931
24864
  if (audio.size > maxSizeBytes) {
23932
- throw new TranscriptionServiceError({
24865
+ throw new TranscriptionServiceError2({
23933
24866
  code: "too_large",
23934
24867
  message: "Audio is too large."
23935
24868
  });
@@ -23943,7 +24876,7 @@ async function audioRequest(request, maxSizeBytes) {
23943
24876
  }
23944
24877
  const mimeType = contentType.split(";", 1)[0]?.trim() ?? "";
23945
24878
  if (!mimeType.toLowerCase().startsWith("audio/")) {
23946
- throw new TranscriptionServiceError({
24879
+ throw new TranscriptionServiceError2({
23947
24880
  code: "not_supported",
23948
24881
  message: "Unsupported audio format."
23949
24882
  });
@@ -23957,7 +24890,7 @@ async function audioRequest(request, maxSizeBytes) {
23957
24890
  }
23958
24891
  async function readBounded(body2, maxSizeBytes, signal) {
23959
24892
  if (!body2)
23960
- throw new TranscriptionServiceError({
24893
+ throw new TranscriptionServiceError2({
23961
24894
  code: "invalid_audio",
23962
24895
  message: "Audio is required."
23963
24896
  });
@@ -23972,7 +24905,7 @@ async function readBounded(body2, maxSizeBytes, signal) {
23972
24905
  size += next.value.byteLength;
23973
24906
  if (size > maxSizeBytes) {
23974
24907
  await reader.cancel();
23975
- throw new TranscriptionServiceError({
24908
+ throw new TranscriptionServiceError2({
23976
24909
  code: "too_large",
23977
24910
  message: "Audio is too large."
23978
24911
  });
@@ -23994,7 +24927,7 @@ function durationValue(value) {
23994
24927
  if (value == null || value.trim() === "") return void 0;
23995
24928
  const duration = Number(value);
23996
24929
  if (!Number.isFinite(duration)) {
23997
- throw new TranscriptionServiceError({
24930
+ throw new TranscriptionServiceError2({
23998
24931
  code: "invalid_audio",
23999
24932
  message: "Invalid audio duration."
24000
24933
  });
@@ -24013,19 +24946,21 @@ import { resolveVoiceInputProviderRegistry } from "@opengeni/config";
24013
24946
  import { VOICE_INPUT_ACCEPTED_MIME_TYPES } from "@opengeni/contracts";
24014
24947
  import {
24015
24948
  filenameForMimeType,
24016
- isAcceptedMimeType,
24017
- normalizeMimeType,
24018
- TranscriptionServiceError as TranscriptionServiceError4
24949
+ isAcceptedMimeType as isAcceptedMimeType2,
24950
+ normalizeMimeType as normalizeMimeType2,
24951
+ TRANSCRIPTION_PROVIDER_REQUEST_TIMEOUT_MILLISECONDS as TRANSCRIPTION_PROVIDER_REQUEST_TIMEOUT_MILLISECONDS2,
24952
+ TranscriptionServiceError as TranscriptionServiceError5
24019
24953
  } from "@opengeni/core";
24020
24954
 
24021
24955
  // src/transcription/providers/openai.ts
24022
- import { TranscriptionServiceError as TranscriptionServiceError2 } from "@opengeni/core";
24956
+ import { TranscriptionServiceError as TranscriptionServiceError3 } from "@opengeni/core";
24023
24957
  function createOpenAiTranscriptionProvider(input) {
24024
24958
  const fetchImpl = input.fetch ?? fetch;
24025
24959
  return {
24026
24960
  id: "openai",
24961
+ supportsServerDeadline: true,
24027
24962
  available: () => true,
24028
- async transcribe({ audio, mimeType, filename, signal }) {
24963
+ async transcribe({ audio, mimeType, filename, requestId, signal }) {
24029
24964
  const form = new FormData();
24030
24965
  form.append("file", audioBlob(audio, mimeType), filename);
24031
24966
  form.append("model", input.model);
@@ -24033,7 +24968,11 @@ function createOpenAiTranscriptionProvider(input) {
24033
24968
  try {
24034
24969
  response = await fetchImpl(`${input.baseUrl}/audio/transcriptions`, {
24035
24970
  method: "POST",
24036
- headers: { Authorization: `Bearer ${input.apiKey}` },
24971
+ headers: {
24972
+ Authorization: `Bearer ${input.apiKey}`,
24973
+ // Observability only; the upstream API is not treated as idempotent.
24974
+ "x-opengeni-request-id": requestId
24975
+ },
24037
24976
  body: form,
24038
24977
  ...signal ? { signal } : {}
24039
24978
  });
@@ -24043,7 +24982,7 @@ function createOpenAiTranscriptionProvider(input) {
24043
24982
  if (!response.ok) throw responseError(response.status);
24044
24983
  const body2 = await response.json().catch(() => null);
24045
24984
  if (!body2 || typeof body2.text !== "string") {
24046
- throw new TranscriptionServiceError2({
24985
+ throw new TranscriptionServiceError3({
24047
24986
  code: "provider",
24048
24987
  message: "Invalid transcription response."
24049
24988
  });
@@ -24060,31 +24999,31 @@ function audioBlob(audio, mimeType) {
24060
24999
  }
24061
25000
  function responseError(status) {
24062
25001
  if (status === 401 || status === 403) {
24063
- return new TranscriptionServiceError2({
25002
+ return new TranscriptionServiceError3({
24064
25003
  code: "unavailable",
24065
25004
  message: "Transcription is unavailable."
24066
25005
  });
24067
25006
  }
24068
25007
  if (status === 413) {
24069
- return new TranscriptionServiceError2({
25008
+ return new TranscriptionServiceError3({
24070
25009
  code: "too_large",
24071
25010
  message: "Audio is too large."
24072
25011
  });
24073
25012
  }
24074
25013
  if (status === 400 || status === 422) {
24075
- return new TranscriptionServiceError2({
25014
+ return new TranscriptionServiceError3({
24076
25015
  code: "invalid_audio",
24077
25016
  message: "Audio could not be transcribed."
24078
25017
  });
24079
25018
  }
24080
25019
  if (status === 408 || status === 504) {
24081
- return new TranscriptionServiceError2({
25020
+ return new TranscriptionServiceError3({
24082
25021
  code: "timeout",
24083
25022
  message: "Transcription timed out.",
24084
25023
  retryable: true
24085
25024
  });
24086
25025
  }
24087
- return new TranscriptionServiceError2({
25026
+ return new TranscriptionServiceError3({
24088
25027
  code: status >= 500 ? "unavailable" : "provider",
24089
25028
  message: "Transcription provider failed.",
24090
25029
  retryable: status >= 500
@@ -24092,12 +25031,12 @@ function responseError(status) {
24092
25031
  }
24093
25032
  function fetchError(error) {
24094
25033
  if (error instanceof DOMException && error.name === "AbortError") {
24095
- return new TranscriptionServiceError2({
25034
+ return new TranscriptionServiceError3({
24096
25035
  code: "cancelled",
24097
25036
  message: "Transcription was cancelled."
24098
25037
  });
24099
25038
  }
24100
- return new TranscriptionServiceError2({
25039
+ return new TranscriptionServiceError3({
24101
25040
  code: "network",
24102
25041
  message: "Transcription provider is unreachable.",
24103
25042
  retryable: true
@@ -24110,11 +25049,12 @@ function createAzureOpenAiTranscriptionProvider(input) {
24110
25049
  const url = `${input.endpoint}/openai/deployments/${encodeURIComponent(input.deployment)}/audio/transcriptions?api-version=${encodeURIComponent(input.apiVersion)}`;
24111
25050
  return {
24112
25051
  id: "azure-openai",
25052
+ supportsServerDeadline: true,
24113
25053
  available: () => Boolean(input.apiKey || input.adToken),
24114
- async transcribe({ audio, mimeType, filename, signal }) {
25054
+ async transcribe({ audio, mimeType, filename, requestId, signal }) {
24115
25055
  const form = new FormData();
24116
25056
  form.append("file", new Blob([Uint8Array.from(audio).buffer], { type: mimeType }), filename);
24117
- const headers = input.apiKey ? { "api-key": input.apiKey } : { Authorization: `Bearer ${input.adToken}` };
25057
+ const headers = input.apiKey ? { "api-key": input.apiKey, "x-opengeni-request-id": requestId } : { Authorization: `Bearer ${input.adToken}`, "x-opengeni-request-id": requestId };
24118
25058
  let response;
24119
25059
  try {
24120
25060
  response = await fetchImpl(url, {
@@ -24142,7 +25082,7 @@ function createAzureOpenAiTranscriptionProvider(input) {
24142
25082
  // src/transcription/providers/codex-subscription.ts
24143
25083
  import { CODEX_CLIENT_VERSION as CODEX_CLIENT_VERSION3, CODEX_ORIGINATOR } from "@opengeni/codex/constants";
24144
25084
  import {
24145
- TranscriptionServiceError as TranscriptionServiceError3
25085
+ TranscriptionServiceError as TranscriptionServiceError4
24146
25086
  } from "@opengeni/core";
24147
25087
  import { buildCodexTokenResolver as buildCodexTokenResolver3, listCodexAccountStatuses as listCodexAccountStatuses3 } from "@opengeni/db";
24148
25088
  var TRANSCRIBE_URL = "https://chatgpt.com/backend-api/transcribe";
@@ -24160,14 +25100,15 @@ function createCodexSubscriptionTranscriptionProvider(input) {
24160
25100
  });
24161
25101
  return {
24162
25102
  id: "codex-subscription",
25103
+ supportsServerDeadline: true,
24163
25104
  experimental: true,
24164
25105
  available: probe,
24165
- async transcribe({ audio, mimeType, filename, workspaceId, signal }) {
25106
+ async transcribe({ audio, mimeType, filename, workspaceId, requestId, signal }) {
24166
25107
  const account = (await listCodexAccountStatuses3(input.db, workspaceId)).find(
24167
25108
  (candidate) => candidate.isActive && candidate.status === "active"
24168
25109
  );
24169
25110
  if (!account) {
24170
- throw new TranscriptionServiceError3({
25111
+ throw new TranscriptionServiceError4({
24171
25112
  code: "unavailable",
24172
25113
  message: "Transcription is unavailable."
24173
25114
  });
@@ -24177,7 +25118,7 @@ function createCodexSubscriptionTranscriptionProvider(input) {
24177
25118
  try {
24178
25119
  token = await resolver.getToken();
24179
25120
  } catch {
24180
- throw new TranscriptionServiceError3({
25121
+ throw new TranscriptionServiceError4({
24181
25122
  code: "unavailable",
24182
25123
  message: "Transcription is unavailable."
24183
25124
  });
@@ -24196,7 +25137,9 @@ function createCodexSubscriptionTranscriptionProvider(input) {
24196
25137
  ...accountId ? { "ChatGPT-Account-ID": accountId } : {},
24197
25138
  originator: CODEX_ORIGINATOR,
24198
25139
  "User-Agent": `${CODEX_ORIGINATOR}/${CODEX_CLIENT_VERSION3}`,
24199
- version: CODEX_CLIENT_VERSION3
25140
+ version: CODEX_CLIENT_VERSION3,
25141
+ // Observability only; the upstream API is not treated as idempotent.
25142
+ "x-opengeni-request-id": requestId
24200
25143
  },
24201
25144
  body: form,
24202
25145
  ...signal ? { signal } : {}
@@ -24255,6 +25198,8 @@ function createTranscriptionService(input) {
24255
25198
  maxSizeBytes: input.settings.voiceInputMaxSizeBytes,
24256
25199
  acceptedMimeTypes: [...VOICE_INPUT_ACCEPTED_MIME_TYPES]
24257
25200
  };
25201
+ const providerRequestTimeoutMilliseconds = input.providerRequestTimeoutMilliseconds ?? TRANSCRIPTION_PROVIDER_REQUEST_TIMEOUT_MILLISECONDS2;
25202
+ const now = input.now ?? (() => /* @__PURE__ */ new Date());
24258
25203
  return {
24259
25204
  limits: () => limits,
24260
25205
  async available(context) {
@@ -24262,43 +25207,81 @@ function createTranscriptionService(input) {
24262
25207
  Boolean
24263
25208
  );
24264
25209
  },
25210
+ async selectProvider(context) {
25211
+ return (await firstAvailable(providers, context))?.id ?? null;
25212
+ },
24265
25213
  async transcribe(request) {
24266
- const mimeType = normalizeMimeType(request.mimeType);
24267
- if (!isAcceptedMimeType(mimeType, limits.acceptedMimeTypes)) {
24268
- throw new TranscriptionServiceError4({
25214
+ const mimeType = normalizeMimeType2(request.mimeType);
25215
+ if (!isAcceptedMimeType2(mimeType, limits.acceptedMimeTypes)) {
25216
+ throw new TranscriptionServiceError5({
24269
25217
  code: "not_supported",
24270
25218
  message: "Unsupported audio format."
24271
25219
  });
24272
25220
  }
24273
25221
  if (request.audio.byteLength > limits.maxSizeBytes) {
24274
- throw new TranscriptionServiceError4({
25222
+ throw new TranscriptionServiceError5({
24275
25223
  code: "too_large",
24276
25224
  message: "Audio is too large."
24277
25225
  });
24278
25226
  }
24279
25227
  if (request.durationSeconds !== void 0 && (!Number.isFinite(request.durationSeconds) || request.durationSeconds < 0 || request.durationSeconds > limits.maxDurationSeconds)) {
24280
- throw new TranscriptionServiceError4({
25228
+ throw new TranscriptionServiceError5({
24281
25229
  code: "invalid_audio",
24282
25230
  message: "Invalid audio duration."
24283
25231
  });
24284
25232
  }
24285
- const provider = await firstAvailable(providers, {
24286
- workspaceId: request.workspaceId
24287
- });
25233
+ const provider = request.providerId ? await exactAvailable(providers, request.providerId, { workspaceId: request.workspaceId }) : await firstAvailable(providers, { workspaceId: request.workspaceId });
24288
25234
  if (!provider) {
24289
- throw new TranscriptionServiceError4({
25235
+ throw new TranscriptionServiceError5({
24290
25236
  code: "unavailable",
24291
25237
  message: "Transcription is unavailable."
24292
25238
  });
24293
25239
  }
25240
+ if (provider.supportsServerDeadline !== true) {
25241
+ throw new TranscriptionServiceError5({
25242
+ code: "unavailable",
25243
+ message: "Transcription provider does not support bounded requests."
25244
+ });
25245
+ }
24294
25246
  const startedAt = performance.now();
24295
- const result = await provider.transcribe({
24296
- audio: request.audio,
24297
- mimeType,
24298
- filename: filenameForMimeType(mimeType),
24299
- workspaceId: request.workspaceId,
24300
- signal: request.signal
24301
- });
25247
+ const remainingMilliseconds = request.providerDeadlineAt ? remainingTranscriptionProviderRequestMilliseconds(request.providerDeadlineAt, now()) : providerRequestTimeoutMilliseconds;
25248
+ if (request.providerDeadlineAt && (!Number.isFinite(remainingMilliseconds) || remainingMilliseconds <= 0)) {
25249
+ throw new TranscriptionServiceError5({
25250
+ code: "timeout",
25251
+ message: "Transcription provider deadline expired.",
25252
+ retryable: true
25253
+ });
25254
+ }
25255
+ const deadline = createProviderRequestDeadline(request.signal, remainingMilliseconds);
25256
+ let result;
25257
+ try {
25258
+ result = await provider.transcribe({
25259
+ audio: request.audio,
25260
+ mimeType,
25261
+ filename: filenameForMimeType(mimeType),
25262
+ workspaceId: request.workspaceId,
25263
+ requestId: request.requestId,
25264
+ signal: deadline.signal
25265
+ });
25266
+ if (deadline.timedOut && !request.signal?.aborted) {
25267
+ throw new TranscriptionServiceError5({
25268
+ code: "timeout",
25269
+ message: "Transcription provider timed out.",
25270
+ retryable: true
25271
+ });
25272
+ }
25273
+ } catch (error) {
25274
+ if (deadline.timedOut && !request.signal?.aborted) {
25275
+ throw new TranscriptionServiceError5({
25276
+ code: "timeout",
25277
+ message: "Transcription provider timed out.",
25278
+ retryable: true
25279
+ });
25280
+ }
25281
+ throw error;
25282
+ } finally {
25283
+ deadline.dispose();
25284
+ }
24302
25285
  return {
24303
25286
  ...result,
24304
25287
  providerId: provider.id,
@@ -24308,15 +25291,50 @@ function createTranscriptionService(input) {
24308
25291
  }
24309
25292
  };
24310
25293
  }
25294
+ function remainingTranscriptionProviderRequestMilliseconds(providerDeadlineAt, now) {
25295
+ return providerDeadlineAt.getTime() - now.getTime();
25296
+ }
25297
+ function createProviderRequestDeadline(parentSignal, timeoutMilliseconds) {
25298
+ const controller = new AbortController();
25299
+ let timedOut = false;
25300
+ const timeout = setTimeout(
25301
+ () => {
25302
+ timedOut = true;
25303
+ controller.abort(new DOMException("Transcription provider timed out", "TimeoutError"));
25304
+ },
25305
+ Math.max(1, Math.ceil(timeoutMilliseconds))
25306
+ );
25307
+ const abortFromParent = () => {
25308
+ controller.abort(parentSignal?.reason);
25309
+ };
25310
+ if (parentSignal) {
25311
+ if (parentSignal.aborted) abortFromParent();
25312
+ else parentSignal.addEventListener("abort", abortFromParent, { once: true });
25313
+ }
25314
+ return {
25315
+ signal: controller.signal,
25316
+ get timedOut() {
25317
+ return timedOut;
25318
+ },
25319
+ dispose: () => {
25320
+ clearTimeout(timeout);
25321
+ parentSignal?.removeEventListener("abort", abortFromParent);
25322
+ }
25323
+ };
25324
+ }
24311
25325
  async function firstAvailable(providers, context) {
24312
25326
  for (const provider of providers) {
24313
25327
  if (await provider.available(context)) return provider;
24314
25328
  }
24315
25329
  return null;
24316
25330
  }
25331
+ async function exactAvailable(providers, providerId, context) {
25332
+ const provider = providers.find((candidate) => candidate.id === providerId);
25333
+ return provider && await provider.available(context) ? provider : null;
25334
+ }
24317
25335
 
24318
25336
  // src/integrations/slack-interactions.ts
24319
- import { createHash as createHash11, createHmac as createHmac2, timingSafeEqual as timingSafeEqual2 } from "crypto";
25337
+ import { createHash as createHash12, createHmac as createHmac2, timingSafeEqual as timingSafeEqual2 } from "crypto";
24320
25338
  import {
24321
25339
  DEFAULT_FIRST_PARTY_MCP_TOOLS as DEFAULT_FIRST_PARTY_MCP_TOOLS2,
24322
25340
  hasOpenGeniSlackReactionScope,
@@ -24342,7 +25360,7 @@ import {
24342
25360
  getSlackInteractionByClientEventId,
24343
25361
  getSlackInteractionByRoute,
24344
25362
  getSessionEventByClientEventId,
24345
- getWorkspace as getWorkspace4,
25363
+ getWorkspace as getWorkspace5,
24346
25364
  getWorkspaceGrant as getWorkspaceGrant6,
24347
25365
  listSessionEventPage as listSessionEventPage3,
24348
25366
  listSessionHumanInputRequests as listSessionHumanInputRequests2,
@@ -24360,7 +25378,7 @@ import {
24360
25378
  controlHumanSessionWorkstream as controlHumanSessionWorkstream3,
24361
25379
  createSessionForRequest as createSessionForRequest3,
24362
25380
  hasPermission as hasPermission14,
24363
- requireAccessGrant as requireAccessGrant23
25381
+ requireAccessGrant as requireAccessGrant24
24364
25382
  } from "@opengeni/core";
24365
25383
  import { publishDurableSessionEvents as publishDurableSessionEvents3 } from "@opengeni/events";
24366
25384
  import { HTTPException as HTTPException33 } from "hono/http-exception";
@@ -24464,7 +25482,7 @@ function slackReactionInboxEntry(payload, bot, settings) {
24464
25482
  if (!settings.enabled || !event || event.type !== "reaction_added" || item?.type !== "message" || !teamId || !eventId || !userId || userId === bot.botUserId || !reaction || reaction !== settings.emoji || !channelId || !workspaceSlackReactionChannelAllowed(settings, channelId) || !timestamp) {
24465
25483
  return null;
24466
25484
  }
24467
- const stableReactionIdentity = createHash11("sha256").update([teamId, userId, channelId, timestamp, reaction].join("\n")).digest("hex");
25485
+ const stableReactionIdentity = createHash12("sha256").update([teamId, userId, channelId, timestamp, reaction].join("\n")).digest("hex");
24468
25486
  return {
24469
25487
  providerEventId: eventId,
24470
25488
  providerMessageId: `reaction:${stableReactionIdentity}`,
@@ -24498,7 +25516,7 @@ function registerSlackInteractionRoutes(app, deps) {
24498
25516
  const event = record3(payload.event);
24499
25517
  if (event?.type === "reaction_added") {
24500
25518
  const [workspace, connection] = await Promise.all([
24501
- getWorkspace4(deps.db, installation.workspaceId),
25519
+ getWorkspace5(deps.db, installation.workspaceId),
24502
25520
  getConnectionMetadata4(deps.db, installation.workspaceId, installation.connectionId, null)
24503
25521
  ]);
24504
25522
  if (!workspace || !connection || !hasOpenGeniSlackReactionScope(connection.grantedScopes)) {
@@ -24597,7 +25615,7 @@ function registerSlackInteractionRoutes(app, deps) {
24597
25615
  });
24598
25616
  app.post("/v1/workspaces/:workspaceId/integrations/slack/user-links", async (c) => {
24599
25617
  const workspaceId = c.req.param("workspaceId");
24600
- const grant = await requireAccessGrant23(c, deps, workspaceId, "sessions:create");
25618
+ const grant = await requireAccessGrant24(c, deps, workspaceId, "sessions:create");
24601
25619
  const body2 = record3(await c.req.json().catch(() => null));
24602
25620
  const linkToken = boundedString(body2?.linkToken, 2048);
24603
25621
  const signingSecret = deps.settings.slackSigningSecret;
@@ -24630,7 +25648,7 @@ function registerSlackInteractionRoutes(app, deps) {
24630
25648
  "/v1/workspaces/:workspaceId/integrations/slack/user-links/:slackUserId",
24631
25649
  async (c) => {
24632
25650
  const workspaceId = c.req.param("workspaceId");
24633
- await requireAccessGrant23(c, deps, workspaceId, "connections:write");
25651
+ await requireAccessGrant24(c, deps, workspaceId, "connections:write");
24634
25652
  const connectionId = boundedString(c.req.query("connectionId"), 64);
24635
25653
  if (!connectionId) throw new HTTPException33(400, { message: "connectionId is required" });
24636
25654
  return c.json({
@@ -24645,7 +25663,7 @@ function registerSlackInteractionRoutes(app, deps) {
24645
25663
  );
24646
25664
  app.get("/v1/workspaces/:workspaceId/integrations/slack/reaction-channels", async (c) => {
24647
25665
  const workspaceId = c.req.param("workspaceId");
24648
- const grant = await requireAccessGrant23(c, deps, workspaceId, "workspace:admin");
25666
+ const grant = await requireAccessGrant24(c, deps, workspaceId, "workspace:admin");
24649
25667
  const connectionId = boundedString(c.req.query("connectionId"), 64);
24650
25668
  if (!connectionId) throw new HTTPException33(400, { message: "connectionId is required" });
24651
25669
  const cursor = boundedString(c.req.query("cursor"), 1024);
@@ -24881,7 +25899,7 @@ async function processSlackInboxEntry(deps, entry) {
24881
25899
  }
24882
25900
  async function processSlackReactionInboxEntry(deps, entry) {
24883
25901
  const [workspace, connection, link] = await Promise.all([
24884
- getWorkspace4(deps.db, entry.workspaceId),
25902
+ getWorkspace5(deps.db, entry.workspaceId),
24885
25903
  getConnectionMetadata4(deps.db, entry.workspaceId, entry.connectionId, null),
24886
25904
  getSlackBotUserLink(deps.db, entry.workspaceId, entry.connectionId, entry.slackUserId)
24887
25905
  ]);
@@ -25465,7 +26483,7 @@ function slackRouteKey(channelId, threadTs) {
25465
26483
  return `${channelId}:${threadTs}`;
25466
26484
  }
25467
26485
  function deterministicUuid(value) {
25468
- const bytes = createHash11("sha256").update(value).digest().subarray(0, 16);
26486
+ const bytes = createHash12("sha256").update(value).digest().subarray(0, 16);
25469
26487
  bytes[6] = bytes[6] & 15 | 80;
25470
26488
  bytes[8] = bytes[8] & 63 | 128;
25471
26489
  const hex = bytes.toString("hex");
@@ -25584,7 +26602,11 @@ import {
25584
26602
  import { workflowIdForSession as workflowIdForSession2 } from "@opengeni/core";
25585
26603
  var API_MAX_REQUEST_BODY_BYTES = 8 * 1024 * 1024;
25586
26604
  function apiRequestBodyLimitBytes(settings) {
25587
- return Math.max(API_MAX_REQUEST_BODY_BYTES, settings.voiceInputMaxSizeBytes + 64 * 1024);
26605
+ return Math.max(
26606
+ API_MAX_REQUEST_BODY_BYTES,
26607
+ settings.voiceInputMaxSizeBytes + 64 * 1024,
26608
+ (settings.voiceInputResumableMaxChunkSizeBytes ?? 0) + 64 * 1024
26609
+ );
25588
26610
  }
25589
26611
  var API_PUBLIC_ERROR_MESSAGE_MAX_BYTES = 512;
25590
26612
  function createApp(deps) {
@@ -25654,6 +26676,9 @@ function createAppComposition(deps) {
25654
26676
  db: deps.db,
25655
26677
  ...deps.codexFetch ? { codexFetch: deps.codexFetch } : {}
25656
26678
  }) : deps.transcription;
26679
+ const transcriptionSegmenter = deps.transcriptionSegmenter === void 0 ? createFfmpegTranscriptionSegmenter({
26680
+ ffmpegPath: deps.settings.voiceInputFfmpegPath
26681
+ }) : deps.transcriptionSegmenter;
25657
26682
  const routeDeps = {
25658
26683
  ...deps,
25659
26684
  observability,
@@ -25663,15 +26688,16 @@ function createAppComposition(deps) {
25663
26688
  documentIndexer,
25664
26689
  getDocumentServices,
25665
26690
  transcription,
26691
+ transcriptionSegmenter,
25666
26692
  ...sandboxClient ? { sandboxClient } : {},
25667
26693
  resumeBoxById
25668
26694
  };
25669
26695
  const app = new Hono();
25670
26696
  const correlationIds = /* @__PURE__ */ new WeakMap();
25671
26697
  app.use("*", async (c, next) => {
25672
- const correlationId = boundedCorrelationId(c.req.header(OPENGENI_CORRELATION_HEADER)) ?? crypto.randomUUID();
25673
- correlationIds.set(c.req.raw, correlationId);
25674
- c.header(OPENGENI_CORRELATION_HEADER, correlationId);
26698
+ const correlationId2 = boundedCorrelationId(c.req.header(OPENGENI_CORRELATION_HEADER)) ?? crypto.randomUUID();
26699
+ correlationIds.set(c.req.raw, correlationId2);
26700
+ c.header(OPENGENI_CORRELATION_HEADER, correlationId2);
25675
26701
  await next();
25676
26702
  });
25677
26703
  const corsHeaders = {
@@ -25721,7 +26747,7 @@ function createAppComposition(deps) {
25721
26747
  app.use("*", async (c, next) => {
25722
26748
  const url = new URL(c.req.url);
25723
26749
  const route = routeLabel(url.pathname);
25724
- const correlationId = correlationIds.get(c.req.raw) ?? crypto.randomUUID();
26750
+ const correlationId2 = correlationIds.get(c.req.raw) ?? crypto.randomUUID();
25725
26751
  const start = performance.now();
25726
26752
  const span = observability.startSpan(`HTTP ${c.req.method} ${route}`, {
25727
26753
  "http.request.method": c.req.method,
@@ -25751,7 +26777,7 @@ function createAppComposition(deps) {
25751
26777
  durationMs: Math.round(durationSeconds * 1e3),
25752
26778
  traceId: span.traceId,
25753
26779
  spanId: span.spanId,
25754
- correlationId
26780
+ correlationId: correlationId2
25755
26781
  });
25756
26782
  } catch (error) {
25757
26783
  const status = httpStatusForError(error);
@@ -25782,7 +26808,7 @@ function createAppComposition(deps) {
25782
26808
  durationMs: Math.round(durationSeconds * 1e3),
25783
26809
  traceId: span.traceId,
25784
26810
  spanId: span.spanId,
25785
- correlationId,
26811
+ correlationId: correlationId2,
25786
26812
  errorCode,
25787
26813
  errorClass: error instanceof Error ? error.name : "NonErrorThrown"
25788
26814
  });
@@ -25861,7 +26887,18 @@ function createAppComposition(deps) {
25861
26887
  available: await transcription?.available() ?? false,
25862
26888
  maxDurationSeconds: deps.settings.voiceInputMaxDurationSeconds,
25863
26889
  maxSizeBytes: deps.settings.voiceInputMaxSizeBytes,
25864
- acceptedMimeTypes: [...VOICE_INPUT_ACCEPTED_MIME_TYPES2]
26890
+ acceptedMimeTypes: [...VOICE_INPUT_ACCEPTED_MIME_TYPES2],
26891
+ ...deps.settings.voiceInputResumableEnabled && objectStorage && transcription && transcriptionSegmenter && await transcription.available() && await transcriptionSegmenter.available() ? {
26892
+ resumable: {
26893
+ maxDurationSeconds: deps.settings.voiceInputResumableMaxDurationSeconds,
26894
+ maxSizeBytes: deps.settings.voiceInputResumableMaxSizeBytes,
26895
+ maxChunkSizeBytes: deps.settings.voiceInputResumableMaxChunkSizeBytes,
26896
+ providerSegmentSeconds: Math.min(
26897
+ TRANSCRIPTION_RECORDING_PROVIDER_SEGMENT_SECONDS2,
26898
+ deps.settings.voiceInputMaxDurationSeconds
26899
+ )
26900
+ }
26901
+ } : {}
25865
26902
  },
25866
26903
  productAccessMode: deps.settings.productAccessMode,
25867
26904
  auth: clientAuthConfig(deps.settings),
@@ -25927,7 +26964,7 @@ function createAppComposition(deps) {
25927
26964
  throw error;
25928
26965
  }
25929
26966
  }
25930
- const workspace = await getWorkspace5(routeDeps.db, workspaceId);
26967
+ const workspace = await getWorkspace6(routeDeps.db, workspaceId);
25931
26968
  const workspaceMemoryEnabled = resolveWorkspaceMemoryEnabled(workspace?.settings);
25932
26969
  const transport = new WebStandardStreamableHTTPServerTransport3({
25933
26970
  enableJsonResponse: true
@@ -26018,7 +27055,7 @@ function appendVary(current, value) {
26018
27055
  return values.join(", ");
26019
27056
  }
26020
27057
  async function requireMcpAccessGrant(c, deps, workspaceId) {
26021
- const grant = await requireAccessGrant24(c, deps, workspaceId);
27058
+ const grant = await requireAccessGrant25(c, deps, workspaceId);
26022
27059
  if (hasPermission15(grant.permissions, "workspace:read")) {
26023
27060
  return grant;
26024
27061
  }
@@ -26617,4 +27654,4 @@ export {
26617
27654
  withDefaultEnabledCapabilityMcpTools,
26618
27655
  workflowIdForSession2 as workflowIdForSession
26619
27656
  };
26620
- //# sourceMappingURL=chunk-J4JS2F7L.js.map
27657
+ //# sourceMappingURL=chunk-HWXJW5C7.js.map