@opengeni/api-router 0.30.3 → 2.2.0-canary.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (165) hide show
  1. package/dist/api-websocket.d.ts +18 -0
  2. package/dist/app.js +1 -1
  3. package/dist/auth/managed-auth.d.ts +6 -0
  4. package/dist/{chunk-WCXHI3FF.js → chunk-3TP54PPX.js} +26692 -15389
  5. package/dist/chunk-3TP54PPX.js.map +1 -0
  6. package/dist/codemode.d.ts +8 -1
  7. package/dist/company-brain-okf.d.ts +24 -0
  8. package/dist/connected-machine-computer-access.d.ts +11 -0
  9. package/dist/connection-authority-owner.d.ts +59 -0
  10. package/dist/connection-ownership.d.ts +47 -0
  11. package/dist/controller-data-plane.d.ts +16 -0
  12. package/dist/editable-artifact-websocket.d.ts +5 -15
  13. package/dist/editable-artifact-workspace-files.d.ts +1 -1
  14. package/dist/http/api-error.d.ts +10 -0
  15. package/dist/http/cors.d.ts +5 -0
  16. package/dist/http/interaction-control-error.d.ts +13 -0
  17. package/dist/http/sse.d.ts +2 -3
  18. package/dist/index.d.ts +5 -2
  19. package/dist/index.js +326 -82
  20. package/dist/index.js.map +1 -1
  21. package/dist/integrations/atlassian.d.ts +17 -0
  22. package/dist/integrations/fiken.d.ts +7 -2
  23. package/dist/integrations/google-drive.d.ts +18 -0
  24. package/dist/integrations/oauth-client.d.ts +22 -3
  25. package/dist/integrations/oauth-profiles.d.ts +195 -0
  26. package/dist/integrations/personal-github-repositories.d.ts +45 -0
  27. package/dist/integrations/personal-github.d.ts +40 -0
  28. package/dist/integrations/pr-review-provider.d.ts +22 -0
  29. package/dist/integrations/provider-oauth.d.ts +8 -0
  30. package/dist/integrations/slack-app-home.d.ts +24 -0
  31. package/dist/integrations/slack-bot.d.ts +157 -9
  32. package/dist/integrations/slack-interactions.d.ts +39 -5
  33. package/dist/integrations/slack-routing.d.ts +120 -0
  34. package/dist/integrations/social-oauth.d.ts +5 -0
  35. package/dist/interaction-frame-proxy.d.ts +68 -0
  36. package/dist/mcp/company-brain-governed-writes.d.ts +26 -0
  37. package/dist/mcp/company-profile-agent-admin.d.ts +44 -0
  38. package/dist/mcp/documents.d.ts +1 -0
  39. package/dist/mcp/remember.d.ts +28 -0
  40. package/dist/mcp/request-abort.d.ts +19 -0
  41. package/dist/mcp/scheduled-task-view.d.ts +4 -4
  42. package/dist/mcp/server.d.ts +15 -2
  43. package/dist/mcp/session-view.d.ts +4 -0
  44. package/dist/mcp/session-wait.d.ts +137 -0
  45. package/dist/routes/automations.d.ts +4 -0
  46. package/dist/routes/billing.d.ts +5 -0
  47. package/dist/routes/browser-sessions.d.ts +4 -1
  48. package/dist/routes/company-brain.d.ts +3 -0
  49. package/dist/routes/company-profile.d.ts +2 -1
  50. package/dist/routes/connection-authorities.d.ts +3 -0
  51. package/dist/routes/personal-github-git-broker.d.ts +29 -0
  52. package/dist/routes/personal-github.d.ts +3 -0
  53. package/dist/routes/pr-review.d.ts +3 -0
  54. package/dist/routes/sessions.d.ts +1 -0
  55. package/dist/routes/user-resource-authorities.d.ts +3 -0
  56. package/dist/routes/workspace-capture.d.ts +10 -2
  57. package/dist/routes/workspace-learning.d.ts +3 -0
  58. package/dist/routes/workspaces.d.ts +19 -0
  59. package/dist/sandbox/channel-a.d.ts +11 -1
  60. package/dist/sandbox/connection-authority.d.ts +3 -0
  61. package/dist/sandbox/enrollment.d.ts +2 -0
  62. package/dist/sandbox/machines.d.ts +2 -2
  63. package/dist/sandbox/metrics-ingestion.d.ts +10 -20
  64. package/dist/sandbox/viewer.d.ts +24 -1
  65. package/dist/sandbox-file-artifacts.d.ts +11 -0
  66. package/dist/scheduled-task-deletion.d.ts +14 -0
  67. package/dist/slack-reaction-files.d.ts +1 -1
  68. package/dist/temporal-schedule-cleanup.d.ts +1 -0
  69. package/package.json +20 -18
  70. package/src/api-websocket.ts +23 -0
  71. package/src/app.ts +161 -23
  72. package/src/auth/managed-auth.ts +25 -3
  73. package/src/codemode.ts +53 -23
  74. package/src/codex-realtime.ts +8 -2
  75. package/src/company-brain-okf.ts +340 -0
  76. package/src/connected-machine-computer-access.ts +33 -0
  77. package/src/connection-authority-owner.ts +61 -0
  78. package/src/connection-ownership.ts +180 -0
  79. package/src/controller-data-plane.ts +47 -0
  80. package/src/editable-artifact-native-kernel.ts +26 -15
  81. package/src/editable-artifact-office-import.ts +28 -3
  82. package/src/editable-artifact-production.ts +13 -7
  83. package/src/editable-artifact-websocket.ts +11 -18
  84. package/src/editable-artifact-workspace-files.ts +31 -6
  85. package/src/http/api-error.ts +28 -0
  86. package/src/http/auth.ts +4 -0
  87. package/src/http/cors.ts +35 -0
  88. package/src/http/interaction-control-error.ts +164 -0
  89. package/src/http/sse.ts +112 -66
  90. package/src/index.ts +46 -6
  91. package/src/integrations/atlassian.ts +146 -35
  92. package/src/integrations/fiken.ts +102 -22
  93. package/src/integrations/google-drive.ts +301 -92
  94. package/src/integrations/oauth-client.ts +272 -143
  95. package/src/integrations/oauth-profiles.ts +477 -0
  96. package/src/integrations/personal-github-repositories.ts +445 -0
  97. package/src/integrations/personal-github.ts +705 -0
  98. package/src/integrations/pr-review-provider.ts +246 -0
  99. package/src/integrations/provider-oauth.ts +121 -5
  100. package/src/integrations/slack-app-home.ts +300 -0
  101. package/src/integrations/slack-bot.ts +342 -57
  102. package/src/integrations/slack-interactions.ts +1898 -259
  103. package/src/integrations/slack-routing.ts +337 -0
  104. package/src/integrations/social-oauth.ts +25 -0
  105. package/src/interaction-frame-proxy.ts +409 -0
  106. package/src/mcp/company-brain-governed-writes.ts +262 -0
  107. package/src/mcp/company-profile-agent-admin.ts +205 -0
  108. package/src/mcp/documents.ts +37 -6
  109. package/src/mcp/files.ts +16 -1
  110. package/src/mcp/remember.ts +181 -0
  111. package/src/mcp/request-abort.ts +44 -0
  112. package/src/mcp/scheduled-task-view.ts +1 -0
  113. package/src/mcp/server.ts +719 -369
  114. package/src/mcp/session-view.ts +54 -0
  115. package/src/mcp/session-wait.ts +554 -0
  116. package/src/model-catalog.ts +20 -12
  117. package/src/routes/api-integrations.ts +4 -0
  118. package/src/routes/api-keys.ts +1 -0
  119. package/src/routes/automations.ts +534 -0
  120. package/src/routes/billing.ts +57 -1
  121. package/src/routes/browser-sessions.ts +329 -79
  122. package/src/routes/channels.ts +17 -1
  123. package/src/routes/codex.ts +50 -0
  124. package/src/routes/company-brain.ts +367 -0
  125. package/src/routes/company-profile.ts +1 -1
  126. package/src/routes/computer-sessions.ts +320 -93
  127. package/src/routes/connection-authorities.ts +139 -0
  128. package/src/routes/connections.ts +144 -34
  129. package/src/routes/documents.ts +353 -4
  130. package/src/routes/editable-artifacts.ts +11 -3
  131. package/src/routes/enrollments.ts +116 -26
  132. package/src/routes/environments.ts +127 -55
  133. package/src/routes/files.ts +72 -11
  134. package/src/routes/install.ts +82 -28
  135. package/src/routes/integration-facets.ts +29 -0
  136. package/src/routes/interaction-resources.ts +20 -2
  137. package/src/routes/machines.ts +275 -13
  138. package/src/routes/organization-memberships.ts +717 -28
  139. package/src/routes/packs.ts +4 -3
  140. package/src/routes/personal-github-git-broker.ts +785 -0
  141. package/src/routes/personal-github.ts +319 -0
  142. package/src/routes/pr-review.ts +531 -0
  143. package/src/routes/rigs.ts +104 -39
  144. package/src/routes/scheduled-tasks.ts +19 -21
  145. package/src/routes/sessions.ts +775 -38
  146. package/src/routes/social.ts +14 -2
  147. package/src/routes/supergrok.ts +23 -4
  148. package/src/routes/transcription-recordings.ts +8 -2
  149. package/src/routes/user-resource-authorities.ts +138 -0
  150. package/src/routes/workspace-artifacts.ts +4 -1
  151. package/src/routes/workspace-capture.ts +21 -0
  152. package/src/routes/workspace-learning.ts +228 -0
  153. package/src/routes/workspaces.ts +59 -6
  154. package/src/sandbox/auth-callout.ts +38 -12
  155. package/src/sandbox/channel-a.ts +242 -19
  156. package/src/sandbox/connection-authority.ts +3 -0
  157. package/src/sandbox/enrollment.ts +15 -3
  158. package/src/sandbox/machines.ts +186 -64
  159. package/src/sandbox/metrics-ingestion.ts +220 -58
  160. package/src/sandbox/viewer.ts +243 -19
  161. package/src/sandbox-file-artifacts.ts +329 -0
  162. package/src/scheduled-task-deletion.ts +127 -0
  163. package/src/slack-reaction-files.ts +16 -5
  164. package/src/temporal-schedule-cleanup.ts +13 -0
  165. package/dist/chunk-WCXHI3FF.js.map +0 -1
