@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.
- package/dist/app.d.ts +1 -0
- package/dist/app.js +1 -1
- package/dist/{chunk-J4JS2F7L.js → chunk-HWXJW5C7.js} +1210 -173
- package/dist/chunk-HWXJW5C7.js.map +1 -0
- package/dist/index.js +1 -1
- package/dist/integrations/slack-interactions.d.ts +1 -1
- package/dist/routes/transcription-recordings.d.ts +3 -0
- package/dist/transcription/segmenter.d.ts +10 -0
- package/dist/transcription/service.d.ts +5 -0
- package/package.json +12 -12
- package/src/app.ts +36 -2
- package/src/mcp/server.ts +5 -80
- package/src/mcp/toolspace.ts +91 -69
- package/src/routes/codex.ts +138 -8
- package/src/routes/transcription-recordings.ts +722 -0
- package/src/routes/transcriptions.ts +2 -0
- package/src/transcription/providers/azure-openai.ts +4 -3
- package/src/transcription/providers/codex-subscription.ts +4 -1
- package/src/transcription/providers/openai.ts +7 -2
- package/src/transcription/segmenter.ts +260 -0
- package/src/transcription/service.ts +111 -10
- package/dist/chunk-J4JS2F7L.js.map +0 -1
|
@@ -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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
9711
|
-
|
|
9712
|
-
|
|
9713
|
-
|
|
9714
|
-
|
|
9715
|
-
|
|
9716
|
-
|
|
9717
|
-
|
|
9718
|
-
|
|
9719
|
-
|
|
9720
|
-
|
|
9721
|
-
|
|
9722
|
-
|
|
9723
|
-
|
|
9724
|
-
|
|
9725
|
-
|
|
9726
|
-
|
|
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(
|
|
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 {
|
|
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
|
|
23879
|
-
const workspace = await
|
|
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 (
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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: {
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
25034
|
+
return new TranscriptionServiceError3({
|
|
24096
25035
|
code: "cancelled",
|
|
24097
25036
|
message: "Transcription was cancelled."
|
|
24098
25037
|
});
|
|
24099
25038
|
}
|
|
24100
|
-
return new
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
24267
|
-
if (!
|
|
24268
|
-
throw new
|
|
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
|
|
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
|
|
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
|
|
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
|
|
24296
|
-
|
|
24297
|
-
|
|
24298
|
-
|
|
24299
|
-
|
|
24300
|
-
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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(
|
|
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
|
|
25673
|
-
correlationIds.set(c.req.raw,
|
|
25674
|
-
c.header(OPENGENI_CORRELATION_HEADER,
|
|
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
|
|
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
|
|
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
|
|
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-
|
|
27657
|
+
//# sourceMappingURL=chunk-HWXJW5C7.js.map
|