@opengeni/api-router 2.4.2-canary.0 → 2.6.3-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/app.js +1 -1
- package/dist/auth/managed-auth-attempt-context.d.ts +5 -1
- package/dist/auth/managed-auth.d.ts +24 -1
- package/dist/{chunk-L7GVVSSQ.js → chunk-XTLI3CBH.js} +2884 -483
- package/dist/chunk-XTLI3CBH.js.map +1 -0
- package/dist/http/sse.d.ts +9 -0
- package/dist/index.js +104 -4
- package/dist/index.js.map +1 -1
- package/dist/integrations/slack-interactions.d.ts +1 -1
- package/dist/interaction-metrics.d.ts +2 -0
- package/dist/mcp/company-brain-governed-writes.d.ts +1 -1
- package/dist/mcp/company-profile-agent-admin.d.ts +6 -6
- package/dist/mcp/remember.d.ts +2 -2
- package/dist/mcp/server.d.ts +34 -0
- package/dist/mcp/session-view.d.ts +1 -0
- package/dist/mcp/session-wait.d.ts +19 -0
- package/dist/routes/api-keys.d.ts +2 -0
- package/dist/routes/browser-sessions.d.ts +1 -0
- package/dist/routes/computer-sessions.d.ts +12 -0
- package/dist/routes/managed-auth-session-sets.d.ts +7 -0
- package/dist/routes/sessions.d.ts +17 -5
- package/dist/routes/workspaces.d.ts +1 -1
- package/dist/sandbox/metrics-ingestion.d.ts +16 -0
- package/dist/work-discovery-observability.d.ts +33 -0
- package/dist/workspace-delete-observability.d.ts +10 -0
- package/package.json +19 -19
- package/src/app.ts +172 -20
- package/src/auth/managed-auth-attempt-context.ts +40 -3
- package/src/auth/managed-auth-session-adapter.ts +1 -0
- package/src/auth/managed-auth.ts +164 -4
- package/src/http/sse.ts +279 -45
- package/src/integrations/oauth-client.ts +8 -1
- package/src/integrations/provider-oauth.ts +12 -2
- package/src/interaction-metrics.ts +30 -0
- package/src/mcp/company-brain-governed-writes.ts +38 -23
- package/src/mcp/company-profile-agent-admin.ts +7 -7
- package/src/mcp/remember.ts +19 -8
- package/src/mcp/server.ts +621 -54
- package/src/mcp/session-wait.ts +56 -8
- package/src/routes/api-integrations.ts +2 -2
- package/src/routes/api-keys.ts +149 -7
- package/src/routes/browser-sessions.ts +10 -2
- package/src/routes/capabilities.ts +3 -3
- package/src/routes/codex.ts +483 -74
- package/src/routes/company-profile.ts +64 -0
- package/src/routes/computer-sessions.ts +103 -1
- package/src/routes/integration-facets.ts +8 -5
- package/src/routes/interaction-resources.ts +7 -1
- package/src/routes/managed-auth-session-sets.ts +199 -2
- package/src/routes/organization-memberships.ts +28 -8
- package/src/routes/packs.ts +5 -5
- package/src/routes/plugins.ts +2 -2
- package/src/routes/scheduled-tasks.ts +9 -0
- package/src/routes/sessions.ts +380 -87
- package/src/routes/skills.ts +3 -3
- package/src/routes/workspaces.ts +293 -54
- package/src/sandbox/channel-a.ts +10 -4
- package/src/sandbox/machines.ts +13 -6
- package/src/sandbox/metrics-ingestion.ts +157 -3
- package/src/sandbox/viewer.ts +20 -1
- package/src/work-discovery-observability.ts +121 -0
- package/src/workspace-delete-observability.ts +75 -0
- package/dist/chunk-L7GVVSSQ.js.map +0 -1
package/src/mcp/session-wait.ts
CHANGED
|
@@ -81,7 +81,51 @@ export const SESSION_WAIT_EVENT_TYPES = [
|
|
|
81
81
|
"goal.continuation",
|
|
82
82
|
] as const satisfies readonly SessionEventType[];
|
|
83
83
|
|
|
84
|
-
|
|
84
|
+
/**
|
|
85
|
+
* Settlement and blocking events that make a child result usable.
|
|
86
|
+
* Goal facts are deliberately absent: an agent can complete its durable goal
|
|
87
|
+
* before it emits the final assistant message and settles the turn. Completed
|
|
88
|
+
* agent messages are also absent because commentary messages use the same
|
|
89
|
+
* event type; an ordinary result-bearing `turn.completed` carries the
|
|
90
|
+
* authoritative final output.
|
|
91
|
+
*/
|
|
92
|
+
export const SESSION_WAIT_COMPLETION_EVENT_TYPES = [
|
|
93
|
+
"turn.completed",
|
|
94
|
+
"turn.failed",
|
|
95
|
+
"turn.cancelled",
|
|
96
|
+
"turn.superseded",
|
|
97
|
+
"turn.capacity_waiting",
|
|
98
|
+
"session.requiresAction",
|
|
99
|
+
"session.humanInput.requested",
|
|
100
|
+
"session.control.paused",
|
|
101
|
+
"tool.auth_needed",
|
|
102
|
+
"credential.auth_needed",
|
|
103
|
+
"rig.setup.failed",
|
|
104
|
+
"goal.paused",
|
|
105
|
+
] as const satisfies readonly SessionEventType[];
|
|
106
|
+
|
|
107
|
+
const SESSION_WAIT_COMPLETION_EVENT_TYPE_SET: ReadonlySet<string> = new Set(
|
|
108
|
+
SESSION_WAIT_COMPLETION_EVENT_TYPES,
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* A completed turn is result-bearing only when it carries the ordinary final
|
|
113
|
+
* output. Segment-limit and maintenance turns settle one execution segment
|
|
114
|
+
* while the session still has work to do, so they must not release a parent.
|
|
115
|
+
*/
|
|
116
|
+
export function sessionWaitCompletionEventMatches(event: SessionEvent): boolean {
|
|
117
|
+
if (!SESSION_WAIT_COMPLETION_EVENT_TYPE_SET.has(event.type)) return false;
|
|
118
|
+
if (event.type !== "turn.completed") return true;
|
|
119
|
+
if (event.payload === null || typeof event.payload !== "object" || Array.isArray(event.payload)) {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
const payload = event.payload as Record<string, unknown>;
|
|
123
|
+
return (
|
|
124
|
+
Object.prototype.hasOwnProperty.call(payload, "output") &&
|
|
125
|
+
!Object.prototype.hasOwnProperty.call(payload, "segmentLimit") &&
|
|
126
|
+
!Object.prototype.hasOwnProperty.call(payload, "maintenance")
|
|
127
|
+
);
|
|
128
|
+
}
|
|
85
129
|
|
|
86
130
|
/** The self-session event that announces a newly pending machine input. */
|
|
87
131
|
export const SESSION_WAIT_OWN_PENDING_EVENT_TYPE =
|
|
@@ -188,6 +232,10 @@ export type SessionWaitInput = {
|
|
|
188
232
|
targets: readonly SessionWaitTarget[];
|
|
189
233
|
ownSessionId: string | null;
|
|
190
234
|
maxWaitMs: number;
|
|
235
|
+
/** Target events that end the wait. Defaults to the ordinary activity set. */
|
|
236
|
+
targetEventTypes?: readonly SessionEventType[] | undefined;
|
|
237
|
+
/** Optional payload-aware refinement applied after the event-type filter. */
|
|
238
|
+
targetEventMatches?: ((event: SessionEvent) => boolean) | undefined;
|
|
191
239
|
source: SessionWaitSource;
|
|
192
240
|
signal?: AbortSignal | undefined;
|
|
193
241
|
now?: (() => number) | undefined;
|
|
@@ -206,6 +254,11 @@ export async function waitForSessionChanges(input: SessionWaitInput): Promise<Se
|
|
|
206
254
|
let liveFanout = true;
|
|
207
255
|
let waited = false;
|
|
208
256
|
const ownSessionId = input.source.readOwnPendingUpdateKinds ? input.ownSessionId : null;
|
|
257
|
+
const targetEventTypeSet: ReadonlySet<string> = new Set(
|
|
258
|
+
input.targetEventTypes ?? SESSION_WAIT_EVENT_TYPES,
|
|
259
|
+
);
|
|
260
|
+
const targetEventMatches = (event: SessionEvent): boolean =>
|
|
261
|
+
targetEventTypeSet.has(event.type) && (input.targetEventMatches?.(event) ?? true);
|
|
209
262
|
|
|
210
263
|
// One subscription per distinct session; a session may be both a target and
|
|
211
264
|
// the caller's own session, in which case either condition wakes the wait.
|
|
@@ -230,11 +283,7 @@ export async function waitForSessionChanges(input: SessionWaitInput): Promise<Se
|
|
|
230
283
|
const after = targetAfter.get(sessionId);
|
|
231
284
|
for (const event of events) {
|
|
232
285
|
if (event.sessionId !== sessionId) continue;
|
|
233
|
-
if (
|
|
234
|
-
after !== undefined &&
|
|
235
|
-
event.sequence > after &&
|
|
236
|
-
SESSION_WAIT_EVENT_TYPE_SET.has(event.type)
|
|
237
|
-
) {
|
|
286
|
+
if (after !== undefined && event.sequence > after && targetEventMatches(event)) {
|
|
238
287
|
return true;
|
|
239
288
|
}
|
|
240
289
|
if (sessionId === ownSessionId && event.type === SESSION_WAIT_OWN_PENDING_EVENT_TYPE) {
|
|
@@ -274,8 +323,7 @@ export async function waitForSessionChanges(input: SessionWaitInput): Promise<Se
|
|
|
274
323
|
const changed: SessionWaitTargetResult[] = [];
|
|
275
324
|
for (const { target, read } of targetReads) {
|
|
276
325
|
const events = read.events.filter(
|
|
277
|
-
(event) =>
|
|
278
|
-
event.sequence > target.afterSequence && SESSION_WAIT_EVENT_TYPE_SET.has(event.type),
|
|
326
|
+
(event) => event.sequence > target.afterSequence && targetEventMatches(event),
|
|
279
327
|
);
|
|
280
328
|
if (events.length === 0) continue;
|
|
281
329
|
changed.push({
|
|
@@ -148,7 +148,7 @@ export function registerApiIntegrationRoutes(
|
|
|
148
148
|
|
|
149
149
|
app.post("/v1/workspaces/:workspaceId/integrations/install", async (c) => {
|
|
150
150
|
const workspaceId = c.req.param("workspaceId");
|
|
151
|
-
const grant = await requireAccessGrant(c, deps, workspaceId, "
|
|
151
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "capabilities:manage");
|
|
152
152
|
const payload = InstallApiIntegrationRequest.parse(await c.req.json());
|
|
153
153
|
const resolved = await resolveForRoute({
|
|
154
154
|
deps,
|
|
@@ -261,7 +261,7 @@ export function registerApiIntegrationRoutes(
|
|
|
261
261
|
"/v1/workspaces/:workspaceId/integrations/:capabilityId/instances/:instanceKey",
|
|
262
262
|
async (c) => {
|
|
263
263
|
const workspaceId = c.req.param("workspaceId");
|
|
264
|
-
const grant = await requireAccessGrant(c, deps, workspaceId, "
|
|
264
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "capabilities:manage");
|
|
265
265
|
const capabilityId = decodeURIComponent(c.req.param("capabilityId"));
|
|
266
266
|
const instanceKey = decodeURIComponent(c.req.param("instanceKey"));
|
|
267
267
|
const payload = UninstallApiIntegrationRequest.parse(await c.req.json());
|
package/src/routes/api-keys.ts
CHANGED
|
@@ -1,12 +1,41 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
CreateApiKeyRequest,
|
|
3
|
+
CreateApiKeyResponse,
|
|
4
|
+
CreateOrganizationApiKeyRequest,
|
|
5
|
+
Permission,
|
|
6
|
+
type AccessContext,
|
|
7
|
+
} from "@opengeni/contracts";
|
|
8
|
+
import {
|
|
9
|
+
createApiKey,
|
|
10
|
+
createOrganizationApiKey as createOrganizationApiKeyRecord,
|
|
11
|
+
listApiKeys,
|
|
12
|
+
listOrganizationApiKeys,
|
|
13
|
+
OrganizationApiKeyLimitExceededError,
|
|
14
|
+
revokeApiKey,
|
|
15
|
+
revokeOrganizationApiKey,
|
|
16
|
+
} from "@opengeni/db";
|
|
17
|
+
import { configuredStaticUsageLimits } from "@opengeni/config";
|
|
3
18
|
import { zValidator } from "@hono/zod-validator";
|
|
4
19
|
import type { Hono } from "hono";
|
|
5
20
|
import { HTTPException } from "hono/http-exception";
|
|
6
21
|
import type { ApiRouteDeps } from "@opengeni/core";
|
|
7
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
accountScopedApiKeyWorkspaceAuthority,
|
|
24
|
+
requireAccessContext,
|
|
25
|
+
requireAccessGrant,
|
|
26
|
+
requireAccessGrantAuthorization,
|
|
27
|
+
type AccessGrantAuthorization,
|
|
28
|
+
} from "@opengeni/core";
|
|
8
29
|
import { requireLimit } from "@opengeni/core";
|
|
9
30
|
|
|
31
|
+
export const organizationApiKeyPermissions: Permission[] = [
|
|
32
|
+
"account:read",
|
|
33
|
+
"workspace:create",
|
|
34
|
+
"workspace:read",
|
|
35
|
+
"workspace:admin",
|
|
36
|
+
"api_keys:manage",
|
|
37
|
+
];
|
|
38
|
+
|
|
10
39
|
export function registerApiKeyRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
11
40
|
app.get("/v1/workspaces/:workspaceId/api-keys", async (c) => {
|
|
12
41
|
const workspaceId = c.req.param("workspaceId");
|
|
@@ -19,11 +48,17 @@ export function registerApiKeyRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
19
48
|
zValidator("json", CreateApiKeyRequest.omit({ workspaceId: true })),
|
|
20
49
|
async (c) => {
|
|
21
50
|
const workspaceId = c.req.param("workspaceId");
|
|
22
|
-
const
|
|
51
|
+
const authorization = await requireAccessGrantAuthorization(
|
|
52
|
+
c,
|
|
53
|
+
deps,
|
|
54
|
+
workspaceId,
|
|
55
|
+
"api_keys:manage",
|
|
56
|
+
);
|
|
57
|
+
const grant = authorization.grant;
|
|
23
58
|
const body = c.req.valid("json");
|
|
24
59
|
const permissions: Permission[] =
|
|
25
60
|
body.permissions.length > 0 ? (body.permissions as Permission[]) : ["workspace:read"];
|
|
26
|
-
ensureDelegablePermissions(
|
|
61
|
+
ensureDelegablePermissions(authorization, permissions);
|
|
27
62
|
await requireLimit(deps, {
|
|
28
63
|
accountId: grant.accountId,
|
|
29
64
|
workspaceId,
|
|
@@ -51,12 +86,90 @@ export function registerApiKeyRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
51
86
|
await requireAccessGrant(c, deps, workspaceId, "api_keys:manage");
|
|
52
87
|
return c.json(await revokeApiKey(deps.db, workspaceId, c.req.param("apiKeyId")));
|
|
53
88
|
});
|
|
89
|
+
|
|
90
|
+
app.get("/v1/organizations/:organizationId/api-keys", async (c) => {
|
|
91
|
+
const organizationId = c.req.param("organizationId");
|
|
92
|
+
const context = await requireAccessContext(c, deps);
|
|
93
|
+
requireOrganizationApiKeyControlPermission(context, organizationId);
|
|
94
|
+
return c.json({ apiKeys: await listOrganizationApiKeys(deps.db, organizationId) });
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
app.post(
|
|
98
|
+
"/v1/organizations/:organizationId/api-keys",
|
|
99
|
+
zValidator("json", CreateOrganizationApiKeyRequest),
|
|
100
|
+
async (c) => {
|
|
101
|
+
const organizationId = c.req.param("organizationId");
|
|
102
|
+
const context = await requireAccessContext(c, deps);
|
|
103
|
+
requireOrganizationApiKeyControlPermission(context, organizationId);
|
|
104
|
+
const body = c.req.valid("json");
|
|
105
|
+
const token = generateApiKeyToken();
|
|
106
|
+
try {
|
|
107
|
+
const apiKey = await createOrganizationApiKeyRecord(deps.db, {
|
|
108
|
+
accountId: organizationId,
|
|
109
|
+
name: body.name,
|
|
110
|
+
description: body.description ?? null,
|
|
111
|
+
prefix: token.slice(0, 14),
|
|
112
|
+
keyHash: await sha256Hex(token),
|
|
113
|
+
permissions: organizationApiKeyPermissions,
|
|
114
|
+
expiresAt: body.expiresAt ? new Date(body.expiresAt) : null,
|
|
115
|
+
maxActiveKeys: organizationApiKeyLimit(deps),
|
|
116
|
+
rotationSourceApiKeyId: authenticatedApiKeyId(context),
|
|
117
|
+
});
|
|
118
|
+
return c.json(CreateApiKeyResponse.parse({ apiKey, token }), 201);
|
|
119
|
+
} catch (error) {
|
|
120
|
+
if (error instanceof OrganizationApiKeyLimitExceededError) {
|
|
121
|
+
throw new HTTPException(429, { message: error.message });
|
|
122
|
+
}
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
app.delete("/v1/organizations/:organizationId/api-keys/:apiKeyId", async (c) => {
|
|
129
|
+
const organizationId = c.req.param("organizationId");
|
|
130
|
+
const context = await requireAccessContext(c, deps);
|
|
131
|
+
requireOrganizationApiKeyControlPermission(context, organizationId);
|
|
132
|
+
const apiKey = await revokeOrganizationApiKey(deps.db, organizationId, c.req.param("apiKeyId"));
|
|
133
|
+
if (!apiKey) {
|
|
134
|
+
throw new HTTPException(404, { message: "API key not found" });
|
|
135
|
+
}
|
|
136
|
+
return c.json(apiKey);
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function requireAccountPermission(
|
|
141
|
+
context: AccessContext,
|
|
142
|
+
accountId: string,
|
|
143
|
+
permission: Permission,
|
|
144
|
+
): void {
|
|
145
|
+
const grant = context.accountGrants.find((candidate) => candidate.accountId === accountId);
|
|
146
|
+
if (
|
|
147
|
+
!grant ||
|
|
148
|
+
(!grant.permissions.includes(permission) && !grant.permissions.includes("account:admin"))
|
|
149
|
+
) {
|
|
150
|
+
throw new HTTPException(403, { message: `missing permission: ${permission}` });
|
|
151
|
+
}
|
|
54
152
|
}
|
|
55
153
|
|
|
56
|
-
function ensureDelegablePermissions(
|
|
154
|
+
function ensureDelegablePermissions(
|
|
155
|
+
authorization: AccessGrantAuthorization,
|
|
156
|
+
requested: Permission[],
|
|
157
|
+
): void {
|
|
158
|
+
const grantPermissions = authorization.grant.permissions;
|
|
57
159
|
if (grantPermissions.includes("workspace:admin")) {
|
|
160
|
+
const accountLiteralPermissions = new Set<Permission>([
|
|
161
|
+
"account:read",
|
|
162
|
+
"account:admin",
|
|
163
|
+
"workspace:create",
|
|
164
|
+
"billing:read",
|
|
165
|
+
"billing:manage",
|
|
166
|
+
]);
|
|
167
|
+
const workspaceLiteralPermissions = new Set<Permission>(["members:manage", "secrets:read"]);
|
|
58
168
|
const highTrustMissing = requested.filter(
|
|
59
|
-
(permission) =>
|
|
169
|
+
(permission) =>
|
|
170
|
+
(accountLiteralPermissions.has(permission) &&
|
|
171
|
+
!authorization.accountGrant?.permissions.includes(permission)) ||
|
|
172
|
+
(workspaceLiteralPermissions.has(permission) && !grantPermissions.includes(permission)),
|
|
60
173
|
);
|
|
61
174
|
if (highTrustMissing.length === 0) return;
|
|
62
175
|
throw new HTTPException(403, {
|
|
@@ -71,6 +184,35 @@ function ensureDelegablePermissions(grantPermissions: Permission[], requested: P
|
|
|
71
184
|
}
|
|
72
185
|
}
|
|
73
186
|
|
|
187
|
+
function requireOrganizationApiKeyControlPermission(
|
|
188
|
+
context: AccessContext,
|
|
189
|
+
organizationId: string,
|
|
190
|
+
): void {
|
|
191
|
+
requireAccountPermission(context, organizationId, "api_keys:manage");
|
|
192
|
+
if (!context.subjectId.startsWith("api_key:")) return;
|
|
193
|
+
const authority = accountScopedApiKeyWorkspaceAuthority(context);
|
|
194
|
+
if (
|
|
195
|
+
!authority ||
|
|
196
|
+
authority.accountId !== organizationId ||
|
|
197
|
+
!authority.permissions.includes("api_keys:manage")
|
|
198
|
+
) {
|
|
199
|
+
throw new HTTPException(403, { message: "organization API key authority required" });
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function authenticatedApiKeyId(context: AccessContext): string | null {
|
|
204
|
+
return context.subjectId.startsWith("api_key:")
|
|
205
|
+
? context.subjectId.slice("api_key:".length)
|
|
206
|
+
: null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function organizationApiKeyLimit(deps: ApiRouteDeps): number | null {
|
|
210
|
+
if (deps.settings.usageLimitsMode !== "static" && deps.settings.usageLimitsMode !== "managed") {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
return configuredStaticUsageLimits(deps.settings).maxApiKeysPerWorkspace ?? null;
|
|
214
|
+
}
|
|
215
|
+
|
|
74
216
|
function generateApiKeyToken(): string {
|
|
75
217
|
const bytes = new Uint8Array(32);
|
|
76
218
|
crypto.getRandomValues(bytes);
|
|
@@ -3435,7 +3435,14 @@ async function ensureHeadedBrowserDisplayStack(
|
|
|
3435
3435
|
headless: boolean,
|
|
3436
3436
|
linkedComputer: boolean,
|
|
3437
3437
|
): Promise<void> {
|
|
3438
|
-
if (
|
|
3438
|
+
if (
|
|
3439
|
+
!browserNeedsStandaloneDisplayStack({
|
|
3440
|
+
headless,
|
|
3441
|
+
linkedComputer,
|
|
3442
|
+
nativeBrowserControl: typeof session.ensureBrowserControl === "function",
|
|
3443
|
+
})
|
|
3444
|
+
)
|
|
3445
|
+
return;
|
|
3439
3446
|
if (typeof session.exec !== "function" && typeof session.execCommand !== "function") return;
|
|
3440
3447
|
await ensureDisplayStack(session, {
|
|
3441
3448
|
telemetryContext: { callerKind: "viewer" },
|
|
@@ -3445,8 +3452,9 @@ async function ensureHeadedBrowserDisplayStack(
|
|
|
3445
3452
|
export function browserNeedsStandaloneDisplayStack(input: {
|
|
3446
3453
|
headless: boolean;
|
|
3447
3454
|
linkedComputer: boolean;
|
|
3455
|
+
nativeBrowserControl: boolean;
|
|
3448
3456
|
}): boolean {
|
|
3449
|
-
return !input.headless && !input.linkedComputer;
|
|
3457
|
+
return !input.headless && !input.linkedComputer && !input.nativeBrowserControl;
|
|
3450
3458
|
}
|
|
3451
3459
|
|
|
3452
3460
|
async function ensureLinkedComputerController(
|
|
@@ -32,7 +32,7 @@ export function registerCapabilityRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
32
32
|
|
|
33
33
|
app.post("/v1/workspaces/:workspaceId/capabilities", async (c) => {
|
|
34
34
|
const workspaceId = c.req.param("workspaceId");
|
|
35
|
-
const grant = await requireAccessGrant(c, deps, workspaceId, "
|
|
35
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "capabilities:manage");
|
|
36
36
|
const payload = CreateCapabilityCatalogItemRequest.parse(await c.req.json());
|
|
37
37
|
return c.json(
|
|
38
38
|
await createCatalogItem({ db, accountId: grant.accountId, workspaceId, payload }),
|
|
@@ -62,7 +62,7 @@ export function registerCapabilityRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
62
62
|
|
|
63
63
|
app.post("/v1/workspaces/:workspaceId/capabilities/:capabilityId/enable", async (c) => {
|
|
64
64
|
const workspaceId = c.req.param("workspaceId");
|
|
65
|
-
const grant = await requireAccessGrant(c, deps, workspaceId, "
|
|
65
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "capabilities:manage");
|
|
66
66
|
const payload = EnableCapabilityRequest.parse(await c.req.json());
|
|
67
67
|
const installation = await enableCapability({
|
|
68
68
|
db,
|
|
@@ -78,7 +78,7 @@ export function registerCapabilityRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
78
78
|
|
|
79
79
|
app.post("/v1/workspaces/:workspaceId/capabilities/:capabilityId/disable", async (c) => {
|
|
80
80
|
const workspaceId = c.req.param("workspaceId");
|
|
81
|
-
const grant = await requireAccessGrant(c, deps, workspaceId, "
|
|
81
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "capabilities:manage");
|
|
82
82
|
const installation = await disableCapability({
|
|
83
83
|
db,
|
|
84
84
|
accountId: grant.accountId,
|