@opengeni/api-router 0.14.3 → 0.15.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app.d.ts +5 -1
- package/dist/app.js +3 -1
- package/dist/auth/managed-auth.d.ts +2 -2
- package/dist/{chunk-36PW33ND.js → chunk-UVL2KH56.js} +1470 -213
- package/dist/chunk-UVL2KH56.js.map +1 -0
- package/dist/index.js +16 -5
- package/dist/index.js.map +1 -1
- package/dist/integrations/slack-bot.d.ts +21 -0
- package/dist/integrations/slack-interactions.d.ts +30 -0
- package/dist/routes/workspace-artifacts.d.ts +3 -0
- package/package.json +10 -10
- package/src/app.ts +59 -28
- package/src/http/auth.ts +4 -1
- package/src/index.ts +17 -4
- package/src/integrations/slack-bot.ts +80 -11
- package/src/integrations/slack-interactions.ts +856 -0
- package/src/mcp/server.ts +209 -0
- package/src/routes/sessions.ts +23 -10
- package/src/routes/workspace-artifacts.ts +274 -0
- package/dist/chunk-36PW33ND.js.map +0 -1
|
@@ -26,11 +26,11 @@ import { WebStandardStreamableHTTPServerTransport as WebStandardStreamableHTTPSe
|
|
|
26
26
|
import { Hono } from "hono";
|
|
27
27
|
import { bodyLimit } from "hono/body-limit";
|
|
28
28
|
import { cors } from "hono/cors";
|
|
29
|
-
import { HTTPException as
|
|
29
|
+
import { HTTPException as HTTPException32 } from "hono/http-exception";
|
|
30
30
|
import {
|
|
31
31
|
CodexCompactionV2ProviderLockedError,
|
|
32
|
-
hasPermission as
|
|
33
|
-
requireAccessGrant as
|
|
32
|
+
hasPermission as hasPermission14,
|
|
33
|
+
requireAccessGrant as requireAccessGrant24,
|
|
34
34
|
requirePermission,
|
|
35
35
|
requireSessionAuthorization as requireSessionAuthorization3,
|
|
36
36
|
SessionAuthorizationDeniedError as SessionAuthorizationDeniedError2,
|
|
@@ -291,6 +291,7 @@ function makeResumeBoxById(client) {
|
|
|
291
291
|
import { requireLimit as requireLimit8 } from "@opengeni/core";
|
|
292
292
|
|
|
293
293
|
// src/mcp/server.ts
|
|
294
|
+
import { createHash as createHash2 } from "crypto";
|
|
294
295
|
import {
|
|
295
296
|
CreateScheduledTaskRequest,
|
|
296
297
|
DEFAULT_FIRST_PARTY_MCP_TOOLS,
|
|
@@ -308,7 +309,9 @@ import {
|
|
|
308
309
|
sessionEventLatestClassToSemanticClass,
|
|
309
310
|
SessionMcpCredentialUpdateInput,
|
|
310
311
|
VariableSetVariableName,
|
|
311
|
-
UpdateScheduledTaskRequest
|
|
312
|
+
UpdateScheduledTaskRequest,
|
|
313
|
+
normalizeWorkspaceArtifactSlug,
|
|
314
|
+
WORKSPACE_ARTIFACT_HTML_MAX_UTF8_BYTES
|
|
312
315
|
} from "@opengeni/contracts";
|
|
313
316
|
import {
|
|
314
317
|
correctWorkspaceMemory,
|
|
@@ -352,7 +355,13 @@ import {
|
|
|
352
355
|
updateSessionGoalWithEvent,
|
|
353
356
|
upsertSessionGoalWithEvent,
|
|
354
357
|
RigChangeAlreadyVerifyingError,
|
|
355
|
-
RigChangeTransitionError
|
|
358
|
+
RigChangeTransitionError,
|
|
359
|
+
createWorkspaceArtifact,
|
|
360
|
+
getWorkspaceArtifact,
|
|
361
|
+
getWorkspaceArtifactContentRef,
|
|
362
|
+
listWorkspaceArtifacts,
|
|
363
|
+
publishWorkspaceArtifactVersion,
|
|
364
|
+
rollbackWorkspaceArtifact
|
|
356
365
|
} from "@opengeni/db";
|
|
357
366
|
import { appendAndPublishEvents as appendAndPublishEvents2, publishDurableSessionEvents } from "@opengeni/events";
|
|
358
367
|
import {
|
|
@@ -2156,7 +2165,7 @@ var MAX_PROJECTED_TEXT = 4e3;
|
|
|
2156
2165
|
var SLACK_POST_CLAIM_LEASE_MS = 3e4;
|
|
2157
2166
|
var SLACK_DELETE_CLAIM_LEASE_MS = 3e4;
|
|
2158
2167
|
async function exchangeOpenGeniSlackAuthorizationCode(input, fetchImpl = fetch) {
|
|
2159
|
-
const
|
|
2168
|
+
const body2 = new URLSearchParams({
|
|
2160
2169
|
code: input.code,
|
|
2161
2170
|
client_id: input.clientId,
|
|
2162
2171
|
client_secret: input.clientSecret,
|
|
@@ -2170,28 +2179,34 @@ async function exchangeOpenGeniSlackAuthorizationCode(input, fetchImpl = fetch)
|
|
|
2170
2179
|
accept: "application/json",
|
|
2171
2180
|
"content-type": "application/x-www-form-urlencoded"
|
|
2172
2181
|
},
|
|
2173
|
-
body:
|
|
2182
|
+
body: body2.toString(),
|
|
2174
2183
|
redirect: "error",
|
|
2175
2184
|
signal: AbortSignal.timeout(SLACK_TIMEOUT_MS)
|
|
2176
2185
|
});
|
|
2177
2186
|
} catch {
|
|
2178
|
-
throw new HTTPException2(502, {
|
|
2187
|
+
throw new HTTPException2(502, {
|
|
2188
|
+
message: "Slack installation token exchange failed"
|
|
2189
|
+
});
|
|
2179
2190
|
}
|
|
2180
2191
|
if (!response.ok) {
|
|
2181
|
-
throw new HTTPException2(502, {
|
|
2192
|
+
throw new HTTPException2(502, {
|
|
2193
|
+
message: "Slack installation token exchange failed"
|
|
2194
|
+
});
|
|
2182
2195
|
}
|
|
2183
2196
|
const payload = await readResponseJsonBounded(
|
|
2184
2197
|
response,
|
|
2185
2198
|
SLACK_RESPONSE_MAX_BYTES,
|
|
2186
2199
|
"Slack OAuth response"
|
|
2187
2200
|
);
|
|
2188
|
-
const
|
|
2189
|
-
if (!
|
|
2190
|
-
throw new SlackBotProviderError(slackString(
|
|
2201
|
+
const record4 = slackRecord(payload);
|
|
2202
|
+
if (!record4 || record4.ok !== true) {
|
|
2203
|
+
throw new SlackBotProviderError(slackString(record4?.error) || "oauth_exchange_failed");
|
|
2191
2204
|
}
|
|
2192
|
-
const accessToken = slackString(
|
|
2205
|
+
const accessToken = slackString(record4.access_token);
|
|
2193
2206
|
if (!accessToken?.startsWith("xoxb-")) {
|
|
2194
|
-
throw new HTTPException2(502, {
|
|
2207
|
+
throw new HTTPException2(502, {
|
|
2208
|
+
message: "Slack installation did not return a bot token"
|
|
2209
|
+
});
|
|
2195
2210
|
}
|
|
2196
2211
|
return accessToken;
|
|
2197
2212
|
}
|
|
@@ -2344,6 +2359,10 @@ var OpenGeniSlackBotClient = class {
|
|
|
2344
2359
|
};
|
|
2345
2360
|
});
|
|
2346
2361
|
}
|
|
2362
|
+
async verifyChannelAccess(channelId) {
|
|
2363
|
+
const headers = await this.headersFor("channel_history.read");
|
|
2364
|
+
return await this.requireMemberChannel(headers, channelId);
|
|
2365
|
+
}
|
|
2347
2366
|
async channelHistory(input) {
|
|
2348
2367
|
return await this.withAudit("channel_history.read", async (headers) => {
|
|
2349
2368
|
const info = await this.requireMemberChannel(headers, input.channelId);
|
|
@@ -2453,7 +2472,9 @@ var OpenGeniSlackBotClient = class {
|
|
|
2453
2472
|
const headers = await this.headersFor(operation);
|
|
2454
2473
|
let channelId = input.channelId;
|
|
2455
2474
|
if (input.userId) {
|
|
2456
|
-
const opened = await this.call(headers, "conversations.open", {
|
|
2475
|
+
const opened = await this.call(headers, "conversations.open", {
|
|
2476
|
+
users: input.userId
|
|
2477
|
+
});
|
|
2457
2478
|
channelId = requiredSlackString(slackRecord(opened.channel)?.id, "channel.id");
|
|
2458
2479
|
} else if (channelId) {
|
|
2459
2480
|
await this.requireMemberChannel(headers, channelId);
|
|
@@ -2671,7 +2692,10 @@ var OpenGeniSlackBotClient = class {
|
|
|
2671
2692
|
`${SLACK_API_BASE}chat.getPermalink`
|
|
2672
2693
|
);
|
|
2673
2694
|
try {
|
|
2674
|
-
await this.call(headers, "chat.getPermalink", {
|
|
2695
|
+
await this.call(headers, "chat.getPermalink", {
|
|
2696
|
+
channel: channelId,
|
|
2697
|
+
message_ts: timestamp
|
|
2698
|
+
});
|
|
2675
2699
|
return true;
|
|
2676
2700
|
} catch (error) {
|
|
2677
2701
|
if (error instanceof SlackBotProviderError && error.code === "message_not_found") {
|
|
@@ -2681,7 +2705,9 @@ var OpenGeniSlackBotClient = class {
|
|
|
2681
2705
|
}
|
|
2682
2706
|
}
|
|
2683
2707
|
async requireMemberChannel(headers, channelId) {
|
|
2684
|
-
const payload = await this.call(headers, "conversations.info", {
|
|
2708
|
+
const payload = await this.call(headers, "conversations.info", {
|
|
2709
|
+
channel: channelId
|
|
2710
|
+
});
|
|
2685
2711
|
const projected = projectChannel(payload.channel);
|
|
2686
2712
|
if (!projected || projected.isMember !== true) {
|
|
2687
2713
|
throw new SlackBotProviderError("not_in_channel");
|
|
@@ -2909,19 +2935,31 @@ var OpenGeniSlackBotClient = class {
|
|
|
2909
2935
|
return { type: "subject", id: this.context.subjectId };
|
|
2910
2936
|
}
|
|
2911
2937
|
if (this.context.scheduledTaskId) {
|
|
2912
|
-
return {
|
|
2938
|
+
return {
|
|
2939
|
+
type: "service",
|
|
2940
|
+
id: `scheduler:${this.context.scheduledTaskId}`
|
|
2941
|
+
};
|
|
2913
2942
|
}
|
|
2914
|
-
return {
|
|
2943
|
+
return {
|
|
2944
|
+
type: "service",
|
|
2945
|
+
id: `session:${this.context.sessionId ?? "workspace"}`
|
|
2946
|
+
};
|
|
2915
2947
|
}
|
|
2916
2948
|
fileListPage(input) {
|
|
2917
2949
|
const key = environmentsEncryptionKeyBytes(this.settings);
|
|
2918
2950
|
if (!key) throw new Error("connection encryption is not configured");
|
|
2919
|
-
return resolveSlackFilesListPage(input, {
|
|
2951
|
+
return resolveSlackFilesListPage(input, {
|
|
2952
|
+
connectionId: this.connection.id,
|
|
2953
|
+
key
|
|
2954
|
+
});
|
|
2920
2955
|
}
|
|
2921
2956
|
fileListCursor(input) {
|
|
2922
2957
|
const key = environmentsEncryptionKeyBytes(this.settings);
|
|
2923
2958
|
if (!key) throw new Error("connection encryption is not configured");
|
|
2924
|
-
return createSlackFilesListCursor(input, {
|
|
2959
|
+
return createSlackFilesListCursor(input, {
|
|
2960
|
+
connectionId: this.connection.id,
|
|
2961
|
+
key
|
|
2962
|
+
});
|
|
2925
2963
|
}
|
|
2926
2964
|
async recordAudit(operation, outcome, failureCode, operationId) {
|
|
2927
2965
|
await recordAuditEvent(this.db, {
|
|
@@ -2954,6 +2992,32 @@ function createOpenGeniSlackBotClient(deps, resolved) {
|
|
|
2954
2992
|
deps.slackFetch
|
|
2955
2993
|
);
|
|
2956
2994
|
}
|
|
2995
|
+
async function createOpenGeniSlackBotInteractionClient(deps, input) {
|
|
2996
|
+
const connection = await requireOpenGeniSlackBotConnection(
|
|
2997
|
+
deps.db,
|
|
2998
|
+
input.workspaceId,
|
|
2999
|
+
input.connectionId
|
|
3000
|
+
);
|
|
3001
|
+
if (connection.accountId !== input.accountId) {
|
|
3002
|
+
throw new Error("OpenGeni Slack bot connection tenant mismatch");
|
|
3003
|
+
}
|
|
3004
|
+
const metadata = openGeniSlackBotMetadata(connection.metadata);
|
|
3005
|
+
if (!metadata) throw new Error("OpenGeni Slack bot connection metadata is invalid");
|
|
3006
|
+
return new OpenGeniSlackBotClient(
|
|
3007
|
+
deps.db,
|
|
3008
|
+
deps.settings,
|
|
3009
|
+
connection,
|
|
3010
|
+
metadata,
|
|
3011
|
+
{
|
|
3012
|
+
accountId: input.accountId,
|
|
3013
|
+
workspaceId: input.workspaceId,
|
|
3014
|
+
subjectId: input.subjectId,
|
|
3015
|
+
sessionId: input.sessionId ?? null,
|
|
3016
|
+
scheduledTaskId: null
|
|
3017
|
+
},
|
|
3018
|
+
deps.slackFetch
|
|
3019
|
+
);
|
|
3020
|
+
}
|
|
2957
3021
|
async function slackApiFetch(fetchImpl, method, token, params) {
|
|
2958
3022
|
return await slackApiFetchWithHeaders(
|
|
2959
3023
|
fetchImpl,
|
|
@@ -2967,7 +3031,7 @@ async function slackApiFetchWithHeaders(fetchImpl, method, credentialHeaders, pa
|
|
|
2967
3031
|
throw new Error("invalid Slack API method");
|
|
2968
3032
|
}
|
|
2969
3033
|
const url = new URL(method, SLACK_API_BASE);
|
|
2970
|
-
const
|
|
3034
|
+
const body2 = new URLSearchParams(params);
|
|
2971
3035
|
let response;
|
|
2972
3036
|
try {
|
|
2973
3037
|
response = await fetchImpl(url, {
|
|
@@ -2977,7 +3041,7 @@ async function slackApiFetchWithHeaders(fetchImpl, method, credentialHeaders, pa
|
|
|
2977
3041
|
accept: "application/json",
|
|
2978
3042
|
"content-type": "application/x-www-form-urlencoded"
|
|
2979
3043
|
},
|
|
2980
|
-
body,
|
|
3044
|
+
body: body2,
|
|
2981
3045
|
signal: AbortSignal.timeout(SLACK_TIMEOUT_MS)
|
|
2982
3046
|
});
|
|
2983
3047
|
} catch {
|
|
@@ -3121,9 +3185,9 @@ function isSupportedSlackTextContentType(value) {
|
|
|
3121
3185
|
return value.startsWith("text/") || value === "application/json" || value === "application/xml" || value === "application/xhtml+xml" || value === "application/vnd.slack-docs" || value === "application/vnd.slack-huddle-transcript";
|
|
3122
3186
|
}
|
|
3123
3187
|
function embeddedHuddleTranscription(fileRecord, parentFileRecord) {
|
|
3124
|
-
for (const
|
|
3125
|
-
if (!
|
|
3126
|
-
const transcription = slackRecord(
|
|
3188
|
+
for (const record4 of [fileRecord, parentFileRecord]) {
|
|
3189
|
+
if (!record4) continue;
|
|
3190
|
+
const transcription = slackRecord(record4.huddle_transcription);
|
|
3127
3191
|
if (!transcription) continue;
|
|
3128
3192
|
const content = JSON.stringify(transcription);
|
|
3129
3193
|
if (content !== "{}") {
|
|
@@ -3336,7 +3400,12 @@ var FIRST_PARTY_TOOL_AUTHORIZATION = {
|
|
|
3336
3400
|
slack_bot_file_info: { allOf: ["connections:read"] },
|
|
3337
3401
|
slack_bot_file_content: { allOf: ["connections:read"] },
|
|
3338
3402
|
slack_bot_post_message: { allOf: ["connections:read"] },
|
|
3339
|
-
slack_bot_delete_message: { allOf: ["connections:read"] }
|
|
3403
|
+
slack_bot_delete_message: { allOf: ["connections:read"] },
|
|
3404
|
+
artifacts_list: { sessionRequired: true, allOf: ["artifacts:read"] },
|
|
3405
|
+
artifacts_get_source: { sessionRequired: true, allOf: ["artifacts:read"] },
|
|
3406
|
+
artifacts_create: { sessionRequired: true, allOf: ["artifacts:publish"] },
|
|
3407
|
+
artifacts_publish: { sessionRequired: true, allOf: ["artifacts:publish"] },
|
|
3408
|
+
artifacts_rollback: { sessionRequired: true, allOf: ["artifacts:publish"] }
|
|
3340
3409
|
};
|
|
3341
3410
|
var FIRST_PARTY_MCP_TOOL_NAME_SET = new Set(FIRST_PARTY_MCP_TOOL_NAMES);
|
|
3342
3411
|
var PolicyMcpServer = class extends McpServer {
|
|
@@ -3431,6 +3500,9 @@ function buildOpenGeniMcpServer(deps, grant, options = {}) {
|
|
|
3431
3500
|
if (!toolspaceMode && sessionId !== null && preferenceAttemptClaims(grant) !== null) {
|
|
3432
3501
|
registerPreferenceRegistryTools(server, deps, grant, json);
|
|
3433
3502
|
}
|
|
3503
|
+
if (!toolspaceMode && sessionId !== null && preferenceAttemptClaims(grant) !== null) {
|
|
3504
|
+
registerWorkspaceArtifactTools(server, deps, grant, sessionId, json);
|
|
3505
|
+
}
|
|
3434
3506
|
if (!toolspaceMode && sessionId !== null && deps.settings.sandboxSelfhostedEnabled) {
|
|
3435
3507
|
registerFleetTools(server, deps, grant, sessionId, json);
|
|
3436
3508
|
}
|
|
@@ -4171,6 +4243,172 @@ async function authorizeFirstPartySession(deps, grant, sessionId, operation) {
|
|
|
4171
4243
|
surface: "first_party_mcp"
|
|
4172
4244
|
});
|
|
4173
4245
|
}
|
|
4246
|
+
function registerWorkspaceArtifactTools(server, deps, grant, sessionId, json) {
|
|
4247
|
+
const attempt = () => {
|
|
4248
|
+
const claims = preferenceAttemptClaims(grant);
|
|
4249
|
+
if (!claims) throw new Error("Exact signed artifact attempt authority is required.");
|
|
4250
|
+
return claims;
|
|
4251
|
+
};
|
|
4252
|
+
const authorize = async () => {
|
|
4253
|
+
await authorizeFirstPartySession(deps, grant, sessionId, "session.first_party_mcp.call");
|
|
4254
|
+
};
|
|
4255
|
+
const prepare = (html) => {
|
|
4256
|
+
if (!deps.objectStorage) throw new Error("Object storage is not configured");
|
|
4257
|
+
const bytes = new TextEncoder().encode(html);
|
|
4258
|
+
if (bytes.byteLength < 1 || bytes.byteLength > WORKSPACE_ARTIFACT_HTML_MAX_UTF8_BYTES) {
|
|
4259
|
+
throw new Error(
|
|
4260
|
+
`Artifact HTML must be 1-${WORKSPACE_ARTIFACT_HTML_MAX_UTF8_BYTES} UTF-8 bytes`
|
|
4261
|
+
);
|
|
4262
|
+
}
|
|
4263
|
+
const contentSha256 = createHash2("sha256").update(bytes).digest("hex");
|
|
4264
|
+
const contentKey = `workspaces/${grant.workspaceId}/workspace-artifacts/blobs/${contentSha256}.html`;
|
|
4265
|
+
return {
|
|
4266
|
+
contentKey,
|
|
4267
|
+
contentSha256,
|
|
4268
|
+
sizeBytes: bytes.byteLength,
|
|
4269
|
+
persistContent: async () => {
|
|
4270
|
+
await deps.objectStorage.putObject({
|
|
4271
|
+
key: contentKey,
|
|
4272
|
+
contentType: "text/html; charset=utf-8",
|
|
4273
|
+
body: bytes,
|
|
4274
|
+
sha256: contentSha256
|
|
4275
|
+
});
|
|
4276
|
+
}
|
|
4277
|
+
};
|
|
4278
|
+
};
|
|
4279
|
+
const provenance2 = (idempotencyKey, sourceToolName) => {
|
|
4280
|
+
const claims = attempt();
|
|
4281
|
+
return {
|
|
4282
|
+
accountId: grant.accountId,
|
|
4283
|
+
workspaceId: grant.workspaceId,
|
|
4284
|
+
operationKey: `attempt:${createHash2("sha256").update(
|
|
4285
|
+
`${claims.sessionId}:${claims.turnId}:${claims.attemptId}:${claims.executionGeneration}:${idempotencyKey}`
|
|
4286
|
+
).digest("hex")}`,
|
|
4287
|
+
actorSubjectId: grant.subjectId,
|
|
4288
|
+
sourceSessionId: claims.sessionId,
|
|
4289
|
+
sourceTurnId: claims.turnId,
|
|
4290
|
+
sourceAttemptId: claims.attemptId,
|
|
4291
|
+
sourceExecutionGeneration: claims.executionGeneration,
|
|
4292
|
+
sourceToolName
|
|
4293
|
+
};
|
|
4294
|
+
};
|
|
4295
|
+
server.registerTool(
|
|
4296
|
+
"artifacts_list",
|
|
4297
|
+
{
|
|
4298
|
+
description: "List the generic published artifacts in this workspace and their current versions.",
|
|
4299
|
+
inputSchema: {}
|
|
4300
|
+
},
|
|
4301
|
+
async () => {
|
|
4302
|
+
await authorize();
|
|
4303
|
+
return json(await listWorkspaceArtifacts(deps.db, grant.workspaceId));
|
|
4304
|
+
}
|
|
4305
|
+
);
|
|
4306
|
+
server.registerTool(
|
|
4307
|
+
"artifacts_get_source",
|
|
4308
|
+
{
|
|
4309
|
+
description: "Read an artifact's metadata and exact HTML source. Omit versionId for the current version.",
|
|
4310
|
+
inputSchema: {
|
|
4311
|
+
artifactId: z4.string().uuid(),
|
|
4312
|
+
versionId: z4.string().uuid().optional()
|
|
4313
|
+
}
|
|
4314
|
+
},
|
|
4315
|
+
async ({ artifactId: artifactId2, versionId }) => {
|
|
4316
|
+
await authorize();
|
|
4317
|
+
if (!deps.objectStorage) throw new Error("Object storage is not configured");
|
|
4318
|
+
const [detail, ref] = await Promise.all([
|
|
4319
|
+
getWorkspaceArtifact(deps.db, grant.workspaceId, artifactId2),
|
|
4320
|
+
getWorkspaceArtifactContentRef(deps.db, grant.workspaceId, artifactId2, versionId)
|
|
4321
|
+
]);
|
|
4322
|
+
const object5 = await deps.objectStorage.getObjectBytes(ref.contentKey);
|
|
4323
|
+
if (!object5) throw new Error("Artifact content is unavailable");
|
|
4324
|
+
const actualHash = createHash2("sha256").update(object5.bytes).digest("hex");
|
|
4325
|
+
if (actualHash !== ref.version.contentSha256)
|
|
4326
|
+
throw new Error("Artifact content failed integrity verification");
|
|
4327
|
+
return json({ detail, version: ref.version, html: new TextDecoder().decode(object5.bytes) });
|
|
4328
|
+
}
|
|
4329
|
+
);
|
|
4330
|
+
server.registerTool(
|
|
4331
|
+
"artifacts_create",
|
|
4332
|
+
{
|
|
4333
|
+
description: "Create and publish a generic static workspace artifact from a complete, self-contained HTML document with inline CSS. JavaScript and active or navigation-capable markup do not render in the MVP.",
|
|
4334
|
+
inputSchema: {
|
|
4335
|
+
title: z4.string().min(1).max(120),
|
|
4336
|
+
description: z4.string().max(2e3).nullable().optional(),
|
|
4337
|
+
slug: z4.string().regex(/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/).max(96).optional(),
|
|
4338
|
+
html: z4.string().min(1).max(WORKSPACE_ARTIFACT_HTML_MAX_UTF8_BYTES),
|
|
4339
|
+
idempotencyKey: z4.string().min(1).max(200)
|
|
4340
|
+
}
|
|
4341
|
+
},
|
|
4342
|
+
async ({ title, description, slug, html, idempotencyKey }) => {
|
|
4343
|
+
await authorize();
|
|
4344
|
+
const artifactId2 = crypto.randomUUID();
|
|
4345
|
+
const slugBase = slug ?? (normalizeWorkspaceArtifactSlug(title) || "artifact");
|
|
4346
|
+
const resolvedSlug = slug ?? `${slugBase.slice(0, 87)}-${artifactId2.slice(0, 8)}`;
|
|
4347
|
+
return json(
|
|
4348
|
+
await createWorkspaceArtifact(deps.db, {
|
|
4349
|
+
artifactId: artifactId2,
|
|
4350
|
+
slug: resolvedSlug,
|
|
4351
|
+
title,
|
|
4352
|
+
description: description ?? null,
|
|
4353
|
+
...prepare(html),
|
|
4354
|
+
...provenance2(idempotencyKey, "artifacts_create")
|
|
4355
|
+
})
|
|
4356
|
+
);
|
|
4357
|
+
}
|
|
4358
|
+
);
|
|
4359
|
+
server.registerTool(
|
|
4360
|
+
"artifacts_publish",
|
|
4361
|
+
{
|
|
4362
|
+
description: "Publish a new immutable static HTML/CSS version. JavaScript and active or navigation-capable markup do not render in the MVP. First read the current source and pass its version id for optimistic concurrency.",
|
|
4363
|
+
inputSchema: {
|
|
4364
|
+
artifactId: z4.string().uuid(),
|
|
4365
|
+
expectedCurrentVersionId: z4.string().uuid(),
|
|
4366
|
+
title: z4.string().min(1).max(120).optional(),
|
|
4367
|
+
description: z4.string().max(2e3).nullable().optional(),
|
|
4368
|
+
html: z4.string().min(1).max(WORKSPACE_ARTIFACT_HTML_MAX_UTF8_BYTES),
|
|
4369
|
+
idempotencyKey: z4.string().min(1).max(200)
|
|
4370
|
+
}
|
|
4371
|
+
},
|
|
4372
|
+
async ({ artifactId: artifactId2, expectedCurrentVersionId, title, description, html, idempotencyKey }) => {
|
|
4373
|
+
await authorize();
|
|
4374
|
+
return json(
|
|
4375
|
+
await publishWorkspaceArtifactVersion(deps.db, {
|
|
4376
|
+
artifactId: artifactId2,
|
|
4377
|
+
expectedCurrentVersionId,
|
|
4378
|
+
...title !== void 0 ? { title } : {},
|
|
4379
|
+
...description !== void 0 ? { description } : {},
|
|
4380
|
+
...prepare(html),
|
|
4381
|
+
...provenance2(idempotencyKey, "artifacts_publish")
|
|
4382
|
+
})
|
|
4383
|
+
);
|
|
4384
|
+
}
|
|
4385
|
+
);
|
|
4386
|
+
server.registerTool(
|
|
4387
|
+
"artifacts_rollback",
|
|
4388
|
+
{
|
|
4389
|
+
description: "Promote an existing immutable artifact version back to current without rewriting history.",
|
|
4390
|
+
inputSchema: {
|
|
4391
|
+
artifactId: z4.string().uuid(),
|
|
4392
|
+
versionId: z4.string().uuid(),
|
|
4393
|
+
expectedCurrentVersionId: z4.string().uuid(),
|
|
4394
|
+
reason: z4.string().min(1).max(4096),
|
|
4395
|
+
idempotencyKey: z4.string().min(1).max(200)
|
|
4396
|
+
}
|
|
4397
|
+
},
|
|
4398
|
+
async ({ artifactId: artifactId2, versionId, expectedCurrentVersionId, reason, idempotencyKey }) => {
|
|
4399
|
+
await authorize();
|
|
4400
|
+
return json(
|
|
4401
|
+
await rollbackWorkspaceArtifact(deps.db, {
|
|
4402
|
+
artifactId: artifactId2,
|
|
4403
|
+
versionId,
|
|
4404
|
+
expectedCurrentVersionId,
|
|
4405
|
+
reason,
|
|
4406
|
+
...provenance2(idempotencyKey, "artifacts_rollback")
|
|
4407
|
+
})
|
|
4408
|
+
);
|
|
4409
|
+
}
|
|
4410
|
+
);
|
|
4411
|
+
}
|
|
4174
4412
|
function preferenceAttemptClaims(grant) {
|
|
4175
4413
|
const metadata = grant.metadata ?? {};
|
|
4176
4414
|
if (typeof metadata["sessionId"] !== "string" || typeof metadata["turnId"] !== "string" || typeof metadata["attemptId"] !== "string" || typeof metadata["executionGeneration"] !== "number" || !Number.isSafeInteger(metadata["executionGeneration"]) || metadata["executionGeneration"] < 1 || metadata["executionGeneration"] > 2147483647) {
|
|
@@ -5651,7 +5889,7 @@ import {
|
|
|
5651
5889
|
mcpSerializedSizeBytes
|
|
5652
5890
|
} from "@opengeni/runtime/mcp-network";
|
|
5653
5891
|
import { Buffer as Buffer2 } from "buffer";
|
|
5654
|
-
import { createHash as
|
|
5892
|
+
import { createHash as createHash3 } from "crypto";
|
|
5655
5893
|
var APPROVAL_REQUIRED_MESSAGE = "requires approval - invoke via the agent";
|
|
5656
5894
|
var TOOLSPACE_AUTH_NEEDED_ERROR = {
|
|
5657
5895
|
// OpenGeni application-defined JSON-RPC code. Keep this positive so it cannot
|
|
@@ -6169,7 +6407,7 @@ function toolspaceAuditSummary(value) {
|
|
|
6169
6407
|
return {
|
|
6170
6408
|
redacted: true,
|
|
6171
6409
|
sizeBytes: Buffer2.byteLength(serialized),
|
|
6172
|
-
sha256:
|
|
6410
|
+
sha256: createHash3("sha256").update(serialized).digest("hex")
|
|
6173
6411
|
};
|
|
6174
6412
|
}
|
|
6175
6413
|
function toolspaceErrorAttributes(error) {
|
|
@@ -6390,12 +6628,12 @@ async function authNeededFetchResponse(input, request, auth) {
|
|
|
6390
6628
|
return new Response("Authentication required for MCP server connection", { status: 401 });
|
|
6391
6629
|
}
|
|
6392
6630
|
async function mcpRequestInfo(input, init) {
|
|
6393
|
-
const
|
|
6394
|
-
if (!
|
|
6631
|
+
const body2 = typeof init?.body === "string" ? init.body : input instanceof Request && (init?.method ?? input.method).toUpperCase() === "POST" ? await input.clone().text().catch(() => "") : "";
|
|
6632
|
+
if (!body2) {
|
|
6395
6633
|
return {};
|
|
6396
6634
|
}
|
|
6397
6635
|
try {
|
|
6398
|
-
const parsed = JSON.parse(
|
|
6636
|
+
const parsed = JSON.parse(body2);
|
|
6399
6637
|
const method = typeof parsed.method === "string" ? parsed.method : void 0;
|
|
6400
6638
|
const id = typeof parsed.id === "string" || typeof parsed.id === "number" || parsed.id === null ? parsed.id : void 0;
|
|
6401
6639
|
const toolName = method === "tools/call" && typeof parsed.params?.name === "string" ? parsed.params.name : void 0;
|
|
@@ -6451,9 +6689,9 @@ async function loadAsset(file) {
|
|
|
6451
6689
|
if (cached !== void 0) {
|
|
6452
6690
|
return cached;
|
|
6453
6691
|
}
|
|
6454
|
-
const
|
|
6455
|
-
assetCache.set(file,
|
|
6456
|
-
return
|
|
6692
|
+
const body2 = await readFile(new URL(file, INSTALL_DIR), "utf8");
|
|
6693
|
+
assetCache.set(file, body2);
|
|
6694
|
+
return body2;
|
|
6457
6695
|
}
|
|
6458
6696
|
var DEFAULT_BASE_REWRITES = {
|
|
6459
6697
|
"install.sh": (base) => ({
|
|
@@ -6465,16 +6703,16 @@ var DEFAULT_BASE_REWRITES = {
|
|
|
6465
6703
|
to: `$OpengeniInstallDefaultBaseUrl = '${base}'`
|
|
6466
6704
|
})
|
|
6467
6705
|
};
|
|
6468
|
-
function rewriteDefaultBaseUrl(file,
|
|
6706
|
+
function rewriteDefaultBaseUrl(file, body2, publicBaseUrl) {
|
|
6469
6707
|
if (!publicBaseUrl || !/^https?:\/\//.test(publicBaseUrl)) {
|
|
6470
|
-
return
|
|
6708
|
+
return body2;
|
|
6471
6709
|
}
|
|
6472
6710
|
const base = publicBaseUrl.replace(/\/+$/, "");
|
|
6473
6711
|
const rule = DEFAULT_BASE_REWRITES[file]?.(base);
|
|
6474
|
-
if (!rule || !
|
|
6475
|
-
return
|
|
6712
|
+
if (!rule || !body2.includes(rule.from)) {
|
|
6713
|
+
return body2;
|
|
6476
6714
|
}
|
|
6477
|
-
return
|
|
6715
|
+
return body2.replace(rule.from, rule.to);
|
|
6478
6716
|
}
|
|
6479
6717
|
function bakedContentType(asset) {
|
|
6480
6718
|
if (asset.endsWith(".sha256") || asset.endsWith(".minisig")) {
|
|
@@ -6502,8 +6740,8 @@ function registerInstallRoutes(app, deps) {
|
|
|
6502
6740
|
const stableAgentTag = `agent-v${deps.settings.agentStableVersion}`;
|
|
6503
6741
|
for (const [path, { file, contentType }] of Object.entries(TEXT_ASSETS)) {
|
|
6504
6742
|
app.get(path, async (c) => {
|
|
6505
|
-
const
|
|
6506
|
-
return c.text(
|
|
6743
|
+
const body2 = rewriteDefaultBaseUrl(file, await loadAsset(file), deps.settings.publicBaseUrl);
|
|
6744
|
+
return c.text(body2, 200, {
|
|
6507
6745
|
"content-type": contentType,
|
|
6508
6746
|
// Short cache: the edge serves the latest committed copy; new installs
|
|
6509
6747
|
// should pick up script fixes promptly, but a brief cache absorbs bursts.
|
|
@@ -6582,7 +6820,7 @@ function isAuthExempt(c, settings) {
|
|
|
6582
6820
|
if (path === "/v1/github/setup" || path === "/v1/github/install/callback" || path === "/v1/github/oauth/callback" || path === "/v1/github/app-manifest/callback") {
|
|
6583
6821
|
return true;
|
|
6584
6822
|
}
|
|
6585
|
-
if (path === "/v1/integrations/oauth/callback" || path === "/v1/integrations/oauth/client-metadata.json" || path === "/v1/integrations/slack/callback") {
|
|
6823
|
+
if (path === "/v1/integrations/oauth/callback" || path === "/v1/integrations/oauth/client-metadata.json" || path === "/v1/integrations/slack/callback" || path === "/v1/integrations/slack/events" || path === "/v1/integrations/slack/commands" || path === "/v1/integrations/slack/interactions") {
|
|
6586
6824
|
return true;
|
|
6587
6825
|
}
|
|
6588
6826
|
if (path.startsWith("/v1/catalog-assets/")) {
|
|
@@ -7450,12 +7688,12 @@ function registerCodexRoutes(app, deps) {
|
|
|
7450
7688
|
app.patch("/v1/workspaces/:workspaceId/codex/settings", async (c) => {
|
|
7451
7689
|
const workspaceId = c.req.param("workspaceId");
|
|
7452
7690
|
const grant = await requireAccessGrant2(c, deps, workspaceId, "workspace:admin");
|
|
7453
|
-
const
|
|
7691
|
+
const body2 = await c.req.json().catch(() => ({}));
|
|
7454
7692
|
const patch = {};
|
|
7455
|
-
if (typeof
|
|
7456
|
-
patch.rotationEnabled =
|
|
7693
|
+
if (typeof body2.rotationEnabled === "boolean") {
|
|
7694
|
+
patch.rotationEnabled = body2.rotationEnabled;
|
|
7457
7695
|
}
|
|
7458
|
-
if (patch.rotationEnabled === void 0 &&
|
|
7696
|
+
if (patch.rotationEnabled === void 0 && body2.rotationStrategy === void 0) {
|
|
7459
7697
|
throw new HTTPException6(400, { message: "no settings to update" });
|
|
7460
7698
|
}
|
|
7461
7699
|
if (patch.rotationEnabled === void 0) {
|
|
@@ -7488,8 +7726,8 @@ function registerCodexRoutes(app, deps) {
|
|
|
7488
7726
|
const workspaceId = c.req.param("workspaceId");
|
|
7489
7727
|
await requireAccessGrant2(c, deps, workspaceId, "workspace:admin");
|
|
7490
7728
|
const accountId = c.req.param("accountId");
|
|
7491
|
-
const
|
|
7492
|
-
const label = typeof
|
|
7729
|
+
const body2 = await c.req.json();
|
|
7730
|
+
const label = typeof body2.label === "string" ? body2.label : null;
|
|
7493
7731
|
const renamed = await renameCodexAccount(db, workspaceId, accountId, label);
|
|
7494
7732
|
if (!renamed) {
|
|
7495
7733
|
throw new HTTPException6(404, { message: "codex account not found" });
|
|
@@ -8000,7 +8238,7 @@ function registerCodexRoutes(app, deps) {
|
|
|
8000
8238
|
}
|
|
8001
8239
|
|
|
8002
8240
|
// src/routes/connections.ts
|
|
8003
|
-
import { createHash as
|
|
8241
|
+
import { createHash as createHash6 } from "crypto";
|
|
8004
8242
|
import {
|
|
8005
8243
|
ConnectionResponse,
|
|
8006
8244
|
CreateConnectionRequest,
|
|
@@ -8044,7 +8282,7 @@ import {
|
|
|
8044
8282
|
import { HTTPException as HTTPException10 } from "hono/http-exception";
|
|
8045
8283
|
|
|
8046
8284
|
// src/integrations/google-drive.ts
|
|
8047
|
-
import { createHash as
|
|
8285
|
+
import { createHash as createHash5, randomBytes as randomBytes2 } from "crypto";
|
|
8048
8286
|
import {
|
|
8049
8287
|
GOOGLE_DRIVE_CREDENTIAL_LABEL,
|
|
8050
8288
|
GOOGLE_DRIVE_CREDENTIAL_ROLE,
|
|
@@ -8105,7 +8343,7 @@ import {
|
|
|
8105
8343
|
validateHttpUrl
|
|
8106
8344
|
} from "@opengeni/network";
|
|
8107
8345
|
import { Buffer as Buffer3 } from "buffer";
|
|
8108
|
-
import { createHash as
|
|
8346
|
+
import { createHash as createHash4, randomBytes } from "crypto";
|
|
8109
8347
|
import { HTTPException as HTTPException8 } from "hono/http-exception";
|
|
8110
8348
|
|
|
8111
8349
|
// src/integrations/provider-domain.ts
|
|
@@ -8861,28 +9099,28 @@ async function clientForState(db, settings, state) {
|
|
|
8861
9099
|
};
|
|
8862
9100
|
}
|
|
8863
9101
|
async function exchangeAuthorizationCode(settings, input) {
|
|
8864
|
-
const
|
|
8865
|
-
|
|
8866
|
-
|
|
8867
|
-
|
|
8868
|
-
|
|
8869
|
-
|
|
9102
|
+
const body2 = new URLSearchParams();
|
|
9103
|
+
body2.set("grant_type", "authorization_code");
|
|
9104
|
+
body2.set("code", input.code);
|
|
9105
|
+
body2.set("redirect_uri", input.redirectUri);
|
|
9106
|
+
body2.set("code_verifier", input.verifier);
|
|
9107
|
+
body2.set("resource", input.resource);
|
|
8870
9108
|
const headers = {
|
|
8871
9109
|
"content-type": "application/x-www-form-urlencoded",
|
|
8872
9110
|
accept: "application/json"
|
|
8873
9111
|
};
|
|
8874
9112
|
if (input.client.clientSecret && input.client.tokenEndpointAuthMethod === "client_secret_post") {
|
|
8875
|
-
|
|
8876
|
-
|
|
9113
|
+
body2.set("client_id", input.client.clientId);
|
|
9114
|
+
body2.set("client_secret", input.client.clientSecret);
|
|
8877
9115
|
} else if (input.client.clientSecret && input.client.tokenEndpointAuthMethod === "client_secret_basic") {
|
|
8878
9116
|
headers.authorization = `Basic ${Buffer3.from(`${input.client.clientId}:${input.client.clientSecret}`).toString("base64")}`;
|
|
8879
9117
|
} else {
|
|
8880
|
-
|
|
9118
|
+
body2.set("client_id", input.client.clientId);
|
|
8881
9119
|
}
|
|
8882
9120
|
const response = await fetchOAuth(input.tokenEndpoint, settings, {
|
|
8883
9121
|
method: "POST",
|
|
8884
9122
|
headers,
|
|
8885
|
-
body
|
|
9123
|
+
body: body2
|
|
8886
9124
|
});
|
|
8887
9125
|
if (!response.ok) {
|
|
8888
9126
|
const oauthError = await oauthErrorFromResponse(response);
|
|
@@ -9256,7 +9494,7 @@ function expiresAtFromTokenResponse(payload) {
|
|
|
9256
9494
|
return null;
|
|
9257
9495
|
}
|
|
9258
9496
|
function pkceChallenge(verifier) {
|
|
9259
|
-
return
|
|
9497
|
+
return createHash4("sha256").update(verifier).digest("base64url");
|
|
9260
9498
|
}
|
|
9261
9499
|
function randomPkceVerifier() {
|
|
9262
9500
|
return randomBytes(32).toString("base64url");
|
|
@@ -9328,7 +9566,7 @@ async function startGoogleDriveOAuth(deps, input) {
|
|
|
9328
9566
|
authorizationUrl.searchParams.set("code_challenge_method", "S256");
|
|
9329
9567
|
authorizationUrl.searchParams.set(
|
|
9330
9568
|
"code_challenge",
|
|
9331
|
-
|
|
9569
|
+
createHash5("sha256").update(verifier).digest("base64url")
|
|
9332
9570
|
);
|
|
9333
9571
|
return GoogleDriveOAuthStartResponse.parse({
|
|
9334
9572
|
authorizationUrl: authorizationUrl.toString(),
|
|
@@ -9515,8 +9753,8 @@ async function browseGoogleDrive(deps, input) {
|
|
|
9515
9753
|
url,
|
|
9516
9754
|
label: "Google Drive file list"
|
|
9517
9755
|
});
|
|
9518
|
-
const
|
|
9519
|
-
const items = Array.isArray(
|
|
9756
|
+
const record4 = objectRecord(payload);
|
|
9757
|
+
const items = Array.isArray(record4.files) ? record4.files.map(parseDriveItem).filter((item) => item !== null) : [];
|
|
9520
9758
|
const current = await getConnectionMetadata2(
|
|
9521
9759
|
deps.db,
|
|
9522
9760
|
input.workspaceId,
|
|
@@ -9528,8 +9766,8 @@ async function browseGoogleDrive(deps, input) {
|
|
|
9528
9766
|
parentId,
|
|
9529
9767
|
current: currentItem,
|
|
9530
9768
|
items,
|
|
9531
|
-
nextPageToken: optionalString(
|
|
9532
|
-
incompleteSearch:
|
|
9769
|
+
nextPageToken: optionalString(record4.nextPageToken),
|
|
9770
|
+
incompleteSearch: record4.incompleteSearch === true
|
|
9533
9771
|
});
|
|
9534
9772
|
}
|
|
9535
9773
|
async function saveGoogleDriveSource(deps, input) {
|
|
@@ -9724,7 +9962,7 @@ async function requireGoogleDriveCallbackGrant(deps, state) {
|
|
|
9724
9962
|
}
|
|
9725
9963
|
}
|
|
9726
9964
|
async function exchangeGoogleAuthorizationCode(input, fetchImpl) {
|
|
9727
|
-
const
|
|
9965
|
+
const body2 = new URLSearchParams({
|
|
9728
9966
|
code: input.code,
|
|
9729
9967
|
code_verifier: input.verifier,
|
|
9730
9968
|
client_id: input.clientId,
|
|
@@ -9738,7 +9976,7 @@ async function exchangeGoogleAuthorizationCode(input, fetchImpl) {
|
|
|
9738
9976
|
accept: "application/json",
|
|
9739
9977
|
"content-type": "application/x-www-form-urlencoded"
|
|
9740
9978
|
},
|
|
9741
|
-
body
|
|
9979
|
+
body: body2
|
|
9742
9980
|
});
|
|
9743
9981
|
if (!response.ok) {
|
|
9744
9982
|
await response.body?.cancel().catch(() => void 0);
|
|
@@ -10087,7 +10325,7 @@ function registerConnectionRoutes(app, deps) {
|
|
|
10087
10325
|
accountId: state.accountId,
|
|
10088
10326
|
workspaceId: state.workspaceId,
|
|
10089
10327
|
subjectId: state.subjectId,
|
|
10090
|
-
callbackDigest:
|
|
10328
|
+
callbackDigest: createHash6("sha256").update(state.nonce).digest("hex"),
|
|
10091
10329
|
installMode: state.connectionId ? "reinstall" : "connect",
|
|
10092
10330
|
...failure
|
|
10093
10331
|
});
|
|
@@ -11121,27 +11359,27 @@ function registerFileRoutes(app, deps) {
|
|
|
11121
11359
|
app.get("/v1/workspaces/:workspaceId/artifacts/:artifactId", async (c) => {
|
|
11122
11360
|
const workspaceId = c.req.param("workspaceId");
|
|
11123
11361
|
await requireAccessGrant4(c, deps, workspaceId, "files:read");
|
|
11124
|
-
const
|
|
11125
|
-
const artifact = await getRetainedFileArtifact(db, workspaceId,
|
|
11362
|
+
const artifactId2 = retainedArtifactId(c.req.param("artifactId"));
|
|
11363
|
+
const artifact = await getRetainedFileArtifact(db, workspaceId, artifactId2);
|
|
11126
11364
|
if (!artifact) {
|
|
11127
|
-
return c.json(retainedArtifactUnavailable(
|
|
11365
|
+
return c.json(retainedArtifactUnavailable(artifactId2, "deleted"), 404);
|
|
11128
11366
|
}
|
|
11129
11367
|
return c.json(retainedArtifactMetadata(artifact));
|
|
11130
11368
|
});
|
|
11131
11369
|
app.get("/v1/workspaces/:workspaceId/artifacts/:artifactId/content", async (c) => {
|
|
11132
11370
|
const workspaceId = c.req.param("workspaceId");
|
|
11133
11371
|
await requireAccessGrant4(c, deps, workspaceId, "files:read");
|
|
11134
|
-
const
|
|
11135
|
-
const artifact = await getRetainedFileArtifact(db, workspaceId,
|
|
11372
|
+
const artifactId2 = retainedArtifactId(c.req.param("artifactId"));
|
|
11373
|
+
const artifact = await getRetainedFileArtifact(db, workspaceId, artifactId2);
|
|
11136
11374
|
if (!artifact) {
|
|
11137
|
-
return c.json(retainedArtifactUnavailable(
|
|
11375
|
+
return c.json(retainedArtifactUnavailable(artifactId2, "deleted"), 404);
|
|
11138
11376
|
}
|
|
11139
11377
|
const metadata = retainedArtifactMetadata(artifact);
|
|
11140
11378
|
if (!metadata.available) {
|
|
11141
11379
|
return c.json(metadata, retainedArtifactUnavailableStatus(metadata.reason));
|
|
11142
11380
|
}
|
|
11143
11381
|
if (!objectStorage) {
|
|
11144
|
-
return c.json(retainedArtifactUnavailable(
|
|
11382
|
+
return c.json(retainedArtifactUnavailable(artifactId2, "missing_storage"), 503);
|
|
11145
11383
|
}
|
|
11146
11384
|
const rangeHeader = c.req.header("range");
|
|
11147
11385
|
const range = resolveRetainedOutputRange(
|
|
@@ -11180,7 +11418,7 @@ function registerFileRoutes(app, deps) {
|
|
|
11180
11418
|
};
|
|
11181
11419
|
if (range.kind === "empty") {
|
|
11182
11420
|
if (!await objectStorage.fileExists(artifact.file)) {
|
|
11183
|
-
return c.json(retainedArtifactUnavailable(
|
|
11421
|
+
return c.json(retainedArtifactUnavailable(artifactId2, "missing_storage"), 410);
|
|
11184
11422
|
}
|
|
11185
11423
|
return c.body(null, 200, headers);
|
|
11186
11424
|
}
|
|
@@ -11189,7 +11427,7 @@ function registerFileRoutes(app, deps) {
|
|
|
11189
11427
|
end: range.end
|
|
11190
11428
|
});
|
|
11191
11429
|
if (!bytes) {
|
|
11192
|
-
return c.json(retainedArtifactUnavailable(
|
|
11430
|
+
return c.json(retainedArtifactUnavailable(artifactId2, "missing_storage"), 410);
|
|
11193
11431
|
}
|
|
11194
11432
|
if (bytes.byteLength !== range.length) {
|
|
11195
11433
|
throw new HTTPException11(502, { message: "object storage returned an invalid byte range" });
|
|
@@ -11233,8 +11471,8 @@ function retainedArtifactId(value) {
|
|
|
11233
11471
|
}
|
|
11234
11472
|
return parsed.data;
|
|
11235
11473
|
}
|
|
11236
|
-
function retainedArtifactUnavailable(
|
|
11237
|
-
return RetainedArtifactMetadataSchema.parse({ available: false, artifactId, reason });
|
|
11474
|
+
function retainedArtifactUnavailable(artifactId2, reason) {
|
|
11475
|
+
return RetainedArtifactMetadataSchema.parse({ available: false, artifactId: artifactId2, reason });
|
|
11238
11476
|
}
|
|
11239
11477
|
function retainedArtifactMetadata(artifact) {
|
|
11240
11478
|
const reference = retainedArtifactReferenceFromFile(artifact.file);
|
|
@@ -11920,18 +12158,18 @@ async function lookupDeviceEnrollment(services, input) {
|
|
|
11920
12158
|
const { db } = services;
|
|
11921
12159
|
return await getPendingDeviceEnrollmentRequestByUserCodeGlobal(db, input.userCode);
|
|
11922
12160
|
}
|
|
11923
|
-
function toLookupResponse(
|
|
12161
|
+
function toLookupResponse(record4) {
|
|
11924
12162
|
return {
|
|
11925
|
-
workspaceId:
|
|
11926
|
-
userCode:
|
|
12163
|
+
workspaceId: record4.workspaceId,
|
|
12164
|
+
userCode: record4.userCode,
|
|
11927
12165
|
machine: {
|
|
11928
|
-
machineName:
|
|
11929
|
-
os:
|
|
11930
|
-
arch:
|
|
11931
|
-
canOfferDisplay:
|
|
11932
|
-
requestsScreenControl:
|
|
12166
|
+
machineName: record4.machineName,
|
|
12167
|
+
os: record4.os,
|
|
12168
|
+
arch: record4.arch,
|
|
12169
|
+
canOfferDisplay: record4.canOfferDisplay,
|
|
12170
|
+
requestsScreenControl: record4.requestsScreenControl
|
|
11933
12171
|
},
|
|
11934
|
-
expiresAt:
|
|
12172
|
+
expiresAt: record4.expiresAt
|
|
11935
12173
|
};
|
|
11936
12174
|
}
|
|
11937
12175
|
async function denyDeviceEnrollment(services, input) {
|
|
@@ -12114,8 +12352,8 @@ function registerEnrollmentRoutes(app, deps) {
|
|
|
12114
12352
|
if (!parsed.success) {
|
|
12115
12353
|
throw new HTTPException13(400, { message: "invalid device-start request" });
|
|
12116
12354
|
}
|
|
12117
|
-
const
|
|
12118
|
-
const workspace = await getWorkspace(db,
|
|
12355
|
+
const body2 = parsed.data;
|
|
12356
|
+
const workspace = await getWorkspace(db, body2.workspaceId);
|
|
12119
12357
|
if (!workspace) {
|
|
12120
12358
|
throw new HTTPException13(404, { message: "workspace not found" });
|
|
12121
12359
|
}
|
|
@@ -12124,12 +12362,12 @@ function registerEnrollmentRoutes(app, deps) {
|
|
|
12124
12362
|
{
|
|
12125
12363
|
accountId: workspace.accountId,
|
|
12126
12364
|
workspaceId: workspace.id,
|
|
12127
|
-
publicKey:
|
|
12128
|
-
os:
|
|
12129
|
-
arch:
|
|
12130
|
-
machineName:
|
|
12131
|
-
canOfferDisplay:
|
|
12132
|
-
requestsScreenControl:
|
|
12365
|
+
publicKey: body2.publicKey,
|
|
12366
|
+
os: body2.os,
|
|
12367
|
+
arch: body2.arch,
|
|
12368
|
+
machineName: body2.machineName ?? null,
|
|
12369
|
+
canOfferDisplay: body2.canOfferDisplay,
|
|
12370
|
+
requestsScreenControl: body2.requestsScreenControl,
|
|
12133
12371
|
// The approve page is served at the SAME origin as this request.
|
|
12134
12372
|
verificationOrigin: new URL(c.req.url).origin
|
|
12135
12373
|
}
|
|
@@ -12156,19 +12394,19 @@ function registerEnrollmentRoutes(app, deps) {
|
|
|
12156
12394
|
if (!parsed.success) {
|
|
12157
12395
|
throw new HTTPException13(400, { message: "invalid device-lookup request" });
|
|
12158
12396
|
}
|
|
12159
|
-
const
|
|
12397
|
+
const record4 = await lookupDeviceEnrollment(
|
|
12160
12398
|
{ db, settings },
|
|
12161
12399
|
{ userCode: parsed.data.userCode }
|
|
12162
12400
|
);
|
|
12163
|
-
if (!
|
|
12401
|
+
if (!record4) {
|
|
12164
12402
|
throw new HTTPException13(404, { message: "no pending enrollment for that code" });
|
|
12165
12403
|
}
|
|
12166
12404
|
try {
|
|
12167
|
-
await requireAccessGrant6(c, deps,
|
|
12405
|
+
await requireAccessGrant6(c, deps, record4.workspaceId, "enrollments:read");
|
|
12168
12406
|
} catch {
|
|
12169
12407
|
throw new HTTPException13(404, { message: "no pending enrollment for that code" });
|
|
12170
12408
|
}
|
|
12171
|
-
return c.json(DeviceEnrollmentLookupResponse.parse(toLookupResponse(
|
|
12409
|
+
return c.json(DeviceEnrollmentLookupResponse.parse(toLookupResponse(record4)), 200);
|
|
12172
12410
|
});
|
|
12173
12411
|
app.post("/v1/enrollments/token/exchange", async (c) => {
|
|
12174
12412
|
assertSelfhostedEnabled();
|
|
@@ -12177,16 +12415,16 @@ function registerEnrollmentRoutes(app, deps) {
|
|
|
12177
12415
|
if (!parsed.success) {
|
|
12178
12416
|
throw new HTTPException13(400, { message: "invalid enroll-token-exchange request" });
|
|
12179
12417
|
}
|
|
12180
|
-
const
|
|
12418
|
+
const body2 = parsed.data;
|
|
12181
12419
|
const result = await exchangeEnrollToken(
|
|
12182
12420
|
{ db, settings },
|
|
12183
12421
|
{
|
|
12184
|
-
token:
|
|
12185
|
-
publicKey:
|
|
12186
|
-
os:
|
|
12187
|
-
arch:
|
|
12188
|
-
machineName:
|
|
12189
|
-
canOfferDisplay:
|
|
12422
|
+
token: body2.token,
|
|
12423
|
+
publicKey: body2.publicKey,
|
|
12424
|
+
os: body2.os,
|
|
12425
|
+
arch: body2.arch,
|
|
12426
|
+
machineName: body2.machineName ?? null,
|
|
12427
|
+
canOfferDisplay: body2.canOfferDisplay
|
|
12190
12428
|
}
|
|
12191
12429
|
);
|
|
12192
12430
|
if (!result.ok) {
|
|
@@ -12205,14 +12443,14 @@ function registerEnrollmentRoutes(app, deps) {
|
|
|
12205
12443
|
if (!parsed.success) {
|
|
12206
12444
|
throw new HTTPException13(400, { message: "invalid device-approve request" });
|
|
12207
12445
|
}
|
|
12208
|
-
const
|
|
12446
|
+
const body2 = parsed.data;
|
|
12209
12447
|
const approved = await approveDeviceEnrollment(
|
|
12210
12448
|
{ db, settings },
|
|
12211
12449
|
{
|
|
12212
12450
|
accountId: grant.accountId,
|
|
12213
12451
|
workspaceId,
|
|
12214
|
-
userCode:
|
|
12215
|
-
allowScreenControl:
|
|
12452
|
+
userCode: body2.userCode,
|
|
12453
|
+
allowScreenControl: body2.allowScreenControl,
|
|
12216
12454
|
// The LOUD consent record: WHO consented (the authenticated subject + label).
|
|
12217
12455
|
approvedBySubjectId: grant.subjectId,
|
|
12218
12456
|
approvedBySubjectLabel: grant.subjectLabel ?? null
|
|
@@ -12579,7 +12817,7 @@ function registerMachineRoutes(app, deps) {
|
|
|
12579
12817
|
const grant = await requireAccessGrant7(c, deps, workspaceId, "sessions:control");
|
|
12580
12818
|
assertSelfhostedEnabled();
|
|
12581
12819
|
const sessionId = c.req.param("sessionId");
|
|
12582
|
-
const
|
|
12820
|
+
const body2 = SwapActiveSandboxRequest.parse(await c.req.json());
|
|
12583
12821
|
const ctx = await buildFleetContextForSession2(deps, {
|
|
12584
12822
|
accountId: grant.accountId,
|
|
12585
12823
|
workspaceId,
|
|
@@ -12603,7 +12841,7 @@ function registerMachineRoutes(app, deps) {
|
|
|
12603
12841
|
}
|
|
12604
12842
|
},
|
|
12605
12843
|
ctx,
|
|
12606
|
-
|
|
12844
|
+
body2.target
|
|
12607
12845
|
);
|
|
12608
12846
|
return c.json(SwapActiveSandboxResponse.parse(result));
|
|
12609
12847
|
});
|
|
@@ -12858,8 +13096,8 @@ function registerApiKeyRoutes(app, deps) {
|
|
|
12858
13096
|
async (c) => {
|
|
12859
13097
|
const workspaceId = c.req.param("workspaceId");
|
|
12860
13098
|
const grant = await requireAccessGrant9(c, deps, workspaceId, "api_keys:manage");
|
|
12861
|
-
const
|
|
12862
|
-
const permissions =
|
|
13099
|
+
const body2 = c.req.valid("json");
|
|
13100
|
+
const permissions = body2.permissions.length > 0 ? body2.permissions : ["workspace:read"];
|
|
12863
13101
|
ensureDelegablePermissions(grant.permissions, permissions);
|
|
12864
13102
|
await requireLimit4(deps, {
|
|
12865
13103
|
accountId: grant.accountId,
|
|
@@ -12872,11 +13110,11 @@ function registerApiKeyRoutes(app, deps) {
|
|
|
12872
13110
|
const apiKey = await createApiKey(deps.db, {
|
|
12873
13111
|
accountId: grant.accountId,
|
|
12874
13112
|
workspaceId: grant.workspaceId,
|
|
12875
|
-
name:
|
|
13113
|
+
name: body2.name,
|
|
12876
13114
|
prefix,
|
|
12877
13115
|
keyHash: await sha256Hex(token),
|
|
12878
13116
|
permissions,
|
|
12879
|
-
expiresAt:
|
|
13117
|
+
expiresAt: body2.expiresAt ? new Date(body2.expiresAt) : null
|
|
12880
13118
|
});
|
|
12881
13119
|
return c.json(CreateApiKeyResponse.parse({ apiKey, token }), 201);
|
|
12882
13120
|
}
|
|
@@ -12977,9 +13215,9 @@ function registerBillingRoutes(app, deps) {
|
|
|
12977
13215
|
message: parsed.error.issues[0]?.message ?? "invalid checkout request"
|
|
12978
13216
|
});
|
|
12979
13217
|
}
|
|
12980
|
-
const
|
|
12981
|
-
const accountId = requireSelectedAccount(context,
|
|
12982
|
-
const amountCents = usdToCents(
|
|
13218
|
+
const body2 = parsed.data;
|
|
13219
|
+
const accountId = requireSelectedAccount(context, body2.accountId, "billing:manage");
|
|
13220
|
+
const amountCents = usdToCents(body2.amountUsd);
|
|
12983
13221
|
const amountMicros = centsToMicros(amountCents);
|
|
12984
13222
|
const stripe = stripeClient(deps);
|
|
12985
13223
|
const customerId = await getOrCreateStripeCustomer(deps, stripe, context, accountId);
|
|
@@ -12992,8 +13230,8 @@ function registerBillingRoutes(app, deps) {
|
|
|
12992
13230
|
amountMicros,
|
|
12993
13231
|
creditsProductId: deps.settings.stripeCreditsProductId,
|
|
12994
13232
|
publicBaseUrl: deps.settings.publicBaseUrl,
|
|
12995
|
-
successUrl:
|
|
12996
|
-
cancelUrl:
|
|
13233
|
+
successUrl: body2.successUrl,
|
|
13234
|
+
cancelUrl: body2.cancelUrl,
|
|
12997
13235
|
idempotencyKey
|
|
12998
13236
|
}),
|
|
12999
13237
|
{ idempotencyKey }
|
|
@@ -14605,8 +14843,8 @@ function registerScheduledTaskRoutes(app, deps) {
|
|
|
14605
14843
|
quantity: 1,
|
|
14606
14844
|
model: task.agentConfig.model ?? deps.settings.openaiModel
|
|
14607
14845
|
});
|
|
14608
|
-
const
|
|
14609
|
-
const { triggerId } = TriggerScheduledTaskRequest.parse(
|
|
14846
|
+
const body2 = await c.req.json().catch(() => ({}));
|
|
14847
|
+
const { triggerId } = TriggerScheduledTaskRequest.parse(body2 ?? {});
|
|
14610
14848
|
const triggerToken = scheduledTaskTriggerToken2(triggerId);
|
|
14611
14849
|
const agentRunUsageIdempotencyKey = manualScheduledTaskTriggerUsageKey2(
|
|
14612
14850
|
workspaceId,
|
|
@@ -15103,7 +15341,7 @@ function createByteBoundedSseStream(options = {}) {
|
|
|
15103
15341
|
if (!Number.isSafeInteger(stallTimeoutMs) || stallTimeoutMs <= 0) {
|
|
15104
15342
|
throw new RangeError("SSE write stall timeout must be a positive safe integer");
|
|
15105
15343
|
}
|
|
15106
|
-
const
|
|
15344
|
+
const encoder3 = new TextEncoder();
|
|
15107
15345
|
let controller;
|
|
15108
15346
|
let stopped = false;
|
|
15109
15347
|
let capacityWake = null;
|
|
@@ -15149,7 +15387,7 @@ function createByteBoundedSseStream(options = {}) {
|
|
|
15149
15387
|
return {
|
|
15150
15388
|
stream,
|
|
15151
15389
|
write: async (frame) => {
|
|
15152
|
-
const chunk =
|
|
15390
|
+
const chunk = encoder3.encode(frame);
|
|
15153
15391
|
if (chunk.byteLength > maxQueuedBytes) {
|
|
15154
15392
|
const error = new RangeError(
|
|
15155
15393
|
`SSE frame cannot fit in the configured queue (${chunk.byteLength} > ${maxQueuedBytes} bytes)`
|
|
@@ -15862,10 +16100,6 @@ function registerSessionRoutes(app, deps) {
|
|
|
15862
16100
|
)
|
|
15863
16101
|
});
|
|
15864
16102
|
const authorizeSessionHttp = async (c, next) => {
|
|
15865
|
-
if (!deps.sessionAuthorization) {
|
|
15866
|
-
await next();
|
|
15867
|
-
return;
|
|
15868
|
-
}
|
|
15869
16103
|
const workspaceId = c.req.param("workspaceId") ?? "";
|
|
15870
16104
|
const sessionId = c.req.param("sessionId") ?? "";
|
|
15871
16105
|
const operation = sessionAuthorizationOperationForHttp(
|
|
@@ -15880,6 +16114,10 @@ function registerSessionRoutes(app, deps) {
|
|
|
15880
16114
|
if (!operation) {
|
|
15881
16115
|
throw sessionAuthorizationHttpError(new SessionAuthorizationUnavailableError());
|
|
15882
16116
|
}
|
|
16117
|
+
if (operation === "session.codex_account.write" && !deps.sessionAuthorization) {
|
|
16118
|
+
await next();
|
|
16119
|
+
return;
|
|
16120
|
+
}
|
|
15883
16121
|
const grant = await requireAccessGrant14(c, deps, workspaceId);
|
|
15884
16122
|
try {
|
|
15885
16123
|
const authorization = await requireSessionAuthorization2(deps, grant, {
|
|
@@ -16112,15 +16350,26 @@ function registerSessionRoutes(app, deps) {
|
|
|
16112
16350
|
});
|
|
16113
16351
|
app.post("/v1/workspaces/:workspaceId/sessions/:sessionId/codex-account", async (c) => {
|
|
16114
16352
|
const workspaceId = c.req.param("workspaceId");
|
|
16115
|
-
await requireAccessGrant14(c, deps, workspaceId, "sessions:control");
|
|
16353
|
+
const grant = await requireAccessGrant14(c, deps, workspaceId, "sessions:control");
|
|
16116
16354
|
const sessionId = c.req.param("sessionId");
|
|
16117
|
-
const
|
|
16118
|
-
const target = typeof
|
|
16355
|
+
const body2 = await c.req.json();
|
|
16356
|
+
const target = typeof body2.target === "string" ? body2.target : "";
|
|
16119
16357
|
if (!target) {
|
|
16120
16358
|
throw new HTTPException23(400, {
|
|
16121
16359
|
message: 'target is required ("auto" or an account id)'
|
|
16122
16360
|
});
|
|
16123
16361
|
}
|
|
16362
|
+
if (!deps.sessionAuthorization) {
|
|
16363
|
+
try {
|
|
16364
|
+
await requireSessionAuthorization2(deps, grant, {
|
|
16365
|
+
sessionId,
|
|
16366
|
+
operation: "session.codex_account.write",
|
|
16367
|
+
surface: "http"
|
|
16368
|
+
});
|
|
16369
|
+
} catch (error) {
|
|
16370
|
+
throw sessionAuthorizationHttpError(error);
|
|
16371
|
+
}
|
|
16372
|
+
}
|
|
16124
16373
|
const pinned = target === "auto" ? null : target;
|
|
16125
16374
|
const mutation = await withCodexCapacityMutation2(
|
|
16126
16375
|
db,
|
|
@@ -19044,6 +19293,265 @@ function registerWorkspaceStateRoutes(app, deps) {
|
|
|
19044
19293
|
});
|
|
19045
19294
|
}
|
|
19046
19295
|
|
|
19296
|
+
// src/routes/workspace-artifacts.ts
|
|
19297
|
+
import { createHash as createHash7 } from "crypto";
|
|
19298
|
+
import {
|
|
19299
|
+
CreateWorkspaceArtifactRequest,
|
|
19300
|
+
PublishWorkspaceArtifactVersionRequest,
|
|
19301
|
+
RollbackWorkspaceArtifactRequest,
|
|
19302
|
+
WorkspaceArtifactContentResponse,
|
|
19303
|
+
WorkspaceArtifactDetailResponse,
|
|
19304
|
+
WorkspaceArtifactListQuery,
|
|
19305
|
+
WorkspaceArtifactListResponse,
|
|
19306
|
+
WorkspaceArtifactMutationResponse,
|
|
19307
|
+
normalizeWorkspaceArtifactSlug as normalizeWorkspaceArtifactSlug2
|
|
19308
|
+
} from "@opengeni/contracts";
|
|
19309
|
+
import { requireAccessGrant as requireAccessGrant19 } from "@opengeni/core";
|
|
19310
|
+
import {
|
|
19311
|
+
createWorkspaceArtifact as createWorkspaceArtifact2,
|
|
19312
|
+
getWorkspaceArtifact as getWorkspaceArtifact2,
|
|
19313
|
+
getWorkspaceArtifactContentRef as getWorkspaceArtifactContentRef2,
|
|
19314
|
+
listWorkspaceArtifacts as listWorkspaceArtifacts2,
|
|
19315
|
+
publishWorkspaceArtifactVersion as publishWorkspaceArtifactVersion2,
|
|
19316
|
+
rollbackWorkspaceArtifact as rollbackWorkspaceArtifact2,
|
|
19317
|
+
WorkspaceArtifactConflictError,
|
|
19318
|
+
WorkspaceArtifactNotFoundError,
|
|
19319
|
+
WorkspaceArtifactOperationError
|
|
19320
|
+
} from "@opengeni/db";
|
|
19321
|
+
import { HTTPException as HTTPException28 } from "hono/http-exception";
|
|
19322
|
+
import { z as z8 } from "zod";
|
|
19323
|
+
var ArtifactId = z8.string().uuid();
|
|
19324
|
+
var encoder2 = new TextEncoder();
|
|
19325
|
+
var decoder = new TextDecoder("utf-8", { fatal: true });
|
|
19326
|
+
async function body(context, schema) {
|
|
19327
|
+
const parsed = schema.safeParse(await context.req.json().catch(() => null));
|
|
19328
|
+
if (!parsed.success) throw new HTTPException28(422, { message: "Invalid artifact request" });
|
|
19329
|
+
return parsed.data;
|
|
19330
|
+
}
|
|
19331
|
+
function artifactId(context) {
|
|
19332
|
+
const parsed = ArtifactId.safeParse(context.req.param("artifactId"));
|
|
19333
|
+
if (!parsed.success) throw new HTTPException28(422, { message: "Invalid artifact id" });
|
|
19334
|
+
return parsed.data;
|
|
19335
|
+
}
|
|
19336
|
+
function errorResponse(context, error) {
|
|
19337
|
+
if (error instanceof WorkspaceArtifactNotFoundError) {
|
|
19338
|
+
return context.json({ code: "WORKSPACE_ARTIFACT_NOT_FOUND", message: error.message }, 404);
|
|
19339
|
+
}
|
|
19340
|
+
if (error instanceof WorkspaceArtifactConflictError) {
|
|
19341
|
+
return context.json(
|
|
19342
|
+
{
|
|
19343
|
+
code: "WORKSPACE_ARTIFACT_CONFLICT",
|
|
19344
|
+
message: error.message,
|
|
19345
|
+
currentVersionId: error.currentVersionId
|
|
19346
|
+
},
|
|
19347
|
+
409
|
|
19348
|
+
);
|
|
19349
|
+
}
|
|
19350
|
+
if (error instanceof WorkspaceArtifactOperationError) {
|
|
19351
|
+
return context.json(
|
|
19352
|
+
{ code: "INVALID_WORKSPACE_ARTIFACT_OPERATION", message: error.message },
|
|
19353
|
+
422
|
|
19354
|
+
);
|
|
19355
|
+
}
|
|
19356
|
+
if (typeof error === "object" && error !== null && "code" in error && error.code === "23505") {
|
|
19357
|
+
return context.json(
|
|
19358
|
+
{ code: "WORKSPACE_ARTIFACT_CONFLICT", message: "Artifact slug or operation already exists" },
|
|
19359
|
+
409
|
|
19360
|
+
);
|
|
19361
|
+
}
|
|
19362
|
+
throw error;
|
|
19363
|
+
}
|
|
19364
|
+
function contentMetadata(workspaceId, html) {
|
|
19365
|
+
const bytes = encoder2.encode(html);
|
|
19366
|
+
const sha256 = createHash7("sha256").update(bytes).digest("hex");
|
|
19367
|
+
return {
|
|
19368
|
+
bytes,
|
|
19369
|
+
contentSha256: sha256,
|
|
19370
|
+
sizeBytes: bytes.byteLength,
|
|
19371
|
+
contentKey: `workspaces/${workspaceId}/workspace-artifacts/blobs/${sha256}.html`
|
|
19372
|
+
};
|
|
19373
|
+
}
|
|
19374
|
+
function prepareHtml(deps, workspaceId, html) {
|
|
19375
|
+
if (!deps.objectStorage)
|
|
19376
|
+
throw new HTTPException28(503, { message: "Object storage is not configured" });
|
|
19377
|
+
const content = contentMetadata(workspaceId, html);
|
|
19378
|
+
return {
|
|
19379
|
+
...content,
|
|
19380
|
+
persistContent: async () => {
|
|
19381
|
+
await deps.objectStorage.putObject({
|
|
19382
|
+
key: content.contentKey,
|
|
19383
|
+
contentType: "text/html; charset=utf-8",
|
|
19384
|
+
body: content.bytes,
|
|
19385
|
+
sha256: content.contentSha256
|
|
19386
|
+
});
|
|
19387
|
+
}
|
|
19388
|
+
};
|
|
19389
|
+
}
|
|
19390
|
+
function provenance(subjectId, idempotencyKey) {
|
|
19391
|
+
return {
|
|
19392
|
+
operationKey: `subject:${createHash7("sha256").update(`${subjectId}:${idempotencyKey}`).digest("hex")}`,
|
|
19393
|
+
actorSubjectId: subjectId,
|
|
19394
|
+
sourceSessionId: null,
|
|
19395
|
+
sourceTurnId: null,
|
|
19396
|
+
sourceAttemptId: null,
|
|
19397
|
+
sourceExecutionGeneration: null,
|
|
19398
|
+
sourceToolName: null
|
|
19399
|
+
};
|
|
19400
|
+
}
|
|
19401
|
+
function registerWorkspaceArtifactRoutes(app, deps) {
|
|
19402
|
+
const base = "/v1/workspaces/:workspaceId/published-artifacts";
|
|
19403
|
+
app.get(base, async (context) => {
|
|
19404
|
+
const workspaceId = context.req.param("workspaceId");
|
|
19405
|
+
await requireAccessGrant19(context, deps, workspaceId, "artifacts:read");
|
|
19406
|
+
const query = WorkspaceArtifactListQuery.safeParse({
|
|
19407
|
+
limit: context.req.query("limit"),
|
|
19408
|
+
cursor: context.req.query("cursor")
|
|
19409
|
+
});
|
|
19410
|
+
if (!query.success) throw new HTTPException28(422, { message: "Invalid artifact list query" });
|
|
19411
|
+
try {
|
|
19412
|
+
return context.json(
|
|
19413
|
+
WorkspaceArtifactListResponse.parse(
|
|
19414
|
+
await listWorkspaceArtifacts2(deps.db, workspaceId, {
|
|
19415
|
+
limit: query.data.limit,
|
|
19416
|
+
...query.data.cursor ? { cursor: query.data.cursor } : {}
|
|
19417
|
+
})
|
|
19418
|
+
)
|
|
19419
|
+
);
|
|
19420
|
+
} catch (error) {
|
|
19421
|
+
return errorResponse(context, error);
|
|
19422
|
+
}
|
|
19423
|
+
});
|
|
19424
|
+
app.post(base, async (context) => {
|
|
19425
|
+
const workspaceId = context.req.param("workspaceId");
|
|
19426
|
+
const grant = await requireAccessGrant19(context, deps, workspaceId, "artifacts:publish");
|
|
19427
|
+
const request = await body(context, CreateWorkspaceArtifactRequest);
|
|
19428
|
+
const id = crypto.randomUUID();
|
|
19429
|
+
const slugBase = request.slug ?? (normalizeWorkspaceArtifactSlug2(request.title) || "artifact");
|
|
19430
|
+
const slug = request.slug ?? `${slugBase.slice(0, 87)}-${id.slice(0, 8)}`;
|
|
19431
|
+
const content = prepareHtml(deps, workspaceId, request.html);
|
|
19432
|
+
try {
|
|
19433
|
+
return context.json(
|
|
19434
|
+
WorkspaceArtifactMutationResponse.parse(
|
|
19435
|
+
await createWorkspaceArtifact2(deps.db, {
|
|
19436
|
+
accountId: grant.accountId,
|
|
19437
|
+
workspaceId,
|
|
19438
|
+
artifactId: id,
|
|
19439
|
+
slug,
|
|
19440
|
+
title: request.title,
|
|
19441
|
+
description: request.description ?? null,
|
|
19442
|
+
...content,
|
|
19443
|
+
...provenance(grant.subjectId, request.idempotencyKey)
|
|
19444
|
+
})
|
|
19445
|
+
),
|
|
19446
|
+
201
|
|
19447
|
+
);
|
|
19448
|
+
} catch (error) {
|
|
19449
|
+
return errorResponse(context, error);
|
|
19450
|
+
}
|
|
19451
|
+
});
|
|
19452
|
+
app.get(`${base}/:artifactId`, async (context) => {
|
|
19453
|
+
const workspaceId = context.req.param("workspaceId");
|
|
19454
|
+
await requireAccessGrant19(context, deps, workspaceId, "artifacts:read");
|
|
19455
|
+
try {
|
|
19456
|
+
return context.json(
|
|
19457
|
+
WorkspaceArtifactDetailResponse.parse(
|
|
19458
|
+
await getWorkspaceArtifact2(deps.db, workspaceId, artifactId(context))
|
|
19459
|
+
)
|
|
19460
|
+
);
|
|
19461
|
+
} catch (error) {
|
|
19462
|
+
return errorResponse(context, error);
|
|
19463
|
+
}
|
|
19464
|
+
});
|
|
19465
|
+
app.get(`${base}/:artifactId/content`, async (context) => {
|
|
19466
|
+
const workspaceId = context.req.param("workspaceId");
|
|
19467
|
+
await requireAccessGrant19(context, deps, workspaceId, "artifacts:read");
|
|
19468
|
+
if (!deps.objectStorage)
|
|
19469
|
+
throw new HTTPException28(503, { message: "Object storage is not configured" });
|
|
19470
|
+
const parsedVersion = context.req.query("versionId");
|
|
19471
|
+
if (parsedVersion && !ArtifactId.safeParse(parsedVersion).success) {
|
|
19472
|
+
throw new HTTPException28(422, { message: "Invalid artifact version id" });
|
|
19473
|
+
}
|
|
19474
|
+
try {
|
|
19475
|
+
const ref = await getWorkspaceArtifactContentRef2(
|
|
19476
|
+
deps.db,
|
|
19477
|
+
workspaceId,
|
|
19478
|
+
artifactId(context),
|
|
19479
|
+
parsedVersion
|
|
19480
|
+
);
|
|
19481
|
+
const object5 = await deps.objectStorage.getObjectBytes(ref.contentKey);
|
|
19482
|
+
if (!object5) throw new HTTPException28(503, { message: "Artifact content is unavailable" });
|
|
19483
|
+
const actualHash = createHash7("sha256").update(object5.bytes).digest("hex");
|
|
19484
|
+
if (actualHash !== ref.version.contentSha256) {
|
|
19485
|
+
throw new HTTPException28(503, { message: "Artifact content failed integrity verification" });
|
|
19486
|
+
}
|
|
19487
|
+
let html;
|
|
19488
|
+
try {
|
|
19489
|
+
html = decoder.decode(object5.bytes);
|
|
19490
|
+
} catch {
|
|
19491
|
+
throw new HTTPException28(503, { message: "Artifact content is not valid UTF-8" });
|
|
19492
|
+
}
|
|
19493
|
+
return context.json(
|
|
19494
|
+
WorkspaceArtifactContentResponse.parse({
|
|
19495
|
+
artifactId: ref.artifactId,
|
|
19496
|
+
versionId: ref.version.id,
|
|
19497
|
+
contentType: "text/html",
|
|
19498
|
+
contentSha256: ref.version.contentSha256,
|
|
19499
|
+
html
|
|
19500
|
+
})
|
|
19501
|
+
);
|
|
19502
|
+
} catch (error) {
|
|
19503
|
+
return errorResponse(context, error);
|
|
19504
|
+
}
|
|
19505
|
+
});
|
|
19506
|
+
app.post(`${base}/:artifactId/versions`, async (context) => {
|
|
19507
|
+
const workspaceId = context.req.param("workspaceId");
|
|
19508
|
+
const grant = await requireAccessGrant19(context, deps, workspaceId, "artifacts:publish");
|
|
19509
|
+
const request = await body(context, PublishWorkspaceArtifactVersionRequest);
|
|
19510
|
+
const id = artifactId(context);
|
|
19511
|
+
const content = prepareHtml(deps, workspaceId, request.html);
|
|
19512
|
+
try {
|
|
19513
|
+
return context.json(
|
|
19514
|
+
WorkspaceArtifactMutationResponse.parse(
|
|
19515
|
+
await publishWorkspaceArtifactVersion2(deps.db, {
|
|
19516
|
+
accountId: grant.accountId,
|
|
19517
|
+
workspaceId,
|
|
19518
|
+
artifactId: id,
|
|
19519
|
+
expectedCurrentVersionId: request.expectedCurrentVersionId,
|
|
19520
|
+
...request.title !== void 0 ? { title: request.title } : {},
|
|
19521
|
+
...request.description !== void 0 ? { description: request.description } : {},
|
|
19522
|
+
...content,
|
|
19523
|
+
...provenance(grant.subjectId, request.idempotencyKey)
|
|
19524
|
+
})
|
|
19525
|
+
)
|
|
19526
|
+
);
|
|
19527
|
+
} catch (error) {
|
|
19528
|
+
return errorResponse(context, error);
|
|
19529
|
+
}
|
|
19530
|
+
});
|
|
19531
|
+
app.post(`${base}/:artifactId/rollback`, async (context) => {
|
|
19532
|
+
const workspaceId = context.req.param("workspaceId");
|
|
19533
|
+
const grant = await requireAccessGrant19(context, deps, workspaceId, "artifacts:publish");
|
|
19534
|
+
const request = await body(context, RollbackWorkspaceArtifactRequest);
|
|
19535
|
+
try {
|
|
19536
|
+
return context.json(
|
|
19537
|
+
WorkspaceArtifactMutationResponse.parse(
|
|
19538
|
+
await rollbackWorkspaceArtifact2(deps.db, {
|
|
19539
|
+
accountId: grant.accountId,
|
|
19540
|
+
workspaceId,
|
|
19541
|
+
artifactId: artifactId(context),
|
|
19542
|
+
versionId: request.versionId,
|
|
19543
|
+
expectedCurrentVersionId: request.expectedCurrentVersionId,
|
|
19544
|
+
reason: request.reason,
|
|
19545
|
+
...provenance(grant.subjectId, request.idempotencyKey)
|
|
19546
|
+
})
|
|
19547
|
+
)
|
|
19548
|
+
);
|
|
19549
|
+
} catch (error) {
|
|
19550
|
+
return errorResponse(context, error);
|
|
19551
|
+
}
|
|
19552
|
+
});
|
|
19553
|
+
}
|
|
19554
|
+
|
|
19047
19555
|
// src/routes/preference-registry.ts
|
|
19048
19556
|
import {
|
|
19049
19557
|
ActivatePreferenceRegistryRevisionRequest,
|
|
@@ -19065,7 +19573,7 @@ import {
|
|
|
19065
19573
|
} from "@opengeni/contracts";
|
|
19066
19574
|
import {
|
|
19067
19575
|
hasPermission as hasPermission12,
|
|
19068
|
-
requireAccessGrant as
|
|
19576
|
+
requireAccessGrant as requireAccessGrant20,
|
|
19069
19577
|
requireAccessGrantAuthorization
|
|
19070
19578
|
} from "@opengeni/core";
|
|
19071
19579
|
import {
|
|
@@ -19088,13 +19596,13 @@ import {
|
|
|
19088
19596
|
rejectPreferenceRegistryProposal,
|
|
19089
19597
|
supersedePreferenceRegistry
|
|
19090
19598
|
} from "@opengeni/db";
|
|
19091
|
-
import { HTTPException as
|
|
19092
|
-
import { z as
|
|
19093
|
-
var Id =
|
|
19599
|
+
import { HTTPException as HTTPException29 } from "hono/http-exception";
|
|
19600
|
+
import { z as z9 } from "zod";
|
|
19601
|
+
var Id = z9.string().uuid();
|
|
19094
19602
|
async function parseBody2(context, schema) {
|
|
19095
19603
|
const parsed = schema.safeParse(await context.req.json().catch(() => null));
|
|
19096
19604
|
if (!parsed.success)
|
|
19097
|
-
throw new
|
|
19605
|
+
throw new HTTPException29(422, { message: "Invalid preference registry request" });
|
|
19098
19606
|
return parsed.data;
|
|
19099
19607
|
}
|
|
19100
19608
|
function exactAttemptClaims(grant) {
|
|
@@ -19108,14 +19616,14 @@ function exactAttemptClaims(grant) {
|
|
|
19108
19616
|
const present = Object.values(values).filter((value) => value !== void 0).length;
|
|
19109
19617
|
if (present === 0) return null;
|
|
19110
19618
|
if (typeof values.sessionId !== "string" || typeof values.turnId !== "string" || typeof values.attemptId !== "string" || typeof values.executionGeneration !== "number" || !Number.isSafeInteger(values.executionGeneration) || values.executionGeneration < 1 || values.executionGeneration > 2147483647) {
|
|
19111
|
-
throw new
|
|
19619
|
+
throw new HTTPException29(403, { message: "Incomplete signed preference attempt authority" });
|
|
19112
19620
|
}
|
|
19113
19621
|
return values;
|
|
19114
19622
|
}
|
|
19115
19623
|
function requiredAttemptClaims(grant) {
|
|
19116
19624
|
const claims = exactAttemptClaims(grant);
|
|
19117
19625
|
if (!claims) {
|
|
19118
|
-
throw new
|
|
19626
|
+
throw new HTTPException29(403, {
|
|
19119
19627
|
message: "Preference snapshot retrieval requires a signed session, turn, and attempt"
|
|
19120
19628
|
});
|
|
19121
19629
|
}
|
|
@@ -19128,7 +19636,7 @@ function requiredAttemptClaims(grant) {
|
|
|
19128
19636
|
function requireHumanMutation(access) {
|
|
19129
19637
|
const { grant } = access;
|
|
19130
19638
|
if (!access.contextIntegrity || access.authenticatedSubjectId !== grant.subjectId || grant.principalKind !== "human_session" || exactAttemptClaims(grant) !== null || grant.serviceInitiator || grant.serviceInitiatorContext || grant.subjectId.startsWith("api_key:")) {
|
|
19131
|
-
throw new
|
|
19639
|
+
throw new HTTPException29(403, {
|
|
19132
19640
|
message: "Preference activation and scope mutation require a direct human-authorized request"
|
|
19133
19641
|
});
|
|
19134
19642
|
}
|
|
@@ -19138,12 +19646,12 @@ function authorizePreferenceRegistryScopeMutation(access, scope) {
|
|
|
19138
19646
|
requireHumanMutation(access);
|
|
19139
19647
|
if (scope === "organization") {
|
|
19140
19648
|
if (!access.accountGrant?.permissions.includes("account:admin")) {
|
|
19141
|
-
throw new
|
|
19649
|
+
throw new HTTPException29(403, { message: "missing permission: account:admin" });
|
|
19142
19650
|
}
|
|
19143
19651
|
return;
|
|
19144
19652
|
}
|
|
19145
19653
|
if (scope === "workspace" && !hasPermission12(grant.permissions, "workspace:admin")) {
|
|
19146
|
-
throw new
|
|
19654
|
+
throw new HTTPException29(403, { message: "missing permission: workspace:admin" });
|
|
19147
19655
|
}
|
|
19148
19656
|
}
|
|
19149
19657
|
function preferenceError(context, error) {
|
|
@@ -19180,21 +19688,21 @@ function preferenceError(context, error) {
|
|
|
19180
19688
|
}
|
|
19181
19689
|
function preferenceId(context) {
|
|
19182
19690
|
const parsed = Id.safeParse(context.req.param("preferenceId"));
|
|
19183
|
-
if (!parsed.success) throw new
|
|
19691
|
+
if (!parsed.success) throw new HTTPException29(422, { message: "Invalid preference id" });
|
|
19184
19692
|
return parsed.data;
|
|
19185
19693
|
}
|
|
19186
19694
|
function registerPreferenceRegistryRoutes(app, deps) {
|
|
19187
19695
|
const base = "/v1/workspaces/:workspaceId/preferences";
|
|
19188
19696
|
app.get(base, async (context) => {
|
|
19189
19697
|
const workspaceId = context.req.param("workspaceId");
|
|
19190
|
-
const grant = await
|
|
19698
|
+
const grant = await requireAccessGrant20(context, deps, workspaceId, "workspace:read");
|
|
19191
19699
|
const query = PreferenceRegistryListQuery.safeParse({
|
|
19192
19700
|
scope: context.req.query("scope"),
|
|
19193
19701
|
status: context.req.query("status"),
|
|
19194
19702
|
limit: context.req.query("limit")
|
|
19195
19703
|
});
|
|
19196
19704
|
if (!query.success)
|
|
19197
|
-
throw new
|
|
19705
|
+
throw new HTTPException29(422, { message: "Invalid preference registry query" });
|
|
19198
19706
|
try {
|
|
19199
19707
|
const claims = exactAttemptClaims(grant);
|
|
19200
19708
|
const result = claims ? await listPreferenceRegistryForAttempt(deps.db, {
|
|
@@ -19242,7 +19750,7 @@ function registerPreferenceRegistryRoutes(app, deps) {
|
|
|
19242
19750
|
});
|
|
19243
19751
|
app.get(`${base}/summary`, async (context) => {
|
|
19244
19752
|
const workspaceId = context.req.param("workspaceId");
|
|
19245
|
-
const grant = await
|
|
19753
|
+
const grant = await requireAccessGrant20(context, deps, workspaceId, "workspace:read");
|
|
19246
19754
|
try {
|
|
19247
19755
|
return context.json(
|
|
19248
19756
|
PreferenceRegistrySnapshot.parse(
|
|
@@ -19255,7 +19763,7 @@ function registerPreferenceRegistryRoutes(app, deps) {
|
|
|
19255
19763
|
});
|
|
19256
19764
|
app.post(`${base}/full-content`, async (context) => {
|
|
19257
19765
|
const workspaceId = context.req.param("workspaceId");
|
|
19258
|
-
const grant = await
|
|
19766
|
+
const grant = await requireAccessGrant20(context, deps, workspaceId, "workspace:read");
|
|
19259
19767
|
const request = await parseBody2(context, PreferenceRegistryFullContentRequest);
|
|
19260
19768
|
try {
|
|
19261
19769
|
return context.json(
|
|
@@ -19273,7 +19781,7 @@ function registerPreferenceRegistryRoutes(app, deps) {
|
|
|
19273
19781
|
});
|
|
19274
19782
|
app.get(`${base}/:preferenceId`, async (context) => {
|
|
19275
19783
|
const workspaceId = context.req.param("workspaceId");
|
|
19276
|
-
const grant = await
|
|
19784
|
+
const grant = await requireAccessGrant20(context, deps, workspaceId, "workspace:read");
|
|
19277
19785
|
try {
|
|
19278
19786
|
const claims = exactAttemptClaims(grant);
|
|
19279
19787
|
const id = preferenceId(context);
|
|
@@ -19470,16 +19978,16 @@ function registerPreferenceRegistryRoutes(app, deps) {
|
|
|
19470
19978
|
|
|
19471
19979
|
// src/routes/insights.ts
|
|
19472
19980
|
import { InsightsRange, WorkspaceInsightsResponse } from "@opengeni/contracts";
|
|
19473
|
-
import { getWorkspaceInsights, requireAccessGrant as
|
|
19474
|
-
import { HTTPException as
|
|
19981
|
+
import { getWorkspaceInsights, requireAccessGrant as requireAccessGrant21 } from "@opengeni/core";
|
|
19982
|
+
import { HTTPException as HTTPException30 } from "hono/http-exception";
|
|
19475
19983
|
function registerInsightsRoutes(app, deps) {
|
|
19476
19984
|
app.get("/v1/workspaces/:workspaceId/insights", async (c) => {
|
|
19477
19985
|
const workspaceId = c.req.param("workspaceId");
|
|
19478
|
-
await
|
|
19986
|
+
await requireAccessGrant21(c, deps, workspaceId, "workspace:admin");
|
|
19479
19987
|
const rangeRaw = c.req.query("range") ?? "week";
|
|
19480
19988
|
const rangeParsed = InsightsRange.safeParse(rangeRaw);
|
|
19481
19989
|
if (!rangeParsed.success) {
|
|
19482
|
-
throw new
|
|
19990
|
+
throw new HTTPException30(400, {
|
|
19483
19991
|
message: "range must be one of today|week|month|ytd"
|
|
19484
19992
|
});
|
|
19485
19993
|
}
|
|
@@ -19500,12 +20008,12 @@ function registerInsightsRoutes(app, deps) {
|
|
|
19500
20008
|
import {
|
|
19501
20009
|
resolveWorkspaceVoiceInputEnabled
|
|
19502
20010
|
} from "@opengeni/contracts";
|
|
19503
|
-
import { requireAccessGrant as
|
|
20011
|
+
import { requireAccessGrant as requireAccessGrant22, TranscriptionServiceError } from "@opengeni/core";
|
|
19504
20012
|
import { getWorkspace as getWorkspace3 } from "@opengeni/db";
|
|
19505
20013
|
function registerTranscriptionRoutes(app, deps) {
|
|
19506
20014
|
app.post("/v1/workspaces/:workspaceId/transcriptions", async (c) => {
|
|
19507
20015
|
const workspaceId = c.req.param("workspaceId");
|
|
19508
|
-
const grant = await
|
|
20016
|
+
const grant = await requireAccessGrant22(c, deps, workspaceId, "sessions:create");
|
|
19509
20017
|
const workspace = await getWorkspace3(deps.db, workspaceId);
|
|
19510
20018
|
if (!workspace) return c.json({ code: "not_found" }, 404);
|
|
19511
20019
|
if (resolveWorkspaceVoiceInputEnabled(workspace.settings) === false) {
|
|
@@ -19516,13 +20024,13 @@ function registerTranscriptionRoutes(app, deps) {
|
|
|
19516
20024
|
return c.json({ code: "unavailable" }, 503);
|
|
19517
20025
|
}
|
|
19518
20026
|
try {
|
|
19519
|
-
const
|
|
20027
|
+
const body2 = await audioRequest(c.req.raw, service.limits().maxSizeBytes);
|
|
19520
20028
|
const result = await service.transcribe({
|
|
19521
20029
|
workspaceId,
|
|
19522
20030
|
accountId: grant.accountId,
|
|
19523
|
-
audio:
|
|
19524
|
-
mimeType:
|
|
19525
|
-
durationSeconds:
|
|
20031
|
+
audio: body2.audio,
|
|
20032
|
+
mimeType: body2.mimeType,
|
|
20033
|
+
durationSeconds: body2.durationSeconds,
|
|
19526
20034
|
signal: c.req.raw.signal,
|
|
19527
20035
|
requestId: c.req.header("x-opengeni-correlation-id") ?? crypto.randomUUID()
|
|
19528
20036
|
});
|
|
@@ -19585,13 +20093,13 @@ async function audioRequest(request, maxSizeBytes) {
|
|
|
19585
20093
|
...durationSeconds === void 0 ? {} : { durationSeconds }
|
|
19586
20094
|
};
|
|
19587
20095
|
}
|
|
19588
|
-
async function readBounded(
|
|
19589
|
-
if (!
|
|
20096
|
+
async function readBounded(body2, maxSizeBytes, signal) {
|
|
20097
|
+
if (!body2)
|
|
19590
20098
|
throw new TranscriptionServiceError({
|
|
19591
20099
|
code: "invalid_audio",
|
|
19592
20100
|
message: "Audio is required."
|
|
19593
20101
|
});
|
|
19594
|
-
const reader =
|
|
20102
|
+
const reader = body2.getReader();
|
|
19595
20103
|
const chunks = [];
|
|
19596
20104
|
let size = 0;
|
|
19597
20105
|
try {
|
|
@@ -19671,16 +20179,16 @@ function createOpenAiTranscriptionProvider(input) {
|
|
|
19671
20179
|
throw fetchError(error);
|
|
19672
20180
|
}
|
|
19673
20181
|
if (!response.ok) throw responseError(response.status);
|
|
19674
|
-
const
|
|
19675
|
-
if (!
|
|
20182
|
+
const body2 = await response.json().catch(() => null);
|
|
20183
|
+
if (!body2 || typeof body2.text !== "string") {
|
|
19676
20184
|
throw new TranscriptionServiceError2({
|
|
19677
20185
|
code: "provider",
|
|
19678
20186
|
message: "Invalid transcription response."
|
|
19679
20187
|
});
|
|
19680
20188
|
}
|
|
19681
20189
|
return {
|
|
19682
|
-
text:
|
|
19683
|
-
languages: typeof
|
|
20190
|
+
text: body2.text,
|
|
20191
|
+
languages: typeof body2.language === "string" && body2.language ? [body2.language] : []
|
|
19684
20192
|
};
|
|
19685
20193
|
}
|
|
19686
20194
|
};
|
|
@@ -19757,13 +20265,13 @@ function createAzureOpenAiTranscriptionProvider(input) {
|
|
|
19757
20265
|
throw fetchError(error);
|
|
19758
20266
|
}
|
|
19759
20267
|
if (!response.ok) throw responseError(response.status);
|
|
19760
|
-
const
|
|
19761
|
-
if (!
|
|
20268
|
+
const body2 = await response.json().catch(() => null);
|
|
20269
|
+
if (!body2 || typeof body2.text !== "string") {
|
|
19762
20270
|
throw responseError(502);
|
|
19763
20271
|
}
|
|
19764
20272
|
return {
|
|
19765
|
-
text:
|
|
19766
|
-
languages: typeof
|
|
20273
|
+
text: body2.text,
|
|
20274
|
+
languages: typeof body2.language === "string" && body2.language ? [body2.language] : []
|
|
19767
20275
|
};
|
|
19768
20276
|
}
|
|
19769
20277
|
};
|
|
@@ -19843,11 +20351,11 @@ function createCodexSubscriptionTranscriptionProvider(input) {
|
|
|
19843
20351
|
throw fetchError(error);
|
|
19844
20352
|
}
|
|
19845
20353
|
if (!response.ok) throw responseError(response.status);
|
|
19846
|
-
const
|
|
19847
|
-
if (!
|
|
20354
|
+
const body2 = await response.json().catch(() => null);
|
|
20355
|
+
if (!body2 || typeof body2.text !== "string") throw responseError(502);
|
|
19848
20356
|
return {
|
|
19849
|
-
text:
|
|
19850
|
-
languages: typeof
|
|
20357
|
+
text: body2.text,
|
|
20358
|
+
languages: typeof body2.language === "string" && body2.language ? [body2.language] : []
|
|
19851
20359
|
};
|
|
19852
20360
|
}
|
|
19853
20361
|
};
|
|
@@ -19945,6 +20453,738 @@ async function firstAvailable(providers, context) {
|
|
|
19945
20453
|
return null;
|
|
19946
20454
|
}
|
|
19947
20455
|
|
|
20456
|
+
// src/integrations/slack-interactions.ts
|
|
20457
|
+
import { createHash as createHash8, createHmac as createHmac2, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
20458
|
+
import {
|
|
20459
|
+
acceptSessionHumanInputResponse as acceptSessionHumanInputResponse2,
|
|
20460
|
+
advanceSlackInteractionDelivery,
|
|
20461
|
+
bindSlackInteractionSession,
|
|
20462
|
+
claimSlackInteractionDelivery,
|
|
20463
|
+
claimSlackInteractionProgressDelivery,
|
|
20464
|
+
claimSlackInteractionInbox,
|
|
20465
|
+
closeSlackInteractionDelivery,
|
|
20466
|
+
deleteSlackBotUserLink,
|
|
20467
|
+
enqueueSlackInteractionInbox,
|
|
20468
|
+
getOrCreateSlackInteraction,
|
|
20469
|
+
getSlackBotUserLink,
|
|
20470
|
+
getSlackInteractionByRoute,
|
|
20471
|
+
getWorkspaceGrant as getWorkspaceGrant4,
|
|
20472
|
+
listSessionEventPage as listSessionEventPage3,
|
|
20473
|
+
listSessionHumanInputRequests as listSessionHumanInputRequests2,
|
|
20474
|
+
rekeySlackInteractionRoute,
|
|
20475
|
+
reopenSlackInteractionDelivery,
|
|
20476
|
+
releaseSlackInteractionDelivery,
|
|
20477
|
+
releaseSlackInteractionInbox,
|
|
20478
|
+
resolveSlackInstallationRoute,
|
|
20479
|
+
saveSlackBotUserLink,
|
|
20480
|
+
settleSlackInteractionInbox
|
|
20481
|
+
} from "@opengeni/db";
|
|
20482
|
+
import {
|
|
20483
|
+
acceptSessionUserMessage as acceptSessionUserMessage3,
|
|
20484
|
+
controlHumanSessionWorkstream as controlHumanSessionWorkstream3,
|
|
20485
|
+
createSessionForRequest as createSessionForRequest3,
|
|
20486
|
+
hasPermission as hasPermission13,
|
|
20487
|
+
requireAccessGrant as requireAccessGrant23
|
|
20488
|
+
} from "@opengeni/core";
|
|
20489
|
+
import { publishDurableSessionEvents as publishDurableSessionEvents3 } from "@opengeni/events";
|
|
20490
|
+
import { HTTPException as HTTPException31 } from "hono/http-exception";
|
|
20491
|
+
var SLACK_INTERACTION_MAX_BODY_BYTES = 256 * 1024;
|
|
20492
|
+
var SLACK_SIGNATURE_REPLAY_WINDOW_SECONDS = 300;
|
|
20493
|
+
var SLACK_DELIVERY_EVENT_TYPES = [
|
|
20494
|
+
"agent.message.completed",
|
|
20495
|
+
"session.humanInput.requested",
|
|
20496
|
+
"turn.completed",
|
|
20497
|
+
"turn.failed",
|
|
20498
|
+
"turn.cancelled",
|
|
20499
|
+
"session.status.changed"
|
|
20500
|
+
];
|
|
20501
|
+
var MAX_SLACK_TEXT_CHARS = 3500;
|
|
20502
|
+
var MAX_SLACK_INPUT_CHARS = 8e3;
|
|
20503
|
+
var MAX_PROGRESS_MESSAGES = 3;
|
|
20504
|
+
var INBOX_LEASE_MS = 3e4;
|
|
20505
|
+
var DELIVERY_LEASE_MS = 3e4;
|
|
20506
|
+
var SLACK_TASK_INSTRUCTIONS = [
|
|
20507
|
+
"This turn originated from Slack. Slack message and thread context is task-local only.",
|
|
20508
|
+
"Do not write Slack context to Documents, Knowledge, Memory, preferences, Workspace Charter, instructions, or policy unless a separate explicit authorized user action requests it.",
|
|
20509
|
+
"Never expose private reasoning, credentials, secrets, raw logs, or unbounded output.",
|
|
20510
|
+
"Keep user-visible output concise, bounded, and safe to send back to Slack."
|
|
20511
|
+
].join(" ");
|
|
20512
|
+
function verifySlackRequestSignature(input, signingSecret, nowMs = Date.now()) {
|
|
20513
|
+
if (!/^\d{1,16}$/.test(input.timestamp ?? "") || !/^v0=[0-9a-f]{64}$/.test(input.signature ?? "")) {
|
|
20514
|
+
return false;
|
|
20515
|
+
}
|
|
20516
|
+
const timestamp = Number(input.timestamp);
|
|
20517
|
+
if (!Number.isSafeInteger(timestamp)) return false;
|
|
20518
|
+
const nowSeconds = Math.floor(nowMs / 1e3);
|
|
20519
|
+
if (Math.abs(nowSeconds - timestamp) > SLACK_SIGNATURE_REPLAY_WINDOW_SECONDS) return false;
|
|
20520
|
+
const expected = `v0=${createHmac2("sha256", signingSecret).update(`v0:${input.timestamp}:${input.rawBody}`).digest("hex")}`;
|
|
20521
|
+
const actualBytes = Buffer.from(input.signature, "utf8");
|
|
20522
|
+
const expectedBytes = Buffer.from(expected, "utf8");
|
|
20523
|
+
return actualBytes.length === expectedBytes.length && timingSafeEqual2(actualBytes, expectedBytes);
|
|
20524
|
+
}
|
|
20525
|
+
function slackEventInboxEntry(payload, bot) {
|
|
20526
|
+
const envelope = record3(payload);
|
|
20527
|
+
if (!envelope || envelope.type !== "event_callback") return null;
|
|
20528
|
+
const event = record3(envelope.event);
|
|
20529
|
+
const teamId = boundedString(envelope.team_id, 64);
|
|
20530
|
+
const eventId = boundedString(envelope.event_id, 256);
|
|
20531
|
+
if (!event || !teamId || !eventId) return null;
|
|
20532
|
+
if (event.bot_id || event.bot_profile || event.subtype || event.user === bot.botUserId)
|
|
20533
|
+
return null;
|
|
20534
|
+
const userId = boundedString(event.user, 64);
|
|
20535
|
+
const channelId = boundedString(event.channel, 64);
|
|
20536
|
+
const timestamp = boundedString(event.ts, 64);
|
|
20537
|
+
const threadTimestamp = boundedString(event.thread_ts, 64);
|
|
20538
|
+
const text = boundedText(event.text);
|
|
20539
|
+
if (!userId || !channelId || !timestamp || !text) return null;
|
|
20540
|
+
let triggerKind;
|
|
20541
|
+
if (event.type === "app_mention") {
|
|
20542
|
+
triggerKind = "app_mention";
|
|
20543
|
+
} else if (event.type === "message" && threadTimestamp) {
|
|
20544
|
+
triggerKind = "thread_reply";
|
|
20545
|
+
} else if (event.type === "message" && event.channel_type === "im") {
|
|
20546
|
+
triggerKind = "dm";
|
|
20547
|
+
} else {
|
|
20548
|
+
return null;
|
|
20549
|
+
}
|
|
20550
|
+
return {
|
|
20551
|
+
providerEventId: eventId,
|
|
20552
|
+
providerMessageId: `${teamId}:${channelId}:${timestamp}`,
|
|
20553
|
+
slackTeamId: teamId,
|
|
20554
|
+
slackUserId: userId,
|
|
20555
|
+
slackChannelId: channelId,
|
|
20556
|
+
slackMessageTs: timestamp,
|
|
20557
|
+
slackThreadTs: threadTimestamp,
|
|
20558
|
+
triggerKind,
|
|
20559
|
+
text
|
|
20560
|
+
};
|
|
20561
|
+
}
|
|
20562
|
+
function registerSlackInteractionRoutes(app, deps) {
|
|
20563
|
+
app.post("/v1/integrations/slack/events", async (c) => {
|
|
20564
|
+
const signed = await readSignedSlackRequest(c, deps);
|
|
20565
|
+
const payload = parseJsonObject(signed.rawBody);
|
|
20566
|
+
if (payload.type === "url_verification") {
|
|
20567
|
+
const challenge = boundedString(payload.challenge, 512);
|
|
20568
|
+
if (!challenge) throw new HTTPException31(400, { message: "invalid Slack challenge" });
|
|
20569
|
+
return c.json({ challenge });
|
|
20570
|
+
}
|
|
20571
|
+
const teamId = boundedString(payload.team_id, 64);
|
|
20572
|
+
if (!teamId) throw new HTTPException31(400, { message: "invalid Slack event" });
|
|
20573
|
+
const installation = await resolveSlackInstallationRoute(deps.db, teamId);
|
|
20574
|
+
if (!installation)
|
|
20575
|
+
throw new HTTPException31(403, {
|
|
20576
|
+
message: "Slack installation unavailable"
|
|
20577
|
+
});
|
|
20578
|
+
const entry = slackEventInboxEntry(payload, installation);
|
|
20579
|
+
if (entry) await enqueueNormalizedSlackInteraction(deps, installation, entry);
|
|
20580
|
+
return c.json({ ok: true });
|
|
20581
|
+
});
|
|
20582
|
+
app.post("/v1/integrations/slack/commands", async (c) => {
|
|
20583
|
+
const signed = await readSignedSlackRequest(c, deps);
|
|
20584
|
+
const form = new URLSearchParams(signed.rawBody);
|
|
20585
|
+
if (form.get("command") !== "/opengeni") {
|
|
20586
|
+
throw new HTTPException31(400, { message: "invalid Slack command" });
|
|
20587
|
+
}
|
|
20588
|
+
const entry = normalizedFormInteraction(form, "slash_command");
|
|
20589
|
+
const installation = await resolveSlackInstallationRoute(deps.db, entry.slackTeamId);
|
|
20590
|
+
if (!installation)
|
|
20591
|
+
throw new HTTPException31(403, {
|
|
20592
|
+
message: "Slack installation unavailable"
|
|
20593
|
+
});
|
|
20594
|
+
await enqueueNormalizedSlackInteraction(deps, installation, entry);
|
|
20595
|
+
return c.text("OpenGeni accepted this task and will reply in a thread.", 200);
|
|
20596
|
+
});
|
|
20597
|
+
app.post("/v1/integrations/slack/interactions", async (c) => {
|
|
20598
|
+
const signed = await readSignedSlackRequest(c, deps);
|
|
20599
|
+
const form = new URLSearchParams(signed.rawBody);
|
|
20600
|
+
const payload = parseJsonObject(form.get("payload") ?? "");
|
|
20601
|
+
if (payload.type !== "message_action") {
|
|
20602
|
+
throw new HTTPException31(400, {
|
|
20603
|
+
message: "unsupported Slack interaction"
|
|
20604
|
+
});
|
|
20605
|
+
}
|
|
20606
|
+
const team = record3(payload.team);
|
|
20607
|
+
const user = record3(payload.user);
|
|
20608
|
+
const channel = record3(payload.channel);
|
|
20609
|
+
const message = record3(payload.message);
|
|
20610
|
+
const triggerId = boundedString(payload.trigger_id, 256);
|
|
20611
|
+
const teamId = boundedString(team?.id, 64);
|
|
20612
|
+
const userId = boundedString(user?.id, 64);
|
|
20613
|
+
const channelId = boundedString(channel?.id, 64);
|
|
20614
|
+
const messageTs = boundedString(message?.ts, 64);
|
|
20615
|
+
const threadTs = boundedString(message?.thread_ts, 64);
|
|
20616
|
+
const text = boundedText(message?.text);
|
|
20617
|
+
if (!triggerId || !teamId || !userId || !channelId || !messageTs || !text) {
|
|
20618
|
+
throw new HTTPException31(400, {
|
|
20619
|
+
message: "invalid Slack message shortcut"
|
|
20620
|
+
});
|
|
20621
|
+
}
|
|
20622
|
+
const installation = await resolveSlackInstallationRoute(deps.db, teamId);
|
|
20623
|
+
if (!installation)
|
|
20624
|
+
throw new HTTPException31(403, {
|
|
20625
|
+
message: "Slack installation unavailable"
|
|
20626
|
+
});
|
|
20627
|
+
await enqueueNormalizedSlackInteraction(deps, installation, {
|
|
20628
|
+
providerEventId: `shortcut:${triggerId}`,
|
|
20629
|
+
providerMessageId: `shortcut:${triggerId}`,
|
|
20630
|
+
slackTeamId: teamId,
|
|
20631
|
+
slackUserId: userId,
|
|
20632
|
+
slackChannelId: channelId,
|
|
20633
|
+
slackMessageTs: messageTs,
|
|
20634
|
+
slackThreadTs: threadTs,
|
|
20635
|
+
triggerKind: "message_shortcut",
|
|
20636
|
+
text
|
|
20637
|
+
});
|
|
20638
|
+
return c.json({ ok: true });
|
|
20639
|
+
});
|
|
20640
|
+
app.post("/v1/workspaces/:workspaceId/integrations/slack/user-links", async (c) => {
|
|
20641
|
+
const workspaceId = c.req.param("workspaceId");
|
|
20642
|
+
const grant = await requireAccessGrant23(c, deps, workspaceId, "connections:write");
|
|
20643
|
+
const body2 = record3(await c.req.json().catch(() => null));
|
|
20644
|
+
const connectionId = boundedString(body2?.connectionId, 64);
|
|
20645
|
+
const slackTeamId = boundedString(body2?.slackTeamId, 64);
|
|
20646
|
+
const slackUserId = boundedString(body2?.slackUserId, 64);
|
|
20647
|
+
if (!connectionId || !slackTeamId || !slackUserId) {
|
|
20648
|
+
throw new HTTPException31(400, {
|
|
20649
|
+
message: "invalid Slack identity link request"
|
|
20650
|
+
});
|
|
20651
|
+
}
|
|
20652
|
+
const route = await resolveSlackInstallationRoute(deps.db, slackTeamId);
|
|
20653
|
+
if (!route || route.workspaceId !== workspaceId || route.connectionId !== connectionId) {
|
|
20654
|
+
throw new HTTPException31(404, {
|
|
20655
|
+
message: "Slack installation not found"
|
|
20656
|
+
});
|
|
20657
|
+
}
|
|
20658
|
+
return c.json(
|
|
20659
|
+
await saveSlackBotUserLink(deps.db, {
|
|
20660
|
+
accountId: grant.accountId,
|
|
20661
|
+
workspaceId,
|
|
20662
|
+
connectionId,
|
|
20663
|
+
slackTeamId,
|
|
20664
|
+
slackUserId,
|
|
20665
|
+
subjectId: grant.subjectId,
|
|
20666
|
+
linkedBySubjectId: grant.subjectId
|
|
20667
|
+
}),
|
|
20668
|
+
201
|
|
20669
|
+
);
|
|
20670
|
+
});
|
|
20671
|
+
app.delete(
|
|
20672
|
+
"/v1/workspaces/:workspaceId/integrations/slack/user-links/:slackUserId",
|
|
20673
|
+
async (c) => {
|
|
20674
|
+
const workspaceId = c.req.param("workspaceId");
|
|
20675
|
+
await requireAccessGrant23(c, deps, workspaceId, "connections:write");
|
|
20676
|
+
const connectionId = boundedString(c.req.query("connectionId"), 64);
|
|
20677
|
+
if (!connectionId) throw new HTTPException31(400, { message: "connectionId is required" });
|
|
20678
|
+
return c.json({
|
|
20679
|
+
deleted: await deleteSlackBotUserLink(
|
|
20680
|
+
deps.db,
|
|
20681
|
+
workspaceId,
|
|
20682
|
+
connectionId,
|
|
20683
|
+
c.req.param("slackUserId")
|
|
20684
|
+
)
|
|
20685
|
+
});
|
|
20686
|
+
}
|
|
20687
|
+
);
|
|
20688
|
+
}
|
|
20689
|
+
async function drainSlackInteractionsOnce(deps) {
|
|
20690
|
+
const holder = crypto.randomUUID();
|
|
20691
|
+
const entry = await claimSlackInteractionInbox(deps.db, holder, INBOX_LEASE_MS);
|
|
20692
|
+
if (entry) {
|
|
20693
|
+
try {
|
|
20694
|
+
await processSlackInboxEntry(deps, entry);
|
|
20695
|
+
await settleSlackInteractionInbox(deps.db, {
|
|
20696
|
+
entry,
|
|
20697
|
+
claimHolderId: holder,
|
|
20698
|
+
outcome: "processed"
|
|
20699
|
+
});
|
|
20700
|
+
} catch (error) {
|
|
20701
|
+
const code = safeErrorCode(error);
|
|
20702
|
+
if (entry.attemptCount >= 5 || permanentSlackInteractionError(error)) {
|
|
20703
|
+
await settleSlackInteractionInbox(deps.db, {
|
|
20704
|
+
entry,
|
|
20705
|
+
claimHolderId: holder,
|
|
20706
|
+
outcome: "failed",
|
|
20707
|
+
errorCode: code
|
|
20708
|
+
});
|
|
20709
|
+
} else {
|
|
20710
|
+
await releaseSlackInteractionInbox(deps.db, {
|
|
20711
|
+
entry,
|
|
20712
|
+
claimHolderId: holder,
|
|
20713
|
+
errorCode: code
|
|
20714
|
+
});
|
|
20715
|
+
}
|
|
20716
|
+
}
|
|
20717
|
+
return true;
|
|
20718
|
+
}
|
|
20719
|
+
const deliveryHolder = crypto.randomUUID();
|
|
20720
|
+
const interaction = await claimSlackInteractionDelivery(
|
|
20721
|
+
deps.db,
|
|
20722
|
+
deliveryHolder,
|
|
20723
|
+
DELIVERY_LEASE_MS
|
|
20724
|
+
);
|
|
20725
|
+
if (!interaction) return false;
|
|
20726
|
+
try {
|
|
20727
|
+
await deliverSlackSessionEvents(deps, interaction, deliveryHolder);
|
|
20728
|
+
} catch {
|
|
20729
|
+
await releaseSlackInteractionDelivery(deps.db, {
|
|
20730
|
+
...interaction,
|
|
20731
|
+
claimHolderId: deliveryHolder
|
|
20732
|
+
}).catch(() => void 0);
|
|
20733
|
+
}
|
|
20734
|
+
return true;
|
|
20735
|
+
}
|
|
20736
|
+
function startSlackInteractionPump(deps, options = {}) {
|
|
20737
|
+
let stopped = false;
|
|
20738
|
+
let running = false;
|
|
20739
|
+
const intervalMs = Math.max(250, options.intervalMs ?? 1e3);
|
|
20740
|
+
const maxPerTick = Math.max(1, Math.min(50, options.maxPerTick ?? 10));
|
|
20741
|
+
const tick = async () => {
|
|
20742
|
+
if (stopped || running) return;
|
|
20743
|
+
running = true;
|
|
20744
|
+
try {
|
|
20745
|
+
for (let index = 0; index < maxPerTick; index += 1) {
|
|
20746
|
+
if (!await drainSlackInteractionsOnce(deps)) break;
|
|
20747
|
+
}
|
|
20748
|
+
} finally {
|
|
20749
|
+
running = false;
|
|
20750
|
+
}
|
|
20751
|
+
};
|
|
20752
|
+
const timer = setInterval(() => void tick(), intervalMs);
|
|
20753
|
+
void tick();
|
|
20754
|
+
return () => {
|
|
20755
|
+
stopped = true;
|
|
20756
|
+
clearInterval(timer);
|
|
20757
|
+
};
|
|
20758
|
+
}
|
|
20759
|
+
async function processSlackInboxEntry(deps, entry) {
|
|
20760
|
+
const routeKey = slackRouteKey(entry.slackChannelId, entry.slackThreadTs ?? entry.slackMessageTs);
|
|
20761
|
+
const existing = await getSlackInteractionByRoute(
|
|
20762
|
+
deps.db,
|
|
20763
|
+
entry.workspaceId,
|
|
20764
|
+
entry.connectionId,
|
|
20765
|
+
routeKey
|
|
20766
|
+
);
|
|
20767
|
+
if (entry.triggerKind === "thread_reply" && !existing) return;
|
|
20768
|
+
const link = await getSlackBotUserLink(
|
|
20769
|
+
deps.db,
|
|
20770
|
+
entry.workspaceId,
|
|
20771
|
+
entry.connectionId,
|
|
20772
|
+
entry.slackUserId
|
|
20773
|
+
);
|
|
20774
|
+
const client = await createOpenGeniSlackBotInteractionClient(deps, {
|
|
20775
|
+
accountId: entry.accountId,
|
|
20776
|
+
workspaceId: entry.workspaceId,
|
|
20777
|
+
connectionId: entry.connectionId,
|
|
20778
|
+
subjectId: link?.subjectId ?? "service:slack-interaction",
|
|
20779
|
+
...existing?.sessionId ? { sessionId: existing.sessionId } : {}
|
|
20780
|
+
});
|
|
20781
|
+
await client.verifyChannelAccess(entry.slackChannelId);
|
|
20782
|
+
if (!link) {
|
|
20783
|
+
await client.postMessage({
|
|
20784
|
+
operationId: deterministicUuid(`slack-link:${entry.id}`),
|
|
20785
|
+
channelId: entry.slackChannelId,
|
|
20786
|
+
...entry.slackThreadTs ? { threadTimestamp: entry.slackThreadTs } : {},
|
|
20787
|
+
text: `Link your Slack identity to OpenGeni before starting work: ${linkUrl(deps, entry)}. No session was created.`
|
|
20788
|
+
});
|
|
20789
|
+
return;
|
|
20790
|
+
}
|
|
20791
|
+
const grant = await getWorkspaceGrant4(deps.db, link.subjectId, entry.workspaceId, {
|
|
20792
|
+
principalKind: "human_session"
|
|
20793
|
+
});
|
|
20794
|
+
if (!grant || grant.accountId !== entry.accountId) {
|
|
20795
|
+
throw new SlackInteractionPermanentError("identity_access_revoked");
|
|
20796
|
+
}
|
|
20797
|
+
if (existing?.sessionId) {
|
|
20798
|
+
await continueSlackSession(deps, grant, existing, entry);
|
|
20799
|
+
return;
|
|
20800
|
+
}
|
|
20801
|
+
if (!hasPermission13(grant.permissions, "sessions:create")) {
|
|
20802
|
+
throw new SlackInteractionPermanentError("sessions_create_denied");
|
|
20803
|
+
}
|
|
20804
|
+
const { interaction } = await getOrCreateSlackInteraction(deps.db, {
|
|
20805
|
+
accountId: entry.accountId,
|
|
20806
|
+
workspaceId: entry.workspaceId,
|
|
20807
|
+
connectionId: entry.connectionId,
|
|
20808
|
+
slackTeamId: entry.slackTeamId,
|
|
20809
|
+
slackChannelId: entry.slackChannelId,
|
|
20810
|
+
slackThreadTs: entry.slackThreadTs ?? entry.slackMessageTs,
|
|
20811
|
+
routeKey,
|
|
20812
|
+
triggeringProviderEventId: entry.providerEventId,
|
|
20813
|
+
owningSubjectId: grant.subjectId,
|
|
20814
|
+
visibility: entry.triggerKind === "dm" ? "private" : "workspace"
|
|
20815
|
+
});
|
|
20816
|
+
if (interaction.sessionId) {
|
|
20817
|
+
await continueSlackSession(deps, grant, interaction, entry);
|
|
20818
|
+
return;
|
|
20819
|
+
}
|
|
20820
|
+
const session = await createSessionForRequest3(deps, grant, entry.workspaceId, {
|
|
20821
|
+
requestedSessionId: interaction.sessionReservationId,
|
|
20822
|
+
initialMessage: entry.text,
|
|
20823
|
+
turnInstructions: SLACK_TASK_INSTRUCTIONS,
|
|
20824
|
+
idempotencyKey: `slack:${entry.connectionId}:${entry.providerEventId}`,
|
|
20825
|
+
clientEventId: `slack:${entry.providerEventId}`
|
|
20826
|
+
});
|
|
20827
|
+
const bound = await bindSlackInteractionSession(deps.db, {
|
|
20828
|
+
...interaction,
|
|
20829
|
+
owningSubjectId: grant.subjectId,
|
|
20830
|
+
sessionId: session.id
|
|
20831
|
+
});
|
|
20832
|
+
if (!bound) throw new Error("Slack route could not bind its durable session");
|
|
20833
|
+
const ack = await client.postMessage({
|
|
20834
|
+
operationId: deterministicUuid(`slack-ack:${interaction.id}`),
|
|
20835
|
+
channelId: entry.slackChannelId,
|
|
20836
|
+
...entry.triggerKind === "slash_command" ? {} : { threadTimestamp: entry.slackThreadTs ?? entry.slackMessageTs },
|
|
20837
|
+
text: `OpenGeni started this task. ${openSessionText(deps, entry.workspaceId, session.id)} Reply in this thread to continue, or reply \`stop\` to stop. Start a new top-level DM or invoke /opengeni again for a new session.`
|
|
20838
|
+
});
|
|
20839
|
+
if (entry.triggerKind === "slash_command") {
|
|
20840
|
+
await rekeySlackInteractionRoute(deps.db, {
|
|
20841
|
+
...interaction,
|
|
20842
|
+
routeKey: slackRouteKey(entry.slackChannelId, ack.timestamp),
|
|
20843
|
+
slackThreadTs: ack.timestamp,
|
|
20844
|
+
ackSlackMessageTs: ack.timestamp
|
|
20845
|
+
});
|
|
20846
|
+
}
|
|
20847
|
+
}
|
|
20848
|
+
async function continueSlackSession(deps, grant, interaction, entry) {
|
|
20849
|
+
if (!interaction.sessionId || interaction.visibility === "private" && interaction.owningSubjectId !== grant.subjectId) {
|
|
20850
|
+
throw new SlackInteractionPermanentError("session_owner_mismatch");
|
|
20851
|
+
}
|
|
20852
|
+
await reopenSlackInteractionDelivery(deps.db, interaction);
|
|
20853
|
+
if (entry.text.trim().toLowerCase() === "stop") {
|
|
20854
|
+
if (!hasPermission13(grant.permissions, "sessions:control")) {
|
|
20855
|
+
throw new SlackInteractionPermanentError("sessions_control_denied");
|
|
20856
|
+
}
|
|
20857
|
+
await controlHumanSessionWorkstream3(
|
|
20858
|
+
deps,
|
|
20859
|
+
{
|
|
20860
|
+
accountId: grant.accountId,
|
|
20861
|
+
workspaceId: grant.workspaceId,
|
|
20862
|
+
subjectId: grant.subjectId,
|
|
20863
|
+
sessionId: interaction.sessionId
|
|
20864
|
+
},
|
|
20865
|
+
{
|
|
20866
|
+
action: "pause",
|
|
20867
|
+
clientEventId: deterministicUuid(`slack-stop:${entry.providerEventId}`),
|
|
20868
|
+
reason: "Stopped from the originating Slack thread"
|
|
20869
|
+
}
|
|
20870
|
+
);
|
|
20871
|
+
return;
|
|
20872
|
+
}
|
|
20873
|
+
const pending = await listSessionHumanInputRequests2(
|
|
20874
|
+
deps.db,
|
|
20875
|
+
entry.workspaceId,
|
|
20876
|
+
interaction.sessionId,
|
|
20877
|
+
{ status: "pending", limit: 2 }
|
|
20878
|
+
);
|
|
20879
|
+
if (pending.length === 1) {
|
|
20880
|
+
const response = humanInputResponse(pending[0].questions, entry.text);
|
|
20881
|
+
if (response) {
|
|
20882
|
+
const accepted = await acceptSessionHumanInputResponse2(deps.db, {
|
|
20883
|
+
accountId: grant.accountId,
|
|
20884
|
+
workspaceId: grant.workspaceId,
|
|
20885
|
+
sessionId: interaction.sessionId,
|
|
20886
|
+
requestId: pending[0].id,
|
|
20887
|
+
response,
|
|
20888
|
+
respondedBy: grant.subjectId,
|
|
20889
|
+
clientEventId: `slack:${entry.providerEventId}`
|
|
20890
|
+
});
|
|
20891
|
+
if (accepted.action === "accepted") {
|
|
20892
|
+
await publishDurableSessionEvents3(
|
|
20893
|
+
deps.bus,
|
|
20894
|
+
grant.workspaceId,
|
|
20895
|
+
interaction.sessionId,
|
|
20896
|
+
accepted.events
|
|
20897
|
+
);
|
|
20898
|
+
if (accepted.workflowWakeRevision !== null) {
|
|
20899
|
+
await deps.workflowClient.signalApprovalDecision({
|
|
20900
|
+
accountId: grant.accountId,
|
|
20901
|
+
workspaceId: grant.workspaceId,
|
|
20902
|
+
sessionId: interaction.sessionId,
|
|
20903
|
+
eventId: accepted.events[0]?.id ?? pending[0].id,
|
|
20904
|
+
workflowId: `session-${interaction.sessionId}`,
|
|
20905
|
+
workflowWakeRevision: accepted.workflowWakeRevision
|
|
20906
|
+
});
|
|
20907
|
+
}
|
|
20908
|
+
return;
|
|
20909
|
+
}
|
|
20910
|
+
}
|
|
20911
|
+
}
|
|
20912
|
+
if (!hasPermission13(grant.permissions, "sessions:control")) {
|
|
20913
|
+
throw new SlackInteractionPermanentError("sessions_control_denied");
|
|
20914
|
+
}
|
|
20915
|
+
await acceptSessionUserMessage3(deps, grant, entry.workspaceId, interaction.sessionId, {
|
|
20916
|
+
text: entry.text,
|
|
20917
|
+
turnInstructions: SLACK_TASK_INSTRUCTIONS,
|
|
20918
|
+
clientEventId: `slack:${entry.providerEventId}`
|
|
20919
|
+
});
|
|
20920
|
+
}
|
|
20921
|
+
async function deliverSlackSessionEvents(deps, interaction, claimHolderId) {
|
|
20922
|
+
if (!interaction.sessionId) return;
|
|
20923
|
+
const page = await listSessionEventPage3(deps.db, interaction.workspaceId, interaction.sessionId, {
|
|
20924
|
+
after: interaction.lastDeliveredSessionEventSequence,
|
|
20925
|
+
limit: 100,
|
|
20926
|
+
includeTypes: [...SLACK_DELIVERY_EVENT_TYPES],
|
|
20927
|
+
authoritativeLatest: true,
|
|
20928
|
+
maxBytes: 256 * 1024
|
|
20929
|
+
});
|
|
20930
|
+
if (page.events.length === 0) {
|
|
20931
|
+
await releaseSlackInteractionDelivery(deps.db, {
|
|
20932
|
+
...interaction,
|
|
20933
|
+
claimHolderId
|
|
20934
|
+
});
|
|
20935
|
+
return;
|
|
20936
|
+
}
|
|
20937
|
+
const client = await createOpenGeniSlackBotInteractionClient(deps, {
|
|
20938
|
+
accountId: interaction.accountId,
|
|
20939
|
+
workspaceId: interaction.workspaceId,
|
|
20940
|
+
connectionId: interaction.connectionId,
|
|
20941
|
+
subjectId: interaction.owningSubjectId,
|
|
20942
|
+
sessionId: interaction.sessionId
|
|
20943
|
+
});
|
|
20944
|
+
let lastSequence = interaction.lastDeliveredSessionEventSequence;
|
|
20945
|
+
let terminal = null;
|
|
20946
|
+
let latestAssistantText = "";
|
|
20947
|
+
for (const event of [...page.events].sort((left, right) => left.sequence - right.sequence)) {
|
|
20948
|
+
lastSequence = Math.max(lastSequence, event.sequence);
|
|
20949
|
+
if (event.type === "agent.message.completed") {
|
|
20950
|
+
latestAssistantText = safePayloadText(event.payload, "text");
|
|
20951
|
+
if (latestAssistantText) {
|
|
20952
|
+
const progress = await claimSlackInteractionProgressDelivery(deps.db, {
|
|
20953
|
+
accountId: interaction.accountId,
|
|
20954
|
+
workspaceId: interaction.workspaceId,
|
|
20955
|
+
interactionId: interaction.id,
|
|
20956
|
+
claimHolderId,
|
|
20957
|
+
sessionEventSequence: event.sequence,
|
|
20958
|
+
maxProgress: MAX_PROGRESS_MESSAGES
|
|
20959
|
+
});
|
|
20960
|
+
if (progress.kind === "not_owned") {
|
|
20961
|
+
throw new Error("Slack progress delivery lost its durable interaction claim");
|
|
20962
|
+
}
|
|
20963
|
+
if (progress.kind === "claimed") {
|
|
20964
|
+
await postDelivery(
|
|
20965
|
+
client,
|
|
20966
|
+
interaction,
|
|
20967
|
+
event,
|
|
20968
|
+
latestAssistantText,
|
|
20969
|
+
"progress",
|
|
20970
|
+
progress.delivery.operationId
|
|
20971
|
+
);
|
|
20972
|
+
}
|
|
20973
|
+
}
|
|
20974
|
+
} else if (event.type === "session.humanInput.requested") {
|
|
20975
|
+
const requests = await listSessionHumanInputRequests2(
|
|
20976
|
+
deps.db,
|
|
20977
|
+
interaction.workspaceId,
|
|
20978
|
+
interaction.sessionId,
|
|
20979
|
+
{ status: "pending", limit: 1 }
|
|
20980
|
+
);
|
|
20981
|
+
const request = requests[0];
|
|
20982
|
+
if (request) {
|
|
20983
|
+
await postDelivery(
|
|
20984
|
+
client,
|
|
20985
|
+
interaction,
|
|
20986
|
+
event,
|
|
20987
|
+
`OpenGeni needs your input:
|
|
20988
|
+
${formatQuestions(request.questions)}
|
|
20989
|
+
Reply in this thread, or use ${openSessionText(deps, interaction.workspaceId, interaction.sessionId)}.`,
|
|
20990
|
+
"human-input"
|
|
20991
|
+
);
|
|
20992
|
+
}
|
|
20993
|
+
} else if (event.type === "turn.completed") {
|
|
20994
|
+
const output = safePayloadText(event.payload, "output") || latestAssistantText;
|
|
20995
|
+
await postDelivery(
|
|
20996
|
+
client,
|
|
20997
|
+
interaction,
|
|
20998
|
+
event,
|
|
20999
|
+
`${output || "OpenGeni finished this task."}
|
|
21000
|
+
|
|
21001
|
+
${openSessionText(deps, interaction.workspaceId, interaction.sessionId)} Reply in this thread to continue.`,
|
|
21002
|
+
"final"
|
|
21003
|
+
);
|
|
21004
|
+
terminal = "completed";
|
|
21005
|
+
} else if (event.type === "turn.failed") {
|
|
21006
|
+
await postDelivery(
|
|
21007
|
+
client,
|
|
21008
|
+
interaction,
|
|
21009
|
+
event,
|
|
21010
|
+
`OpenGeni could not complete this task. ${openSessionText(deps, interaction.workspaceId, interaction.sessionId)} for the bounded failure details.`,
|
|
21011
|
+
"failed"
|
|
21012
|
+
);
|
|
21013
|
+
terminal = "failed";
|
|
21014
|
+
} else if (event.type === "turn.cancelled") {
|
|
21015
|
+
await postDelivery(client, interaction, event, "OpenGeni stopped this task.", "cancelled");
|
|
21016
|
+
terminal = "cancelled";
|
|
21017
|
+
}
|
|
21018
|
+
}
|
|
21019
|
+
if (terminal) {
|
|
21020
|
+
await closeSlackInteractionDelivery(deps.db, {
|
|
21021
|
+
...interaction,
|
|
21022
|
+
claimHolderId,
|
|
21023
|
+
sequence: lastSequence,
|
|
21024
|
+
state: terminal
|
|
21025
|
+
});
|
|
21026
|
+
} else {
|
|
21027
|
+
await advanceSlackInteractionDelivery(deps.db, {
|
|
21028
|
+
...interaction,
|
|
21029
|
+
claimHolderId,
|
|
21030
|
+
sequence: lastSequence
|
|
21031
|
+
});
|
|
21032
|
+
await releaseSlackInteractionDelivery(deps.db, {
|
|
21033
|
+
...interaction,
|
|
21034
|
+
claimHolderId
|
|
21035
|
+
});
|
|
21036
|
+
}
|
|
21037
|
+
}
|
|
21038
|
+
async function postDelivery(client, interaction, event, text, kind, operationId = deterministicUuid(`slack-delivery:${interaction.id}:${event.sequence}:${kind}`)) {
|
|
21039
|
+
await client.postMessage({
|
|
21040
|
+
operationId,
|
|
21041
|
+
channelId: interaction.slackChannelId,
|
|
21042
|
+
threadTimestamp: interaction.slackThreadTs,
|
|
21043
|
+
text: boundedOutput(text)
|
|
21044
|
+
});
|
|
21045
|
+
}
|
|
21046
|
+
async function enqueueNormalizedSlackInteraction(deps, route, entry) {
|
|
21047
|
+
await enqueueSlackInteractionInbox(deps.db, {
|
|
21048
|
+
accountId: route.accountId,
|
|
21049
|
+
workspaceId: route.workspaceId,
|
|
21050
|
+
connectionId: route.connectionId,
|
|
21051
|
+
...entry
|
|
21052
|
+
});
|
|
21053
|
+
}
|
|
21054
|
+
async function readSignedSlackRequest(c, deps) {
|
|
21055
|
+
const signingSecret = deps.settings.slackSigningSecret;
|
|
21056
|
+
if (!signingSecret)
|
|
21057
|
+
throw new HTTPException31(503, {
|
|
21058
|
+
message: "Slack interactions are disabled"
|
|
21059
|
+
});
|
|
21060
|
+
const rawBytes = new Uint8Array(await c.req.raw.arrayBuffer());
|
|
21061
|
+
if (rawBytes.byteLength === 0 || rawBytes.byteLength > SLACK_INTERACTION_MAX_BODY_BYTES) {
|
|
21062
|
+
throw new HTTPException31(rawBytes.byteLength > SLACK_INTERACTION_MAX_BODY_BYTES ? 413 : 400, {
|
|
21063
|
+
message: "invalid Slack request body"
|
|
21064
|
+
});
|
|
21065
|
+
}
|
|
21066
|
+
const rawBody = new TextDecoder("utf-8", { fatal: true }).decode(rawBytes);
|
|
21067
|
+
if (!verifySlackRequestSignature(
|
|
21068
|
+
{
|
|
21069
|
+
timestamp: c.req.header("x-slack-request-timestamp") ?? null,
|
|
21070
|
+
signature: c.req.header("x-slack-signature") ?? null,
|
|
21071
|
+
rawBody
|
|
21072
|
+
},
|
|
21073
|
+
signingSecret
|
|
21074
|
+
)) {
|
|
21075
|
+
throw new HTTPException31(401, { message: "invalid Slack signature" });
|
|
21076
|
+
}
|
|
21077
|
+
return { rawBody };
|
|
21078
|
+
}
|
|
21079
|
+
function normalizedFormInteraction(form, triggerKind) {
|
|
21080
|
+
const teamId = boundedString(form.get("team_id"), 64);
|
|
21081
|
+
const userId = boundedString(form.get("user_id"), 64);
|
|
21082
|
+
const channelId = boundedString(form.get("channel_id"), 64);
|
|
21083
|
+
const triggerId = boundedString(form.get("trigger_id"), 256);
|
|
21084
|
+
const text = boundedText(form.get("text"));
|
|
21085
|
+
if (!teamId || !userId || !channelId || !triggerId || !text) {
|
|
21086
|
+
throw new HTTPException31(400, { message: "invalid Slack command payload" });
|
|
21087
|
+
}
|
|
21088
|
+
return {
|
|
21089
|
+
providerEventId: `command:${triggerId}`,
|
|
21090
|
+
providerMessageId: `command:${triggerId}`,
|
|
21091
|
+
slackTeamId: teamId,
|
|
21092
|
+
slackUserId: userId,
|
|
21093
|
+
slackChannelId: channelId,
|
|
21094
|
+
slackMessageTs: triggerId.slice(0, 64),
|
|
21095
|
+
slackThreadTs: null,
|
|
21096
|
+
triggerKind,
|
|
21097
|
+
text
|
|
21098
|
+
};
|
|
21099
|
+
}
|
|
21100
|
+
function humanInputResponse(questions, text) {
|
|
21101
|
+
if (questions.length !== 1) return null;
|
|
21102
|
+
const question = questions[0];
|
|
21103
|
+
if (question.kind === "text") {
|
|
21104
|
+
return {
|
|
21105
|
+
outcome: "answered",
|
|
21106
|
+
answers: [{ questionId: question.id, values: [text] }]
|
|
21107
|
+
};
|
|
21108
|
+
}
|
|
21109
|
+
const normalized = text.trim().toLowerCase();
|
|
21110
|
+
const matches = question.options.filter(
|
|
21111
|
+
(option) => option.id.toLowerCase() === normalized || option.label.toLowerCase() === normalized
|
|
21112
|
+
);
|
|
21113
|
+
if (matches.length !== 1) return null;
|
|
21114
|
+
return {
|
|
21115
|
+
outcome: "answered",
|
|
21116
|
+
answers: [{ questionId: question.id, values: [matches[0].id] }]
|
|
21117
|
+
};
|
|
21118
|
+
}
|
|
21119
|
+
function formatQuestions(questions) {
|
|
21120
|
+
return questions.slice(0, 5).map((question, index) => {
|
|
21121
|
+
const options = question.options.slice(0, 10).map((option) => option.label).join(", ");
|
|
21122
|
+
return `${index + 1}. ${boundedOutput(question.prompt)}${options ? ` (${options})` : ""}`;
|
|
21123
|
+
}).join("\n");
|
|
21124
|
+
}
|
|
21125
|
+
function openSessionText(deps, workspaceId, sessionId) {
|
|
21126
|
+
const base = deps.settings.webBaseUrl ?? deps.settings.publicBaseUrl;
|
|
21127
|
+
return base ? `Open in OpenGeni: ${new URL(`/workspaces/${workspaceId}/sessions/${sessionId}`, base).toString()}` : "Open this session in OpenGeni";
|
|
21128
|
+
}
|
|
21129
|
+
function linkUrl(deps, entry) {
|
|
21130
|
+
const base = deps.settings.webBaseUrl ?? deps.settings.publicBaseUrl;
|
|
21131
|
+
if (!base) return "OpenGeni Settings \u2192 Integrations \u2192 Slack";
|
|
21132
|
+
const url = new URL("/settings/integrations/slack", base);
|
|
21133
|
+
url.searchParams.set("workspaceId", entry.workspaceId);
|
|
21134
|
+
url.searchParams.set("connectionId", entry.connectionId);
|
|
21135
|
+
url.searchParams.set("teamId", entry.slackTeamId);
|
|
21136
|
+
url.searchParams.set("userId", entry.slackUserId);
|
|
21137
|
+
return url.toString();
|
|
21138
|
+
}
|
|
21139
|
+
function slackRouteKey(channelId, threadTs) {
|
|
21140
|
+
return `${channelId}:${threadTs}`;
|
|
21141
|
+
}
|
|
21142
|
+
function deterministicUuid(value) {
|
|
21143
|
+
const bytes = createHash8("sha256").update(value).digest().subarray(0, 16);
|
|
21144
|
+
bytes[6] = bytes[6] & 15 | 80;
|
|
21145
|
+
bytes[8] = bytes[8] & 63 | 128;
|
|
21146
|
+
const hex = bytes.toString("hex");
|
|
21147
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
21148
|
+
}
|
|
21149
|
+
function parseJsonObject(value) {
|
|
21150
|
+
try {
|
|
21151
|
+
const parsed = JSON.parse(value);
|
|
21152
|
+
const result = record3(parsed);
|
|
21153
|
+
if (result) return result;
|
|
21154
|
+
} catch {
|
|
21155
|
+
}
|
|
21156
|
+
throw new HTTPException31(400, { message: "invalid Slack JSON payload" });
|
|
21157
|
+
}
|
|
21158
|
+
function record3(value) {
|
|
21159
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
21160
|
+
}
|
|
21161
|
+
function boundedString(value, max) {
|
|
21162
|
+
return typeof value === "string" && value.length > 0 && Buffer.byteLength(value) <= max ? value : null;
|
|
21163
|
+
}
|
|
21164
|
+
function boundedText(value) {
|
|
21165
|
+
if (typeof value !== "string") return null;
|
|
21166
|
+
const trimmed = value.trim();
|
|
21167
|
+
if (!trimmed) return null;
|
|
21168
|
+
return trimmed.slice(0, MAX_SLACK_INPUT_CHARS);
|
|
21169
|
+
}
|
|
21170
|
+
function boundedOutput(value) {
|
|
21171
|
+
return value.length <= MAX_SLACK_TEXT_CHARS ? value : `${value.slice(0, MAX_SLACK_TEXT_CHARS - 20)}
|
|
21172
|
+
\u2026 output truncated`;
|
|
21173
|
+
}
|
|
21174
|
+
function safePayloadText(payload, field) {
|
|
21175
|
+
const value = record3(payload)?.[field];
|
|
21176
|
+
return typeof value === "string" ? boundedOutput(value) : "";
|
|
21177
|
+
}
|
|
21178
|
+
function safeErrorCode(error) {
|
|
21179
|
+
const raw = error instanceof Error ? error.name : "slack_interaction_error";
|
|
21180
|
+
return raw.toLowerCase().replace(/[^a-z0-9_-]/g, "_").slice(0, 128) || "error";
|
|
21181
|
+
}
|
|
21182
|
+
var SlackInteractionPermanentError = class extends Error {
|
|
21183
|
+
};
|
|
21184
|
+
function permanentSlackInteractionError(error) {
|
|
21185
|
+
return error instanceof SlackInteractionPermanentError || error instanceof HTTPException31;
|
|
21186
|
+
}
|
|
21187
|
+
|
|
19948
21188
|
// src/app.ts
|
|
19949
21189
|
import {
|
|
19950
21190
|
mergeResourceRefs,
|
|
@@ -19964,6 +21204,9 @@ function apiRequestBodyLimitBytes(settings) {
|
|
|
19964
21204
|
}
|
|
19965
21205
|
var API_PUBLIC_ERROR_MESSAGE_MAX_BYTES = 512;
|
|
19966
21206
|
function createApp(deps) {
|
|
21207
|
+
return createAppComposition(deps).app;
|
|
21208
|
+
}
|
|
21209
|
+
function createAppComposition(deps) {
|
|
19967
21210
|
const managedAuth = deps.managedAuth ?? createManagedAuth(deps.settings, deps.db);
|
|
19968
21211
|
const objectStorage = deps.objectStorage === void 0 ? createObjectStorage(deps.settings) : deps.objectStorage;
|
|
19969
21212
|
let documentServices = deps.documentServices ?? null;
|
|
@@ -19978,7 +21221,7 @@ function createApp(deps) {
|
|
|
19978
21221
|
documentId
|
|
19979
21222
|
}) => {
|
|
19980
21223
|
if (!objectStorage) {
|
|
19981
|
-
throw new
|
|
21224
|
+
throw new HTTPException32(503, {
|
|
19982
21225
|
message: "object storage is not configured"
|
|
19983
21226
|
});
|
|
19984
21227
|
}
|
|
@@ -20029,28 +21272,29 @@ function createApp(deps) {
|
|
|
20029
21272
|
c.header(OPENGENI_CORRELATION_HEADER, correlationId);
|
|
20030
21273
|
await next();
|
|
20031
21274
|
});
|
|
20032
|
-
|
|
20033
|
-
|
|
20034
|
-
|
|
20035
|
-
|
|
20036
|
-
|
|
20037
|
-
|
|
20038
|
-
|
|
20039
|
-
|
|
20040
|
-
|
|
20041
|
-
|
|
20042
|
-
|
|
20043
|
-
|
|
20044
|
-
|
|
20045
|
-
|
|
20046
|
-
|
|
20047
|
-
|
|
20048
|
-
|
|
20049
|
-
|
|
20050
|
-
|
|
20051
|
-
|
|
20052
|
-
|
|
20053
|
-
|
|
21275
|
+
const corsHeaders = {
|
|
21276
|
+
allowHeaders: [
|
|
21277
|
+
"Accept",
|
|
21278
|
+
"Authorization",
|
|
21279
|
+
"Content-Type",
|
|
21280
|
+
"X-OpenGeni-Access-Key",
|
|
21281
|
+
"X-OpenGeni-Api-Contract",
|
|
21282
|
+
"X-OpenGeni-Correlation-Id",
|
|
21283
|
+
"X-OpenGeni-Subject"
|
|
21284
|
+
],
|
|
21285
|
+
exposeHeaders: ["X-OpenGeni-Api-Contract", "X-OpenGeni-Correlation-Id"]
|
|
21286
|
+
};
|
|
21287
|
+
const publicApiCors = cors({ ...corsHeaders, credentials: false, origin: "*" });
|
|
21288
|
+
const credentialedCors = cors({
|
|
21289
|
+
...corsHeaders,
|
|
21290
|
+
credentials: true,
|
|
21291
|
+
origin: (origin) => allowedCorsOrigin(deps.settings.corsAllowOriginRegex, origin) ? origin : null
|
|
21292
|
+
});
|
|
21293
|
+
app.use("*", (c, next) => {
|
|
21294
|
+
const origin = c.req.header("origin");
|
|
21295
|
+
const middleware = origin && allowedCorsOrigin(deps.settings.corsAllowOriginRegex, origin) ? credentialedCors : publicApiCors;
|
|
21296
|
+
return middleware(c, next);
|
|
21297
|
+
});
|
|
20054
21298
|
app.use(
|
|
20055
21299
|
"*",
|
|
20056
21300
|
bodyLimit({
|
|
@@ -20219,7 +21463,9 @@ function createApp(deps) {
|
|
|
20219
21463
|
boundedRequest = await boundedMcpRequest(c.req.raw);
|
|
20220
21464
|
} catch (error) {
|
|
20221
21465
|
if (error instanceof McpPayloadTooLargeError2) {
|
|
20222
|
-
throw new
|
|
21466
|
+
throw new HTTPException32(413, {
|
|
21467
|
+
message: "MCP request body exceeds the safety limit"
|
|
21468
|
+
});
|
|
20223
21469
|
}
|
|
20224
21470
|
throw error;
|
|
20225
21471
|
}
|
|
@@ -20228,7 +21474,7 @@ function createApp(deps) {
|
|
|
20228
21474
|
const boundSessionId = grant.metadata?.sessionId;
|
|
20229
21475
|
if (toolspaceGrant || typeof boundSessionId === "string") {
|
|
20230
21476
|
if (typeof boundSessionId !== "string") {
|
|
20231
|
-
throw new
|
|
21477
|
+
throw new HTTPException32(404, { message: "session not found" });
|
|
20232
21478
|
}
|
|
20233
21479
|
try {
|
|
20234
21480
|
await requireSessionAuthorization3(routeDeps, grant, {
|
|
@@ -20238,10 +21484,12 @@ function createApp(deps) {
|
|
|
20238
21484
|
});
|
|
20239
21485
|
} catch (error) {
|
|
20240
21486
|
if (error instanceof SessionAuthorizationDeniedError2) {
|
|
20241
|
-
throw new
|
|
21487
|
+
throw new HTTPException32(404, { message: "session not found" });
|
|
20242
21488
|
}
|
|
20243
21489
|
if (error instanceof SessionAuthorizationUnavailableError2) {
|
|
20244
|
-
throw new
|
|
21490
|
+
throw new HTTPException32(503, {
|
|
21491
|
+
message: "session authorization is unavailable"
|
|
21492
|
+
});
|
|
20245
21493
|
}
|
|
20246
21494
|
throw error;
|
|
20247
21495
|
}
|
|
@@ -20249,10 +21497,15 @@ function createApp(deps) {
|
|
|
20249
21497
|
let toolspace = null;
|
|
20250
21498
|
if (toolspaceGrant) {
|
|
20251
21499
|
try {
|
|
20252
|
-
toolspace = await prepareToolspaceMcpSurface({
|
|
21500
|
+
toolspace = await prepareToolspaceMcpSurface({
|
|
21501
|
+
deps: routeDeps,
|
|
21502
|
+
grant
|
|
21503
|
+
});
|
|
20253
21504
|
} catch (error) {
|
|
20254
21505
|
if (error instanceof McpPayloadTooLargeError2) {
|
|
20255
|
-
throw new
|
|
21506
|
+
throw new HTTPException32(413, {
|
|
21507
|
+
message: "MCP tool list exceeds the safety limit"
|
|
21508
|
+
});
|
|
20256
21509
|
}
|
|
20257
21510
|
throw error;
|
|
20258
21511
|
}
|
|
@@ -20284,6 +21537,7 @@ function createApp(deps) {
|
|
|
20284
21537
|
registerInsightsRoutes(app, routeDeps);
|
|
20285
21538
|
registerWorkspaceInstructionPolicyRoutes(app, routeDeps);
|
|
20286
21539
|
registerWorkspaceStateRoutes(app, routeDeps);
|
|
21540
|
+
registerWorkspaceArtifactRoutes(app, routeDeps);
|
|
20287
21541
|
registerPreferenceRegistryRoutes(app, routeDeps);
|
|
20288
21542
|
registerSocialRoutes(app, routeDeps);
|
|
20289
21543
|
registerConnectionRoutes(app, routeDeps);
|
|
@@ -20298,6 +21552,7 @@ function createApp(deps) {
|
|
|
20298
21552
|
registerScheduledTaskRoutes(app, routeDeps);
|
|
20299
21553
|
registerCodexRoutes(app, routeDeps);
|
|
20300
21554
|
registerTranscriptionRoutes(app, routeDeps);
|
|
21555
|
+
registerSlackInteractionRoutes(app, routeDeps);
|
|
20301
21556
|
app.notFound((c) => {
|
|
20302
21557
|
if (!new URL(c.req.url).pathname.startsWith("/v1/")) return c.text("Not Found", 404);
|
|
20303
21558
|
const requestId = correlationIds.get(c.req.raw) ?? crypto.randomUUID();
|
|
@@ -20334,11 +21589,11 @@ function createApp(deps) {
|
|
|
20334
21589
|
});
|
|
20335
21590
|
return c.json(envelope, status);
|
|
20336
21591
|
});
|
|
20337
|
-
return app;
|
|
21592
|
+
return { app, routeDeps };
|
|
20338
21593
|
}
|
|
20339
21594
|
async function requireMcpAccessGrant(c, deps, workspaceId) {
|
|
20340
|
-
const grant = await
|
|
20341
|
-
if (
|
|
21595
|
+
const grant = await requireAccessGrant24(c, deps, workspaceId);
|
|
21596
|
+
if (hasPermission14(grant.permissions, "workspace:read")) {
|
|
20342
21597
|
return grant;
|
|
20343
21598
|
}
|
|
20344
21599
|
if (isToolspaceGrant(deps.settings, grant)) {
|
|
@@ -20378,7 +21633,7 @@ function allowedCorsOrigin(pattern, origin) {
|
|
|
20378
21633
|
}
|
|
20379
21634
|
function codexCompactionV2ProviderLockedError(error) {
|
|
20380
21635
|
if (error instanceof CodexCompactionV2ProviderLockedError) return error;
|
|
20381
|
-
if (error instanceof
|
|
21636
|
+
if (error instanceof HTTPException32 && error.cause instanceof CodexCompactionV2ProviderLockedError) {
|
|
20382
21637
|
return error.cause;
|
|
20383
21638
|
}
|
|
20384
21639
|
return null;
|
|
@@ -20387,7 +21642,7 @@ function httpStatusForError(error) {
|
|
|
20387
21642
|
if (codexCompactionV2ProviderLockedError(error)) {
|
|
20388
21643
|
return 422;
|
|
20389
21644
|
}
|
|
20390
|
-
if (error instanceof
|
|
21645
|
+
if (error instanceof HTTPException32) {
|
|
20391
21646
|
return error.status;
|
|
20392
21647
|
}
|
|
20393
21648
|
if (error instanceof McpPayloadTooLargeError2) {
|
|
@@ -20416,7 +21671,7 @@ function publicErrorMessage(error, status) {
|
|
|
20416
21671
|
if (status >= 500) {
|
|
20417
21672
|
return "OpenGeni could not complete the request.";
|
|
20418
21673
|
}
|
|
20419
|
-
if (error instanceof
|
|
21674
|
+
if (error instanceof HTTPException32) {
|
|
20420
21675
|
return boundedPublicMessage(error.message) ?? "Request failed.";
|
|
20421
21676
|
}
|
|
20422
21677
|
if (error instanceof McpPayloadTooLargeError2) {
|
|
@@ -20882,7 +22137,7 @@ function isApiContractProtectedMutation(method, pathname) {
|
|
|
20882
22137
|
if (!pathname.startsWith("/v1/")) {
|
|
20883
22138
|
return false;
|
|
20884
22139
|
}
|
|
20885
|
-
if (pathname.startsWith("/v1/auth/") || pathname.startsWith("/v1/webhooks/") || pathname.startsWith("/v1/integrations/oauth/") || pathname.startsWith("/v1/github/") || pathname === "/v1/enrollments/device/start" || pathname === "/v1/enrollments/device/poll" || pathname === "/v1/enrollments/token/exchange") {
|
|
22140
|
+
if (pathname.startsWith("/v1/auth/") || pathname.startsWith("/v1/webhooks/") || pathname.startsWith("/v1/integrations/oauth/") || pathname === "/v1/integrations/slack/events" || pathname === "/v1/integrations/slack/commands" || pathname === "/v1/integrations/slack/interactions" || pathname.startsWith("/v1/github/") || pathname === "/v1/enrollments/device/start" || pathname === "/v1/enrollments/device/poll" || pathname === "/v1/enrollments/token/exchange") {
|
|
20886
22141
|
return false;
|
|
20887
22142
|
}
|
|
20888
22143
|
return !pathname.split("/").includes("mcp");
|
|
@@ -20892,9 +22147,11 @@ export {
|
|
|
20892
22147
|
sseSessionStream,
|
|
20893
22148
|
replaySessionEvents,
|
|
20894
22149
|
sseWorkspaceControlStream,
|
|
22150
|
+
startSlackInteractionPump,
|
|
20895
22151
|
API_MAX_REQUEST_BODY_BYTES,
|
|
20896
22152
|
apiRequestBodyLimitBytes,
|
|
20897
22153
|
createApp,
|
|
22154
|
+
createAppComposition,
|
|
20898
22155
|
allowedCorsOrigin,
|
|
20899
22156
|
httpStatusForError,
|
|
20900
22157
|
errorCodeForStatus,
|
|
@@ -20911,4 +22168,4 @@ export {
|
|
|
20911
22168
|
withDefaultEnabledCapabilityMcpTools,
|
|
20912
22169
|
workflowIdForSession2 as workflowIdForSession
|
|
20913
22170
|
};
|
|
20914
|
-
//# sourceMappingURL=chunk-
|
|
22171
|
+
//# sourceMappingURL=chunk-UVL2KH56.js.map
|