@opengeni/api-router 0.30.3 → 2.2.0-canary.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api-websocket.d.ts +18 -0
- package/dist/app.js +1 -1
- package/dist/auth/managed-auth.d.ts +6 -0
- package/dist/{chunk-WCXHI3FF.js → chunk-3TP54PPX.js} +26692 -15389
- package/dist/chunk-3TP54PPX.js.map +1 -0
- package/dist/codemode.d.ts +8 -1
- package/dist/company-brain-okf.d.ts +24 -0
- package/dist/connected-machine-computer-access.d.ts +11 -0
- package/dist/connection-authority-owner.d.ts +59 -0
- package/dist/connection-ownership.d.ts +47 -0
- package/dist/controller-data-plane.d.ts +16 -0
- package/dist/editable-artifact-websocket.d.ts +5 -15
- package/dist/editable-artifact-workspace-files.d.ts +1 -1
- package/dist/http/api-error.d.ts +10 -0
- package/dist/http/cors.d.ts +5 -0
- package/dist/http/interaction-control-error.d.ts +13 -0
- package/dist/http/sse.d.ts +2 -3
- package/dist/index.d.ts +5 -2
- package/dist/index.js +326 -82
- package/dist/index.js.map +1 -1
- package/dist/integrations/atlassian.d.ts +17 -0
- package/dist/integrations/fiken.d.ts +7 -2
- package/dist/integrations/google-drive.d.ts +18 -0
- package/dist/integrations/oauth-client.d.ts +22 -3
- package/dist/integrations/oauth-profiles.d.ts +195 -0
- package/dist/integrations/personal-github-repositories.d.ts +45 -0
- package/dist/integrations/personal-github.d.ts +40 -0
- package/dist/integrations/pr-review-provider.d.ts +22 -0
- package/dist/integrations/provider-oauth.d.ts +8 -0
- package/dist/integrations/slack-app-home.d.ts +24 -0
- package/dist/integrations/slack-bot.d.ts +157 -9
- package/dist/integrations/slack-interactions.d.ts +39 -5
- package/dist/integrations/slack-routing.d.ts +120 -0
- package/dist/integrations/social-oauth.d.ts +5 -0
- package/dist/interaction-frame-proxy.d.ts +68 -0
- package/dist/mcp/company-brain-governed-writes.d.ts +26 -0
- package/dist/mcp/company-profile-agent-admin.d.ts +44 -0
- package/dist/mcp/documents.d.ts +1 -0
- package/dist/mcp/remember.d.ts +28 -0
- package/dist/mcp/request-abort.d.ts +19 -0
- package/dist/mcp/scheduled-task-view.d.ts +4 -4
- package/dist/mcp/server.d.ts +15 -2
- package/dist/mcp/session-view.d.ts +4 -0
- package/dist/mcp/session-wait.d.ts +137 -0
- package/dist/routes/automations.d.ts +4 -0
- package/dist/routes/billing.d.ts +5 -0
- package/dist/routes/browser-sessions.d.ts +4 -1
- package/dist/routes/company-brain.d.ts +3 -0
- package/dist/routes/company-profile.d.ts +2 -1
- package/dist/routes/connection-authorities.d.ts +3 -0
- package/dist/routes/personal-github-git-broker.d.ts +29 -0
- package/dist/routes/personal-github.d.ts +3 -0
- package/dist/routes/pr-review.d.ts +3 -0
- package/dist/routes/sessions.d.ts +1 -0
- package/dist/routes/user-resource-authorities.d.ts +3 -0
- package/dist/routes/workspace-capture.d.ts +10 -2
- package/dist/routes/workspace-learning.d.ts +3 -0
- package/dist/routes/workspaces.d.ts +19 -0
- package/dist/sandbox/channel-a.d.ts +11 -1
- package/dist/sandbox/connection-authority.d.ts +3 -0
- package/dist/sandbox/enrollment.d.ts +2 -0
- package/dist/sandbox/machines.d.ts +2 -2
- package/dist/sandbox/metrics-ingestion.d.ts +10 -20
- package/dist/sandbox/viewer.d.ts +24 -1
- package/dist/sandbox-file-artifacts.d.ts +11 -0
- package/dist/scheduled-task-deletion.d.ts +14 -0
- package/dist/slack-reaction-files.d.ts +1 -1
- package/dist/temporal-schedule-cleanup.d.ts +1 -0
- package/package.json +20 -18
- package/src/api-websocket.ts +23 -0
- package/src/app.ts +161 -23
- package/src/auth/managed-auth.ts +25 -3
- package/src/codemode.ts +53 -23
- package/src/codex-realtime.ts +8 -2
- package/src/company-brain-okf.ts +340 -0
- package/src/connected-machine-computer-access.ts +33 -0
- package/src/connection-authority-owner.ts +61 -0
- package/src/connection-ownership.ts +180 -0
- package/src/controller-data-plane.ts +47 -0
- package/src/editable-artifact-native-kernel.ts +26 -15
- package/src/editable-artifact-office-import.ts +28 -3
- package/src/editable-artifact-production.ts +13 -7
- package/src/editable-artifact-websocket.ts +11 -18
- package/src/editable-artifact-workspace-files.ts +31 -6
- package/src/http/api-error.ts +28 -0
- package/src/http/auth.ts +4 -0
- package/src/http/cors.ts +35 -0
- package/src/http/interaction-control-error.ts +164 -0
- package/src/http/sse.ts +112 -66
- package/src/index.ts +46 -6
- package/src/integrations/atlassian.ts +146 -35
- package/src/integrations/fiken.ts +102 -22
- package/src/integrations/google-drive.ts +301 -92
- package/src/integrations/oauth-client.ts +272 -143
- package/src/integrations/oauth-profiles.ts +477 -0
- package/src/integrations/personal-github-repositories.ts +445 -0
- package/src/integrations/personal-github.ts +705 -0
- package/src/integrations/pr-review-provider.ts +246 -0
- package/src/integrations/provider-oauth.ts +121 -5
- package/src/integrations/slack-app-home.ts +300 -0
- package/src/integrations/slack-bot.ts +342 -57
- package/src/integrations/slack-interactions.ts +1898 -259
- package/src/integrations/slack-routing.ts +337 -0
- package/src/integrations/social-oauth.ts +25 -0
- package/src/interaction-frame-proxy.ts +409 -0
- package/src/mcp/company-brain-governed-writes.ts +262 -0
- package/src/mcp/company-profile-agent-admin.ts +205 -0
- package/src/mcp/documents.ts +37 -6
- package/src/mcp/files.ts +16 -1
- package/src/mcp/remember.ts +181 -0
- package/src/mcp/request-abort.ts +44 -0
- package/src/mcp/scheduled-task-view.ts +1 -0
- package/src/mcp/server.ts +719 -369
- package/src/mcp/session-view.ts +54 -0
- package/src/mcp/session-wait.ts +554 -0
- package/src/model-catalog.ts +20 -12
- package/src/routes/api-integrations.ts +4 -0
- package/src/routes/api-keys.ts +1 -0
- package/src/routes/automations.ts +534 -0
- package/src/routes/billing.ts +57 -1
- package/src/routes/browser-sessions.ts +329 -79
- package/src/routes/channels.ts +17 -1
- package/src/routes/codex.ts +50 -0
- package/src/routes/company-brain.ts +367 -0
- package/src/routes/company-profile.ts +1 -1
- package/src/routes/computer-sessions.ts +320 -93
- package/src/routes/connection-authorities.ts +139 -0
- package/src/routes/connections.ts +144 -34
- package/src/routes/documents.ts +353 -4
- package/src/routes/editable-artifacts.ts +11 -3
- package/src/routes/enrollments.ts +116 -26
- package/src/routes/environments.ts +127 -55
- package/src/routes/files.ts +72 -11
- package/src/routes/install.ts +82 -28
- package/src/routes/integration-facets.ts +29 -0
- package/src/routes/interaction-resources.ts +20 -2
- package/src/routes/machines.ts +275 -13
- package/src/routes/organization-memberships.ts +717 -28
- package/src/routes/packs.ts +4 -3
- package/src/routes/personal-github-git-broker.ts +785 -0
- package/src/routes/personal-github.ts +319 -0
- package/src/routes/pr-review.ts +531 -0
- package/src/routes/rigs.ts +104 -39
- package/src/routes/scheduled-tasks.ts +19 -21
- package/src/routes/sessions.ts +775 -38
- package/src/routes/social.ts +14 -2
- package/src/routes/supergrok.ts +23 -4
- package/src/routes/transcription-recordings.ts +8 -2
- package/src/routes/user-resource-authorities.ts +138 -0
- package/src/routes/workspace-artifacts.ts +4 -1
- package/src/routes/workspace-capture.ts +21 -0
- package/src/routes/workspace-learning.ts +228 -0
- package/src/routes/workspaces.ts +59 -6
- package/src/sandbox/auth-callout.ts +38 -12
- package/src/sandbox/channel-a.ts +242 -19
- package/src/sandbox/connection-authority.ts +3 -0
- package/src/sandbox/enrollment.ts +15 -3
- package/src/sandbox/machines.ts +186 -64
- package/src/sandbox/metrics-ingestion.ts +220 -58
- package/src/sandbox/viewer.ts +243 -19
- package/src/sandbox-file-artifacts.ts +329 -0
- package/src/scheduled-task-deletion.ts +127 -0
- package/src/slack-reaction-files.ts +16 -5
- package/src/temporal-schedule-cleanup.ts +13 -0
- package/dist/chunk-WCXHI3FF.js.map +0 -1
package/src/model-catalog.ts
CHANGED
|
@@ -36,6 +36,8 @@ export const MODEL_CREDENTIAL_READINESS_OBSERVATION_MAX_AGE_MS = 5 * 60_000;
|
|
|
36
36
|
|
|
37
37
|
/** Static, client-safe definition projection. No provider secret is reachable. */
|
|
38
38
|
export function projectClientModel(model: ConfiguredModel): ClientModel {
|
|
39
|
+
const anonymousProvider =
|
|
40
|
+
model.credentialSource.kind === "deployment" && model.credentialSource.mechanism === "none";
|
|
39
41
|
const source =
|
|
40
42
|
model.credentialSource.kind === "connected_subscription"
|
|
41
43
|
? model.credentialSource.provider === "xai"
|
|
@@ -43,9 +45,12 @@ export function projectClientModel(model: ConfiguredModel): ClientModel {
|
|
|
43
45
|
: "codex"
|
|
44
46
|
: model.credentialSource.kind === "workspace_connection"
|
|
45
47
|
? "workspace_gateway"
|
|
46
|
-
:
|
|
47
|
-
|
|
48
|
-
|
|
48
|
+
: anonymousProvider
|
|
49
|
+
? undefined
|
|
50
|
+
: "opengeni";
|
|
51
|
+
const publicProvider = anonymousProvider
|
|
52
|
+
? { provider: model.providerId, providerLabel: model.providerLabel }
|
|
53
|
+
: source === "codex"
|
|
49
54
|
? { provider: "codex", providerLabel: "Codex" }
|
|
50
55
|
: source === "supergrok"
|
|
51
56
|
? { provider: "supergrok", providerLabel: "SuperGrok" }
|
|
@@ -57,7 +62,7 @@ export function projectClientModel(model: ConfiguredModel): ClientModel {
|
|
|
57
62
|
label: model.label,
|
|
58
63
|
...(model.shortLabel ? { shortLabel: model.shortLabel } : {}),
|
|
59
64
|
...publicProvider,
|
|
60
|
-
source,
|
|
65
|
+
...(source === undefined ? {} : { source }),
|
|
61
66
|
api: model.api,
|
|
62
67
|
...(model.contextWindowTokens === undefined
|
|
63
68
|
? {}
|
|
@@ -165,6 +170,9 @@ function credentialReadinessFor(input: {
|
|
|
165
170
|
checkedAt: null,
|
|
166
171
|
};
|
|
167
172
|
}
|
|
173
|
+
if (source.kind === "deployment" && source.mechanism === "none") {
|
|
174
|
+
return { status: "ready", reason: null, basis: "configuration", checkedAt: null };
|
|
175
|
+
}
|
|
168
176
|
if (source.kind === "deployment" && source.mechanism === "api_key") {
|
|
169
177
|
return input.provider?.apiKey
|
|
170
178
|
? { status: "ready", reason: null, basis: "configuration", checkedAt: null }
|
|
@@ -237,7 +245,7 @@ function xaiGrokAvailabilityFor(input: {
|
|
|
237
245
|
function availabilityFor(input: {
|
|
238
246
|
model: ConfiguredModel;
|
|
239
247
|
credentialReadiness: ModelCredentialReadinessV1;
|
|
240
|
-
|
|
248
|
+
policyAllowed: boolean;
|
|
241
249
|
observation?: ModelAvailabilityObservation | undefined;
|
|
242
250
|
nowMs: number;
|
|
243
251
|
maxAgeMs: number;
|
|
@@ -263,12 +271,7 @@ function availabilityFor(input: {
|
|
|
263
271
|
checkedAt: input.credentialReadiness.checkedAt,
|
|
264
272
|
};
|
|
265
273
|
}
|
|
266
|
-
if (
|
|
267
|
-
!evaluateWorkspaceModelPolicy(input.policy, {
|
|
268
|
-
providerId: input.model.providerId,
|
|
269
|
-
modelId: input.model.id,
|
|
270
|
-
}).allowed
|
|
271
|
-
) {
|
|
274
|
+
if (!input.policyAllowed) {
|
|
272
275
|
return {
|
|
273
276
|
status: "unavailable",
|
|
274
277
|
selectable: false,
|
|
@@ -349,6 +352,10 @@ export function buildWorkspaceModelCatalog(input: {
|
|
|
349
352
|
: MODEL_CREDENTIAL_READINESS_OBSERVATION_MAX_AGE_MS;
|
|
350
353
|
const models = configuredModels(catalogSettings).map((model) => {
|
|
351
354
|
const provider = providers.get(model.providerId);
|
|
355
|
+
const policyAllowed = evaluateWorkspaceModelPolicy(input.policy, {
|
|
356
|
+
providerId: model.providerId,
|
|
357
|
+
modelId: model.id,
|
|
358
|
+
}).allowed;
|
|
352
359
|
const credentialReadiness = credentialReadinessFor({
|
|
353
360
|
model,
|
|
354
361
|
provider,
|
|
@@ -362,10 +369,11 @@ export function buildWorkspaceModelCatalog(input: {
|
|
|
362
369
|
return {
|
|
363
370
|
...projectClientModel(model),
|
|
364
371
|
credentialReadiness,
|
|
372
|
+
policyAllowed,
|
|
365
373
|
availability: availabilityFor({
|
|
366
374
|
model,
|
|
367
375
|
credentialReadiness,
|
|
368
|
-
|
|
376
|
+
policyAllowed,
|
|
369
377
|
observation: input.observations?.[model.definitionVersion],
|
|
370
378
|
nowMs,
|
|
371
379
|
maxAgeMs,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
CORE_INTEGRATION_DEFINITIONS,
|
|
3
|
+
INTEGRATION_DEFINITION_PRESENTATIONS,
|
|
3
4
|
createPinnedIntegrationTransport,
|
|
4
5
|
integrationFacetDefinitions,
|
|
5
6
|
type IntegrationCredentialResolver,
|
|
@@ -70,6 +71,9 @@ export function registerApiIntegrationRoutes(
|
|
|
70
71
|
kind: definition.authentication.kind,
|
|
71
72
|
scopes: [...definition.authentication.scopes],
|
|
72
73
|
},
|
|
74
|
+
...(INTEGRATION_DEFINITION_PRESENTATIONS[definition.id]
|
|
75
|
+
? { presentation: INTEGRATION_DEFINITION_PRESENTATIONS[definition.id] }
|
|
76
|
+
: {}),
|
|
73
77
|
facets: definition.facets.map((facet) => ({
|
|
74
78
|
facetKey: facet.facetKey,
|
|
75
79
|
kind: facet.kind,
|
package/src/routes/api-keys.ts
CHANGED
|
@@ -36,6 +36,7 @@ export function registerApiKeyRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
36
36
|
accountId: grant.accountId,
|
|
37
37
|
workspaceId: grant.workspaceId,
|
|
38
38
|
name: body.name,
|
|
39
|
+
description: body.description ?? null,
|
|
39
40
|
prefix,
|
|
40
41
|
keyHash: await sha256Hex(token),
|
|
41
42
|
permissions,
|
|
@@ -0,0 +1,534 @@
|
|
|
1
|
+
import { environmentsEncryptionKeyBytes } from "@opengeni/config";
|
|
2
|
+
import {
|
|
3
|
+
AUTOMATION_WEBHOOK_MAX_BYTES,
|
|
4
|
+
AUTOMATION_MAX_MATCHED_TRIGGERS,
|
|
5
|
+
AutomationNormalizedEvent,
|
|
6
|
+
AutomationWebhookResult,
|
|
7
|
+
CreateAutomationSourceRequest,
|
|
8
|
+
CreateAutomationTriggerRequest,
|
|
9
|
+
TriggerAutomationManuallyRequest,
|
|
10
|
+
UpdateAutomationSourceRequest,
|
|
11
|
+
UpdateAutomationTriggerRequest,
|
|
12
|
+
stableJson,
|
|
13
|
+
} from "@opengeni/contracts";
|
|
14
|
+
import {
|
|
15
|
+
AutomationDeliveryConflictError,
|
|
16
|
+
AutomationRevisionConflictError,
|
|
17
|
+
createAutomationRun,
|
|
18
|
+
createAutomationSource,
|
|
19
|
+
createAutomationTrigger,
|
|
20
|
+
decryptVariableSetValue,
|
|
21
|
+
encryptVariableSetValue,
|
|
22
|
+
getAutomationSourceSecret,
|
|
23
|
+
getAutomationTriggerRevisions,
|
|
24
|
+
listActiveAutomationTriggersForSource,
|
|
25
|
+
listAutomationRuns,
|
|
26
|
+
listAutomationSources,
|
|
27
|
+
listAutomationTriggers,
|
|
28
|
+
recordAutomationEvent,
|
|
29
|
+
resolveAutomationWebhookEndpoint,
|
|
30
|
+
updateAutomationSource,
|
|
31
|
+
updateAutomationTrigger,
|
|
32
|
+
type AutomationSourceSecret,
|
|
33
|
+
} from "@opengeni/db";
|
|
34
|
+
import {
|
|
35
|
+
automationRequestDigest,
|
|
36
|
+
buildAutomationAcceptedExecution,
|
|
37
|
+
requireAccessGrant,
|
|
38
|
+
requireAutomationAdapter,
|
|
39
|
+
requirePermission,
|
|
40
|
+
type ApiRouteDeps,
|
|
41
|
+
} from "@opengeni/core";
|
|
42
|
+
import type { Hono } from "hono";
|
|
43
|
+
import { HTTPException } from "hono/http-exception";
|
|
44
|
+
|
|
45
|
+
export function registerAutomationRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
46
|
+
app.get("/v1/workspaces/:workspaceId/automations/sources", async (c) => {
|
|
47
|
+
const workspaceId = c.req.param("workspaceId");
|
|
48
|
+
await requireAccessGrant(c, deps, workspaceId, "workspace:read");
|
|
49
|
+
return c.json({
|
|
50
|
+
sources: await listAutomationSources(deps.db, workspaceId),
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
app.post("/v1/workspaces/:workspaceId/automations/sources", async (c) => {
|
|
55
|
+
const workspaceId = c.req.param("workspaceId");
|
|
56
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
|
|
57
|
+
requirePermission(grant, "secrets:write");
|
|
58
|
+
const request = CreateAutomationSourceRequest.parse(await c.req.json());
|
|
59
|
+
requireAutomationAdapter(request.adapterId).validateSourceConfiguration(request.configuration);
|
|
60
|
+
const key = requireEncryptionKey(deps);
|
|
61
|
+
const source = await createAutomationSource(deps.db, {
|
|
62
|
+
accountId: grant.accountId,
|
|
63
|
+
workspaceId,
|
|
64
|
+
createdBySubjectId: grant.subjectId,
|
|
65
|
+
request,
|
|
66
|
+
webhookSecretEncrypted: encryptVariableSetValue(key, request.webhookSecret),
|
|
67
|
+
});
|
|
68
|
+
return c.json(source, 201);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
app.patch("/v1/workspaces/:workspaceId/automations/sources/:sourceId", async (c) => {
|
|
72
|
+
const workspaceId = c.req.param("workspaceId");
|
|
73
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
|
|
74
|
+
const request = UpdateAutomationSourceRequest.parse(await c.req.json());
|
|
75
|
+
const current = await requireSource(
|
|
76
|
+
deps,
|
|
77
|
+
grant.accountId,
|
|
78
|
+
workspaceId,
|
|
79
|
+
c.req.param("sourceId"),
|
|
80
|
+
);
|
|
81
|
+
assertGenericSourceMutable(current);
|
|
82
|
+
if (request.configuration) {
|
|
83
|
+
requireAutomationAdapter(current.adapterId).validateSourceConfiguration(
|
|
84
|
+
request.configuration,
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
let webhookSecretEncrypted: string | undefined;
|
|
88
|
+
if (request.webhookSecret !== undefined) {
|
|
89
|
+
requirePermission(grant, "secrets:write");
|
|
90
|
+
webhookSecretEncrypted = encryptVariableSetValue(
|
|
91
|
+
requireEncryptionKey(deps),
|
|
92
|
+
request.webhookSecret,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
const source = await updateAutomationSource(deps.db, {
|
|
96
|
+
workspaceId,
|
|
97
|
+
sourceId: current.id,
|
|
98
|
+
request,
|
|
99
|
+
...(webhookSecretEncrypted ? { webhookSecretEncrypted } : {}),
|
|
100
|
+
});
|
|
101
|
+
if (!source)
|
|
102
|
+
throw new HTTPException(404, {
|
|
103
|
+
message: "automation source not found",
|
|
104
|
+
});
|
|
105
|
+
return c.json(source);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
app.delete("/v1/workspaces/:workspaceId/automations/sources/:sourceId", async (c) => {
|
|
109
|
+
const workspaceId = c.req.param("workspaceId");
|
|
110
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
|
|
111
|
+
const current = await requireSource(
|
|
112
|
+
deps,
|
|
113
|
+
grant.accountId,
|
|
114
|
+
workspaceId,
|
|
115
|
+
c.req.param("sourceId"),
|
|
116
|
+
);
|
|
117
|
+
assertGenericSourceMutable(current);
|
|
118
|
+
const source = await updateAutomationSource(deps.db, {
|
|
119
|
+
workspaceId,
|
|
120
|
+
sourceId: c.req.param("sourceId"),
|
|
121
|
+
request: { status: "disabled" },
|
|
122
|
+
});
|
|
123
|
+
if (!source)
|
|
124
|
+
throw new HTTPException(404, {
|
|
125
|
+
message: "automation source not found",
|
|
126
|
+
});
|
|
127
|
+
return c.body(null, 204);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
app.get("/v1/workspaces/:workspaceId/automations/triggers", async (c) => {
|
|
131
|
+
const workspaceId = c.req.param("workspaceId");
|
|
132
|
+
await requireAccessGrant(c, deps, workspaceId, "workspace:read");
|
|
133
|
+
return c.json({
|
|
134
|
+
triggers: await listAutomationTriggers(deps.db, workspaceId),
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
app.post("/v1/workspaces/:workspaceId/automations/triggers", async (c) => {
|
|
139
|
+
const workspaceId = c.req.param("workspaceId");
|
|
140
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
|
|
141
|
+
const request = CreateAutomationTriggerRequest.parse(await c.req.json());
|
|
142
|
+
if (request.packInstallationId || request.packTemplateId) {
|
|
143
|
+
throw new HTTPException(409, {
|
|
144
|
+
message: "Pack-owned automation triggers must be created through their Pack setup API",
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
const source = await requireSource(deps, grant.accountId, workspaceId, request.sourceId);
|
|
148
|
+
assertGenericSourceMutable(source);
|
|
149
|
+
if (source.status !== "active") {
|
|
150
|
+
throw new HTTPException(409, {
|
|
151
|
+
message: "automation source is disabled",
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
const adapter = requireAutomationAdapter(source.adapterId);
|
|
155
|
+
adapter.validateTriggerConfiguration(request.configuration);
|
|
156
|
+
adapter.validateTriggerParameters(request.parameters);
|
|
157
|
+
for (const permission of request.sessionTemplate.firstPartyMcpPermissions) {
|
|
158
|
+
requirePermission(grant, permission);
|
|
159
|
+
}
|
|
160
|
+
return c.json(
|
|
161
|
+
await createAutomationTrigger(deps.db, {
|
|
162
|
+
accountId: grant.accountId,
|
|
163
|
+
workspaceId,
|
|
164
|
+
createdBySubjectId: grant.subjectId,
|
|
165
|
+
request,
|
|
166
|
+
adapterId: adapter.id,
|
|
167
|
+
}),
|
|
168
|
+
201,
|
|
169
|
+
);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
app.patch("/v1/workspaces/:workspaceId/automations/triggers/:triggerId", async (c) => {
|
|
173
|
+
const workspaceId = c.req.param("workspaceId");
|
|
174
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
|
|
175
|
+
const request = UpdateAutomationTriggerRequest.parse(await c.req.json());
|
|
176
|
+
const existing = (await listAutomationTriggers(deps.db, workspaceId)).find(
|
|
177
|
+
(trigger) => trigger.id === c.req.param("triggerId"),
|
|
178
|
+
);
|
|
179
|
+
if (!existing)
|
|
180
|
+
throw new HTTPException(404, {
|
|
181
|
+
message: "automation trigger not found",
|
|
182
|
+
});
|
|
183
|
+
assertGenericTriggerMutable(existing);
|
|
184
|
+
if (request.configuration) {
|
|
185
|
+
requireAutomationAdapter(existing.adapterId).validateTriggerConfiguration(
|
|
186
|
+
request.configuration,
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
if (request.parameters) {
|
|
190
|
+
requireAutomationAdapter(existing.adapterId).validateTriggerParameters(request.parameters);
|
|
191
|
+
}
|
|
192
|
+
if (request.sessionTemplate) {
|
|
193
|
+
for (const permission of request.sessionTemplate.firstPartyMcpPermissions) {
|
|
194
|
+
requirePermission(grant, permission);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
try {
|
|
198
|
+
const trigger = await updateAutomationTrigger(deps.db, {
|
|
199
|
+
workspaceId,
|
|
200
|
+
triggerId: c.req.param("triggerId"),
|
|
201
|
+
subjectId: grant.subjectId,
|
|
202
|
+
request,
|
|
203
|
+
});
|
|
204
|
+
if (!trigger)
|
|
205
|
+
throw new HTTPException(404, {
|
|
206
|
+
message: "automation trigger not found",
|
|
207
|
+
});
|
|
208
|
+
return c.json(trigger);
|
|
209
|
+
} catch (error) {
|
|
210
|
+
if (error instanceof AutomationRevisionConflictError) {
|
|
211
|
+
throw new HTTPException(409, { message: error.message });
|
|
212
|
+
}
|
|
213
|
+
throw error;
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
app.delete("/v1/workspaces/:workspaceId/automations/triggers/:triggerId", async (c) => {
|
|
218
|
+
const workspaceId = c.req.param("workspaceId");
|
|
219
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
|
|
220
|
+
const expectedRevision = Number(c.req.query("expectedRevision"));
|
|
221
|
+
if (!Number.isSafeInteger(expectedRevision) || expectedRevision < 1) {
|
|
222
|
+
throw new HTTPException(400, {
|
|
223
|
+
message: "expectedRevision must be a positive integer",
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
const existing = (await listAutomationTriggers(deps.db, workspaceId)).find(
|
|
227
|
+
(trigger) => trigger.id === c.req.param("triggerId"),
|
|
228
|
+
);
|
|
229
|
+
if (!existing)
|
|
230
|
+
throw new HTTPException(404, {
|
|
231
|
+
message: "automation trigger not found",
|
|
232
|
+
});
|
|
233
|
+
assertGenericTriggerMutable(existing);
|
|
234
|
+
try {
|
|
235
|
+
const trigger = await updateAutomationTrigger(deps.db, {
|
|
236
|
+
workspaceId,
|
|
237
|
+
triggerId: c.req.param("triggerId"),
|
|
238
|
+
subjectId: grant.subjectId,
|
|
239
|
+
request: { expectedRevision, status: "disabled" },
|
|
240
|
+
});
|
|
241
|
+
if (!trigger)
|
|
242
|
+
throw new HTTPException(404, {
|
|
243
|
+
message: "automation trigger not found",
|
|
244
|
+
});
|
|
245
|
+
return c.body(null, 204);
|
|
246
|
+
} catch (error) {
|
|
247
|
+
if (error instanceof AutomationRevisionConflictError) {
|
|
248
|
+
throw new HTTPException(409, { message: error.message });
|
|
249
|
+
}
|
|
250
|
+
throw error;
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
app.get("/v1/workspaces/:workspaceId/automations/runs", async (c) => {
|
|
255
|
+
const workspaceId = c.req.param("workspaceId");
|
|
256
|
+
await requireAccessGrant(c, deps, workspaceId, "workspace:read");
|
|
257
|
+
return c.json({ runs: await listAutomationRuns(deps.db, workspaceId) });
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
app.post("/v1/workspaces/:workspaceId/automations/sources/:sourceId/events", async (c) => {
|
|
261
|
+
const workspaceId = c.req.param("workspaceId");
|
|
262
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
|
|
263
|
+
const request = TriggerAutomationManuallyRequest.parse(await c.req.json());
|
|
264
|
+
const source = await requireSource(deps, grant.accountId, workspaceId, c.req.param("sourceId"));
|
|
265
|
+
if (source.status !== "active") {
|
|
266
|
+
throw new HTTPException(409, {
|
|
267
|
+
message: "automation source is disabled",
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
const normalizedEvent = AutomationNormalizedEvent.parse({
|
|
271
|
+
adapterId: source.adapterId,
|
|
272
|
+
eventType: request.eventType,
|
|
273
|
+
occurrenceKey: request.occurrenceKey,
|
|
274
|
+
occurredAt: request.occurredAt,
|
|
275
|
+
subject: request.subject,
|
|
276
|
+
resource: request.resource,
|
|
277
|
+
payload: request.payload,
|
|
278
|
+
});
|
|
279
|
+
const bytes = new TextEncoder().encode(stableJson(request));
|
|
280
|
+
return c.json(
|
|
281
|
+
await acceptEvent(deps, source, {
|
|
282
|
+
deliveryKey:
|
|
283
|
+
request.deliveryId ?? `manual:${automationRequestDigest(source.adapterId, bytes)}`,
|
|
284
|
+
requestDigest: automationRequestDigest(source.adapterId, bytes),
|
|
285
|
+
normalizedEvent,
|
|
286
|
+
}),
|
|
287
|
+
202,
|
|
288
|
+
);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
app.post("/v1/webhooks/automations/:endpointId", async (c) => {
|
|
292
|
+
const endpointId = c.req.param("endpointId");
|
|
293
|
+
if (
|
|
294
|
+
!/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu.test(
|
|
295
|
+
endpointId,
|
|
296
|
+
)
|
|
297
|
+
) {
|
|
298
|
+
throw new HTTPException(404, {
|
|
299
|
+
message: "automation endpoint not found",
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
const endpoint = await resolveAutomationWebhookEndpoint(deps.db, endpointId);
|
|
303
|
+
if (!endpoint)
|
|
304
|
+
throw new HTTPException(404, {
|
|
305
|
+
message: "automation endpoint not found",
|
|
306
|
+
});
|
|
307
|
+
const source = await requireSource(
|
|
308
|
+
deps,
|
|
309
|
+
endpoint.accountId,
|
|
310
|
+
endpoint.workspaceId,
|
|
311
|
+
endpoint.sourceId,
|
|
312
|
+
);
|
|
313
|
+
if (source.status !== "active") {
|
|
314
|
+
throw new HTTPException(410, {
|
|
315
|
+
message: "automation source is disabled",
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
const rawBody = await readAutomationWebhookBody(c.req.raw, AUTOMATION_WEBHOOK_MAX_BYTES);
|
|
319
|
+
const adapter = requireAutomationAdapter(source.adapterId);
|
|
320
|
+
const secret = decryptVariableSetValue(
|
|
321
|
+
requireEncryptionKey(deps),
|
|
322
|
+
source.webhookSecretEncrypted,
|
|
323
|
+
);
|
|
324
|
+
if (
|
|
325
|
+
!adapter.verify({
|
|
326
|
+
rawBody,
|
|
327
|
+
headers: c.req.raw.headers,
|
|
328
|
+
secret,
|
|
329
|
+
sourceConfiguration: source.configuration,
|
|
330
|
+
})
|
|
331
|
+
) {
|
|
332
|
+
throw new HTTPException(401, {
|
|
333
|
+
message: "automation signature is invalid",
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
const requestDigest = automationRequestDigest(source.adapterId, rawBody);
|
|
337
|
+
try {
|
|
338
|
+
const result = await acceptEvent(deps, source, {
|
|
339
|
+
deliveryKey: adapter.deliveryKey({
|
|
340
|
+
headers: c.req.raw.headers,
|
|
341
|
+
requestDigest,
|
|
342
|
+
}),
|
|
343
|
+
requestDigest,
|
|
344
|
+
normalizedEvent: adapter.normalize({
|
|
345
|
+
rawBody,
|
|
346
|
+
headers: c.req.raw.headers,
|
|
347
|
+
sourceConfiguration: source.configuration,
|
|
348
|
+
}),
|
|
349
|
+
});
|
|
350
|
+
return c.json(result, 202);
|
|
351
|
+
} catch (error) {
|
|
352
|
+
if (error instanceof AutomationDeliveryConflictError) {
|
|
353
|
+
throw new HTTPException(409, { message: error.message });
|
|
354
|
+
}
|
|
355
|
+
throw error;
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
async function acceptEvent(
|
|
361
|
+
deps: ApiRouteDeps,
|
|
362
|
+
source: AutomationSourceSecret,
|
|
363
|
+
input: {
|
|
364
|
+
deliveryKey: string;
|
|
365
|
+
requestDigest: string;
|
|
366
|
+
normalizedEvent: AutomationNormalizedEvent;
|
|
367
|
+
},
|
|
368
|
+
) {
|
|
369
|
+
if (input.normalizedEvent.adapterId !== source.adapterId) {
|
|
370
|
+
throw new HTTPException(422, {
|
|
371
|
+
message: "automation event adapter does not match source",
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
const adapter = requireAutomationAdapter(source.adapterId);
|
|
375
|
+
const triggers = await listActiveAutomationTriggersForSource(deps.db, {
|
|
376
|
+
accountId: source.accountId,
|
|
377
|
+
workspaceId: source.workspaceId,
|
|
378
|
+
sourceId: source.id,
|
|
379
|
+
});
|
|
380
|
+
const matchingAtAcceptance = triggers.filter((trigger) =>
|
|
381
|
+
adapter.matches({ event: input.normalizedEvent, trigger }),
|
|
382
|
+
);
|
|
383
|
+
if (matchingAtAcceptance.length > AUTOMATION_MAX_MATCHED_TRIGGERS) {
|
|
384
|
+
throw new HTTPException(422, {
|
|
385
|
+
message: `automation event matches more than ${AUTOMATION_MAX_MATCHED_TRIGGERS} triggers`,
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
const stored = await recordAutomationEvent(deps.db, {
|
|
389
|
+
accountId: source.accountId,
|
|
390
|
+
workspaceId: source.workspaceId,
|
|
391
|
+
sourceId: source.id,
|
|
392
|
+
sourceVersion: source.version,
|
|
393
|
+
sourceConfiguration: source.configuration,
|
|
394
|
+
matchedTriggerRevisions: matchingAtAcceptance.map((trigger) => ({
|
|
395
|
+
triggerId: trigger.id,
|
|
396
|
+
revision: trigger.revision,
|
|
397
|
+
})),
|
|
398
|
+
deliveryKey: input.deliveryKey,
|
|
399
|
+
requestDigest: input.requestDigest,
|
|
400
|
+
normalizedEvent: input.normalizedEvent,
|
|
401
|
+
ignoredReason: matchingAtAcceptance.length === 0 ? "no_matching_triggers" : null,
|
|
402
|
+
});
|
|
403
|
+
const matching = await getAutomationTriggerRevisions(deps.db, {
|
|
404
|
+
workspaceId: source.workspaceId,
|
|
405
|
+
refs: stored.event.matchedTriggerRevisions,
|
|
406
|
+
});
|
|
407
|
+
const acceptedSource = {
|
|
408
|
+
...source,
|
|
409
|
+
version: stored.event.sourceVersion,
|
|
410
|
+
configuration: stored.event.sourceConfiguration,
|
|
411
|
+
};
|
|
412
|
+
const runIds: string[] = [];
|
|
413
|
+
const triggerAutomationRun = deps.workflowClient.triggerAutomationRun;
|
|
414
|
+
if (matching.length > 0 && !triggerAutomationRun) {
|
|
415
|
+
throw new HTTPException(503, {
|
|
416
|
+
message: "automation dispatcher is unavailable",
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
for (const trigger of matching) {
|
|
420
|
+
const render = adapter.render({
|
|
421
|
+
event: stored.event.normalizedEvent,
|
|
422
|
+
trigger,
|
|
423
|
+
source: acceptedSource,
|
|
424
|
+
});
|
|
425
|
+
const acceptedExecution = buildAutomationAcceptedExecution({
|
|
426
|
+
accountId: source.accountId,
|
|
427
|
+
workspaceId: source.workspaceId,
|
|
428
|
+
source: acceptedSource,
|
|
429
|
+
trigger,
|
|
430
|
+
eventId: stored.event.id,
|
|
431
|
+
event: stored.event.normalizedEvent,
|
|
432
|
+
render,
|
|
433
|
+
});
|
|
434
|
+
const { run } = await createAutomationRun(deps.db, {
|
|
435
|
+
accountId: source.accountId,
|
|
436
|
+
workspaceId: source.workspaceId,
|
|
437
|
+
sourceId: source.id,
|
|
438
|
+
triggerId: trigger.id,
|
|
439
|
+
triggerRevision: trigger.revision,
|
|
440
|
+
eventId: stored.event.id,
|
|
441
|
+
occurrenceKey: stored.event.normalizedEvent.occurrenceKey,
|
|
442
|
+
acceptedExecution,
|
|
443
|
+
});
|
|
444
|
+
runIds.push(run.id);
|
|
445
|
+
await triggerAutomationRun!({
|
|
446
|
+
accountId: source.accountId,
|
|
447
|
+
workspaceId: source.workspaceId,
|
|
448
|
+
runId: run.id,
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
return AutomationWebhookResult.parse({
|
|
452
|
+
accepted: true,
|
|
453
|
+
duplicate: stored.duplicate,
|
|
454
|
+
ignoredReason: stored.event.ignoredReason,
|
|
455
|
+
eventId: stored.event.id,
|
|
456
|
+
runIds,
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
async function requireSource(
|
|
461
|
+
deps: ApiRouteDeps,
|
|
462
|
+
accountId: string,
|
|
463
|
+
workspaceId: string,
|
|
464
|
+
sourceId: string,
|
|
465
|
+
): Promise<AutomationSourceSecret> {
|
|
466
|
+
const source = await getAutomationSourceSecret(deps.db, {
|
|
467
|
+
accountId,
|
|
468
|
+
workspaceId,
|
|
469
|
+
sourceId,
|
|
470
|
+
});
|
|
471
|
+
if (!source) throw new HTTPException(404, { message: "automation source not found" });
|
|
472
|
+
return source;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function assertGenericSourceMutable(source: AutomationSourceSecret): void {
|
|
476
|
+
if (source.packInstallationId) {
|
|
477
|
+
throw new HTTPException(409, {
|
|
478
|
+
message: "Pack-owned automation sources must be managed through their Pack setup API",
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function assertGenericTriggerMutable(trigger: { packInstallationId: string | null }): void {
|
|
484
|
+
if (trigger.packInstallationId) {
|
|
485
|
+
throw new HTTPException(409, {
|
|
486
|
+
message: "Pack-owned automation triggers must be managed through their Pack setup API",
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function requireEncryptionKey(deps: ApiRouteDeps): Uint8Array {
|
|
492
|
+
const key = environmentsEncryptionKeyBytes(deps.settings);
|
|
493
|
+
if (!key) {
|
|
494
|
+
throw new HTTPException(503, {
|
|
495
|
+
message: "OPENGENI_ENVIRONMENTS_ENCRYPTION_KEY is required for automations",
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
return key;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
export async function readAutomationWebhookBody(
|
|
502
|
+
request: Request,
|
|
503
|
+
maxBytes: number,
|
|
504
|
+
): Promise<Uint8Array> {
|
|
505
|
+
const declared = Number(request.headers.get("content-length"));
|
|
506
|
+
if (Number.isFinite(declared) && declared > maxBytes) {
|
|
507
|
+
throw new HTTPException(413, {
|
|
508
|
+
message: "automation webhook payload is too large",
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
if (!request.body) return new Uint8Array();
|
|
512
|
+
const reader = request.body.getReader();
|
|
513
|
+
const chunks: Uint8Array[] = [];
|
|
514
|
+
let length = 0;
|
|
515
|
+
while (true) {
|
|
516
|
+
const { value, done } = await reader.read();
|
|
517
|
+
if (done) break;
|
|
518
|
+
length += value.byteLength;
|
|
519
|
+
if (length > maxBytes) {
|
|
520
|
+
await reader.cancel();
|
|
521
|
+
throw new HTTPException(413, {
|
|
522
|
+
message: "automation webhook payload is too large",
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
chunks.push(value);
|
|
526
|
+
}
|
|
527
|
+
const result = new Uint8Array(length);
|
|
528
|
+
let offset = 0;
|
|
529
|
+
for (const chunk of chunks) {
|
|
530
|
+
result.set(chunk, offset);
|
|
531
|
+
offset += chunk.byteLength;
|
|
532
|
+
}
|
|
533
|
+
return result;
|
|
534
|
+
}
|