@opengeni/api-router 2.6.4 → 2.10.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/access-grant-rls.d.ts +3 -0
- package/dist/app.d.ts +2 -1
- package/dist/app.js +3 -1
- package/dist/auth/organization-user-setup.d.ts +23 -0
- package/dist/{chunk-XTLI3CBH.js → chunk-XXH7DW34.js} +6866 -3554
- package/dist/chunk-XXH7DW34.js.map +1 -0
- package/dist/codemode.d.ts +6 -0
- package/dist/github-access.d.ts +25 -2
- package/dist/http/request-source.d.ts +14 -0
- package/dist/index.js +19 -3
- package/dist/index.js.map +1 -1
- package/dist/integrations/oauth-client.d.ts +2 -11
- package/dist/integrations/personal-github-repositories.d.ts +41 -2
- package/dist/integrations/slack-interactions.d.ts +1 -1
- package/dist/mcp/server.d.ts +158 -1
- package/dist/mcp/session-view.d.ts +56 -2
- package/dist/mcp/session-wait.d.ts +1 -1
- package/dist/mcp-oauth.d.ts +19 -0
- package/dist/model-catalog.d.ts +5 -31
- package/dist/routes/codex.d.ts +27 -2
- package/dist/routes/github.d.ts +2 -0
- package/dist/routes/organization-model-providers.d.ts +3 -0
- package/dist/routes/workspace-artifacts.d.ts +2 -1
- package/dist/work-discovery-observability.d.ts +1 -1
- package/dist/workspace-artifact-content.d.ts +28 -0
- package/dist/workspace-artifact-provenance.d.ts +7 -0
- package/dist/workspace-deletion.d.ts +6 -0
- package/dist/workspace-tool-gateway-observability.d.ts +17 -0
- package/dist/workspace-tool-gateway.d.ts +118 -0
- package/package.json +19 -18
- package/src/access-grant-rls.ts +40 -0
- package/src/app.ts +296 -53
- package/src/auth/organization-user-setup.ts +95 -5
- package/src/codemode.ts +33 -4
- package/src/github-access.ts +117 -1
- package/src/http/auth.ts +11 -0
- package/src/http/request-source.ts +37 -0
- package/src/http/sse.ts +15 -7
- package/src/index.ts +18 -2
- package/src/integrations/oauth-client.ts +168 -203
- package/src/integrations/personal-github-repositories.ts +238 -9
- package/src/integrations/slack-app-home.ts +2 -2
- package/src/integrations/slack-interactions.ts +77 -37
- 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/mcp/server.ts +504 -133
- package/src/mcp/session-view.ts +20 -1
- package/src/mcp/session-wait.ts +3 -0
- package/src/mcp-oauth.ts +539 -0
- package/src/model-catalog.ts +45 -337
- package/src/routes/automations.ts +178 -19
- package/src/routes/codex.ts +242 -73
- package/src/routes/connections.ts +271 -16
- package/src/routes/documents.ts +67 -19
- package/src/routes/files.ts +54 -6
- package/src/routes/github.ts +116 -15
- package/src/routes/organization-memberships.ts +59 -16
- 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 +72 -4
- package/src/routes/scheduled-tasks.ts +40 -13
- package/src/routes/sessions.ts +153 -65
- package/src/routes/supergrok.ts +3 -2
- package/src/routes/workspace-artifacts.ts +176 -67
- package/src/routes/workspaces.ts +405 -62
- package/src/sandbox/channel-a.ts +17 -4
- package/src/work-discovery-observability.ts +3 -3
- package/src/workspace-artifact-content.ts +163 -0
- package/src/workspace-artifact-provenance.ts +104 -0
- package/src/workspace-deletion.ts +64 -0
- package/src/workspace-tool-gateway-observability.ts +100 -0
- package/src/workspace-tool-gateway.ts +691 -0
- package/dist/chunk-XTLI3CBH.js.map +0 -1
package/src/routes/github.ts
CHANGED
|
@@ -4,6 +4,11 @@ import {
|
|
|
4
4
|
type GitHubInstallationBindingCandidate,
|
|
5
5
|
type GitHubInstallationBindingProof,
|
|
6
6
|
} from "@opengeni/contracts";
|
|
7
|
+
import {
|
|
8
|
+
ListGitHubRepositoryBranchesQuery,
|
|
9
|
+
VerifyPublicGitHubRepositoryRefRequest,
|
|
10
|
+
} from "@opengeni/contracts/github-repository-contracts";
|
|
11
|
+
import { parseCanonicalGitHubRepositoryUrl } from "@opengeni/contracts/github-repository";
|
|
7
12
|
import {
|
|
8
13
|
bindAuthorizedGitHubInstallationRepositories,
|
|
9
14
|
deleteGitHubInstallationBinding,
|
|
@@ -19,12 +24,14 @@ import {
|
|
|
19
24
|
GitHubAppApiError,
|
|
20
25
|
GitHubAppConfigurationError,
|
|
21
26
|
GitHubInstallationAuthorityError,
|
|
27
|
+
GitHubPublicRepositoryVerificationError,
|
|
22
28
|
githubAppMissingSettings,
|
|
23
29
|
githubOAuthAuthorizeUrl,
|
|
24
30
|
organizationAppManifestUrl,
|
|
25
31
|
personalAppManifestUrl,
|
|
26
32
|
readSignedState,
|
|
27
33
|
stateMaxAgeSeconds,
|
|
34
|
+
verifyPublicGitHubRepositoryRef,
|
|
28
35
|
type GitHubSignedStatePayload,
|
|
29
36
|
verifySignedState,
|
|
30
37
|
} from "@opengeni/github";
|
|
@@ -42,7 +49,9 @@ import {
|
|
|
42
49
|
} from "../github-browser-flow";
|
|
43
50
|
import {
|
|
44
51
|
githubBindingStatus,
|
|
52
|
+
GitHubRepositoryBranchAuthorityError,
|
|
45
53
|
listWorkspaceGitHubInstallationBindings,
|
|
54
|
+
listWorkspaceGitHubRepositoryBranches,
|
|
46
55
|
listWorkspaceGitHubRepositories,
|
|
47
56
|
} from "../github-access";
|
|
48
57
|
|
|
@@ -177,6 +186,65 @@ export function registerGitHubRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
177
186
|
}
|
|
178
187
|
});
|
|
179
188
|
|
|
189
|
+
app.post("/v1/workspaces/:workspaceId/github/public-repositories/verify", async (c) => {
|
|
190
|
+
const workspaceId = c.req.param("workspaceId");
|
|
191
|
+
const grant = await requireAccessGrant(c, deps, workspaceId);
|
|
192
|
+
requirePublicGitHubRepositoryVerificationPermission(grant);
|
|
193
|
+
const request = VerifyPublicGitHubRepositoryRefRequest.parse(await c.req.json());
|
|
194
|
+
let repository: ReturnType<typeof parseCanonicalGitHubRepositoryUrl>;
|
|
195
|
+
try {
|
|
196
|
+
repository = parseCanonicalGitHubRepositoryUrl(request.url);
|
|
197
|
+
} catch (error) {
|
|
198
|
+
throw new HTTPException(422, {
|
|
199
|
+
message: error instanceof Error ? error.message : "GitHub repository URL is invalid.",
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
try {
|
|
203
|
+
return c.json(
|
|
204
|
+
await verifyPublicGitHubRepositoryRef(
|
|
205
|
+
{ repository, ref: request.ref },
|
|
206
|
+
deps.githubAnonymousFetch ?? fetch,
|
|
207
|
+
),
|
|
208
|
+
);
|
|
209
|
+
} catch (error) {
|
|
210
|
+
if (error instanceof GitHubPublicRepositoryVerificationError) {
|
|
211
|
+
throw new HTTPException(error.code === "provider_unavailable" ? 503 : 422, {
|
|
212
|
+
message: error.message,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
throw new HTTPException(503, {
|
|
216
|
+
message: "GitHub public repository verification is temporarily unavailable.",
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
app.get(
|
|
222
|
+
"/v1/workspaces/:workspaceId/github/installations/:installationId/repositories/:repositoryId/branches",
|
|
223
|
+
async (c) => {
|
|
224
|
+
const workspaceId = c.req.param("workspaceId");
|
|
225
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "github:use");
|
|
226
|
+
const installationId = parsePositiveInteger(c.req.param("installationId"));
|
|
227
|
+
const repositoryId = parsePositiveInteger(c.req.param("repositoryId"));
|
|
228
|
+
if (installationId === null || repositoryId === null) {
|
|
229
|
+
throw new HTTPException(400, { message: "invalid GitHub repository identity" });
|
|
230
|
+
}
|
|
231
|
+
const query = ListGitHubRepositoryBranchesQuery.parse(c.req.query());
|
|
232
|
+
try {
|
|
233
|
+
return c.json(
|
|
234
|
+
await listWorkspaceGitHubRepositoryBranches(deps, {
|
|
235
|
+
accountId: grant.accountId,
|
|
236
|
+
workspaceId: grant.workspaceId,
|
|
237
|
+
installationId,
|
|
238
|
+
repositoryId,
|
|
239
|
+
query,
|
|
240
|
+
}),
|
|
241
|
+
);
|
|
242
|
+
} catch (error) {
|
|
243
|
+
throw githubRepositoryBranchesRouteError(error);
|
|
244
|
+
}
|
|
245
|
+
},
|
|
246
|
+
);
|
|
247
|
+
|
|
180
248
|
app.delete("/v1/workspaces/:workspaceId/github/installations/:installationId", async (c) => {
|
|
181
249
|
const workspaceId = c.req.param("workspaceId");
|
|
182
250
|
const grant = await requireAccessGrant(c, deps, workspaceId, "github:manage");
|
|
@@ -184,14 +252,11 @@ export function registerGitHubRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
184
252
|
if (installationId === null) {
|
|
185
253
|
throw new HTTPException(400, { message: "invalid GitHub installation id" });
|
|
186
254
|
}
|
|
187
|
-
|
|
255
|
+
await deleteGitHubInstallationBinding(db, {
|
|
188
256
|
accountId: grant.accountId,
|
|
189
257
|
workspaceId: grant.workspaceId,
|
|
190
258
|
installationId,
|
|
191
259
|
});
|
|
192
|
-
if (!deleted) {
|
|
193
|
-
throw new HTTPException(404, { message: "GitHub installation binding not found" });
|
|
194
|
-
}
|
|
195
260
|
return c.body(null, 204);
|
|
196
261
|
});
|
|
197
262
|
|
|
@@ -428,14 +493,6 @@ export function registerGitHubRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
428
493
|
if (candidates.length === 0) {
|
|
429
494
|
return redirectToGitHubInstallation(c, deps, selectionState);
|
|
430
495
|
}
|
|
431
|
-
if (candidates.length === 1) {
|
|
432
|
-
return redirectToExactGitHubAuthorization(
|
|
433
|
-
c,
|
|
434
|
-
deps,
|
|
435
|
-
selectionState,
|
|
436
|
-
candidates[0]!.installation.installationId,
|
|
437
|
-
);
|
|
438
|
-
}
|
|
439
496
|
setGitHubStateCookie(c, deps, selectionState);
|
|
440
497
|
return c.html(
|
|
441
498
|
githubInstallationChooserHtml(
|
|
@@ -757,6 +814,50 @@ function githubAuthorityHttpError(error: unknown): HTTPException {
|
|
|
757
814
|
return new HTTPException(502, { message: "GitHub authority verification failed" });
|
|
758
815
|
}
|
|
759
816
|
|
|
817
|
+
function githubRepositoryBranchesRouteError(error: unknown): Error {
|
|
818
|
+
if (error instanceof HTTPException) {
|
|
819
|
+
return error;
|
|
820
|
+
}
|
|
821
|
+
if (error instanceof GitHubRepositoryBranchAuthorityError) {
|
|
822
|
+
return error.code === "not_authorized"
|
|
823
|
+
? new HTTPException(404, {
|
|
824
|
+
message: "GitHub repository is not authorized for this workspace",
|
|
825
|
+
})
|
|
826
|
+
: new HTTPException(409, {
|
|
827
|
+
message: "GitHub repository authorization changed; refresh and try again",
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
if (error instanceof GitHubAppConfigurationError) {
|
|
831
|
+
return new HTTPException(409, { message: "GitHub App is not configured" });
|
|
832
|
+
}
|
|
833
|
+
if (error instanceof GitHubAppApiError) {
|
|
834
|
+
if (error.status === 429) {
|
|
835
|
+
return new HTTPException(503, { message: "GitHub is temporarily rate limited" });
|
|
836
|
+
}
|
|
837
|
+
if (error.status === 401 || error.status === 403 || error.status === 404) {
|
|
838
|
+
return new HTTPException(409, {
|
|
839
|
+
message: "GitHub repository access changed; refresh and try again",
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
return new HTTPException(502, { message: "GitHub branch discovery is unavailable" });
|
|
843
|
+
}
|
|
844
|
+
return new HTTPException(502, { message: "GitHub branch discovery is unavailable" });
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
export function requirePublicGitHubRepositoryVerificationPermission(
|
|
848
|
+
grant: Pick<AccessGrant, "permissions">,
|
|
849
|
+
): void {
|
|
850
|
+
if (
|
|
851
|
+
hasPermission(grant.permissions, "sessions:create") ||
|
|
852
|
+
hasPermission(grant.permissions, "sessions:control")
|
|
853
|
+
) {
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
856
|
+
throw new HTTPException(403, {
|
|
857
|
+
message: "missing permission: sessions:create or sessions:control",
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
|
|
760
861
|
function isSecureRequest(c: Context, deps: ApiRouteDeps): boolean {
|
|
761
862
|
return (
|
|
762
863
|
deps.settings.publicBaseUrl?.startsWith("https://") ||
|
|
@@ -783,15 +884,15 @@ function githubInstallationChooserHtml(
|
|
|
783
884
|
): string {
|
|
784
885
|
const action = `${baseUrl}/v1/workspaces/${encodeURIComponent(workspaceId)}/github/installations/select`;
|
|
785
886
|
const options = candidates
|
|
786
|
-
.map(({ installation, authorityKind }) => {
|
|
887
|
+
.map(({ installation, authorityKind }, index) => {
|
|
787
888
|
const account = escapeHtml(
|
|
788
889
|
installation.accountLogin ?? `installation ${installation.installationId}`,
|
|
789
890
|
);
|
|
790
891
|
const label = authorityKind === "personal_owner" ? "Personal account" : "Organization owner";
|
|
791
|
-
return `<label class="option"><input type="radio" name="installation_id" value="${installation.installationId}" required><span><strong>${account}</strong><small>${label}</small></span></label>`;
|
|
892
|
+
return `<label class="option"><input type="radio" name="installation_id" value="${installation.installationId}" required${candidates.length === 1 && index === 0 ? " checked" : ""}><span><strong>${account}</strong><small>${label}</small></span></label>`;
|
|
792
893
|
})
|
|
793
894
|
.join("");
|
|
794
|
-
return `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Choose GitHub installation</title><style>body{font-family:system-ui,sans-serif;margin:0;min-height:100vh;display:grid;place-items:center;background:#0b0b0d;color:#f4f4f5}main{width:min(640px,calc(100vw - 32px));border:1px solid #27272a;border-radius:12px;padding:28px;background:#111114}h1{margin:0 0 10px;font-size:24px}p{margin:0 0 18px;color:#d4d4d8}.options{display:grid;gap:8px;margin-bottom:18px}.option{display:flex;align-items:center;gap:12px;border:1px solid #3f3f46;border-radius:8px;padding:12px;cursor:pointer}.option span{display:grid;gap:2px}.option small{color:#a1a1aa}button{min-height:38px;border-radius:7px;border:1px solid #3f3f46;padding:0 14px;background:#f4f4f5;color:#09090b;font:600 14px system-ui,sans-serif;cursor:pointer}.secondary{margin-left:8px;background:transparent;color:#f4f4f5}</style></head><body><main><h1>Choose a GitHub account</h1><p>
|
|
895
|
+
return `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Choose GitHub installation</title><style>body{font-family:system-ui,sans-serif;margin:0;min-height:100vh;display:grid;place-items:center;background:#0b0b0d;color:#f4f4f5}main{width:min(640px,calc(100vw - 32px));border:1px solid #27272a;border-radius:12px;padding:28px;background:#111114}h1{margin:0 0 10px;font-size:24px}p{margin:0 0 18px;color:#d4d4d8}.options{display:grid;gap:8px;margin-bottom:18px}.option{display:flex;align-items:center;gap:12px;border:1px solid #3f3f46;border-radius:8px;padding:12px;cursor:pointer}.option span{display:grid;gap:2px}.option small{color:#a1a1aa}button{min-height:38px;border-radius:7px;border:1px solid #3f3f46;padding:0 14px;background:#f4f4f5;color:#09090b;font:600 14px system-ui,sans-serif;cursor:pointer}.secondary{margin-left:8px;background:transparent;color:#f4f4f5}</style></head><body><main><h1>Choose a GitHub account</h1><p>These accounts already have the OpenGeni GitHub App installed and GitHub proved you are the personal owner or an active organization owner.</p><form method="get" action="${escapeHtml(action)}"><input type="hidden" name="state" value="${escapeHtml(state)}"><div class="options">${options}</div><button type="submit">Connect selected</button><button class="secondary" type="submit" name="installation_id" value="new" formnovalidate>Install on another account</button></form></main></body></html>`;
|
|
795
896
|
}
|
|
796
897
|
|
|
797
898
|
function githubSetupPendingHtml(): string {
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
AcceptOrganizationInvitationRequest,
|
|
3
|
+
CreateAdditionalOrganizationRequest,
|
|
4
|
+
CreateAdditionalOrganizationResponse,
|
|
3
5
|
CreateOrganizationRequest,
|
|
4
6
|
CreateOrganizationResponse,
|
|
5
7
|
CreateOrganizationWorkspaceRequest,
|
|
@@ -40,6 +42,7 @@ import {
|
|
|
40
42
|
acceptOrganizationInvitation,
|
|
41
43
|
bindPendingOrganizationInvitationsForVerifiedEmail,
|
|
42
44
|
claimOrganizationUserSetupDelivery,
|
|
45
|
+
createAdditionalManagedOrganization,
|
|
43
46
|
createManagedOrganization,
|
|
44
47
|
createOrganizationWorkspace,
|
|
45
48
|
createOrganizationInvitation,
|
|
@@ -68,12 +71,11 @@ import {
|
|
|
68
71
|
import type { Context, Hono } from "hono";
|
|
69
72
|
import { HTTPException } from "hono/http-exception";
|
|
70
73
|
import { z } from "zod";
|
|
74
|
+
import { deleteWorkspaceForRequest } from "../workspace-deletion";
|
|
71
75
|
|
|
72
76
|
import {
|
|
73
77
|
assertOrganizationUserSetupDeliveryConfigured,
|
|
74
|
-
|
|
75
|
-
organizationUserSetupPayloadDigest,
|
|
76
|
-
renderOrganizationUserSetupEmail,
|
|
78
|
+
resolveOrganizationUserSetupDeliveryEmail,
|
|
77
79
|
} from "../auth/organization-user-setup";
|
|
78
80
|
|
|
79
81
|
const OrganizationId = z.string().uuid();
|
|
@@ -117,7 +119,9 @@ async function requireOrganizationAdministrator(
|
|
|
117
119
|
);
|
|
118
120
|
return { subjectId };
|
|
119
121
|
}
|
|
120
|
-
throw new HTTPException(401, {
|
|
122
|
+
throw new HTTPException(401, {
|
|
123
|
+
message: "organization administrator session required",
|
|
124
|
+
});
|
|
121
125
|
}
|
|
122
126
|
|
|
123
127
|
async function parseBody<S extends z.ZodType>(context: Context, schema: S): Promise<z.infer<S>> {
|
|
@@ -136,8 +140,14 @@ function parseId(schema: z.ZodString, value: string, label: string): string {
|
|
|
136
140
|
return parsed.data;
|
|
137
141
|
}
|
|
138
142
|
|
|
139
|
-
function rethrowMembershipError(error: unknown): never {
|
|
140
|
-
const
|
|
143
|
+
function rethrowMembershipError(error: unknown, resourceLimitMessage?: string): never {
|
|
144
|
+
const sqlState = nestedPostgresSqlState(error);
|
|
145
|
+
if (sqlState === "54000" && resourceLimitMessage) {
|
|
146
|
+
throw new HTTPException(409, {
|
|
147
|
+
message: resourceLimitMessage,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
const status = organizationMembershipHttpStatus(sqlState);
|
|
141
151
|
if (status !== null) {
|
|
142
152
|
throw new HTTPException(status, {
|
|
143
153
|
message:
|
|
@@ -173,6 +183,25 @@ export function registerOrganizationMembershipRoutes(app: Hono, deps: ApiRouteDe
|
|
|
173
183
|
}
|
|
174
184
|
});
|
|
175
185
|
|
|
186
|
+
app.post("/v1/organizations/additional", async (context) => {
|
|
187
|
+
const { session, subjectId } = await requireManagedHuman(context, deps);
|
|
188
|
+
const payload = await parseBody(context, CreateAdditionalOrganizationRequest);
|
|
189
|
+
try {
|
|
190
|
+
return context.json(
|
|
191
|
+
CreateAdditionalOrganizationResponse.parse(
|
|
192
|
+
await createAdditionalManagedOrganization(deps.db, {
|
|
193
|
+
subjectId,
|
|
194
|
+
subjectLabel: session.user.email || session.user.name,
|
|
195
|
+
...payload,
|
|
196
|
+
}),
|
|
197
|
+
),
|
|
198
|
+
201,
|
|
199
|
+
);
|
|
200
|
+
} catch (error) {
|
|
201
|
+
rethrowMembershipError(error, "additional organization limit reached");
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
|
|
176
205
|
app.get("/v1/organization-memberships", async (context) => {
|
|
177
206
|
const { session } = await requireManagedHuman(context, deps);
|
|
178
207
|
try {
|
|
@@ -328,6 +357,22 @@ export function registerOrganizationMembershipRoutes(app: Hono, deps: ApiRouteDe
|
|
|
328
357
|
}
|
|
329
358
|
});
|
|
330
359
|
|
|
360
|
+
app.delete("/v1/organizations/:organizationId/workspaces/:workspaceId", async (context) => {
|
|
361
|
+
const organizationId = parseId(
|
|
362
|
+
OrganizationId,
|
|
363
|
+
context.req.param("organizationId"),
|
|
364
|
+
"organization id",
|
|
365
|
+
);
|
|
366
|
+
const { subjectId } = await requireOrganizationAdministrator(context, deps, organizationId);
|
|
367
|
+
const workspaceId = parseId(WorkspaceId, context.req.param("workspaceId"), "workspace id");
|
|
368
|
+
await deleteWorkspaceForRequest(deps, {
|
|
369
|
+
accountId: organizationId,
|
|
370
|
+
workspaceId,
|
|
371
|
+
organizationAdministratorSubjectId: subjectId,
|
|
372
|
+
});
|
|
373
|
+
return context.body(null, 204);
|
|
374
|
+
});
|
|
375
|
+
|
|
331
376
|
app.patch(
|
|
332
377
|
"/v1/organizations/:organizationId/workspaces/:workspaceId/settings",
|
|
333
378
|
async (context) => {
|
|
@@ -768,18 +813,18 @@ async function deliverOrganizationUserSetup(
|
|
|
768
813
|
) {
|
|
769
814
|
const claim = await claimOrganizationUserSetupDelivery(deps.db, input);
|
|
770
815
|
if (!claim.claimed) return claim.delivery;
|
|
771
|
-
const
|
|
816
|
+
const prepared = await resolveOrganizationUserSetupDeliveryEmail(deps.settings, {
|
|
772
817
|
invitationId: claim.invitationId,
|
|
773
818
|
deliveryId: claim.delivery.id,
|
|
774
|
-
});
|
|
775
|
-
const message = renderOrganizationUserSetupEmail({
|
|
776
819
|
senderEmail: deps.managedEmailTransport.sender,
|
|
777
820
|
recipientEmail: claim.recipientEmail,
|
|
778
821
|
recipientName: claim.recipientName,
|
|
779
822
|
organizationName: claim.organizationName,
|
|
780
823
|
organizationRole: claim.organizationRole,
|
|
781
824
|
sharedWorkspaceAccess: claim.sharedWorkspaceAccess,
|
|
782
|
-
|
|
825
|
+
providerIdempotencyScope: deps.managedEmailTransport.idempotency.scope,
|
|
826
|
+
frozenTransport: claim.setupTokenTransport,
|
|
827
|
+
frozenPayloadDigest: claim.payloadDigest,
|
|
783
828
|
});
|
|
784
829
|
await prepareOrganizationUserSetupDelivery(deps.db, {
|
|
785
830
|
organizationId: input.organizationId,
|
|
@@ -787,11 +832,9 @@ async function deliverOrganizationUserSetup(
|
|
|
787
832
|
deliveryId: claim.delivery.id,
|
|
788
833
|
attemptId: claim.attemptId,
|
|
789
834
|
claimHolderId: claim.claimHolderId,
|
|
790
|
-
tokenDigest:
|
|
791
|
-
payloadDigest:
|
|
792
|
-
|
|
793
|
-
providerIdempotencyScope: deps.managedEmailTransport.idempotency.scope,
|
|
794
|
-
}),
|
|
835
|
+
tokenDigest: prepared.tokenDigest,
|
|
836
|
+
payloadDigest: prepared.payloadDigest,
|
|
837
|
+
setupTokenTransport: prepared.transport,
|
|
795
838
|
providerIdempotencyScope: deps.managedEmailTransport.idempotency.scope,
|
|
796
839
|
providerIdempotencyRetentionSeconds: deps.managedEmailTransport.idempotency.retentionSeconds,
|
|
797
840
|
});
|
|
@@ -801,7 +844,7 @@ async function deliverOrganizationUserSetup(
|
|
|
801
844
|
try {
|
|
802
845
|
outcome = await deps.managedEmailTransport.send({
|
|
803
846
|
kind: "organization_user_setup",
|
|
804
|
-
...message,
|
|
847
|
+
...prepared.message,
|
|
805
848
|
idempotencyKey: claim.providerKey,
|
|
806
849
|
});
|
|
807
850
|
} catch {
|
|
@@ -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) => {
|