@opengeni/api-router 2.6.4-canary.0 → 2.8.1-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/{chunk-UOI7KAP3.js → chunk-HHKQMVY7.js} +2056 -1346
- package/dist/chunk-HHKQMVY7.js.map +1 -0
- package/dist/github-access.d.ts +25 -2
- package/dist/index.js +1 -1
- package/dist/integrations/personal-github-repositories.d.ts +41 -2
- package/dist/routes/codex.d.ts +8 -1
- package/dist/routes/github.d.ts +2 -0
- package/dist/routes/organization-model-providers.d.ts +3 -0
- package/dist/workspace-deletion.d.ts +6 -0
- package/package.json +18 -18
- package/src/app.ts +21 -0
- package/src/auth/organization-user-setup.ts +3 -3
- package/src/github-access.ts +117 -1
- package/src/http/sse.ts +15 -7
- package/src/integrations/personal-github-repositories.ts +238 -9
- package/src/integrations/slack-app-home.ts +2 -2
- package/src/mcp/company-brain-governed-writes.ts +2 -2
- package/src/mcp/company-profile-agent-admin.ts +1 -1
- package/src/mcp/remember.ts +1 -1
- package/src/routes/automations.ts +6 -14
- package/src/routes/codex.ts +136 -32
- package/src/routes/github.ts +116 -7
- package/src/routes/organization-memberships.ts +50 -3
- package/src/routes/organization-model-providers.ts +231 -0
- package/src/routes/packs.ts +1 -0
- package/src/routes/personal-github.ts +39 -0
- package/src/routes/pr-review.ts +6 -14
- package/src/routes/sessions.ts +16 -5
- package/src/routes/workspaces.ts +32 -59
- package/src/workspace-deletion.ts +64 -0
- package/dist/chunk-UOI7KAP3.js.map +0 -1
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CreateOrganizationProviderCustomModelRequest,
|
|
3
|
+
DeleteOrganizationProviderCustomModelRequest,
|
|
4
|
+
OrganizationModelProviderConnectionResponse,
|
|
5
|
+
OrganizationModelProviderKind,
|
|
6
|
+
OrganizationProviderCustomModel,
|
|
7
|
+
OrganizationProviderCustomModelsResponse,
|
|
8
|
+
RevokeOrganizationModelProviderConnectionRequest,
|
|
9
|
+
UpsertOrganizationModelProviderConnectionRequest,
|
|
10
|
+
} from "@opengeni/contracts";
|
|
11
|
+
import { requireEnvironmentEncryption, type ApiRouteDeps } from "@opengeni/core";
|
|
12
|
+
import {
|
|
13
|
+
createOrganizationModelProviderCustomModel,
|
|
14
|
+
encryptEnvironmentValue,
|
|
15
|
+
getOrganizationModelProviderConnection,
|
|
16
|
+
listOrganizationModelProviderCustomModels,
|
|
17
|
+
organizationModelProviderCredentialDigest,
|
|
18
|
+
OrganizationModelProviderConflictError,
|
|
19
|
+
OrganizationModelProviderLimitError,
|
|
20
|
+
retireOrganizationModelProviderCustomModel,
|
|
21
|
+
revokeOrganizationModelProviderConnection,
|
|
22
|
+
upsertOrganizationModelProviderConnection,
|
|
23
|
+
} from "@opengeni/db";
|
|
24
|
+
import type { Context, Hono } from "hono";
|
|
25
|
+
import { HTTPException } from "hono/http-exception";
|
|
26
|
+
import { z } from "zod";
|
|
27
|
+
|
|
28
|
+
import { requireOrganizationCodexHuman, requireSameOriginBrowserMutation } from "./codex";
|
|
29
|
+
|
|
30
|
+
const OrganizationId = z.string().uuid();
|
|
31
|
+
|
|
32
|
+
function parseOrganizationId(value: string): string {
|
|
33
|
+
const parsed = OrganizationId.safeParse(value);
|
|
34
|
+
if (!parsed.success) throw new HTTPException(404, { message: "organization not found" });
|
|
35
|
+
return parsed.data;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function jsonBody<T>(c: Context, schema: z.ZodType<T>, message: string): Promise<T> {
|
|
39
|
+
const parsed = schema.safeParse(await c.req.json().catch(() => null));
|
|
40
|
+
if (!parsed.success) throw new HTTPException(422, { message });
|
|
41
|
+
return parsed.data;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function providerKind(value: string) {
|
|
45
|
+
const parsed = OrganizationModelProviderKind.safeParse(value);
|
|
46
|
+
if (!parsed.success) throw new HTTPException(404, { message: "model provider not found" });
|
|
47
|
+
return parsed.data;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function connectionJson(connection: {
|
|
51
|
+
providerKind: "vercel_gateway" | "openrouter";
|
|
52
|
+
status: "active" | "revoked";
|
|
53
|
+
version: number;
|
|
54
|
+
createdAt: Date;
|
|
55
|
+
updatedAt: Date;
|
|
56
|
+
}) {
|
|
57
|
+
return OrganizationModelProviderConnectionResponse.parse({
|
|
58
|
+
...connection,
|
|
59
|
+
createdAt: connection.createdAt.toISOString(),
|
|
60
|
+
updatedAt: connection.updatedAt.toISOString(),
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function modelJson(model: {
|
|
65
|
+
id: string;
|
|
66
|
+
upstreamModelId: string;
|
|
67
|
+
label: string | null;
|
|
68
|
+
version: number;
|
|
69
|
+
createdAt: Date;
|
|
70
|
+
updatedAt: Date;
|
|
71
|
+
}) {
|
|
72
|
+
return OrganizationProviderCustomModel.parse({
|
|
73
|
+
...model,
|
|
74
|
+
createdAt: model.createdAt.toISOString(),
|
|
75
|
+
updatedAt: model.updatedAt.toISOString(),
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function conflict(error: unknown): never {
|
|
80
|
+
if (error instanceof OrganizationModelProviderConflictError) {
|
|
81
|
+
throw new HTTPException(409, { message: "organization model provider version conflict" });
|
|
82
|
+
}
|
|
83
|
+
if (error instanceof OrganizationModelProviderLimitError) {
|
|
84
|
+
throw new HTTPException(409, { message: "organization custom model limit reached" });
|
|
85
|
+
}
|
|
86
|
+
throw error;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function registerOrganizationModelProviderRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
90
|
+
app.get("/v1/organizations/:organizationId/model-providers/:providerKind", async (c) => {
|
|
91
|
+
c.header("cache-control", "private, no-store");
|
|
92
|
+
const organizationId = parseOrganizationId(c.req.param("organizationId"));
|
|
93
|
+
const human = await requireOrganizationCodexHuman(c, deps, organizationId);
|
|
94
|
+
const connection = await getOrganizationModelProviderConnection(deps.db, {
|
|
95
|
+
organizationId,
|
|
96
|
+
actorSubjectId: human.subjectId,
|
|
97
|
+
providerKind: providerKind(c.req.param("providerKind")),
|
|
98
|
+
});
|
|
99
|
+
return connection ? c.json(connectionJson(connection)) : c.json(null);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
app.put("/v1/organizations/:organizationId/model-providers/:providerKind", async (c) => {
|
|
103
|
+
c.header("cache-control", "private, no-store");
|
|
104
|
+
requireSameOriginBrowserMutation(c, deps);
|
|
105
|
+
const organizationId = parseOrganizationId(c.req.param("organizationId"));
|
|
106
|
+
const human = await requireOrganizationCodexHuman(c, deps, organizationId);
|
|
107
|
+
const payload = await jsonBody(
|
|
108
|
+
c,
|
|
109
|
+
UpsertOrganizationModelProviderConnectionRequest,
|
|
110
|
+
"invalid organization model provider connection",
|
|
111
|
+
);
|
|
112
|
+
try {
|
|
113
|
+
const connection = await upsertOrganizationModelProviderConnection(deps.db, {
|
|
114
|
+
organizationId,
|
|
115
|
+
actorSubjectId: human.subjectId,
|
|
116
|
+
providerKind: providerKind(c.req.param("providerKind")),
|
|
117
|
+
credentialEncrypted: encryptEnvironmentValue(
|
|
118
|
+
requireEnvironmentEncryption(deps.settings),
|
|
119
|
+
payload.apiKey,
|
|
120
|
+
),
|
|
121
|
+
credentialDigest: organizationModelProviderCredentialDigest(payload.apiKey),
|
|
122
|
+
operationId: payload.operationId,
|
|
123
|
+
...(payload.expectedVersion === undefined
|
|
124
|
+
? {}
|
|
125
|
+
: { expectedVersion: payload.expectedVersion }),
|
|
126
|
+
});
|
|
127
|
+
return c.json(connectionJson(connection));
|
|
128
|
+
} catch (error) {
|
|
129
|
+
conflict(error);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
app.delete("/v1/organizations/:organizationId/model-providers/:providerKind", async (c) => {
|
|
134
|
+
c.header("cache-control", "private, no-store");
|
|
135
|
+
requireSameOriginBrowserMutation(c, deps);
|
|
136
|
+
const organizationId = parseOrganizationId(c.req.param("organizationId"));
|
|
137
|
+
const human = await requireOrganizationCodexHuman(c, deps, organizationId);
|
|
138
|
+
const payload = await jsonBody(
|
|
139
|
+
c,
|
|
140
|
+
RevokeOrganizationModelProviderConnectionRequest,
|
|
141
|
+
"invalid organization model provider disconnect",
|
|
142
|
+
);
|
|
143
|
+
try {
|
|
144
|
+
const connection = await revokeOrganizationModelProviderConnection(deps.db, {
|
|
145
|
+
organizationId,
|
|
146
|
+
actorSubjectId: human.subjectId,
|
|
147
|
+
providerKind: providerKind(c.req.param("providerKind")),
|
|
148
|
+
...payload,
|
|
149
|
+
});
|
|
150
|
+
return c.json(connectionJson(connection));
|
|
151
|
+
} catch (error) {
|
|
152
|
+
conflict(error);
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
app.get(
|
|
157
|
+
"/v1/organizations/:organizationId/model-providers/:providerKind/custom-models",
|
|
158
|
+
async (c) => {
|
|
159
|
+
c.header("cache-control", "private, no-store");
|
|
160
|
+
const organizationId = parseOrganizationId(c.req.param("organizationId"));
|
|
161
|
+
const human = await requireOrganizationCodexHuman(c, deps, organizationId);
|
|
162
|
+
const models = await listOrganizationModelProviderCustomModels(deps.db, {
|
|
163
|
+
organizationId,
|
|
164
|
+
actorSubjectId: human.subjectId,
|
|
165
|
+
providerKind: providerKind(c.req.param("providerKind")),
|
|
166
|
+
});
|
|
167
|
+
return c.json(
|
|
168
|
+
OrganizationProviderCustomModelsResponse.parse({ models: models.map(modelJson) }),
|
|
169
|
+
);
|
|
170
|
+
},
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
app.post(
|
|
174
|
+
"/v1/organizations/:organizationId/model-providers/:providerKind/custom-models",
|
|
175
|
+
async (c) => {
|
|
176
|
+
c.header("cache-control", "private, no-store");
|
|
177
|
+
requireSameOriginBrowserMutation(c, deps);
|
|
178
|
+
const organizationId = parseOrganizationId(c.req.param("organizationId"));
|
|
179
|
+
const human = await requireOrganizationCodexHuman(c, deps, organizationId);
|
|
180
|
+
const payload = await jsonBody(
|
|
181
|
+
c,
|
|
182
|
+
CreateOrganizationProviderCustomModelRequest,
|
|
183
|
+
"invalid organization custom model",
|
|
184
|
+
);
|
|
185
|
+
try {
|
|
186
|
+
const model = await createOrganizationModelProviderCustomModel(deps.db, {
|
|
187
|
+
organizationId,
|
|
188
|
+
actorSubjectId: human.subjectId,
|
|
189
|
+
providerKind: providerKind(c.req.param("providerKind")),
|
|
190
|
+
operationId: payload.operationId,
|
|
191
|
+
upstreamModelId: payload.upstreamModelId,
|
|
192
|
+
...(payload.label === undefined ? {} : { label: payload.label }),
|
|
193
|
+
});
|
|
194
|
+
return c.json(modelJson(model), 201);
|
|
195
|
+
} catch (error) {
|
|
196
|
+
conflict(error);
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
app.delete(
|
|
202
|
+
"/v1/organizations/:organizationId/model-providers/:providerKind/custom-models/:customModelId",
|
|
203
|
+
async (c) => {
|
|
204
|
+
c.header("cache-control", "private, no-store");
|
|
205
|
+
requireSameOriginBrowserMutation(c, deps);
|
|
206
|
+
const organizationId = parseOrganizationId(c.req.param("organizationId"));
|
|
207
|
+
const human = await requireOrganizationCodexHuman(c, deps, organizationId);
|
|
208
|
+
const payload = await jsonBody(
|
|
209
|
+
c,
|
|
210
|
+
DeleteOrganizationProviderCustomModelRequest,
|
|
211
|
+
"invalid organization custom model deletion",
|
|
212
|
+
);
|
|
213
|
+
const customModelId = z.string().uuid().safeParse(c.req.param("customModelId"));
|
|
214
|
+
if (!customModelId.success) {
|
|
215
|
+
throw new HTTPException(422, { message: "invalid organization custom model id" });
|
|
216
|
+
}
|
|
217
|
+
try {
|
|
218
|
+
const model = await retireOrganizationModelProviderCustomModel(deps.db, {
|
|
219
|
+
organizationId,
|
|
220
|
+
actorSubjectId: human.subjectId,
|
|
221
|
+
providerKind: providerKind(c.req.param("providerKind")),
|
|
222
|
+
customModelId: customModelId.data,
|
|
223
|
+
...payload,
|
|
224
|
+
});
|
|
225
|
+
return c.json(modelJson(model));
|
|
226
|
+
} catch (error) {
|
|
227
|
+
conflict(error);
|
|
228
|
+
}
|
|
229
|
+
},
|
|
230
|
+
);
|
|
231
|
+
}
|
package/src/routes/packs.ts
CHANGED
|
@@ -277,6 +277,7 @@ export function registerPackRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
277
277
|
sourcePath: inline.sourcePath,
|
|
278
278
|
name: inline.name,
|
|
279
279
|
description: inline.description,
|
|
280
|
+
activationMode: inline.activationMode,
|
|
280
281
|
contentSha256: inline.contentSha256,
|
|
281
282
|
totalBytes: inline.totalBytes,
|
|
282
283
|
files: inline.files,
|
|
@@ -3,10 +3,15 @@ import {
|
|
|
3
3
|
ListPersonalGitHubRepositoriesResponse,
|
|
4
4
|
PersonalGitHubConnectionStatusResponse,
|
|
5
5
|
PersonalGitHubOAuthStartRequest,
|
|
6
|
+
PersonalGitHubRepositoryId,
|
|
6
7
|
PersonalGitHubRepositorySelectionState,
|
|
7
8
|
ReplacePersonalGitHubRepositorySelectionsRequest,
|
|
8
9
|
VerifyPersonalGitHubRepositorySelectionsRequest,
|
|
9
10
|
} from "@opengeni/contracts/personal-github";
|
|
11
|
+
import {
|
|
12
|
+
ListGitHubRepositoryBranchesQuery,
|
|
13
|
+
GitHubRepositoryBranchesResponse,
|
|
14
|
+
} from "@opengeni/contracts/github-repository-contracts";
|
|
10
15
|
import {
|
|
11
16
|
requireAccessGrant,
|
|
12
17
|
requireAccessGrantAuthorization,
|
|
@@ -31,6 +36,7 @@ import {
|
|
|
31
36
|
startPersonalGitHubOAuth,
|
|
32
37
|
} from "../integrations/personal-github";
|
|
33
38
|
import {
|
|
39
|
+
listLivePersonalGitHubRepositoryBranches,
|
|
34
40
|
listLivePersonalGitHubRepositories,
|
|
35
41
|
personalGitHubRepositoryProviderHttpError,
|
|
36
42
|
PersonalGitHubRepositoryProviderError,
|
|
@@ -187,6 +193,39 @@ export function registerPersonalGitHubRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
187
193
|
},
|
|
188
194
|
);
|
|
189
195
|
|
|
196
|
+
app.get(
|
|
197
|
+
"/v1/workspaces/:workspaceId/connections/:connectionId/github/repositories/:repositoryId/branches",
|
|
198
|
+
async (c) => {
|
|
199
|
+
const workspaceId = c.req.param("workspaceId");
|
|
200
|
+
const connectionId = c.req.param("connectionId");
|
|
201
|
+
const access = await requireAccessGrantAuthorization(
|
|
202
|
+
c,
|
|
203
|
+
deps,
|
|
204
|
+
workspaceId,
|
|
205
|
+
"connections:read",
|
|
206
|
+
);
|
|
207
|
+
assertPersonalConnectionOwnerPrincipal(access, "My GitHub repositories");
|
|
208
|
+
const query = ListGitHubRepositoryBranchesQuery.parse(c.req.query());
|
|
209
|
+
const repositoryId = PersonalGitHubRepositoryId.parse(c.req.param("repositoryId"));
|
|
210
|
+
try {
|
|
211
|
+
return c.json(
|
|
212
|
+
GitHubRepositoryBranchesResponse.parse(
|
|
213
|
+
await listLivePersonalGitHubRepositoryBranches(deps, {
|
|
214
|
+
accountId: access.grant.accountId,
|
|
215
|
+
workspaceId,
|
|
216
|
+
subjectId: access.grant.subjectId,
|
|
217
|
+
connectionId,
|
|
218
|
+
repositoryId,
|
|
219
|
+
query,
|
|
220
|
+
}),
|
|
221
|
+
),
|
|
222
|
+
);
|
|
223
|
+
} catch (error) {
|
|
224
|
+
throw personalGitHubRepositoryRouteError(error);
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
);
|
|
228
|
+
|
|
190
229
|
app.post(
|
|
191
230
|
"/v1/workspaces/:workspaceId/connections/:connectionId/github/repositories/verify",
|
|
192
231
|
async (c) => {
|
package/src/routes/pr-review.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
defaultPrReviewProviderBaseUrl,
|
|
15
15
|
getCapabilityPack,
|
|
16
16
|
workspaceCustomModelReference,
|
|
17
|
+
lockActiveCustomModelForAdmission,
|
|
17
18
|
prReviewWebhookAuthKind,
|
|
18
19
|
normalizePrReviewProviderBaseUrl,
|
|
19
20
|
prReviewPackConnectorId,
|
|
@@ -34,8 +35,6 @@ import {
|
|
|
34
35
|
getPackInstallation,
|
|
35
36
|
listPrReviewAppRegistrations,
|
|
36
37
|
listPrReviewRepositoryBindings,
|
|
37
|
-
lockActiveWorkspaceGatewayCustomModelForAdmission,
|
|
38
|
-
lockActiveWorkspaceOpenRouterCustomModelForAdmission,
|
|
39
38
|
nestedPostgresSqlState,
|
|
40
39
|
recordAuditEvent,
|
|
41
40
|
updatePrReviewAppRegistration,
|
|
@@ -557,18 +556,11 @@ function prReviewCustomModelCommitGuard(input: {
|
|
|
557
556
|
const reference = workspaceCustomModelReference(input.settings, input.modelId);
|
|
558
557
|
if (!reference) return undefined;
|
|
559
558
|
return async (tx): Promise<void> => {
|
|
560
|
-
const active =
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
upstreamModelId: reference.upstreamModelId,
|
|
566
|
-
})
|
|
567
|
-
: await lockActiveWorkspaceGatewayCustomModelForAdmission(tx, {
|
|
568
|
-
accountId: input.accountId,
|
|
569
|
-
workspaceId: input.workspaceId,
|
|
570
|
-
upstreamModelId: reference.upstreamModelId,
|
|
571
|
-
});
|
|
559
|
+
const active = await lockActiveCustomModelForAdmission(tx, {
|
|
560
|
+
accountId: input.accountId,
|
|
561
|
+
workspaceId: input.workspaceId,
|
|
562
|
+
reference,
|
|
563
|
+
});
|
|
572
564
|
if (!active) {
|
|
573
565
|
throw new HTTPException(422, {
|
|
574
566
|
message: `model is not available: ${input.modelId}`,
|
package/src/routes/sessions.ts
CHANGED
|
@@ -197,7 +197,7 @@ import {
|
|
|
197
197
|
import {
|
|
198
198
|
appendAndPublishEvents,
|
|
199
199
|
boundSessionEventHttpPage,
|
|
200
|
-
|
|
200
|
+
coalesceSessionEventDeltasWithCoverage,
|
|
201
201
|
publishDurableSessionEvents,
|
|
202
202
|
} from "@opengeni/events";
|
|
203
203
|
import {
|
|
@@ -2621,10 +2621,16 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
2621
2621
|
c.header("X-OpenGeni-Covered-Last", String(result.coveredSequence.last));
|
|
2622
2622
|
return c.json(result);
|
|
2623
2623
|
}
|
|
2624
|
-
const
|
|
2624
|
+
const compactProjection = compact ? coalesceSessionEventDeltasWithCoverage(events) : null;
|
|
2625
|
+
const projected = compactProjection?.events ?? events;
|
|
2626
|
+
const forensicExact =
|
|
2627
|
+
!compact && mode === "forensic" && payloadMode === "full" && dbPage.fullPayloadsExact;
|
|
2625
2628
|
const page = boundSessionEventHttpPage(projected, {
|
|
2626
2629
|
direction,
|
|
2627
|
-
eventProjection:
|
|
2630
|
+
eventProjection: forensicExact ? "exact" : "bounded",
|
|
2631
|
+
...(compactProjection
|
|
2632
|
+
? { coveredThroughBySequence: compactProjection.coveredThroughBySequence }
|
|
2633
|
+
: {}),
|
|
2628
2634
|
});
|
|
2629
2635
|
const hasMore = dbPage.hasMore || page.truncated;
|
|
2630
2636
|
c.header("X-OpenGeni-Page-Bytes", String(page.bytes));
|
|
@@ -2634,9 +2640,14 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
2634
2640
|
c.header("X-OpenGeni-Event-Mode", mode);
|
|
2635
2641
|
c.header("X-OpenGeni-Event-Direction", direction);
|
|
2636
2642
|
c.header("X-OpenGeni-Payload-Mode", payloadMode);
|
|
2637
|
-
c.header("X-OpenGeni-Forensic-Exact", String(
|
|
2643
|
+
c.header("X-OpenGeni-Forensic-Exact", String(forensicExact));
|
|
2638
2644
|
const coveredFirst = page.events[0]?.sequence;
|
|
2639
|
-
const
|
|
2645
|
+
const coveredLastEvent = page.events.at(-1);
|
|
2646
|
+
const coveredLast =
|
|
2647
|
+
coveredLastEvent === undefined
|
|
2648
|
+
? undefined
|
|
2649
|
+
: (compactProjection?.coveredThroughBySequence.get(coveredLastEvent.sequence) ??
|
|
2650
|
+
coveredLastEvent.sequence);
|
|
2640
2651
|
if (coveredFirst !== undefined) c.header("X-OpenGeni-Covered-First", String(coveredFirst));
|
|
2641
2652
|
if (coveredLast !== undefined) c.header("X-OpenGeni-Covered-Last", String(coveredLast));
|
|
2642
2653
|
const truncatedBy = page.truncated ? "http_bytes" : dbPage.truncatedBy;
|
package/src/routes/workspaces.ts
CHANGED
|
@@ -35,7 +35,6 @@ import {
|
|
|
35
35
|
import {
|
|
36
36
|
allWorkspacePermissions,
|
|
37
37
|
createWorkspace,
|
|
38
|
-
deleteWorkspaceIfQuiescent,
|
|
39
38
|
ensureWorkspaceByExternalIdentity,
|
|
40
39
|
findWorkspaceByExternalIdentity,
|
|
41
40
|
getManagedUserProfilesByIds,
|
|
@@ -69,6 +68,8 @@ import {
|
|
|
69
68
|
workspaceXaiSubscriptionActive,
|
|
70
69
|
workspaceVercelAiGatewayConnectionActive,
|
|
71
70
|
workspaceOpenRouterConnectionActive,
|
|
71
|
+
organizationModelProviderConnectionActiveForWorkspace,
|
|
72
|
+
listOrganizationModelProviderCustomModelsForWorkspace,
|
|
72
73
|
WorkspaceGatewayCustomModelHistoryLimitError,
|
|
73
74
|
WorkspaceOpenRouterCustomModelHistoryLimitError,
|
|
74
75
|
WorkspaceExternalIdentityConflictError,
|
|
@@ -99,8 +100,7 @@ import { boundedLimit } from "../http/common";
|
|
|
99
100
|
import { ApiHttpError } from "../http/api-error";
|
|
100
101
|
import { browserSseDeliveryOptions, sseWorkspaceControlStream } from "../http/sse";
|
|
101
102
|
import { buildWorkspaceModelCatalog } from "../model-catalog";
|
|
102
|
-
import {
|
|
103
|
-
import { workspaceDeleteObserver } from "../workspace-delete-observability";
|
|
103
|
+
import { deleteWorkspaceForRequest } from "../workspace-deletion";
|
|
104
104
|
import {
|
|
105
105
|
AI_GATEWAY_REALTIME_MODELS,
|
|
106
106
|
CODEX_REALTIME_MODEL_ID,
|
|
@@ -398,6 +398,10 @@ export function registerWorkspaceRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
398
398
|
workspaceGatewayCustomModels,
|
|
399
399
|
openRouterConnectionActive,
|
|
400
400
|
workspaceOpenRouterCustomModels,
|
|
401
|
+
organizationGatewayConnectionActive,
|
|
402
|
+
organizationOpenRouterConnectionActive,
|
|
403
|
+
organizationGatewayCustomModels,
|
|
404
|
+
organizationOpenRouterCustomModels,
|
|
401
405
|
] = await Promise.all([
|
|
402
406
|
deps.resolveCatalogSettings(),
|
|
403
407
|
getWorkspaceModelPolicy(deps.db, workspaceId),
|
|
@@ -413,6 +417,26 @@ export function registerWorkspaceRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
413
417
|
accountId: grant.accountId,
|
|
414
418
|
workspaceId,
|
|
415
419
|
}),
|
|
420
|
+
organizationModelProviderConnectionActiveForWorkspace(deps.db, {
|
|
421
|
+
accountId: grant.accountId,
|
|
422
|
+
workspaceId,
|
|
423
|
+
providerKind: "vercel_gateway",
|
|
424
|
+
}),
|
|
425
|
+
organizationModelProviderConnectionActiveForWorkspace(deps.db, {
|
|
426
|
+
accountId: grant.accountId,
|
|
427
|
+
workspaceId,
|
|
428
|
+
providerKind: "openrouter",
|
|
429
|
+
}),
|
|
430
|
+
listOrganizationModelProviderCustomModelsForWorkspace(deps.db, {
|
|
431
|
+
accountId: grant.accountId,
|
|
432
|
+
workspaceId,
|
|
433
|
+
providerKind: "vercel_gateway",
|
|
434
|
+
}),
|
|
435
|
+
listOrganizationModelProviderCustomModelsForWorkspace(deps.db, {
|
|
436
|
+
accountId: grant.accountId,
|
|
437
|
+
workspaceId,
|
|
438
|
+
providerKind: "openrouter",
|
|
439
|
+
}),
|
|
416
440
|
]);
|
|
417
441
|
c.header("cache-control", "private, no-store");
|
|
418
442
|
return c.json(
|
|
@@ -426,6 +450,10 @@ export function registerWorkspaceRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
426
450
|
workspaceGatewayCustomModels,
|
|
427
451
|
workspaceOpenRouterConnectionActive: openRouterConnectionActive,
|
|
428
452
|
workspaceOpenRouterCustomModels,
|
|
453
|
+
organizationGatewayConnectionActive,
|
|
454
|
+
organizationOpenRouterConnectionActive,
|
|
455
|
+
organizationGatewayCustomModels,
|
|
456
|
+
organizationOpenRouterCustomModels,
|
|
429
457
|
}),
|
|
430
458
|
),
|
|
431
459
|
);
|
|
@@ -883,65 +911,10 @@ export function registerWorkspaceRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
883
911
|
app.delete("/v1/workspaces/:workspaceId", async (c) => {
|
|
884
912
|
const workspaceId = c.req.param("workspaceId");
|
|
885
913
|
const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
|
|
886
|
-
|
|
887
|
-
// session/lease before checking runtime quiescence, then returns the exact
|
|
888
|
-
// external schedules removed by the cascade. A racing cold->warm transition
|
|
889
|
-
// can therefore never erase the only provider/capture ownership receipt.
|
|
890
|
-
const deleteObserver = workspaceDeleteObserver(deps.observability, {
|
|
914
|
+
await deleteWorkspaceForRequest(deps, {
|
|
891
915
|
accountId: grant.accountId,
|
|
892
916
|
workspaceId,
|
|
893
917
|
});
|
|
894
|
-
const deleted = await deleteWorkspaceIfQuiescent(deps.db, {
|
|
895
|
-
accountId: grant.accountId,
|
|
896
|
-
workspaceId,
|
|
897
|
-
...(deleteObserver ? { observer: deleteObserver } : {}),
|
|
898
|
-
});
|
|
899
|
-
if (deleted.status === "not_found") {
|
|
900
|
-
throw new HTTPException(404, { message: "workspace not found" });
|
|
901
|
-
}
|
|
902
|
-
if (deleted.status === "only_workspace") {
|
|
903
|
-
throw new HTTPException(409, {
|
|
904
|
-
message: "cannot delete the account's only workspace",
|
|
905
|
-
});
|
|
906
|
-
}
|
|
907
|
-
if (deleted.status === "active_sessions") {
|
|
908
|
-
throw new HTTPException(409, {
|
|
909
|
-
message: "stop the workspace's running sessions before deleting it",
|
|
910
|
-
});
|
|
911
|
-
}
|
|
912
|
-
if (deleted.status === "active_video_generations") {
|
|
913
|
-
throw new HTTPException(409, {
|
|
914
|
-
message: "wait for the workspace's active video generations to finish before deleting it",
|
|
915
|
-
});
|
|
916
|
-
}
|
|
917
|
-
if (deleted.status === "active_background_commands") {
|
|
918
|
-
throw new HTTPException(409, {
|
|
919
|
-
message: "pause or cancel the workspace's background commands before deleting it",
|
|
920
|
-
});
|
|
921
|
-
}
|
|
922
|
-
if (deleted.status === "live_sandboxes") {
|
|
923
|
-
throw new HTTPException(409, {
|
|
924
|
-
message: "wait for the workspace's active sandboxes to finish draining before deleting it",
|
|
925
|
-
});
|
|
926
|
-
}
|
|
927
|
-
if (deleted.status !== "deleted") {
|
|
928
|
-
throw new Error(`Unhandled workspace deletion outcome: ${deleted.status}`);
|
|
929
|
-
}
|
|
930
|
-
// The cleanup claims were inserted in the same transaction as the cascade.
|
|
931
|
-
// Try them immediately; failures are released to the replica-safe outbox
|
|
932
|
-
// pump, so a process crash or Temporal outage cannot orphan the schedules.
|
|
933
|
-
await processTemporalScheduleCleanupClaims(
|
|
934
|
-
{
|
|
935
|
-
db: deps.db,
|
|
936
|
-
deleteSchedule: async (temporalScheduleId) => {
|
|
937
|
-
await deps.workflowClient.deleteScheduledTaskSchedule({
|
|
938
|
-
temporalScheduleId,
|
|
939
|
-
});
|
|
940
|
-
},
|
|
941
|
-
...(deps.observability ? { observability: deps.observability } : {}),
|
|
942
|
-
},
|
|
943
|
-
deleted.temporalScheduleCleanups,
|
|
944
|
-
);
|
|
945
918
|
return c.body(null, 204);
|
|
946
919
|
});
|
|
947
920
|
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { deleteWorkspaceIfQuiescent, nestedPostgresSqlState } from "@opengeni/db";
|
|
2
|
+
import type { ApiRouteDeps } from "@opengeni/core";
|
|
3
|
+
import { HTTPException } from "hono/http-exception";
|
|
4
|
+
|
|
5
|
+
import { processTemporalScheduleCleanupClaims } from "./temporal-schedule-cleanup";
|
|
6
|
+
import { workspaceDeleteObserver } from "./workspace-delete-observability";
|
|
7
|
+
|
|
8
|
+
export async function deleteWorkspaceForRequest(
|
|
9
|
+
deps: ApiRouteDeps,
|
|
10
|
+
input: {
|
|
11
|
+
accountId: string;
|
|
12
|
+
workspaceId: string;
|
|
13
|
+
organizationAdministratorSubjectId?: string;
|
|
14
|
+
},
|
|
15
|
+
): Promise<void> {
|
|
16
|
+
const observer = workspaceDeleteObserver(deps.observability, input);
|
|
17
|
+
let deleted;
|
|
18
|
+
try {
|
|
19
|
+
deleted = await deleteWorkspaceIfQuiescent(deps.db, {
|
|
20
|
+
...input,
|
|
21
|
+
...(observer ? { observer } : {}),
|
|
22
|
+
});
|
|
23
|
+
} catch (error) {
|
|
24
|
+
const state = nestedPostgresSqlState(error);
|
|
25
|
+
if (state === "42501") {
|
|
26
|
+
throw new HTTPException(403, { message: "organization administration is not authorized" });
|
|
27
|
+
}
|
|
28
|
+
if (state === "P0002") {
|
|
29
|
+
throw new HTTPException(404, { message: "workspace not found" });
|
|
30
|
+
}
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (deleted.status === "not_found") {
|
|
35
|
+
throw new HTTPException(404, { message: "workspace not found" });
|
|
36
|
+
}
|
|
37
|
+
const conflicts = {
|
|
38
|
+
only_workspace: "cannot delete the account's only workspace",
|
|
39
|
+
active_sessions: "stop the workspace's running sessions before deleting it",
|
|
40
|
+
active_video_generations:
|
|
41
|
+
"wait for the workspace's active video generations to finish before deleting it",
|
|
42
|
+
active_background_commands:
|
|
43
|
+
"pause or cancel the workspace's background commands before deleting it",
|
|
44
|
+
live_sandboxes:
|
|
45
|
+
"wait for the workspace's active sandboxes to finish draining before deleting it",
|
|
46
|
+
} as const;
|
|
47
|
+
if (deleted.status in conflicts) {
|
|
48
|
+
throw new HTTPException(409, { message: conflicts[deleted.status as keyof typeof conflicts] });
|
|
49
|
+
}
|
|
50
|
+
if (deleted.status !== "deleted") {
|
|
51
|
+
throw new Error(`Unhandled workspace deletion outcome: ${deleted.status}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
await processTemporalScheduleCleanupClaims(
|
|
55
|
+
{
|
|
56
|
+
db: deps.db,
|
|
57
|
+
deleteSchedule: async (temporalScheduleId) => {
|
|
58
|
+
await deps.workflowClient.deleteScheduledTaskSchedule({ temporalScheduleId });
|
|
59
|
+
},
|
|
60
|
+
...(deps.observability ? { observability: deps.observability } : {}),
|
|
61
|
+
},
|
|
62
|
+
deleted.temporalScheduleCleanups,
|
|
63
|
+
);
|
|
64
|
+
}
|