@opengeni/api-router 0.30.3 → 2.2.0-canary.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api-websocket.d.ts +18 -0
- package/dist/app.js +1 -1
- package/dist/auth/managed-auth.d.ts +6 -0
- package/dist/{chunk-WCXHI3FF.js → chunk-3TP54PPX.js} +26692 -15389
- package/dist/chunk-3TP54PPX.js.map +1 -0
- package/dist/codemode.d.ts +8 -1
- package/dist/company-brain-okf.d.ts +24 -0
- package/dist/connected-machine-computer-access.d.ts +11 -0
- package/dist/connection-authority-owner.d.ts +59 -0
- package/dist/connection-ownership.d.ts +47 -0
- package/dist/controller-data-plane.d.ts +16 -0
- package/dist/editable-artifact-websocket.d.ts +5 -15
- package/dist/editable-artifact-workspace-files.d.ts +1 -1
- package/dist/http/api-error.d.ts +10 -0
- package/dist/http/cors.d.ts +5 -0
- package/dist/http/interaction-control-error.d.ts +13 -0
- package/dist/http/sse.d.ts +2 -3
- package/dist/index.d.ts +5 -2
- package/dist/index.js +326 -82
- package/dist/index.js.map +1 -1
- package/dist/integrations/atlassian.d.ts +17 -0
- package/dist/integrations/fiken.d.ts +7 -2
- package/dist/integrations/google-drive.d.ts +18 -0
- package/dist/integrations/oauth-client.d.ts +22 -3
- package/dist/integrations/oauth-profiles.d.ts +195 -0
- package/dist/integrations/personal-github-repositories.d.ts +45 -0
- package/dist/integrations/personal-github.d.ts +40 -0
- package/dist/integrations/pr-review-provider.d.ts +22 -0
- package/dist/integrations/provider-oauth.d.ts +8 -0
- package/dist/integrations/slack-app-home.d.ts +24 -0
- package/dist/integrations/slack-bot.d.ts +157 -9
- package/dist/integrations/slack-interactions.d.ts +39 -5
- package/dist/integrations/slack-routing.d.ts +120 -0
- package/dist/integrations/social-oauth.d.ts +5 -0
- package/dist/interaction-frame-proxy.d.ts +68 -0
- package/dist/mcp/company-brain-governed-writes.d.ts +26 -0
- package/dist/mcp/company-profile-agent-admin.d.ts +44 -0
- package/dist/mcp/documents.d.ts +1 -0
- package/dist/mcp/remember.d.ts +28 -0
- package/dist/mcp/request-abort.d.ts +19 -0
- package/dist/mcp/scheduled-task-view.d.ts +4 -4
- package/dist/mcp/server.d.ts +15 -2
- package/dist/mcp/session-view.d.ts +4 -0
- package/dist/mcp/session-wait.d.ts +137 -0
- package/dist/routes/automations.d.ts +4 -0
- package/dist/routes/billing.d.ts +5 -0
- package/dist/routes/browser-sessions.d.ts +4 -1
- package/dist/routes/company-brain.d.ts +3 -0
- package/dist/routes/company-profile.d.ts +2 -1
- package/dist/routes/connection-authorities.d.ts +3 -0
- package/dist/routes/personal-github-git-broker.d.ts +29 -0
- package/dist/routes/personal-github.d.ts +3 -0
- package/dist/routes/pr-review.d.ts +3 -0
- package/dist/routes/sessions.d.ts +1 -0
- package/dist/routes/user-resource-authorities.d.ts +3 -0
- package/dist/routes/workspace-capture.d.ts +10 -2
- package/dist/routes/workspace-learning.d.ts +3 -0
- package/dist/routes/workspaces.d.ts +19 -0
- package/dist/sandbox/channel-a.d.ts +11 -1
- package/dist/sandbox/connection-authority.d.ts +3 -0
- package/dist/sandbox/enrollment.d.ts +2 -0
- package/dist/sandbox/machines.d.ts +2 -2
- package/dist/sandbox/metrics-ingestion.d.ts +10 -20
- package/dist/sandbox/viewer.d.ts +24 -1
- package/dist/sandbox-file-artifacts.d.ts +11 -0
- package/dist/scheduled-task-deletion.d.ts +14 -0
- package/dist/slack-reaction-files.d.ts +1 -1
- package/dist/temporal-schedule-cleanup.d.ts +1 -0
- package/package.json +20 -18
- package/src/api-websocket.ts +23 -0
- package/src/app.ts +161 -23
- package/src/auth/managed-auth.ts +25 -3
- package/src/codemode.ts +53 -23
- package/src/codex-realtime.ts +8 -2
- package/src/company-brain-okf.ts +340 -0
- package/src/connected-machine-computer-access.ts +33 -0
- package/src/connection-authority-owner.ts +61 -0
- package/src/connection-ownership.ts +180 -0
- package/src/controller-data-plane.ts +47 -0
- package/src/editable-artifact-native-kernel.ts +26 -15
- package/src/editable-artifact-office-import.ts +28 -3
- package/src/editable-artifact-production.ts +13 -7
- package/src/editable-artifact-websocket.ts +11 -18
- package/src/editable-artifact-workspace-files.ts +31 -6
- package/src/http/api-error.ts +28 -0
- package/src/http/auth.ts +4 -0
- package/src/http/cors.ts +35 -0
- package/src/http/interaction-control-error.ts +164 -0
- package/src/http/sse.ts +112 -66
- package/src/index.ts +46 -6
- package/src/integrations/atlassian.ts +146 -35
- package/src/integrations/fiken.ts +102 -22
- package/src/integrations/google-drive.ts +301 -92
- package/src/integrations/oauth-client.ts +272 -143
- package/src/integrations/oauth-profiles.ts +477 -0
- package/src/integrations/personal-github-repositories.ts +445 -0
- package/src/integrations/personal-github.ts +705 -0
- package/src/integrations/pr-review-provider.ts +246 -0
- package/src/integrations/provider-oauth.ts +121 -5
- package/src/integrations/slack-app-home.ts +300 -0
- package/src/integrations/slack-bot.ts +342 -57
- package/src/integrations/slack-interactions.ts +1898 -259
- package/src/integrations/slack-routing.ts +337 -0
- package/src/integrations/social-oauth.ts +25 -0
- package/src/interaction-frame-proxy.ts +409 -0
- package/src/mcp/company-brain-governed-writes.ts +262 -0
- package/src/mcp/company-profile-agent-admin.ts +205 -0
- package/src/mcp/documents.ts +37 -6
- package/src/mcp/files.ts +16 -1
- package/src/mcp/remember.ts +181 -0
- package/src/mcp/request-abort.ts +44 -0
- package/src/mcp/scheduled-task-view.ts +1 -0
- package/src/mcp/server.ts +719 -369
- package/src/mcp/session-view.ts +54 -0
- package/src/mcp/session-wait.ts +554 -0
- package/src/model-catalog.ts +20 -12
- package/src/routes/api-integrations.ts +4 -0
- package/src/routes/api-keys.ts +1 -0
- package/src/routes/automations.ts +534 -0
- package/src/routes/billing.ts +57 -1
- package/src/routes/browser-sessions.ts +329 -79
- package/src/routes/channels.ts +17 -1
- package/src/routes/codex.ts +50 -0
- package/src/routes/company-brain.ts +367 -0
- package/src/routes/company-profile.ts +1 -1
- package/src/routes/computer-sessions.ts +320 -93
- package/src/routes/connection-authorities.ts +139 -0
- package/src/routes/connections.ts +144 -34
- package/src/routes/documents.ts +353 -4
- package/src/routes/editable-artifacts.ts +11 -3
- package/src/routes/enrollments.ts +116 -26
- package/src/routes/environments.ts +127 -55
- package/src/routes/files.ts +72 -11
- package/src/routes/install.ts +82 -28
- package/src/routes/integration-facets.ts +29 -0
- package/src/routes/interaction-resources.ts +20 -2
- package/src/routes/machines.ts +275 -13
- package/src/routes/organization-memberships.ts +717 -28
- package/src/routes/packs.ts +4 -3
- package/src/routes/personal-github-git-broker.ts +785 -0
- package/src/routes/personal-github.ts +319 -0
- package/src/routes/pr-review.ts +531 -0
- package/src/routes/rigs.ts +104 -39
- package/src/routes/scheduled-tasks.ts +19 -21
- package/src/routes/sessions.ts +775 -38
- package/src/routes/social.ts +14 -2
- package/src/routes/supergrok.ts +23 -4
- package/src/routes/transcription-recordings.ts +8 -2
- package/src/routes/user-resource-authorities.ts +138 -0
- package/src/routes/workspace-artifacts.ts +4 -1
- package/src/routes/workspace-capture.ts +21 -0
- package/src/routes/workspace-learning.ts +228 -0
- package/src/routes/workspaces.ts +59 -6
- package/src/sandbox/auth-callout.ts +38 -12
- package/src/sandbox/channel-a.ts +242 -19
- package/src/sandbox/connection-authority.ts +3 -0
- package/src/sandbox/enrollment.ts +15 -3
- package/src/sandbox/machines.ts +186 -64
- package/src/sandbox/metrics-ingestion.ts +220 -58
- package/src/sandbox/viewer.ts +243 -19
- package/src/sandbox-file-artifacts.ts +329 -0
- package/src/scheduled-task-deletion.ts +127 -0
- package/src/slack-reaction-files.ts +16 -5
- package/src/temporal-schedule-cleanup.ts +13 -0
- package/dist/chunk-WCXHI3FF.js.map +0 -1
|
@@ -0,0 +1,785 @@
|
|
|
1
|
+
import {
|
|
2
|
+
PERSONAL_GITHUB_API_ORIGIN,
|
|
3
|
+
PERSONAL_GITHUB_PROVIDER_DOMAIN,
|
|
4
|
+
PersonalGitHubConnectionMetadata,
|
|
5
|
+
isPersonalGitHubConnection,
|
|
6
|
+
type PersonalGitHubRepositoryPermissions,
|
|
7
|
+
} from "@opengeni/contracts/personal-github";
|
|
8
|
+
import type { McpPersonalConnectionDelegation } from "@opengeni/contracts";
|
|
9
|
+
import type { ApiRouteDeps } from "@opengeni/core";
|
|
10
|
+
import {
|
|
11
|
+
buildConnectionTokenResolver,
|
|
12
|
+
getConnectionMetadata,
|
|
13
|
+
getPersonalGitHubRepositorySelectionState,
|
|
14
|
+
getSessionAuthorityProjection,
|
|
15
|
+
getSessionTurnForAttempt,
|
|
16
|
+
resolveAcceptedConnectionUse,
|
|
17
|
+
} from "@opengeni/db";
|
|
18
|
+
import {
|
|
19
|
+
openPersonalGitHubGitBrokerClaims,
|
|
20
|
+
personalGitHubGitBrokerRouteId,
|
|
21
|
+
type PersonalGitHubGitBrokerClaims,
|
|
22
|
+
type PersonalGitHubGitBrokerRepositoryClaim,
|
|
23
|
+
} from "@opengeni/github";
|
|
24
|
+
import { readResponseTextBounded } from "@opengeni/network";
|
|
25
|
+
import type { Hono } from "hono";
|
|
26
|
+
import { randomUUID } from "node:crypto";
|
|
27
|
+
import JSONBig from "json-bigint";
|
|
28
|
+
|
|
29
|
+
const PERSONAL_GITHUB_SERVER_ID = "github:personal";
|
|
30
|
+
const GITHUB_API_VERSION = "2022-11-28";
|
|
31
|
+
const GITHUB_API_RESPONSE_MAX_BYTES = 256 * 1024;
|
|
32
|
+
const GIT_BROKER_IDLE_TIMEOUT_MS = 60_000;
|
|
33
|
+
const GIT_BROKER_AUTHORIZATION_MAX_BYTES = 8 * 1024;
|
|
34
|
+
|
|
35
|
+
export const PERSONAL_GITHUB_GIT_BROKER_PATH_PREFIX = "/v1/git/personal/";
|
|
36
|
+
|
|
37
|
+
type BrokerOperation = "info_refs" | "upload_pack" | "receive_pack";
|
|
38
|
+
|
|
39
|
+
type BrokerRepository = Omit<PersonalGitHubGitBrokerRepositoryClaim, "routeId">;
|
|
40
|
+
|
|
41
|
+
type BrokerAuthority = {
|
|
42
|
+
claims: PersonalGitHubGitBrokerClaims;
|
|
43
|
+
repository: BrokerRepository;
|
|
44
|
+
providerPrincipalId: string;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export type PersonalGitHubGitBrokerServices = {
|
|
48
|
+
openClaims: (token: string) => PersonalGitHubGitBrokerClaims | null;
|
|
49
|
+
resolveAuthority: (
|
|
50
|
+
claims: PersonalGitHubGitBrokerClaims,
|
|
51
|
+
routeId: string,
|
|
52
|
+
) => Promise<BrokerAuthority>;
|
|
53
|
+
authorizeProviderRequest: (
|
|
54
|
+
authority: BrokerAuthority,
|
|
55
|
+
destinationUrl: string,
|
|
56
|
+
operation: "identity_verify" | "repository_verify" | BrokerOperation,
|
|
57
|
+
) => Promise<string>;
|
|
58
|
+
fetch: typeof fetch;
|
|
59
|
+
idleTimeoutMs?: number;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Register the three closed Git smart-HTTP operations. These routes are not a
|
|
64
|
+
* product-auth bypass: their only credential is the short-lived encrypted,
|
|
65
|
+
* exact-attempt broker bearer and every physical provider request is fenced
|
|
66
|
+
* again against current database authority.
|
|
67
|
+
*/
|
|
68
|
+
export function registerPersonalGitHubGitBrokerRoutes(
|
|
69
|
+
app: Hono,
|
|
70
|
+
deps: ApiRouteDeps,
|
|
71
|
+
services = personalGitHubGitBrokerServices(deps),
|
|
72
|
+
): void {
|
|
73
|
+
app.get(`${PERSONAL_GITHUB_GIT_BROKER_PATH_PREFIX}:routeId/info/refs`, (c) =>
|
|
74
|
+
handlePersonalGitHubGitBrokerRequest(c.req.raw, c.req.param("routeId"), "info_refs", services),
|
|
75
|
+
);
|
|
76
|
+
app.post(`${PERSONAL_GITHUB_GIT_BROKER_PATH_PREFIX}:routeId/git-upload-pack`, (c) =>
|
|
77
|
+
handlePersonalGitHubGitBrokerRequest(
|
|
78
|
+
c.req.raw,
|
|
79
|
+
c.req.param("routeId"),
|
|
80
|
+
"upload_pack",
|
|
81
|
+
services,
|
|
82
|
+
),
|
|
83
|
+
);
|
|
84
|
+
app.post(`${PERSONAL_GITHUB_GIT_BROKER_PATH_PREFIX}:routeId/git-receive-pack`, (c) =>
|
|
85
|
+
handlePersonalGitHubGitBrokerRequest(
|
|
86
|
+
c.req.raw,
|
|
87
|
+
c.req.param("routeId"),
|
|
88
|
+
"receive_pack",
|
|
89
|
+
services,
|
|
90
|
+
),
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function isPersonalGitHubGitBrokerPath(pathname: string): boolean {
|
|
95
|
+
return /^\/v1\/git\/personal\/[A-Za-z0-9_-]{43}\/(?:info\/refs|git-upload-pack|git-receive-pack)$/u.test(
|
|
96
|
+
pathname,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function isPersonalGitHubGitBrokerRequest(method: string, pathname: string): boolean {
|
|
101
|
+
const normalizedMethod = method.toUpperCase();
|
|
102
|
+
if (
|
|
103
|
+
normalizedMethod === "GET" &&
|
|
104
|
+
/^\/v1\/git\/personal\/[A-Za-z0-9_-]{43}\/info\/refs$/u.test(pathname)
|
|
105
|
+
) {
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
return (
|
|
109
|
+
normalizedMethod === "POST" &&
|
|
110
|
+
/^\/v1\/git\/personal\/[A-Za-z0-9_-]{43}\/git-(?:upload|receive)-pack$/u.test(pathname)
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export async function handlePersonalGitHubGitBrokerRequest(
|
|
115
|
+
request: Request,
|
|
116
|
+
routeId: string,
|
|
117
|
+
operation: BrokerOperation,
|
|
118
|
+
services: PersonalGitHubGitBrokerServices,
|
|
119
|
+
): Promise<Response> {
|
|
120
|
+
if (!/^[A-Za-z0-9_-]{43}$/u.test(routeId) || !validGitRequestShape(request, operation)) {
|
|
121
|
+
return brokerError(404, "Git repository route not found.");
|
|
122
|
+
}
|
|
123
|
+
const token = brokerBearer(request.headers.get("authorization"));
|
|
124
|
+
const claims = token ? services.openClaims(token) : null;
|
|
125
|
+
if (!claims) return brokerError(401, "Git broker authorization is unavailable.");
|
|
126
|
+
|
|
127
|
+
let authority: BrokerAuthority;
|
|
128
|
+
try {
|
|
129
|
+
authority = await services.resolveAuthority(claims, routeId);
|
|
130
|
+
} catch {
|
|
131
|
+
return brokerError(401, "Git broker authorization is no longer current.");
|
|
132
|
+
}
|
|
133
|
+
if (authority.repository.access !== "write" && requestsReceivePack(request, operation)) {
|
|
134
|
+
return brokerError(403, "This repository is not authorized for Git pushes.");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const userApiUrl = new URL("/user", PERSONAL_GITHUB_API_ORIGIN);
|
|
138
|
+
let userAuthorization: string;
|
|
139
|
+
try {
|
|
140
|
+
userAuthorization = await services.authorizeProviderRequest(
|
|
141
|
+
authority,
|
|
142
|
+
userApiUrl.toString(),
|
|
143
|
+
"identity_verify",
|
|
144
|
+
);
|
|
145
|
+
} catch {
|
|
146
|
+
return brokerError(401, "Git broker authorization is no longer current.");
|
|
147
|
+
}
|
|
148
|
+
if (
|
|
149
|
+
!(await fetchLiveProviderUser(
|
|
150
|
+
services.fetch,
|
|
151
|
+
userApiUrl,
|
|
152
|
+
userAuthorization,
|
|
153
|
+
authority.providerPrincipalId,
|
|
154
|
+
))
|
|
155
|
+
) {
|
|
156
|
+
return brokerError(403, "The connected GitHub identity is no longer available.");
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const repositoryApiUrl = new URL(
|
|
160
|
+
`/repositories/${authority.repository.repositoryId}`,
|
|
161
|
+
PERSONAL_GITHUB_API_ORIGIN,
|
|
162
|
+
);
|
|
163
|
+
let repositoryAuthorization: string;
|
|
164
|
+
try {
|
|
165
|
+
repositoryAuthorization = await services.authorizeProviderRequest(
|
|
166
|
+
authority,
|
|
167
|
+
repositoryApiUrl.toString(),
|
|
168
|
+
"repository_verify",
|
|
169
|
+
);
|
|
170
|
+
} catch {
|
|
171
|
+
return brokerError(401, "Git broker authorization is no longer current.");
|
|
172
|
+
}
|
|
173
|
+
const repositoryLive = await fetchLiveRepository(
|
|
174
|
+
services.fetch,
|
|
175
|
+
repositoryApiUrl,
|
|
176
|
+
repositoryAuthorization,
|
|
177
|
+
authority.repository,
|
|
178
|
+
);
|
|
179
|
+
if (!repositoryLive) {
|
|
180
|
+
return brokerError(403, "The selected GitHub repository is no longer available.");
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const upstreamUrl = githubGitUrl(authority.repository, operation, request.url);
|
|
184
|
+
let gitAuthorization: string;
|
|
185
|
+
try {
|
|
186
|
+
gitAuthorization = await services.authorizeProviderRequest(
|
|
187
|
+
authority,
|
|
188
|
+
upstreamUrl.toString(),
|
|
189
|
+
operation,
|
|
190
|
+
);
|
|
191
|
+
} catch {
|
|
192
|
+
return brokerError(401, "Git broker authorization is no longer current.");
|
|
193
|
+
}
|
|
194
|
+
const providerToken = bearerToken(gitAuthorization);
|
|
195
|
+
if (!providerToken) {
|
|
196
|
+
return brokerError(401, "Git broker authorization is unavailable.");
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const controller = new AbortController();
|
|
200
|
+
const deadline = idleDeadline(controller, services.idleTimeoutMs ?? GIT_BROKER_IDLE_TIMEOUT_MS);
|
|
201
|
+
const body = operation === "info_refs" ? undefined : request.body;
|
|
202
|
+
const streamedBody = body
|
|
203
|
+
? streamWithActivity(
|
|
204
|
+
body,
|
|
205
|
+
deadline.activity,
|
|
206
|
+
() => undefined,
|
|
207
|
+
() => controller.abort(),
|
|
208
|
+
)
|
|
209
|
+
: undefined;
|
|
210
|
+
try {
|
|
211
|
+
const headers = upstreamRequestHeaders(request, operation, providerToken);
|
|
212
|
+
const init: RequestInit & { duplex?: "half" } = {
|
|
213
|
+
method: operation === "info_refs" ? "GET" : "POST",
|
|
214
|
+
redirect: "manual",
|
|
215
|
+
headers,
|
|
216
|
+
signal: controller.signal,
|
|
217
|
+
...(streamedBody ? { body: streamedBody, duplex: "half" } : {}),
|
|
218
|
+
};
|
|
219
|
+
const upstream = await services.fetch(upstreamUrl, init);
|
|
220
|
+
deadline.activity();
|
|
221
|
+
if (
|
|
222
|
+
!upstream.ok ||
|
|
223
|
+
!validUpstreamContentType(upstream, operation, request.url) ||
|
|
224
|
+
!upstream.body
|
|
225
|
+
) {
|
|
226
|
+
await upstream.body?.cancel().catch(() => undefined);
|
|
227
|
+
deadline.clear();
|
|
228
|
+
return brokerError(
|
|
229
|
+
502,
|
|
230
|
+
operation === "receive_pack"
|
|
231
|
+
? "GitHub push outcome is unknown. Inspect the remote before retrying."
|
|
232
|
+
: "GitHub Git transport is unavailable.",
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
const responseBody = streamWithActivity(upstream.body, deadline.activity, deadline.clear);
|
|
236
|
+
return new Response(responseBody, {
|
|
237
|
+
status: upstream.status,
|
|
238
|
+
headers: upstreamResponseHeaders(upstream, operation),
|
|
239
|
+
});
|
|
240
|
+
} catch {
|
|
241
|
+
deadline.clear();
|
|
242
|
+
return brokerError(
|
|
243
|
+
502,
|
|
244
|
+
operation === "receive_pack"
|
|
245
|
+
? "GitHub push outcome is unknown. Inspect the remote before retrying."
|
|
246
|
+
: "GitHub Git transport is unavailable.",
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function personalGitHubGitBrokerServices(deps: ApiRouteDeps): PersonalGitHubGitBrokerServices {
|
|
252
|
+
const secret = deps.settings.integrationsStateSecret?.trim();
|
|
253
|
+
return {
|
|
254
|
+
openClaims: (token) =>
|
|
255
|
+
deps.settings.githubPersonalOauthEnabled && secret
|
|
256
|
+
? openPersonalGitHubGitBrokerClaims(secret, token)
|
|
257
|
+
: null,
|
|
258
|
+
resolveAuthority: async (claims, routeId) => {
|
|
259
|
+
if (!deps.settings.githubPersonalOauthEnabled || !secret) {
|
|
260
|
+
throw new Error("personal GitHub Git broker is disabled");
|
|
261
|
+
}
|
|
262
|
+
return await resolveBrokerAuthority(deps, secret, claims, routeId);
|
|
263
|
+
},
|
|
264
|
+
authorizeProviderRequest: async (authority, destinationUrl, operation) =>
|
|
265
|
+
await authorizeBrokerProviderRequest(deps, authority, destinationUrl, operation),
|
|
266
|
+
fetch: deps.githubPersonalFetch ?? fetch,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async function resolveBrokerAuthority(
|
|
271
|
+
deps: ApiRouteDeps,
|
|
272
|
+
secret: string,
|
|
273
|
+
claims: PersonalGitHubGitBrokerClaims,
|
|
274
|
+
routeId: string,
|
|
275
|
+
): Promise<BrokerAuthority> {
|
|
276
|
+
const turn = await getSessionTurnForAttempt(
|
|
277
|
+
deps.db,
|
|
278
|
+
claims.workspaceId,
|
|
279
|
+
claims.sessionId,
|
|
280
|
+
claims.attemptId,
|
|
281
|
+
);
|
|
282
|
+
const session = await getSessionAuthorityProjection(
|
|
283
|
+
deps.db,
|
|
284
|
+
claims.workspaceId,
|
|
285
|
+
claims.sessionId,
|
|
286
|
+
);
|
|
287
|
+
if (
|
|
288
|
+
!turn ||
|
|
289
|
+
!session ||
|
|
290
|
+
turn.id !== claims.turnId ||
|
|
291
|
+
turn.executionGeneration !== claims.executionGeneration ||
|
|
292
|
+
session.rootSessionId !== claims.rootSessionId ||
|
|
293
|
+
(session.visibility === "user_private" && session.ownerSubjectId !== claims.ownerSubjectId)
|
|
294
|
+
) {
|
|
295
|
+
throw new Error("accepted Git broker attempt is no longer current");
|
|
296
|
+
}
|
|
297
|
+
const delegation = exactDelegation(turn.personalConnectionDelegations, claims);
|
|
298
|
+
const repository = exactRouteRepository(secret, claims, delegation, routeId);
|
|
299
|
+
const providerIdentity = await assertCurrentConnectionAndSelection(deps, claims, repository);
|
|
300
|
+
return { claims, repository, ...providerIdentity };
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async function authorizeBrokerProviderRequest(
|
|
304
|
+
deps: ApiRouteDeps,
|
|
305
|
+
authority: BrokerAuthority,
|
|
306
|
+
destinationUrl: string,
|
|
307
|
+
operation: "identity_verify" | "repository_verify" | BrokerOperation,
|
|
308
|
+
): Promise<string> {
|
|
309
|
+
const { claims, repository } = authority;
|
|
310
|
+
// Re-read every mutable connection/repository fence before this physical
|
|
311
|
+
// provider request. A previous request's successful check grants nothing to
|
|
312
|
+
// this one.
|
|
313
|
+
const currentIdentity = await assertCurrentConnectionAndSelection(deps, claims, repository);
|
|
314
|
+
if (currentIdentity.providerPrincipalId !== authority.providerPrincipalId) {
|
|
315
|
+
throw new Error("personal GitHub provider identity changed");
|
|
316
|
+
}
|
|
317
|
+
const resolver = buildConnectionTokenResolver(deps.db, deps.settings, undefined, {
|
|
318
|
+
...(deps.githubPersonalFetch
|
|
319
|
+
? { refreshTransport: { fetchImpl: deps.githubPersonalFetch } }
|
|
320
|
+
: {}),
|
|
321
|
+
});
|
|
322
|
+
const credential = await resolver({
|
|
323
|
+
workspaceId: claims.workspaceId,
|
|
324
|
+
subjectId: claims.ownerSubjectId,
|
|
325
|
+
serverId: PERSONAL_GITHUB_SERVER_ID,
|
|
326
|
+
toolName: `git_${operation}`,
|
|
327
|
+
connectionRef: {
|
|
328
|
+
connectionId: claims.connectionId,
|
|
329
|
+
provider: "github",
|
|
330
|
+
providerDomain: PERSONAL_GITHUB_PROVIDER_DOMAIN,
|
|
331
|
+
kind: "oauth2",
|
|
332
|
+
subjectScope: "subject",
|
|
333
|
+
scopes: ["repo"],
|
|
334
|
+
},
|
|
335
|
+
destinationUrl,
|
|
336
|
+
credentialTarget: "http_api",
|
|
337
|
+
connectionUseContext: {
|
|
338
|
+
accountId: claims.accountId,
|
|
339
|
+
workspaceId: claims.workspaceId,
|
|
340
|
+
sessionId: claims.sessionId,
|
|
341
|
+
turnId: claims.turnId,
|
|
342
|
+
attemptId: claims.attemptId,
|
|
343
|
+
executionGeneration: claims.executionGeneration,
|
|
344
|
+
physicalRequestId: randomUUID(),
|
|
345
|
+
usePhase: "credential_resolution",
|
|
346
|
+
},
|
|
347
|
+
});
|
|
348
|
+
if (
|
|
349
|
+
credential.status !== "ok" ||
|
|
350
|
+
credential.connectionId !== claims.connectionId ||
|
|
351
|
+
credential.connectionUseAttribution?.connectionGeneration !==
|
|
352
|
+
claims.connectionAuthorityGeneration ||
|
|
353
|
+
credential.connectionUseAttribution.ownerSubjectId !== claims.ownerSubjectId ||
|
|
354
|
+
credential.connectionUseAttribution.workspaceId !== claims.workspaceId
|
|
355
|
+
) {
|
|
356
|
+
throw new Error("personal GitHub credential resolution was denied");
|
|
357
|
+
}
|
|
358
|
+
await assertCurrentConnectionAndSelection(deps, claims, repository);
|
|
359
|
+
const providerRequest = await resolveAcceptedConnectionUse(deps.db, {
|
|
360
|
+
accountId: claims.accountId,
|
|
361
|
+
workspaceId: claims.workspaceId,
|
|
362
|
+
sessionId: claims.sessionId,
|
|
363
|
+
turnId: claims.turnId,
|
|
364
|
+
attemptId: claims.attemptId,
|
|
365
|
+
executionGeneration: claims.executionGeneration,
|
|
366
|
+
physicalRequestId: randomUUID(),
|
|
367
|
+
usePhase: "provider_request",
|
|
368
|
+
serverId: PERSONAL_GITHUB_SERVER_ID,
|
|
369
|
+
connectionId: claims.connectionId,
|
|
370
|
+
providerDomain: PERSONAL_GITHUB_PROVIDER_DOMAIN,
|
|
371
|
+
connectionKind: "oauth2",
|
|
372
|
+
subjectScope: "subject",
|
|
373
|
+
ownerSubjectId: claims.ownerSubjectId,
|
|
374
|
+
});
|
|
375
|
+
if (
|
|
376
|
+
providerRequest.status !== "authorized" ||
|
|
377
|
+
providerRequest.originWorkspaceId !== claims.originWorkspaceId ||
|
|
378
|
+
providerRequest.attribution.connectionId !== claims.connectionId ||
|
|
379
|
+
providerRequest.attribution.connectionGeneration !== claims.connectionAuthorityGeneration ||
|
|
380
|
+
providerRequest.attribution.ownerSubjectId !== claims.ownerSubjectId
|
|
381
|
+
) {
|
|
382
|
+
throw new Error("personal GitHub provider request was denied");
|
|
383
|
+
}
|
|
384
|
+
const authorization = Object.entries(credential.headers).find(
|
|
385
|
+
([name]) => name.toLowerCase() === "authorization",
|
|
386
|
+
)?.[1];
|
|
387
|
+
if (!authorization) throw new Error("personal GitHub credential has no authorization header");
|
|
388
|
+
return authorization;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function exactDelegation(
|
|
392
|
+
delegations: McpPersonalConnectionDelegation[],
|
|
393
|
+
claims: PersonalGitHubGitBrokerClaims,
|
|
394
|
+
): McpPersonalConnectionDelegation {
|
|
395
|
+
const matches = delegations.filter(
|
|
396
|
+
(delegation) =>
|
|
397
|
+
delegation.serverId === PERSONAL_GITHUB_SERVER_ID &&
|
|
398
|
+
delegation.connectionType === "github_personal" &&
|
|
399
|
+
delegation.connectionId === claims.connectionId &&
|
|
400
|
+
delegation.originWorkspaceId === claims.originWorkspaceId &&
|
|
401
|
+
delegation.ownerSubjectId === claims.ownerSubjectId &&
|
|
402
|
+
delegation.providerDomain === PERSONAL_GITHUB_PROVIDER_DOMAIN &&
|
|
403
|
+
delegation.kind === "oauth2" &&
|
|
404
|
+
delegation.personalGitHubRepositorySelection?.credentialBindingId ===
|
|
405
|
+
claims.credentialBindingId &&
|
|
406
|
+
delegation.personalGitHubRepositorySelection.connectionAuthorityGeneration ===
|
|
407
|
+
claims.connectionAuthorityGeneration &&
|
|
408
|
+
delegation.personalGitHubRepositorySelection.selectionGeneration ===
|
|
409
|
+
claims.selectionGeneration,
|
|
410
|
+
);
|
|
411
|
+
const delegation = matches[0];
|
|
412
|
+
if (matches.length !== 1 || !delegation?.personalGitHubRepositorySelection) {
|
|
413
|
+
throw new Error("personal GitHub delegation changed");
|
|
414
|
+
}
|
|
415
|
+
return delegation;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function exactRouteRepository(
|
|
419
|
+
secret: string,
|
|
420
|
+
claims: PersonalGitHubGitBrokerClaims,
|
|
421
|
+
delegation: McpPersonalConnectionDelegation,
|
|
422
|
+
routeId: string,
|
|
423
|
+
): BrokerRepository {
|
|
424
|
+
const repositories = delegation
|
|
425
|
+
.personalGitHubRepositorySelection!.repositories.map((repository) => ({
|
|
426
|
+
...repository,
|
|
427
|
+
routeId: personalGitHubGitBrokerRouteId(secret, { ...claims, repository }),
|
|
428
|
+
}))
|
|
429
|
+
.filter((repository) => repository.routeId === routeId);
|
|
430
|
+
const match = repositories[0];
|
|
431
|
+
if (repositories.length !== 1 || !match) throw new Error("Git broker route changed");
|
|
432
|
+
const { routeId: _routeId, ...repository } = match;
|
|
433
|
+
return repository;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
async function assertCurrentConnectionAndSelection(
|
|
437
|
+
deps: ApiRouteDeps,
|
|
438
|
+
claims: PersonalGitHubGitBrokerClaims,
|
|
439
|
+
repository: BrokerRepository,
|
|
440
|
+
): Promise<{ providerPrincipalId: string }> {
|
|
441
|
+
const connection = await getConnectionMetadata(
|
|
442
|
+
deps.db,
|
|
443
|
+
claims.originWorkspaceId,
|
|
444
|
+
claims.connectionId,
|
|
445
|
+
claims.ownerSubjectId,
|
|
446
|
+
);
|
|
447
|
+
if (
|
|
448
|
+
!connection ||
|
|
449
|
+
connection.accountId !== claims.accountId ||
|
|
450
|
+
connection.workspaceId !== claims.originWorkspaceId ||
|
|
451
|
+
connection.subjectId !== claims.ownerSubjectId ||
|
|
452
|
+
connection.status !== "active" ||
|
|
453
|
+
connection.grantedScopes.length !== 1 ||
|
|
454
|
+
connection.grantedScopes[0] !== "repo" ||
|
|
455
|
+
!isPersonalGitHubConnection(connection)
|
|
456
|
+
) {
|
|
457
|
+
throw new Error("personal GitHub connection changed");
|
|
458
|
+
}
|
|
459
|
+
// `connections.version` is the credential-refresh CAS and may rotate while
|
|
460
|
+
// an accepted turn runs. Repository use is fenced by the distinct common
|
|
461
|
+
// connection-authority generation returned below by the lifecycle function.
|
|
462
|
+
const metadata = PersonalGitHubConnectionMetadata.parse(connection.metadata);
|
|
463
|
+
if (metadata.credentialBindingId !== claims.credentialBindingId) {
|
|
464
|
+
throw new Error("personal GitHub credential binding changed");
|
|
465
|
+
}
|
|
466
|
+
const selection = await getPersonalGitHubRepositorySelectionState(deps.db, {
|
|
467
|
+
accountId: claims.accountId,
|
|
468
|
+
originWorkspaceId: claims.originWorkspaceId,
|
|
469
|
+
subjectId: claims.ownerSubjectId,
|
|
470
|
+
connectionId: claims.connectionId,
|
|
471
|
+
});
|
|
472
|
+
const current = selection?.repositories.find(
|
|
473
|
+
(candidate) => candidate.repositoryId === repository.repositoryId,
|
|
474
|
+
);
|
|
475
|
+
if (
|
|
476
|
+
!selection ||
|
|
477
|
+
selection.connectionAuthorityGeneration !== claims.connectionAuthorityGeneration ||
|
|
478
|
+
selection.credentialBindingId !== claims.credentialBindingId ||
|
|
479
|
+
selection.credentialBindingId !== metadata.credentialBindingId ||
|
|
480
|
+
selection.providerPrincipalId !== metadata.providerPrincipalId ||
|
|
481
|
+
selection.selectionGeneration !== claims.selectionGeneration ||
|
|
482
|
+
!current ||
|
|
483
|
+
current.fullName !== repository.fullName ||
|
|
484
|
+
current.canonicalUrl !== repository.canonicalUrl ||
|
|
485
|
+
current.selectionGeneration !== repository.selectionGeneration ||
|
|
486
|
+
current.disabled ||
|
|
487
|
+
!canRead(current.permissions) ||
|
|
488
|
+
(repository.access === "write" &&
|
|
489
|
+
(current.selectedAccess !== "write" || current.archived || !canWrite(current.permissions)))
|
|
490
|
+
) {
|
|
491
|
+
throw new Error("personal GitHub repository selection changed");
|
|
492
|
+
}
|
|
493
|
+
if (metadata.githubUserId !== metadata.providerPrincipalId) {
|
|
494
|
+
throw new Error("personal GitHub provider identity changed");
|
|
495
|
+
}
|
|
496
|
+
return {
|
|
497
|
+
providerPrincipalId: metadata.providerPrincipalId,
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
async function fetchLiveRepository(
|
|
502
|
+
fetchImpl: typeof fetch,
|
|
503
|
+
url: URL,
|
|
504
|
+
authorization: string,
|
|
505
|
+
expected: BrokerRepository,
|
|
506
|
+
): Promise<boolean> {
|
|
507
|
+
const payload = await fetchLiveGitHubJson(fetchImpl, url, authorization);
|
|
508
|
+
if (!payload) return false;
|
|
509
|
+
try {
|
|
510
|
+
const permissions = providerPermissions(payload.permissions);
|
|
511
|
+
return (
|
|
512
|
+
String(payload.id) === expected.repositoryId &&
|
|
513
|
+
payload.full_name === expected.fullName &&
|
|
514
|
+
payload.html_url === expected.canonicalUrl &&
|
|
515
|
+
payload.disabled !== true &&
|
|
516
|
+
canRead(permissions) &&
|
|
517
|
+
(expected.access !== "write" || (payload.archived !== true && canWrite(permissions)))
|
|
518
|
+
);
|
|
519
|
+
} catch {
|
|
520
|
+
return false;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
async function fetchLiveProviderUser(
|
|
525
|
+
fetchImpl: typeof fetch,
|
|
526
|
+
url: URL,
|
|
527
|
+
authorization: string,
|
|
528
|
+
expectedPrincipalId: string,
|
|
529
|
+
): Promise<boolean> {
|
|
530
|
+
const payload = await fetchLiveGitHubJson(fetchImpl, url, authorization);
|
|
531
|
+
return (
|
|
532
|
+
payload !== null &&
|
|
533
|
+
String(payload.id) === expectedPrincipalId &&
|
|
534
|
+
typeof payload.login === "string" &&
|
|
535
|
+
payload.login.length > 0
|
|
536
|
+
);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
async function fetchLiveGitHubJson(
|
|
540
|
+
fetchImpl: typeof fetch,
|
|
541
|
+
url: URL,
|
|
542
|
+
authorization: string,
|
|
543
|
+
): Promise<Record<string, unknown> | null> {
|
|
544
|
+
let response: Response;
|
|
545
|
+
try {
|
|
546
|
+
response = await fetchImpl(url, {
|
|
547
|
+
method: "GET",
|
|
548
|
+
redirect: "manual",
|
|
549
|
+
headers: {
|
|
550
|
+
accept: "application/vnd.github+json",
|
|
551
|
+
authorization,
|
|
552
|
+
"user-agent": "OpenGeni",
|
|
553
|
+
"x-github-api-version": GITHUB_API_VERSION,
|
|
554
|
+
},
|
|
555
|
+
signal: AbortSignal.timeout(10_000),
|
|
556
|
+
});
|
|
557
|
+
} catch {
|
|
558
|
+
return null;
|
|
559
|
+
}
|
|
560
|
+
if (!response.ok || (response.status >= 300 && response.status < 400)) {
|
|
561
|
+
await response.body?.cancel().catch(() => undefined);
|
|
562
|
+
return null;
|
|
563
|
+
}
|
|
564
|
+
try {
|
|
565
|
+
return providerJson.parse(
|
|
566
|
+
await readResponseTextBounded(response, GITHUB_API_RESPONSE_MAX_BYTES, "GitHub response"),
|
|
567
|
+
) as Record<string, unknown>;
|
|
568
|
+
} catch {
|
|
569
|
+
return null;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
const providerJson = JSONBig({
|
|
574
|
+
storeAsString: true,
|
|
575
|
+
protoAction: "error",
|
|
576
|
+
constructorAction: "error",
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
function providerPermissions(value: unknown): PersonalGitHubRepositoryPermissions {
|
|
580
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
581
|
+
return { pull: false, triage: false, push: false, maintain: false, admin: false };
|
|
582
|
+
}
|
|
583
|
+
const permissions = value as Record<string, unknown>;
|
|
584
|
+
return {
|
|
585
|
+
pull: permissions.pull === true,
|
|
586
|
+
triage: permissions.triage === true,
|
|
587
|
+
push: permissions.push === true,
|
|
588
|
+
maintain: permissions.maintain === true,
|
|
589
|
+
admin: permissions.admin === true,
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function canRead(permissions: PersonalGitHubRepositoryPermissions): boolean {
|
|
594
|
+
return (
|
|
595
|
+
permissions.pull ||
|
|
596
|
+
permissions.triage ||
|
|
597
|
+
permissions.push ||
|
|
598
|
+
permissions.maintain ||
|
|
599
|
+
permissions.admin
|
|
600
|
+
);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function canWrite(permissions: PersonalGitHubRepositoryPermissions): boolean {
|
|
604
|
+
return permissions.push || permissions.maintain || permissions.admin;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function validGitRequestShape(request: Request, operation: BrokerOperation): boolean {
|
|
608
|
+
const url = new URL(request.url);
|
|
609
|
+
if (operation === "info_refs") {
|
|
610
|
+
if (request.method !== "GET") return false;
|
|
611
|
+
const entries = [...url.searchParams.entries()];
|
|
612
|
+
return (
|
|
613
|
+
entries.length === 1 &&
|
|
614
|
+
entries[0]?.[0] === "service" &&
|
|
615
|
+
(entries[0][1] === "git-upload-pack" || entries[0][1] === "git-receive-pack")
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
if (request.method !== "POST" || url.search || request.body === null) return false;
|
|
619
|
+
const contentType = request.headers.get("content-type")?.split(";", 1)[0]?.trim().toLowerCase();
|
|
620
|
+
const contentEncoding = request.headers.get("content-encoding")?.trim().toLowerCase();
|
|
621
|
+
return (
|
|
622
|
+
contentType === `application/x-git-${operation.replace("_", "-")}-request` &&
|
|
623
|
+
(!contentEncoding || contentEncoding === "gzip")
|
|
624
|
+
);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
function requestsReceivePack(request: Request, operation: BrokerOperation): boolean {
|
|
628
|
+
return (
|
|
629
|
+
operation === "receive_pack" ||
|
|
630
|
+
(operation === "info_refs" &&
|
|
631
|
+
new URL(request.url).searchParams.get("service") === "git-receive-pack")
|
|
632
|
+
);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function githubGitUrl(
|
|
636
|
+
repository: BrokerRepository,
|
|
637
|
+
operation: BrokerOperation,
|
|
638
|
+
requestUrl: string,
|
|
639
|
+
): URL {
|
|
640
|
+
const url = new URL(`https://github.com/${repository.fullName}.git`);
|
|
641
|
+
if (operation === "info_refs") {
|
|
642
|
+
url.pathname += "/info/refs";
|
|
643
|
+
url.searchParams.set("service", new URL(requestUrl).searchParams.get("service")!);
|
|
644
|
+
} else {
|
|
645
|
+
url.pathname += `/git-${operation.replace("_", "-")}`;
|
|
646
|
+
}
|
|
647
|
+
return url;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function upstreamRequestHeaders(
|
|
651
|
+
request: Request,
|
|
652
|
+
operation: BrokerOperation,
|
|
653
|
+
token: string,
|
|
654
|
+
): Headers {
|
|
655
|
+
const headers = new Headers({
|
|
656
|
+
accept:
|
|
657
|
+
operation === "info_refs" ? "*/*" : `application/x-git-${operation.replace("_", "-")}-result`,
|
|
658
|
+
"accept-encoding": "identity",
|
|
659
|
+
authorization: `Basic ${Buffer.from(`x-access-token:${token}`, "utf8").toString("base64")}`,
|
|
660
|
+
"user-agent": "OpenGeni-Git-Broker/1",
|
|
661
|
+
});
|
|
662
|
+
if (operation !== "info_refs") {
|
|
663
|
+
headers.set("content-type", `application/x-git-${operation.replace("_", "-")}-request`);
|
|
664
|
+
if (request.headers.get("content-encoding")?.trim().toLowerCase() === "gzip") {
|
|
665
|
+
headers.set("content-encoding", "gzip");
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
const gitProtocol = request.headers.get("git-protocol");
|
|
669
|
+
if (gitProtocol === "version=2") headers.set("git-protocol", gitProtocol);
|
|
670
|
+
return headers;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
function validUpstreamContentType(
|
|
674
|
+
response: Response,
|
|
675
|
+
operation: BrokerOperation,
|
|
676
|
+
requestUrl: string,
|
|
677
|
+
): boolean {
|
|
678
|
+
const actual = response.headers.get("content-type")?.split(";", 1)[0]?.trim().toLowerCase();
|
|
679
|
+
if (operation !== "info_refs") {
|
|
680
|
+
return actual === `application/x-git-${operation.replace("_", "-")}-result`;
|
|
681
|
+
}
|
|
682
|
+
const requestedService = new URL(requestUrl).searchParams.get("service");
|
|
683
|
+
return actual === `application/x-${requestedService}-advertisement`;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
function upstreamResponseHeaders(response: Response, operation: BrokerOperation): Headers {
|
|
687
|
+
const headers = new Headers({
|
|
688
|
+
"cache-control": "no-store",
|
|
689
|
+
"content-type": response.headers.get("content-type")!,
|
|
690
|
+
"x-content-type-options": "nosniff",
|
|
691
|
+
});
|
|
692
|
+
if (operation === "receive_pack") {
|
|
693
|
+
headers.set("x-opengeni-push-retry-policy", "inspect-remote-before-retry");
|
|
694
|
+
}
|
|
695
|
+
return headers;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
function brokerBearer(authorization: string | null): string | null {
|
|
699
|
+
if (
|
|
700
|
+
!authorization ||
|
|
701
|
+
Buffer.byteLength(authorization, "utf8") > GIT_BROKER_AUTHORIZATION_MAX_BYTES
|
|
702
|
+
) {
|
|
703
|
+
return null;
|
|
704
|
+
}
|
|
705
|
+
if (authorization.startsWith("Bearer ")) return authorization.slice("Bearer ".length) || null;
|
|
706
|
+
if (!authorization.startsWith("Basic ")) return null;
|
|
707
|
+
try {
|
|
708
|
+
const decoded = Buffer.from(authorization.slice("Basic ".length), "base64").toString("utf8");
|
|
709
|
+
const separator = decoded.indexOf(":");
|
|
710
|
+
return separator >= 0 && decoded.length > separator + 1 ? decoded.slice(separator + 1) : null;
|
|
711
|
+
} catch {
|
|
712
|
+
return null;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
function bearerToken(authorization: string): string | null {
|
|
717
|
+
return authorization.startsWith("Bearer ")
|
|
718
|
+
? authorization.slice("Bearer ".length).trim() || null
|
|
719
|
+
: null;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
function idleDeadline(
|
|
723
|
+
controller: AbortController,
|
|
724
|
+
timeoutMs: number,
|
|
725
|
+
): {
|
|
726
|
+
activity: () => void;
|
|
727
|
+
clear: () => void;
|
|
728
|
+
} {
|
|
729
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
730
|
+
const clear = () => {
|
|
731
|
+
if (timer) clearTimeout(timer);
|
|
732
|
+
timer = undefined;
|
|
733
|
+
};
|
|
734
|
+
const activity = () => {
|
|
735
|
+
clear();
|
|
736
|
+
timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
737
|
+
};
|
|
738
|
+
activity();
|
|
739
|
+
return { activity, clear };
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
function streamWithActivity(
|
|
743
|
+
source: ReadableStream<Uint8Array>,
|
|
744
|
+
activity: () => void,
|
|
745
|
+
finish: () => void,
|
|
746
|
+
fail = finish,
|
|
747
|
+
): ReadableStream<Uint8Array> {
|
|
748
|
+
const reader = source.getReader();
|
|
749
|
+
return new ReadableStream<Uint8Array>({
|
|
750
|
+
async pull(controller) {
|
|
751
|
+
try {
|
|
752
|
+
const result = await reader.read();
|
|
753
|
+
if (result.done) {
|
|
754
|
+
finish();
|
|
755
|
+
controller.close();
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
activity();
|
|
759
|
+
controller.enqueue(result.value);
|
|
760
|
+
} catch {
|
|
761
|
+
fail();
|
|
762
|
+
controller.error(new Error("Git provider stream failed"));
|
|
763
|
+
}
|
|
764
|
+
},
|
|
765
|
+
async cancel(reason) {
|
|
766
|
+
fail();
|
|
767
|
+
await reader.cancel(reason).catch(() => undefined);
|
|
768
|
+
},
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
function brokerError(status: 401 | 403 | 404 | 502, message: string): Response {
|
|
773
|
+
const headers = new Headers({
|
|
774
|
+
"cache-control": "no-store",
|
|
775
|
+
"content-type": "text/plain; charset=utf-8",
|
|
776
|
+
"x-content-type-options": "nosniff",
|
|
777
|
+
});
|
|
778
|
+
if (status === 401) {
|
|
779
|
+
headers.set("www-authenticate", 'Basic realm="OpenGeni Git broker", charset="UTF-8"');
|
|
780
|
+
}
|
|
781
|
+
return new Response(`${message}\n`, {
|
|
782
|
+
status,
|
|
783
|
+
headers,
|
|
784
|
+
});
|
|
785
|
+
}
|