@@ -0,0 +1,445 @@
1
+ import {
2
+ PERSONAL_GITHUB_API_ORIGIN,
3
+ PERSONAL_GITHUB_PROVIDER_DOMAIN,
4
+ PERSONAL_GITHUB_REPOSITORY_CATALOG_MAX,
5
+ PersonalGitHubRepository,
6
+ canonicalPersonalGitHubRepositoryUrl,
7
+ isPersonalGitHubConnection,
8
+ type PersonalGitHubRepository as PersonalGitHubRepositoryContract,
9
+ type PersonalGitHubRepositorySelectionInput,
10
+ } from "@opengeni/contracts/personal-github";
11
+ import type { ApiRouteDeps } from "@opengeni/core";
12
+ import {
13
+ buildConnectionTokenResolver,
14
+ getConnectionMetadata,
15
+ getPersonalGitHubRepositorySelectionState,
16
+ } from "@opengeni/db";
17
+ import { readResponseTextBounded } from "@opengeni/network";
18
+ import { HTTPException } from "hono/http-exception";
19
+ import JSONBig from "json-bigint";
20
+
21
+ const GITHUB_API_VERSION = "2022-11-28";
22
+ const GITHUB_REQUEST_TIMEOUT_MS = 10_000;
23
+ const GITHUB_REPOSITORIES_RESPONSE_MAX_BYTES = 1024 * 1024;
24
+ const personalGitHubProviderJsonParser = JSONBig({
25
+ storeAsString: true,
26
+ protoAction: "error",
27
+ constructorAction: "error",
28
+ });
29
+
30
+ export type PersonalGitHubRepositoryConnection = NonNullable<
31
+ Awaited<ReturnType<typeof getConnectionMetadata>>
32
+ >;
33
+
34
+ export type PersonalGitHubRepositoryProviderErrorCode =
35
+ | "connection_changed"
36
+ | "connection_inactive"
37
+ | "invalid_provider_response"
38
+ | "provider_denied"
39
+ | "provider_rate_limited"
40
+ | "provider_unavailable"
41
+ | "repository_archived"
42
+ | "repository_not_found"
43
+ | "repository_read_denied"
44
+ | "repository_write_denied";
45
+
46
+ export class PersonalGitHubRepositoryProviderError extends Error {
47
+ constructor(readonly code: PersonalGitHubRepositoryProviderErrorCode) {
48
+ super(code);
49
+ this.name = "PersonalGitHubRepositoryProviderError";
50
+ }
51
+ }
52
+
53
+ export async function requirePersonalGitHubRepositoryConnection(
54
+ deps: ApiRouteDeps,
55
+ input: { accountId: string; workspaceId: string; subjectId: string; connectionId: string },
56
+ ): Promise<PersonalGitHubRepositoryConnection> {
57
+ if (!deps.settings.githubPersonalOauthEnabled) {
58
+ throw new HTTPException(404, {
59
+ message: "personal GitHub repository discovery is not enabled",
60
+ });
61
+ }
62
+ const connection = await getConnectionMetadata(
63
+ deps.db,
64
+ input.workspaceId,
65
+ input.connectionId,
66
+ input.subjectId,
67
+ );
68
+ if (
69
+ !connection ||
70
+ connection.accountId !== input.accountId ||
71
+ connection.subjectId !== input.subjectId ||
72
+ !isPersonalGitHubConnection(connection)
73
+ ) {
74
+ throw new HTTPException(404, { message: "personal GitHub connection not found" });
75
+ }
76
+ if (connection.status !== "active") {
77
+ throw new HTTPException(409, { message: "personal GitHub connection must be reconnected" });
78
+ }
79
+ return connection;
80
+ }
81
+
82
+ export async function listLivePersonalGitHubRepositories(
83
+ deps: ApiRouteDeps,
84
+ input: {
85
+ workspaceId: string;
86
+ accountId: string;
87
+ subjectId: string;
88
+ connectionId: string;
89
+ expectedConnectionAuthorityGeneration: number;
90
+ page: number;
91
+ limit: number;
92
+ },
93
+ ): Promise<{ repositories: PersonalGitHubRepositoryContract[]; nextPage: number | null }> {
94
+ const connection = await requirePersonalGitHubRepositoryConnection(deps, input);
95
+ const url = new URL("/user/repos", PERSONAL_GITHUB_API_ORIGIN);
96
+ url.searchParams.set("affiliation", "owner,collaborator,organization_member");
97
+ url.searchParams.set("direction", "asc");
98
+ url.searchParams.set("page", String(input.page));
99
+ url.searchParams.set("per_page", String(input.limit));
100
+ url.searchParams.set("sort", "full_name");
101
+ const payload = await personalGitHubProviderJson(
102
+ deps,
103
+ connection,
104
+ input.subjectId,
105
+ url,
106
+ input.expectedConnectionAuthorityGeneration,
107
+ );
108
+ if (!Array.isArray(payload) || payload.length > PERSONAL_GITHUB_REPOSITORY_CATALOG_MAX) {
109
+ throw new PersonalGitHubRepositoryProviderError("invalid_provider_response");
110
+ }
111
+ const repositories: PersonalGitHubRepositoryContract[] = [];
112
+ const seen = new Set<string>();
113
+ for (const value of payload) {
114
+ const repository = parsePersonalGitHubRepository(value);
115
+ if (seen.has(repository.repositoryId)) {
116
+ throw new PersonalGitHubRepositoryProviderError("invalid_provider_response");
117
+ }
118
+ seen.add(repository.repositoryId);
119
+ if (personalGitHubRepositoryCanRead(repository.permissions)) repositories.push(repository);
120
+ }
121
+ return {
122
+ repositories,
123
+ nextPage: payload.length === input.limit && input.page < 10_000 ? input.page + 1 : null,
124
+ };
125
+ }
126
+
127
+ /** Serial by design: one bounded provider request and one authority check at a time. */
128
+ export async function verifyLivePersonalGitHubRepositories(
129
+ deps: ApiRouteDeps,
130
+ input: {
131
+ workspaceId: string;
132
+ accountId: string;
133
+ subjectId: string;
134
+ connectionId: string;
135
+ expectedConnectionAuthorityGeneration: number;
136
+ repositories: PersonalGitHubRepositorySelectionInput[];
137
+ },
138
+ ): Promise<Array<PersonalGitHubRepositoryContract & { selectedAccess: "read" | "write" }>> {
139
+ const connection = await requirePersonalGitHubRepositoryConnection(deps, input);
140
+ const verified: Array<PersonalGitHubRepositoryContract & { selectedAccess: "read" | "write" }> =
141
+ [];
142
+ for (const selection of input.repositories) {
143
+ const [owner, repositoryName] = selection.fullName.split("/");
144
+ if (!owner || !repositoryName) {
145
+ throw new PersonalGitHubRepositoryProviderError("invalid_provider_response");
146
+ }
147
+ const url = new URL(
148
+ `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repositoryName)}`,
149
+ PERSONAL_GITHUB_API_ORIGIN,
150
+ );
151
+ const payload = await personalGitHubProviderJson(
152
+ deps,
153
+ connection,
154
+ input.subjectId,
155
+ url,
156
+ input.expectedConnectionAuthorityGeneration,
157
+ true,
158
+ );
159
+ const repository = parsePersonalGitHubRepository(payload);
160
+ if (
161
+ repository.repositoryId !== selection.repositoryId ||
162
+ repository.fullName.toLowerCase() !== selection.fullName.toLowerCase()
163
+ ) {
164
+ throw new PersonalGitHubRepositoryProviderError("repository_not_found");
165
+ }
166
+ if (!personalGitHubRepositoryCanRead(repository.permissions)) {
167
+ throw new PersonalGitHubRepositoryProviderError("repository_read_denied");
168
+ }
169
+ if (selection.access === "write") {
170
+ if (repository.archived || repository.disabled) {
171
+ throw new PersonalGitHubRepositoryProviderError("repository_archived");
172
+ }
173
+ if (!personalGitHubRepositoryCanWrite(repository.permissions)) {
174
+ throw new PersonalGitHubRepositoryProviderError("repository_write_denied");
175
+ }
176
+ }
177
+ verified.push({ ...repository, selectedAccess: selection.access });
178
+ }
179
+ return verified;
180
+ }
181
+
182
+ export function personalGitHubRepositoryProviderHttpError(
183
+ error: PersonalGitHubRepositoryProviderError,
184
+ ): HTTPException {
185
+ switch (error.code) {
186
+ case "connection_changed":
187
+ return new HTTPException(409, {
188
+ message: "personal GitHub connection changed; refresh and try again",
189
+ });
190
+ case "connection_inactive":
191
+ case "provider_denied":
192
+ return new HTTPException(401, { message: "personal GitHub must be reconnected" });
193
+ case "provider_rate_limited":
194
+ return new HTTPException(503, { message: "GitHub is temporarily rate limited" });
195
+ case "repository_not_found":
196
+ return new HTTPException(422, { message: "a selected GitHub repository is unavailable" });
197
+ case "repository_archived":
198
+ return new HTTPException(422, {
199
+ message: "an archived or disabled GitHub repository cannot be selected for write access",
200
+ });
201
+ case "repository_read_denied":
202
+ return new HTTPException(422, {
203
+ message: "a selected GitHub repository is not readable",
204
+ });
205
+ case "repository_write_denied":
206
+ return new HTTPException(422, {
207
+ message: "a selected GitHub repository does not allow writes",
208
+ });
209
+ case "invalid_provider_response":
210
+ case "provider_unavailable":
211
+ return new HTTPException(502, { message: "GitHub repository discovery is unavailable" });
212
+ }
213
+ }
214
+
215
+ async function personalGitHubProviderJson(
216
+ deps: ApiRouteDeps,
217
+ connection: PersonalGitHubRepositoryConnection,
218
+ subjectId: string,
219
+ url: URL,
220
+ expectedConnectionAuthorityGeneration: number,
221
+ repositoryLookup = false,
222
+ ): Promise<unknown> {
223
+ if (url.origin !== PERSONAL_GITHUB_API_ORIGIN) {
224
+ throw new PersonalGitHubRepositoryProviderError("invalid_provider_response");
225
+ }
226
+ let response = await personalGitHubProviderFetch(
227
+ deps,
228
+ connection,
229
+ subjectId,
230
+ url,
231
+ expectedConnectionAuthorityGeneration,
232
+ repositoryLookup,
233
+ false,
234
+ );
235
+ if (response.status === 401) {
236
+ await cancelResponse(response);
237
+ response = await personalGitHubProviderFetch(
238
+ deps,
239
+ connection,
240
+ subjectId,
241
+ url,
242
+ expectedConnectionAuthorityGeneration,
243
+ repositoryLookup,
244
+ true,
245
+ );
246
+ }
247
+ if (response.status >= 300 && response.status < 400) {
248
+ await cancelResponse(response);
249
+ throw new PersonalGitHubRepositoryProviderError("provider_unavailable");
250
+ }
251
+ if (!response.ok) {
252
+ const status = response.status;
253
+ const rateLimited =
254
+ status === 429 ||
255
+ (status === 403 &&
256
+ (response.headers.get("x-ratelimit-remaining") === "0" ||
257
+ response.headers.has("retry-after")));
258
+ await cancelResponse(response);
259
+ if (status === 401 || status === 403) {
260
+ throw new PersonalGitHubRepositoryProviderError(
261
+ rateLimited ? "provider_rate_limited" : "provider_denied",
262
+ );
263
+ }
264
+ if (status === 429) {
265
+ throw new PersonalGitHubRepositoryProviderError("provider_rate_limited");
266
+ }
267
+ if (status === 404 && repositoryLookup) {
268
+ throw new PersonalGitHubRepositoryProviderError("repository_not_found");
269
+ }
270
+ throw new PersonalGitHubRepositoryProviderError("provider_unavailable");
271
+ }
272
+ try {
273
+ return parsePersonalGitHubProviderJson(
274
+ await readResponseTextBounded(
275
+ response,
276
+ GITHUB_REPOSITORIES_RESPONSE_MAX_BYTES,
277
+ repositoryLookup ? "GitHub repository response" : "GitHub repositories response",
278
+ ),
279
+ );
280
+ } catch {
281
+ throw new PersonalGitHubRepositoryProviderError("invalid_provider_response");
282
+ }
283
+ }
284
+
285
+ /** Keep provider integer lexemes exact before repository IDs become bigint-backed strings. */
286
+ export function parsePersonalGitHubProviderJson(value: string): unknown {
287
+ return personalGitHubProviderJsonParser.parse(value) as unknown;
288
+ }
289
+
290
+ async function personalGitHubProviderFetch(
291
+ deps: ApiRouteDeps,
292
+ connection: PersonalGitHubRepositoryConnection,
293
+ subjectId: string,
294
+ url: URL,
295
+ expectedConnectionAuthorityGeneration: number,
296
+ repositoryLookup: boolean,
297
+ forceRefresh: boolean,
298
+ ): Promise<Response> {
299
+ const resolver = buildConnectionTokenResolver(deps.db, deps.settings, undefined, {
300
+ ...(deps.githubPersonalFetch
301
+ ? { refreshTransport: { fetchImpl: deps.githubPersonalFetch } }
302
+ : {}),
303
+ });
304
+ const credential = await resolver({
305
+ workspaceId: connection.workspaceId,
306
+ subjectId,
307
+ serverId: "github-personal-repository-picker",
308
+ toolName: repositoryLookup ? "repository_verify" : "repositories_list",
309
+ connectionRef: {
310
+ connectionId: connection.id,
311
+ providerDomain: PERSONAL_GITHUB_PROVIDER_DOMAIN,
312
+ kind: "oauth2",
313
+ subjectScope: "subject",
314
+ scopes: ["repo"],
315
+ },
316
+ destinationUrl: url.toString(),
317
+ credentialTarget: "http_api",
318
+ forceRefresh,
319
+ });
320
+ if (credential.status !== "ok") {
321
+ throw new PersonalGitHubRepositoryProviderError("connection_inactive");
322
+ }
323
+ if (credential.connectionId !== connection.id || credential.connectionVersion === undefined) {
324
+ throw new PersonalGitHubRepositoryProviderError("connection_changed");
325
+ }
326
+ const current = await getConnectionMetadata(
327
+ deps.db,
328
+ connection.workspaceId,
329
+ connection.id,
330
+ subjectId,
331
+ );
332
+ if (
333
+ !current ||
334
+ current.version !== credential.connectionVersion ||
335
+ current.subjectId !== subjectId ||
336
+ current.status !== "active" ||
337
+ !isPersonalGitHubConnection(current)
338
+ ) {
339
+ throw new PersonalGitHubRepositoryProviderError("connection_changed");
340
+ }
341
+ const authority = await getPersonalGitHubRepositorySelectionState(deps.db, {
342
+ accountId: connection.accountId,
343
+ originWorkspaceId: connection.workspaceId,
344
+ subjectId,
345
+ connectionId: connection.id,
346
+ });
347
+ if (
348
+ !authority ||
349
+ authority.connectionAuthorityGeneration !== expectedConnectionAuthorityGeneration
350
+ ) {
351
+ throw new PersonalGitHubRepositoryProviderError("connection_changed");
352
+ }
353
+ const authorization = Object.entries(credential.headers).find(
354
+ ([name]) => name.toLowerCase() === "authorization",
355
+ )?.[1];
356
+ if (!authorization) {
357
+ throw new PersonalGitHubRepositoryProviderError("connection_inactive");
358
+ }
359
+ try {
360
+ return await (deps.githubPersonalFetch ?? fetch)(url, {
361
+ method: "GET",
362
+ redirect: "manual",
363
+ headers: {
364
+ accept: "application/vnd.github+json",
365
+ authorization,
366
+ "user-agent": "OpenGeni",
367
+ "x-github-api-version": GITHUB_API_VERSION,
368
+ },
369
+ signal: AbortSignal.timeout(GITHUB_REQUEST_TIMEOUT_MS),
370
+ });
371
+ } catch {
372
+ throw new PersonalGitHubRepositoryProviderError("provider_unavailable");
373
+ }
374
+ }
375
+
376
+ export function personalGitHubRepositoryCanRead(
377
+ permissions: PersonalGitHubRepositoryContract["permissions"],
378
+ ): boolean {
379
+ return (
380
+ permissions.pull ||
381
+ permissions.triage ||
382
+ permissions.push ||
383
+ permissions.maintain ||
384
+ permissions.admin
385
+ );
386
+ }
387
+
388
+ export function personalGitHubRepositoryCanWrite(
389
+ permissions: PersonalGitHubRepositoryContract["permissions"],
390
+ ): boolean {
391
+ return permissions.push || permissions.maintain || permissions.admin;
392
+ }
393
+
394
+ export function parsePersonalGitHubRepository(value: unknown): PersonalGitHubRepositoryContract {
395
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
396
+ throw new PersonalGitHubRepositoryProviderError("invalid_provider_response");
397
+ }
398
+ const payload = value as Record<string, unknown>;
399
+ const repositoryId = providerRepositoryId(payload.id);
400
+ const fullName = requiredProviderString(payload.full_name, 255);
401
+ const defaultBranch = requiredProviderString(payload.default_branch, 255);
402
+ const permissions = payload.permissions;
403
+ if (!permissions || typeof permissions !== "object" || Array.isArray(permissions)) {
404
+ throw new PersonalGitHubRepositoryProviderError("invalid_provider_response");
405
+ }
406
+ const permissionRecord = permissions as Record<string, unknown>;
407
+ const parsed = PersonalGitHubRepository.safeParse({
408
+ repositoryId,
409
+ fullName,
410
+ canonicalUrl: canonicalPersonalGitHubRepositoryUrl(fullName),
411
+ defaultBranch,
412
+ visibility: payload.visibility,
413
+ private: payload.private,
414
+ archived: payload.archived,
415
+ disabled: payload.disabled,
416
+ permissions: {
417
+ pull: permissionRecord.pull,
418
+ push: permissionRecord.push,
419
+ admin: permissionRecord.admin,
420
+ maintain: permissionRecord.maintain,
421
+ triage: permissionRecord.triage,
422
+ },
423
+ });
424
+ if (!parsed.success) {
425
+ throw new PersonalGitHubRepositoryProviderError("invalid_provider_response");
426
+ }
427
+ return parsed.data;
428
+ }
429
+
430
+ function providerRepositoryId(value: unknown): string {
431
+ if (typeof value === "number" && Number.isSafeInteger(value) && value > 0) return String(value);
432
+ if (typeof value === "string" && /^[1-9]\d*$/u.test(value)) return value;
433
+ throw new PersonalGitHubRepositoryProviderError("invalid_provider_response");
434
+ }
435
+
436
+ function requiredProviderString(value: unknown, maxLength: number): string {
437
+ if (typeof value !== "string" || value.length === 0 || value.length > maxLength) {
438
+ throw new PersonalGitHubRepositoryProviderError("invalid_provider_response");
439
+ }
440
+ return value;
441
+ }
442
+
443
+ async function cancelResponse(response: Response): Promise<void> {
444
+ await response.body?.cancel().catch(() => undefined);
445
+ }