@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.
Files changed (165) hide show
  1. package/dist/api-websocket.d.ts +18 -0
  2. package/dist/app.js +1 -1
  3. package/dist/auth/managed-auth.d.ts +6 -0
  4. package/dist/{chunk-WCXHI3FF.js → chunk-3TP54PPX.js} +26692 -15389
  5. package/dist/chunk-3TP54PPX.js.map +1 -0
  6. package/dist/codemode.d.ts +8 -1
  7. package/dist/company-brain-okf.d.ts +24 -0
  8. package/dist/connected-machine-computer-access.d.ts +11 -0
  9. package/dist/connection-authority-owner.d.ts +59 -0
  10. package/dist/connection-ownership.d.ts +47 -0
  11. package/dist/controller-data-plane.d.ts +16 -0
  12. package/dist/editable-artifact-websocket.d.ts +5 -15
  13. package/dist/editable-artifact-workspace-files.d.ts +1 -1
  14. package/dist/http/api-error.d.ts +10 -0
  15. package/dist/http/cors.d.ts +5 -0
  16. package/dist/http/interaction-control-error.d.ts +13 -0
  17. package/dist/http/sse.d.ts +2 -3
  18. package/dist/index.d.ts +5 -2
  19. package/dist/index.js +326 -82
  20. package/dist/index.js.map +1 -1
  21. package/dist/integrations/atlassian.d.ts +17 -0
  22. package/dist/integrations/fiken.d.ts +7 -2
  23. package/dist/integrations/google-drive.d.ts +18 -0
  24. package/dist/integrations/oauth-client.d.ts +22 -3
  25. package/dist/integrations/oauth-profiles.d.ts +195 -0
  26. package/dist/integrations/personal-github-repositories.d.ts +45 -0
  27. package/dist/integrations/personal-github.d.ts +40 -0
  28. package/dist/integrations/pr-review-provider.d.ts +22 -0
  29. package/dist/integrations/provider-oauth.d.ts +8 -0
  30. package/dist/integrations/slack-app-home.d.ts +24 -0
  31. package/dist/integrations/slack-bot.d.ts +157 -9
  32. package/dist/integrations/slack-interactions.d.ts +39 -5
  33. package/dist/integrations/slack-routing.d.ts +120 -0
  34. package/dist/integrations/social-oauth.d.ts +5 -0
  35. package/dist/interaction-frame-proxy.d.ts +68 -0
  36. package/dist/mcp/company-brain-governed-writes.d.ts +26 -0
  37. package/dist/mcp/company-profile-agent-admin.d.ts +44 -0
  38. package/dist/mcp/documents.d.ts +1 -0
  39. package/dist/mcp/remember.d.ts +28 -0
  40. package/dist/mcp/request-abort.d.ts +19 -0
  41. package/dist/mcp/scheduled-task-view.d.ts +4 -4
  42. package/dist/mcp/server.d.ts +15 -2
  43. package/dist/mcp/session-view.d.ts +4 -0
  44. package/dist/mcp/session-wait.d.ts +137 -0
  45. package/dist/routes/automations.d.ts +4 -0
  46. package/dist/routes/billing.d.ts +5 -0
  47. package/dist/routes/browser-sessions.d.ts +4 -1
  48. package/dist/routes/company-brain.d.ts +3 -0
  49. package/dist/routes/company-profile.d.ts +2 -1
  50. package/dist/routes/connection-authorities.d.ts +3 -0
  51. package/dist/routes/personal-github-git-broker.d.ts +29 -0
  52. package/dist/routes/personal-github.d.ts +3 -0
  53. package/dist/routes/pr-review.d.ts +3 -0
  54. package/dist/routes/sessions.d.ts +1 -0
  55. package/dist/routes/user-resource-authorities.d.ts +3 -0
  56. package/dist/routes/workspace-capture.d.ts +10 -2
  57. package/dist/routes/workspace-learning.d.ts +3 -0
  58. package/dist/routes/workspaces.d.ts +19 -0
  59. package/dist/sandbox/channel-a.d.ts +11 -1
  60. package/dist/sandbox/connection-authority.d.ts +3 -0
  61. package/dist/sandbox/enrollment.d.ts +2 -0
  62. package/dist/sandbox/machines.d.ts +2 -2
  63. package/dist/sandbox/metrics-ingestion.d.ts +10 -20
  64. package/dist/sandbox/viewer.d.ts +24 -1
  65. package/dist/sandbox-file-artifacts.d.ts +11 -0
  66. package/dist/scheduled-task-deletion.d.ts +14 -0
  67. package/dist/slack-reaction-files.d.ts +1 -1
  68. package/dist/temporal-schedule-cleanup.d.ts +1 -0
  69. package/package.json +20 -18
  70. package/src/api-websocket.ts +23 -0
  71. package/src/app.ts +161 -23
  72. package/src/auth/managed-auth.ts +25 -3
  73. package/src/codemode.ts +53 -23
  74. package/src/codex-realtime.ts +8 -2
  75. package/src/company-brain-okf.ts +340 -0
  76. package/src/connected-machine-computer-access.ts +33 -0
  77. package/src/connection-authority-owner.ts +61 -0
  78. package/src/connection-ownership.ts +180 -0
  79. package/src/controller-data-plane.ts +47 -0
  80. package/src/editable-artifact-native-kernel.ts +26 -15
  81. package/src/editable-artifact-office-import.ts +28 -3
  82. package/src/editable-artifact-production.ts +13 -7
  83. package/src/editable-artifact-websocket.ts +11 -18
  84. package/src/editable-artifact-workspace-files.ts +31 -6
  85. package/src/http/api-error.ts +28 -0
  86. package/src/http/auth.ts +4 -0
  87. package/src/http/cors.ts +35 -0
  88. package/src/http/interaction-control-error.ts +164 -0
  89. package/src/http/sse.ts +112 -66
  90. package/src/index.ts +46 -6
  91. package/src/integrations/atlassian.ts +146 -35
  92. package/src/integrations/fiken.ts +102 -22
  93. package/src/integrations/google-drive.ts +301 -92
  94. package/src/integrations/oauth-client.ts +272 -143
  95. package/src/integrations/oauth-profiles.ts +477 -0
  96. package/src/integrations/personal-github-repositories.ts +445 -0
  97. package/src/integrations/personal-github.ts +705 -0
  98. package/src/integrations/pr-review-provider.ts +246 -0
  99. package/src/integrations/provider-oauth.ts +121 -5
  100. package/src/integrations/slack-app-home.ts +300 -0
  101. package/src/integrations/slack-bot.ts +342 -57
  102. package/src/integrations/slack-interactions.ts +1898 -259
  103. package/src/integrations/slack-routing.ts +337 -0
  104. package/src/integrations/social-oauth.ts +25 -0
  105. package/src/interaction-frame-proxy.ts +409 -0
  106. package/src/mcp/company-brain-governed-writes.ts +262 -0
  107. package/src/mcp/company-profile-agent-admin.ts +205 -0
  108. package/src/mcp/documents.ts +37 -6
  109. package/src/mcp/files.ts +16 -1
  110. package/src/mcp/remember.ts +181 -0
  111. package/src/mcp/request-abort.ts +44 -0
  112. package/src/mcp/scheduled-task-view.ts +1 -0
  113. package/src/mcp/server.ts +719 -369
  114. package/src/mcp/session-view.ts +54 -0
  115. package/src/mcp/session-wait.ts +554 -0
  116. package/src/model-catalog.ts +20 -12
  117. package/src/routes/api-integrations.ts +4 -0
  118. package/src/routes/api-keys.ts +1 -0
  119. package/src/routes/automations.ts +534 -0
  120. package/src/routes/billing.ts +57 -1
  121. package/src/routes/browser-sessions.ts +329 -79
  122. package/src/routes/channels.ts +17 -1
  123. package/src/routes/codex.ts +50 -0
  124. package/src/routes/company-brain.ts +367 -0
  125. package/src/routes/company-profile.ts +1 -1
  126. package/src/routes/computer-sessions.ts +320 -93
  127. package/src/routes/connection-authorities.ts +139 -0
  128. package/src/routes/connections.ts +144 -34
  129. package/src/routes/documents.ts +353 -4
  130. package/src/routes/editable-artifacts.ts +11 -3
  131. package/src/routes/enrollments.ts +116 -26
  132. package/src/routes/environments.ts +127 -55
  133. package/src/routes/files.ts +72 -11
  134. package/src/routes/install.ts +82 -28
  135. package/src/routes/integration-facets.ts +29 -0
  136. package/src/routes/interaction-resources.ts +20 -2
  137. package/src/routes/machines.ts +275 -13
  138. package/src/routes/organization-memberships.ts +717 -28
  139. package/src/routes/packs.ts +4 -3
  140. package/src/routes/personal-github-git-broker.ts +785 -0
  141. package/src/routes/personal-github.ts +319 -0
  142. package/src/routes/pr-review.ts +531 -0
  143. package/src/routes/rigs.ts +104 -39
  144. package/src/routes/scheduled-tasks.ts +19 -21
  145. package/src/routes/sessions.ts +775 -38
  146. package/src/routes/social.ts +14 -2
  147. package/src/routes/supergrok.ts +23 -4
  148. package/src/routes/transcription-recordings.ts +8 -2
  149. package/src/routes/user-resource-authorities.ts +138 -0
  150. package/src/routes/workspace-artifacts.ts +4 -1
  151. package/src/routes/workspace-capture.ts +21 -0
  152. package/src/routes/workspace-learning.ts +228 -0
  153. package/src/routes/workspaces.ts +59 -6
  154. package/src/sandbox/auth-callout.ts +38 -12
  155. package/src/sandbox/channel-a.ts +242 -19
  156. package/src/sandbox/connection-authority.ts +3 -0
  157. package/src/sandbox/enrollment.ts +15 -3
  158. package/src/sandbox/machines.ts +186 -64
  159. package/src/sandbox/metrics-ingestion.ts +220 -58
  160. package/src/sandbox/viewer.ts +243 -19
  161. package/src/sandbox-file-artifacts.ts +329 -0
  162. package/src/scheduled-task-deletion.ts +127 -0
  163. package/src/slack-reaction-files.ts +16 -5
  164. package/src/temporal-schedule-cleanup.ts +13 -0
  165. package/dist/chunk-WCXHI3FF.js.map +0 -1
@@ -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
- : "opengeni";
47
- const publicProvider =
48
- source === "codex"
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
- policy: WorkspaceModelPolicyContract | null;
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
- policy: input.policy,
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,
@@ -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
+ }