@shipfox/api-integration-github 2.0.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 (179) hide show
  1. package/.turbo/turbo-build.log +2 -0
  2. package/.turbo/turbo-type$colon$emit.log +1 -0
  3. package/.turbo/turbo-type.log +1 -0
  4. package/CHANGELOG.md +138 -0
  5. package/LICENSE +21 -0
  6. package/dist/api/client.d.ts +90 -0
  7. package/dist/api/client.d.ts.map +1 -0
  8. package/dist/api/client.js +306 -0
  9. package/dist/api/client.js.map +1 -0
  10. package/dist/api/installation-token-envelope.d.ts +36 -0
  11. package/dist/api/installation-token-envelope.d.ts.map +1 -0
  12. package/dist/api/installation-token-envelope.js +123 -0
  13. package/dist/api/installation-token-envelope.js.map +1 -0
  14. package/dist/api/installation-token-provider.d.ts +23 -0
  15. package/dist/api/installation-token-provider.d.ts.map +1 -0
  16. package/dist/api/installation-token-provider.js +130 -0
  17. package/dist/api/installation-token-provider.js.map +1 -0
  18. package/dist/api/shared-installation-token-cache.d.ts +43 -0
  19. package/dist/api/shared-installation-token-cache.d.ts.map +1 -0
  20. package/dist/api/shared-installation-token-cache.js +224 -0
  21. package/dist/api/shared-installation-token-cache.js.map +1 -0
  22. package/dist/config.d.ts +15 -0
  23. package/dist/config.d.ts.map +1 -0
  24. package/dist/config.js +44 -0
  25. package/dist/config.js.map +1 -0
  26. package/dist/core/actions-logs.d.ts +47 -0
  27. package/dist/core/actions-logs.d.ts.map +1 -0
  28. package/dist/core/actions-logs.js +134 -0
  29. package/dist/core/actions-logs.js.map +1 -0
  30. package/dist/core/agent-tool-selection.d.ts +4 -0
  31. package/dist/core/agent-tool-selection.d.ts.map +1 -0
  32. package/dist/core/agent-tool-selection.js +38 -0
  33. package/dist/core/agent-tool-selection.js.map +1 -0
  34. package/dist/core/agent-tools.d.ts +50 -0
  35. package/dist/core/agent-tools.d.ts.map +1 -0
  36. package/dist/core/agent-tools.js +1122 -0
  37. package/dist/core/agent-tools.js.map +1 -0
  38. package/dist/core/errors.d.ts +15 -0
  39. package/dist/core/errors.d.ts.map +1 -0
  40. package/dist/core/errors.js +22 -0
  41. package/dist/core/errors.js.map +1 -0
  42. package/dist/core/install.d.ts +38 -0
  43. package/dist/core/install.d.ts.map +1 -0
  44. package/dist/core/install.js +64 -0
  45. package/dist/core/install.js.map +1 -0
  46. package/dist/core/source-control.d.ts +15 -0
  47. package/dist/core/source-control.d.ts.map +1 -0
  48. package/dist/core/source-control.js +175 -0
  49. package/dist/core/source-control.js.map +1 -0
  50. package/dist/core/state.d.ts +12 -0
  51. package/dist/core/state.d.ts.map +1 -0
  52. package/dist/core/state.js +54 -0
  53. package/dist/core/state.js.map +1 -0
  54. package/dist/core/webhook.d.ts +20 -0
  55. package/dist/core/webhook.d.ts.map +1 -0
  56. package/dist/core/webhook.js +216 -0
  57. package/dist/core/webhook.js.map +1 -0
  58. package/dist/db/db.d.ts +433 -0
  59. package/dist/db/db.d.ts.map +1 -0
  60. package/dist/db/db.js +18 -0
  61. package/dist/db/db.js.map +1 -0
  62. package/dist/db/installation-token-lock.d.ts +8 -0
  63. package/dist/db/installation-token-lock.d.ts.map +1 -0
  64. package/dist/db/installation-token-lock.js +37 -0
  65. package/dist/db/installation-token-lock.js.map +1 -0
  66. package/dist/db/installations.d.ts +33 -0
  67. package/dist/db/installations.d.ts.map +1 -0
  68. package/dist/db/installations.js +56 -0
  69. package/dist/db/installations.js.map +1 -0
  70. package/dist/db/migrations.d.ts +2 -0
  71. package/dist/db/migrations.d.ts.map +1 -0
  72. package/dist/db/migrations.js +5 -0
  73. package/dist/db/migrations.js.map +1 -0
  74. package/dist/db/schema/common.d.ts +2 -0
  75. package/dist/db/schema/common.d.ts.map +1 -0
  76. package/dist/db/schema/common.js +4 -0
  77. package/dist/db/schema/common.js.map +1 -0
  78. package/dist/db/schema/installations.d.ts +218 -0
  79. package/dist/db/schema/installations.d.ts.map +1 -0
  80. package/dist/db/schema/installations.js +46 -0
  81. package/dist/db/schema/installations.js.map +1 -0
  82. package/dist/index.d.ts +53 -0
  83. package/dist/index.d.ts.map +1 -0
  84. package/dist/index.js +66 -0
  85. package/dist/index.js.map +1 -0
  86. package/dist/metrics/index.d.ts +2 -0
  87. package/dist/metrics/index.d.ts.map +1 -0
  88. package/dist/metrics/index.js +3 -0
  89. package/dist/metrics/index.js.map +1 -0
  90. package/dist/metrics/instance.d.ts +14 -0
  91. package/dist/metrics/instance.d.ts.map +1 -0
  92. package/dist/metrics/instance.js +75 -0
  93. package/dist/metrics/instance.js.map +1 -0
  94. package/dist/presentation/dto/integrations.d.ts +15 -0
  95. package/dist/presentation/dto/integrations.d.ts.map +1 -0
  96. package/dist/presentation/dto/integrations.js +10 -0
  97. package/dist/presentation/dto/integrations.js.map +1 -0
  98. package/dist/presentation/e2eRoutes/create-connection.d.ts +11 -0
  99. package/dist/presentation/e2eRoutes/create-connection.d.ts.map +1 -0
  100. package/dist/presentation/e2eRoutes/create-connection.js +57 -0
  101. package/dist/presentation/e2eRoutes/create-connection.js.map +1 -0
  102. package/dist/presentation/e2eRoutes/index.d.ts +5 -0
  103. package/dist/presentation/e2eRoutes/index.d.ts.map +1 -0
  104. package/dist/presentation/e2eRoutes/index.js +11 -0
  105. package/dist/presentation/e2eRoutes/index.js.map +1 -0
  106. package/dist/presentation/routes/errors.d.ts +2 -0
  107. package/dist/presentation/routes/errors.d.ts.map +1 -0
  108. package/dist/presentation/routes/errors.js +46 -0
  109. package/dist/presentation/routes/errors.js.map +1 -0
  110. package/dist/presentation/routes/install.d.ts +13 -0
  111. package/dist/presentation/routes/install.d.ts.map +1 -0
  112. package/dist/presentation/routes/install.js +77 -0
  113. package/dist/presentation/routes/install.js.map +1 -0
  114. package/dist/presentation/routes/webhooks.d.ts +16 -0
  115. package/dist/presentation/routes/webhooks.d.ts.map +1 -0
  116. package/dist/presentation/routes/webhooks.js +110 -0
  117. package/dist/presentation/routes/webhooks.js.map +1 -0
  118. package/dist/tsconfig.test.tsbuildinfo +1 -0
  119. package/drizzle/0000_initial.sql +17 -0
  120. package/drizzle/meta/_journal.json +13 -0
  121. package/drizzle.config.ts +7 -0
  122. package/package.json +71 -0
  123. package/src/api/client.test.ts +94 -0
  124. package/src/api/client.ts +525 -0
  125. package/src/api/installation-token-envelope.ts +171 -0
  126. package/src/api/installation-token-provider.test.ts +241 -0
  127. package/src/api/installation-token-provider.ts +224 -0
  128. package/src/api/shared-installation-token-cache.test.ts +294 -0
  129. package/src/api/shared-installation-token-cache.ts +346 -0
  130. package/src/config.test.ts +14 -0
  131. package/src/config.ts +47 -0
  132. package/src/connection-external-url.test.ts +78 -0
  133. package/src/core/actions-logs.test.ts +156 -0
  134. package/src/core/actions-logs.ts +186 -0
  135. package/src/core/agent-tool-selection.test.ts +41 -0
  136. package/src/core/agent-tool-selection.ts +42 -0
  137. package/src/core/agent-tools.test.ts +539 -0
  138. package/src/core/agent-tools.ts +1269 -0
  139. package/src/core/errors.ts +23 -0
  140. package/src/core/install.test.ts +238 -0
  141. package/src/core/install.ts +119 -0
  142. package/src/core/source-control.test.ts +327 -0
  143. package/src/core/source-control.ts +223 -0
  144. package/src/core/state.test.ts +43 -0
  145. package/src/core/state.ts +85 -0
  146. package/src/core/webhook.test.ts +522 -0
  147. package/src/core/webhook.ts +296 -0
  148. package/src/db/db.ts +18 -0
  149. package/src/db/installation-token-lock.test.ts +68 -0
  150. package/src/db/installation-token-lock.ts +39 -0
  151. package/src/db/installations.test.ts +67 -0
  152. package/src/db/installations.ts +108 -0
  153. package/src/db/migrations.ts +4 -0
  154. package/src/db/schema/common.ts +3 -0
  155. package/src/db/schema/installations.ts +46 -0
  156. package/src/index.ts +124 -0
  157. package/src/metrics/index.ts +1 -0
  158. package/src/metrics/instance.ts +82 -0
  159. package/src/presentation/dto/integrations.ts +8 -0
  160. package/src/presentation/e2eRoutes/create-connection.ts +70 -0
  161. package/src/presentation/e2eRoutes/index.test.ts +126 -0
  162. package/src/presentation/e2eRoutes/index.ts +14 -0
  163. package/src/presentation/routes/errors.ts +43 -0
  164. package/src/presentation/routes/install.test.ts +279 -0
  165. package/src/presentation/routes/install.ts +93 -0
  166. package/src/presentation/routes/webhooks.test.ts +560 -0
  167. package/src/presentation/routes/webhooks.ts +115 -0
  168. package/test/env.ts +16 -0
  169. package/test/factories/github-installation.ts +35 -0
  170. package/test/factories/index.ts +1 -0
  171. package/test/fixtures/github-webhook.ts +20 -0
  172. package/test/globalSetup.ts +14 -0
  173. package/test/index.ts +2 -0
  174. package/test/setup.ts +17 -0
  175. package/tsconfig.build.json +9 -0
  176. package/tsconfig.build.tsbuildinfo +1 -0
  177. package/tsconfig.json +3 -0
  178. package/tsconfig.test.json +9 -0
  179. package/vitest.config.ts +12 -0
@@ -0,0 +1,2 @@
1
+ $ shipfox-swc
2
+ Successfully compiled: 28 files with swc (66.52ms)
@@ -0,0 +1 @@
1
+ $ shipfox-tsc-emit
@@ -0,0 +1 @@
1
+ $ shipfox-tsc-check
package/CHANGELOG.md ADDED
@@ -0,0 +1,138 @@
1
+ # @shipfox/api-integration-github
2
+
3
+ ## 2.0.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 1b0d344: Publishes the complete API runtime closure with packed-consumer-safe internal imports and records its exact package set in application releases.
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies [c31a7e0]
12
+ - Updated dependencies [1b0d344]
13
+ - Updated dependencies [521e006]
14
+ - @shipfox/api-workspaces@2.0.0
15
+ - @shipfox/api-auth-context@2.0.0
16
+ - @shipfox/api-integration-core-dto@2.0.0
17
+ - @shipfox/api-integration-github-dto@2.0.0
18
+ - @shipfox/config@1.2.1
19
+ - @shipfox/node-drizzle@0.2.1
20
+ - @shipfox/node-fastify@0.2.1
21
+ - @shipfox/node-opentelemetry@0.5.0
22
+ - @shipfox/node-postgres@0.4.1
23
+
24
+ ## 0.1.2
25
+
26
+ ### Patch Changes
27
+
28
+ - @shipfox/api-workspaces@0.1.2
29
+
30
+ ## 0.1.1
31
+
32
+ ### Patch Changes
33
+
34
+ - Updated dependencies [ec75cd5]
35
+ - Updated dependencies [6a1fb54]
36
+ - @shipfox/node-drizzle@0.2.0
37
+ - @shipfox/node-postgres@0.4.0
38
+ - @shipfox/api-workspaces@0.1.1
39
+
40
+ ## 0.1.0
41
+
42
+ ### Minor Changes
43
+
44
+ - b8e49ff: Add the client-side Sentry install/connect flow and a workspace settings
45
+ integrations hub.
46
+ - `@shipfox/client-integrations`: shared `IntegrationGallerySection` (capability
47
+ filter, lifecycle pills, "Added" date, external link, connected-first
48
+ ordering, degraded status mode), shared `RedirectInstallPage` powering the
49
+ GitHub and new Sentry install pages, `SentryCallbackPage` with an explicit
50
+ workspace confirm (sessionStorage only pre-selects), two-tier retry, and the
51
+ Sentry hooks (`useCreateSentryInstallMutation`, `connectSentry`,
52
+ `useIntegrationConnectionsQuery`).
53
+ - `@shipfox/client-workspace-settings`: new `/workspaces/$wid/settings/integrations`
54
+ page and an Integrations entry in the settings nav.
55
+ - `@shipfox/client-router`: routes for the Sentry install page, the root-level
56
+ Sentry callback, and the settings integrations page.
57
+ - `@shipfox/react-ui`: `sentry` icon (monochrome, theme-aware).
58
+ - `@shipfox/api-integration-core-dto`: optional `external_url` on the connection
59
+ DTO and an optional `connectionExternalUrl` method on `IntegrationProvider`.
60
+ - `@shipfox/api-integration-core`: `GET /integration-connections` now returns
61
+ connections of every lifecycle status (the active-only filter prevented
62
+ clients from surfacing disabled/error state) and resolves `external_url`
63
+ per connection best-effort.
64
+ - `@shipfox/api-integration-sentry` / `@shipfox/api-integration-github`:
65
+ implement `connectionExternalUrl` (Sentry org URL via a new
66
+ by-connection-id installation lookup; GitHub installation settings URL).
67
+
68
+ ### Patch Changes
69
+
70
+ - 115655e: Moves source-event translation to the integration module: source-control providers emit a typed, provider-agnostic `INTEGRATION_SOURCE_COMMIT_PUSHED` event via one transactional publisher, projects subscribes to it instead of decoding GitHub payloads, and branch-deletion pushes are dropped at the source.
71
+ - ce062a9: Validates authored agent step integrations against provider tool catalogs and workspace connection capabilities.
72
+ - f3614ae: Add `createCheckoutSpec()` to the integration source-control service and the GitHub provider. GitHub mints a short-lived, repo-scoped installation access token and returns it as structured `CheckoutCredentials` alongside a clean `repositoryUrl`; the secret is never embedded in the URL. `ref` defaults to the repository default branch, providers without checkout support raise a typed `IntegrationCheckoutUnsupportedError`, and a `redactCheckoutSpec()` helper masks the token for logging. Dormant until the checkout-token endpoint and runner enrichment are wired in; no runtime behavior changes yet.
73
+ - 9f1c0ef: Harden integration connect against a concurrent same-install race. The installation upsert now only (re)points an installation at the connection that already owns it: the `onConflictDoUpdate` carries a `setWhere(connection_id = this connection)` predicate and throws `*InstallationAlreadyLinkedError` when the conflicting row belongs to a different connection. Two concurrent connects of the same provider install to different workspaces no longer leave one workspace with an active orphan connection while the install's webhooks silently route to the other; the losing transaction rolls back and surfaces a 409.
74
+ - 0667cce: Skip publishing source pushes for non-active integration connections. Both the GitHub and Gitea push webhook handlers now treat a connection whose `lifecycleStatus` is not `active` (disabled/error) like an unknown one: the delivery is recorded for dedup but no source-push event is published, so a disabled connection no longer triggers workflow runs.
75
+ - d6d4862: Add a Sentry integration provider that ingests issue webhooks.
76
+ - New `@shipfox/api-integration-sentry` + `-sentry-dto` packages: a webhook
77
+ receiver that verifies the HMAC-SHA256 signature (keyed with the app client
78
+ secret), dedups on `Request-ID`, normalizes the issue payload, and publishes
79
+ `integrations.event.received` with `source: 'sentry'` and `event:
80
+ issue.<action>`. A raw `ignored` action is normalized to `archived`. Malformed,
81
+ bad-JSON, unknown-action, and unknown-resource deliveries are recorded-and-dropped
82
+ with a 204 (deliberate deviation from GitHub's 400 to avoid Sentry disabling the
83
+ webhook).
84
+ - `@shipfox/api-integration-core-dto`: add the `SentryIssuePayload` contract.
85
+ - `@shipfox/api-integration-core`: register the Sentry provider behind
86
+ `INTEGRATIONS_ENABLE_SENTRY_PROVIDER`, add the
87
+ `updateIntegrationConnectionLifecycleStatus` helper, and pin stable
88
+ migration-tracking table names per provider database.
89
+ - `@shipfox/node-fastify`: add the shared `rawBodyPlugin` and `WEBHOOK_BODY_LIMIT`
90
+ exports for webhook receivers.
91
+ - `@shipfox/node-module`: add an optional `migrationsTableName` to `ModuleDatabase`
92
+ so conditionally-composed databases get a position-independent migration table.
93
+ - `@shipfox/api-integration-github`: consume the shared `rawBodyPlugin` instead of
94
+ a local copy (internal refactor, no behavior change).
95
+
96
+ Deploy note: environments with GitHub enabled must rename the existing
97
+ `__drizzle_migrations_integrations_1` table to
98
+ `__drizzle_migrations_integrations_github` as part of this release, or GitHub
99
+ migrations re-run against existing tables.
100
+
101
+ - 82d22e4: Make the workspace-membership gate stateless. `requireWorkspaceAccess` now lives in `@shipfox/api-auth-context` and authorizes a request purely from the verified session-token claims, replacing the `requireMembership` gate in `@shipfox/api-workspaces` that read the workspace row from the database on every workspace-scoped request. Membership and role already travel in the token, so the check needs no database access.
102
+
103
+ This removes the per-request database read and severs the runtime dependency on `@shipfox/api-workspaces` from feature modules that only needed the membership gate (integration providers, secrets, projects, agent, runners). Workspace existence and `active`-status enforcement, which no code path currently exercises, moves off the hot path; enforce it at token issuance when workspace suspension is introduced.
104
+
105
+ - 01be723: Adds a shared GitHub installation token provider with broad REST minting, in-memory reuse, refresh-margin reminting, single-flight dedupe, and a missing-installation provider error reason.
106
+ - Updated dependencies [34ba284]
107
+ - Updated dependencies [5707d6d]
108
+ - Updated dependencies [b9c3f32]
109
+ - Updated dependencies [d02c5fd]
110
+ - Updated dependencies [a81b68c]
111
+ - Updated dependencies [115655e]
112
+ - Updated dependencies [c0a883c]
113
+ - Updated dependencies [72ce351]
114
+ - Updated dependencies [e47f8da]
115
+ - Updated dependencies [ce062a9]
116
+ - Updated dependencies [7b175f5]
117
+ - Updated dependencies [f3614ae]
118
+ - Updated dependencies [f92122b]
119
+ - Updated dependencies [f8f339a]
120
+ - Updated dependencies [857fd73]
121
+ - Updated dependencies [b8e49ff]
122
+ - Updated dependencies [d6d4862]
123
+ - Updated dependencies [c0a883c]
124
+ - Updated dependencies [6077301]
125
+ - Updated dependencies [82d22e4]
126
+ - Updated dependencies [01be723]
127
+ - Updated dependencies [27770eb]
128
+ - Updated dependencies [2933c33]
129
+ - Updated dependencies [6181819]
130
+ - @shipfox/node-fastify@0.2.0
131
+ - @shipfox/node-drizzle@0.1.0
132
+ - @shipfox/api-workspaces@0.1.0
133
+ - @shipfox/api-auth-context@0.1.0
134
+ - @shipfox/api-integration-core-dto@0.1.0
135
+ - @shipfox/node-opentelemetry@0.4.2
136
+ - @shipfox/node-postgres@0.3.2
137
+ - @shipfox/api-integration-github-dto@0.0.1
138
+ - @shipfox/config@1.2.0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Shipfox
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,90 @@
1
+ export interface GithubAccount {
2
+ login: string;
3
+ type: string;
4
+ }
5
+ export interface GithubInstallationDetails {
6
+ id: number;
7
+ account: GithubAccount;
8
+ repositorySelection: string;
9
+ suspendedAt: Date | null;
10
+ htmlUrl: string;
11
+ raw: Record<string, unknown>;
12
+ }
13
+ export interface GithubRepository {
14
+ id: number;
15
+ ownerLogin: string;
16
+ name: string;
17
+ fullName: string;
18
+ defaultBranch: string;
19
+ private: boolean;
20
+ visibility?: string | undefined;
21
+ cloneUrl: string;
22
+ htmlUrl: string;
23
+ }
24
+ export interface GithubRepositoryPage {
25
+ repositories: GithubRepository[];
26
+ nextCursor: string | null;
27
+ }
28
+ export interface GithubFileEntry {
29
+ path: string;
30
+ size: number | null;
31
+ }
32
+ export interface GithubFilePage {
33
+ files: GithubFileEntry[];
34
+ nextCursor: string | null;
35
+ }
36
+ export interface GithubFileContent {
37
+ path: string;
38
+ content: string;
39
+ size: number;
40
+ }
41
+ export interface GithubUserInstallationPage {
42
+ installationIds: number[];
43
+ nextCursor: string | null;
44
+ }
45
+ export interface GithubApiClient {
46
+ exchangeOAuthCode(code: string): Promise<string>;
47
+ listUserInstallations(input: {
48
+ userAccessToken: string;
49
+ cursor?: string | undefined;
50
+ }): Promise<GithubUserInstallationPage>;
51
+ getInstallation(installationId: number): Promise<GithubInstallationDetails>;
52
+ listInstallationRepositories(input: {
53
+ installationId: number;
54
+ limit: number;
55
+ cursor?: string | undefined;
56
+ }): Promise<GithubRepositoryPage>;
57
+ getRepository(input: {
58
+ installationId: number;
59
+ repositoryId: number;
60
+ }): Promise<GithubRepository>;
61
+ listRepositoryFiles(input: {
62
+ installationId: number;
63
+ repositoryId: number;
64
+ ref: string;
65
+ prefix: string;
66
+ limit: number;
67
+ cursor?: string | undefined;
68
+ }): Promise<GithubFilePage>;
69
+ fetchRepositoryFile(input: {
70
+ installationId: number;
71
+ repositoryId: number;
72
+ ref: string;
73
+ path: string;
74
+ }): Promise<GithubFileContent>;
75
+ createInstallationAccessToken(input: {
76
+ installationId: number;
77
+ repositoryId: number;
78
+ permissions?: {
79
+ contents: 'read' | 'write';
80
+ } | undefined;
81
+ }): Promise<GithubInstallationAccessToken>;
82
+ }
83
+ export interface GithubInstallationAccessToken {
84
+ token: string;
85
+ expiresAt: Date;
86
+ permissions?: Record<string, 'read' | 'write' | 'admin'> | undefined;
87
+ }
88
+ export declare function createGithubApiClient(): GithubApiClient;
89
+ export declare function mapGithubError<T>(operation: () => Promise<T>, notFoundReason?: 'repository-not-found' | 'installation-not-found' | 'file-not-found'): Promise<T>;
90
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/api/client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,306 @@
1
+ import { Buffer } from 'node:buffer';
2
+ import { MAX_REPOSITORY_FILE_BYTES } from '@shipfox/api-integration-core-dto';
3
+ import ky, { HTTPError, TimeoutError } from 'ky';
4
+ import { App, Octokit, RequestError } from 'octokit';
5
+ import { config, normalizedGithubApiBaseUrl, normalizedGithubPrivateKey } from '#config.js';
6
+ import { GithubIntegrationProviderError } from '#core/errors.js';
7
+ const NEXT_PAGE_RE = /[?&]page=(\d+)/;
8
+ const TRAILING_SLASHES_RE = /\/+$/;
9
+ const MAX_TREE_WALK_DEPTH = 10;
10
+ export function createGithubApiClient() {
11
+ return new OctokitGithubApiClient();
12
+ }
13
+ let OctokitGithubApiClient = class OctokitGithubApiClient {
14
+ async exchangeOAuthCode(code) {
15
+ const body = await mapGithubOAuthError(()=>ky.post('https://github.com/login/oauth/access_token', {
16
+ headers: {
17
+ accept: 'application/json'
18
+ },
19
+ json: {
20
+ client_id: config.GITHUB_APP_CLIENT_ID,
21
+ client_secret: config.GITHUB_APP_CLIENT_SECRET,
22
+ code
23
+ }
24
+ }).json());
25
+ if (typeof body.access_token !== 'string') {
26
+ throw new GithubIntegrationProviderError('malformed-provider-response', 'GitHub OAuth response did not include an access token');
27
+ }
28
+ return body.access_token;
29
+ }
30
+ async listUserInstallations(input) {
31
+ const octokit = new Octokit({
32
+ auth: input.userAccessToken,
33
+ baseUrl: normalizedGithubApiBaseUrl()
34
+ });
35
+ const page = cursorToPage(input.cursor);
36
+ const response = await mapGithubError(()=>octokit.request('GET /user/installations', {
37
+ per_page: 100,
38
+ page
39
+ }));
40
+ const installations = response.data.installations ?? [];
41
+ return {
42
+ installationIds: installations.map((installation)=>installation.id),
43
+ nextCursor: nextCursorFromLink(response.headers.link)
44
+ };
45
+ }
46
+ async getInstallation(installationId) {
47
+ const response = await mapGithubError(()=>this.getApp().octokit.rest.apps.getInstallation({
48
+ installation_id: installationId
49
+ }));
50
+ const data = response.data;
51
+ const account = data.account;
52
+ if (!account) {
53
+ throw new GithubIntegrationProviderError('malformed-provider-response', 'GitHub installation response did not include an account');
54
+ }
55
+ const login = 'login' in account ? account.login : account.slug;
56
+ return {
57
+ id: data.id,
58
+ account: {
59
+ login,
60
+ type: 'type' in account ? account.type : 'Enterprise'
61
+ },
62
+ repositorySelection: data.repository_selection,
63
+ suspendedAt: data.suspended_at ? new Date(data.suspended_at) : null,
64
+ htmlUrl: data.html_url,
65
+ raw: data
66
+ };
67
+ }
68
+ async listInstallationRepositories(input) {
69
+ const octokit = await this.getApp().getInstallationOctokit(input.installationId);
70
+ const page = cursorToPage(input.cursor);
71
+ const response = await mapGithubError(()=>octokit.rest.apps.listReposAccessibleToInstallation({
72
+ per_page: input.limit,
73
+ page
74
+ }));
75
+ return {
76
+ repositories: response.data.repositories.map(toGithubRepository),
77
+ nextCursor: nextCursorFromLink(response.headers.link)
78
+ };
79
+ }
80
+ async getRepository(input) {
81
+ const octokit = await this.getApp().getInstallationOctokit(input.installationId);
82
+ const response = await mapGithubError(()=>octokit.request('GET /repositories/{repository_id}', {
83
+ repository_id: input.repositoryId
84
+ }));
85
+ return toGithubRepository(response.data);
86
+ }
87
+ async listRepositoryFiles(input) {
88
+ const octokit = await this.getApp().getInstallationOctokit(input.installationId);
89
+ const repository = await this.getRepository({
90
+ installationId: input.installationId,
91
+ repositoryId: input.repositoryId
92
+ });
93
+ const owner = repository.ownerLogin;
94
+ const repo = repository.name;
95
+ const startPath = input.prefix.replace(TRAILING_SLASHES_RE, '');
96
+ const offset = input.cursor ? Number.parseInt(input.cursor, 10) : 0;
97
+ const start = Number.isNaN(offset) || offset < 0 ? 0 : offset;
98
+ const collected = [];
99
+ const overflowLimit = start + input.limit + 1;
100
+ const walk = async (path, depth)=>{
101
+ if (collected.length >= overflowLimit) return;
102
+ if (depth > MAX_TREE_WALK_DEPTH) return;
103
+ let data;
104
+ try {
105
+ const response = await mapGithubError(()=>octokit.rest.repos.getContent({
106
+ owner,
107
+ repo,
108
+ path,
109
+ ref: input.ref
110
+ }), 'file-not-found');
111
+ data = response.data;
112
+ } catch (error) {
113
+ if (error instanceof GithubIntegrationProviderError && error.reason === 'file-not-found') {
114
+ return;
115
+ }
116
+ throw error;
117
+ }
118
+ if (!Array.isArray(data)) {
119
+ if (data.type === 'file' && data.path) {
120
+ collected.push({
121
+ path: data.path,
122
+ size: typeof data.size === 'number' ? data.size : null
123
+ });
124
+ }
125
+ return;
126
+ }
127
+ const entries = [
128
+ ...data
129
+ ].sort((a, b)=>(a.path ?? '').localeCompare(b.path ?? ''));
130
+ for (const entry of entries){
131
+ if (collected.length >= overflowLimit) return;
132
+ if (!entry.path) continue;
133
+ if (entry.type === 'file') {
134
+ collected.push({
135
+ path: entry.path,
136
+ size: typeof entry.size === 'number' ? entry.size : null
137
+ });
138
+ } else if (entry.type === 'dir') {
139
+ await walk(entry.path, depth + 1);
140
+ }
141
+ }
142
+ };
143
+ await walk(startPath, 0);
144
+ const sorted = collected.sort((a, b)=>a.path.localeCompare(b.path));
145
+ const page = sorted.slice(start, start + input.limit);
146
+ const consumed = start + page.length;
147
+ const hasMore = consumed < sorted.length;
148
+ return {
149
+ files: page,
150
+ nextCursor: hasMore ? String(consumed) : null
151
+ };
152
+ }
153
+ async fetchRepositoryFile(input) {
154
+ const octokit = await this.getApp().getInstallationOctokit(input.installationId);
155
+ const repository = await this.getRepository({
156
+ installationId: input.installationId,
157
+ repositoryId: input.repositoryId
158
+ });
159
+ const response = await mapGithubError(()=>octokit.rest.repos.getContent({
160
+ owner: repository.ownerLogin,
161
+ repo: repository.name,
162
+ path: input.path,
163
+ ref: input.ref
164
+ }), 'file-not-found');
165
+ const data = response.data;
166
+ if (Array.isArray(data) || data.type !== 'file') {
167
+ throw new GithubIntegrationProviderError('file-not-found', 'GitHub path is not a file');
168
+ }
169
+ if (data.size > MAX_REPOSITORY_FILE_BYTES) {
170
+ throw new GithubIntegrationProviderError('content-too-large', 'GitHub file content is larger than the supported limit');
171
+ }
172
+ if (typeof data.content !== 'string' || data.encoding !== 'base64') {
173
+ throw new GithubIntegrationProviderError('malformed-provider-response', 'GitHub file response did not include base64 content');
174
+ }
175
+ return {
176
+ path: data.path,
177
+ size: data.size,
178
+ content: Buffer.from(data.content, 'base64').toString('utf8')
179
+ };
180
+ }
181
+ async createInstallationAccessToken(input) {
182
+ const response = await mapGithubError(()=>this.getApp().octokit.rest.apps.createInstallationAccessToken({
183
+ installation_id: input.installationId,
184
+ repository_ids: [
185
+ input.repositoryId
186
+ ],
187
+ permissions: input.permissions ?? {
188
+ contents: 'read'
189
+ }
190
+ }));
191
+ if (typeof response.data.token !== 'string') {
192
+ throw new GithubIntegrationProviderError('malformed-provider-response', 'GitHub installation access token response did not include a token');
193
+ }
194
+ const expiresAt = new Date(response.data.expires_at);
195
+ if (Number.isNaN(expiresAt.getTime())) {
196
+ throw new GithubIntegrationProviderError('malformed-provider-response', 'GitHub installation access token response did not include a valid expiry');
197
+ }
198
+ return {
199
+ token: response.data.token,
200
+ expiresAt,
201
+ ...response.data.permissions === undefined ? {} : {
202
+ permissions: response.data.permissions
203
+ }
204
+ };
205
+ }
206
+ getApp() {
207
+ if (!this.app) {
208
+ this.app = new App({
209
+ appId: config.GITHUB_APP_ID,
210
+ privateKey: normalizedGithubPrivateKey(),
211
+ Octokit: Octokit.defaults({
212
+ baseUrl: normalizedGithubApiBaseUrl()
213
+ })
214
+ });
215
+ }
216
+ return this.app;
217
+ }
218
+ };
219
+ async function mapGithubOAuthError(operation) {
220
+ try {
221
+ return await operation();
222
+ } catch (error) {
223
+ if (error instanceof HTTPError) {
224
+ if (error.response.status === 429) {
225
+ throw new GithubIntegrationProviderError('rate-limited', 'GitHub OAuth code exchange failed', retryAfterSecondsFromHeaders(error.response.headers));
226
+ }
227
+ if (error.response.status >= 500) {
228
+ throw new GithubIntegrationProviderError('provider-unavailable', 'GitHub OAuth code exchange failed');
229
+ }
230
+ throw new GithubIntegrationProviderError('access-denied', 'GitHub OAuth code exchange failed');
231
+ }
232
+ if (error instanceof TimeoutError) {
233
+ throw new GithubIntegrationProviderError('timeout', 'GitHub OAuth request timed out');
234
+ }
235
+ throw error;
236
+ }
237
+ }
238
+ export async function mapGithubError(operation, notFoundReason = 'repository-not-found') {
239
+ try {
240
+ return await operation();
241
+ } catch (error) {
242
+ if (error instanceof GithubIntegrationProviderError) throw error;
243
+ if (error instanceof RequestError) {
244
+ if (error.status === 404) {
245
+ throw new GithubIntegrationProviderError(notFoundReason, error.message);
246
+ }
247
+ if (error.status === 429 || isGithubRateLimitError(error)) {
248
+ throw new GithubIntegrationProviderError('rate-limited', error.message, retryAfterSeconds(error));
249
+ }
250
+ if (error.status === 401 || error.status === 403) {
251
+ throw new GithubIntegrationProviderError('access-denied', error.message, retryAfterSeconds(error));
252
+ }
253
+ if (error.status >= 500) {
254
+ throw new GithubIntegrationProviderError('provider-unavailable', error.message);
255
+ }
256
+ }
257
+ if (error instanceof Error && error.name === 'AbortError') {
258
+ throw new GithubIntegrationProviderError('timeout', 'GitHub request timed out');
259
+ }
260
+ throw error;
261
+ }
262
+ }
263
+ function isGithubRateLimitError(error) {
264
+ return error.status === 403 && error.response?.headers['x-ratelimit-remaining'] === '0';
265
+ }
266
+ function retryAfterSeconds(error) {
267
+ return parseRetryAfterSeconds(error.response?.headers['retry-after']);
268
+ }
269
+ function retryAfterSecondsFromHeaders(headers) {
270
+ return parseRetryAfterSeconds(headers.get('retry-after'));
271
+ }
272
+ function parseRetryAfterSeconds(retryAfter) {
273
+ if (!retryAfter) return undefined;
274
+ const parsed = Number.parseInt(String(retryAfter), 10);
275
+ return Number.isNaN(parsed) ? undefined : parsed;
276
+ }
277
+ function cursorToPage(cursor) {
278
+ if (!cursor) return 1;
279
+ const page = Number.parseInt(cursor, 10);
280
+ return Number.isNaN(page) || page < 1 ? 1 : page;
281
+ }
282
+ function nextCursorFromLink(link) {
283
+ if (!link) return null;
284
+ const next = link.split(',').find((part)=>part.includes('rel="next"'));
285
+ if (!next) return null;
286
+ const match = next.match(NEXT_PAGE_RE);
287
+ return match?.[1] ?? null;
288
+ }
289
+ function toGithubRepository(repository) {
290
+ if (!repository.default_branch || !repository.clone_url || !repository.html_url) {
291
+ throw new GithubIntegrationProviderError('malformed-provider-response', 'GitHub repository response is missing required fields');
292
+ }
293
+ return {
294
+ id: repository.id,
295
+ ownerLogin: repository.owner.login,
296
+ name: repository.name,
297
+ fullName: repository.full_name,
298
+ defaultBranch: repository.default_branch,
299
+ private: repository.private,
300
+ visibility: repository.visibility,
301
+ cloneUrl: repository.clone_url,
302
+ htmlUrl: repository.html_url
303
+ };
304
+ }
305
+
306
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/api/client.ts"],"sourcesContent":["import {Buffer} from 'node:buffer';\nimport {MAX_REPOSITORY_FILE_BYTES} from '@shipfox/api-integration-core-dto';\nimport ky, {HTTPError, TimeoutError} from 'ky';\nimport {App, Octokit, RequestError} from 'octokit';\nimport {config, normalizedGithubApiBaseUrl, normalizedGithubPrivateKey} from '#config.js';\nimport {GithubIntegrationProviderError} from '#core/errors.js';\n\nconst NEXT_PAGE_RE = /[?&]page=(\\d+)/;\nconst TRAILING_SLASHES_RE = /\\/+$/;\nconst MAX_TREE_WALK_DEPTH = 10;\n\nexport interface GithubAccount {\n login: string;\n type: string;\n}\n\nexport interface GithubInstallationDetails {\n id: number;\n account: GithubAccount;\n repositorySelection: string;\n suspendedAt: Date | null;\n htmlUrl: string;\n raw: Record<string, unknown>;\n}\n\nexport interface GithubRepository {\n id: number;\n ownerLogin: string;\n name: string;\n fullName: string;\n defaultBranch: string;\n private: boolean;\n visibility?: string | undefined;\n cloneUrl: string;\n htmlUrl: string;\n}\n\nexport interface GithubRepositoryPage {\n repositories: GithubRepository[];\n nextCursor: string | null;\n}\n\nexport interface GithubFileEntry {\n path: string;\n size: number | null;\n}\n\nexport interface GithubFilePage {\n files: GithubFileEntry[];\n nextCursor: string | null;\n}\n\nexport interface GithubFileContent {\n path: string;\n content: string;\n size: number;\n}\n\nexport interface GithubUserInstallationPage {\n installationIds: number[];\n nextCursor: string | null;\n}\n\nexport interface GithubApiClient {\n exchangeOAuthCode(code: string): Promise<string>;\n listUserInstallations(input: {\n userAccessToken: string;\n cursor?: string | undefined;\n }): Promise<GithubUserInstallationPage>;\n getInstallation(installationId: number): Promise<GithubInstallationDetails>;\n listInstallationRepositories(input: {\n installationId: number;\n limit: number;\n cursor?: string | undefined;\n }): Promise<GithubRepositoryPage>;\n getRepository(input: {installationId: number; repositoryId: number}): Promise<GithubRepository>;\n listRepositoryFiles(input: {\n installationId: number;\n repositoryId: number;\n ref: string;\n prefix: string;\n limit: number;\n cursor?: string | undefined;\n }): Promise<GithubFilePage>;\n fetchRepositoryFile(input: {\n installationId: number;\n repositoryId: number;\n ref: string;\n path: string;\n }): Promise<GithubFileContent>;\n createInstallationAccessToken(input: {\n installationId: number;\n repositoryId: number;\n permissions?: {contents: 'read' | 'write'} | undefined;\n }): Promise<GithubInstallationAccessToken>;\n}\n\nexport interface GithubInstallationAccessToken {\n token: string;\n expiresAt: Date;\n permissions?: Record<string, 'read' | 'write' | 'admin'> | undefined;\n}\n\nexport function createGithubApiClient(): GithubApiClient {\n return new OctokitGithubApiClient();\n}\n\nclass OctokitGithubApiClient implements GithubApiClient {\n private app: App | undefined;\n\n async exchangeOAuthCode(code: string): Promise<string> {\n const body = await mapGithubOAuthError(() =>\n ky\n .post('https://github.com/login/oauth/access_token', {\n headers: {accept: 'application/json'},\n json: {\n client_id: config.GITHUB_APP_CLIENT_ID,\n client_secret: config.GITHUB_APP_CLIENT_SECRET,\n code,\n },\n })\n .json<{access_token?: unknown}>(),\n );\n\n if (typeof body.access_token !== 'string') {\n throw new GithubIntegrationProviderError(\n 'malformed-provider-response',\n 'GitHub OAuth response did not include an access token',\n );\n }\n return body.access_token;\n }\n\n async listUserInstallations(input: {\n userAccessToken: string;\n cursor?: string | undefined;\n }): Promise<GithubUserInstallationPage> {\n const octokit = new Octokit({\n auth: input.userAccessToken,\n baseUrl: normalizedGithubApiBaseUrl(),\n });\n const page = cursorToPage(input.cursor);\n const response = await mapGithubError(() =>\n octokit.request('GET /user/installations', {\n per_page: 100,\n page,\n }),\n );\n\n const installations = response.data.installations ?? [];\n return {\n installationIds: installations.map((installation) => installation.id),\n nextCursor: nextCursorFromLink(response.headers.link),\n };\n }\n\n async getInstallation(installationId: number): Promise<GithubInstallationDetails> {\n const response = await mapGithubError(() =>\n this.getApp().octokit.rest.apps.getInstallation({installation_id: installationId}),\n );\n const data = response.data;\n const account = data.account;\n if (!account) {\n throw new GithubIntegrationProviderError(\n 'malformed-provider-response',\n 'GitHub installation response did not include an account',\n );\n }\n\n const login = 'login' in account ? account.login : account.slug;\n return {\n id: data.id,\n account: {\n login,\n type: 'type' in account ? account.type : 'Enterprise',\n },\n repositorySelection: data.repository_selection,\n suspendedAt: data.suspended_at ? new Date(data.suspended_at) : null,\n htmlUrl: data.html_url,\n raw: data as unknown as Record<string, unknown>,\n };\n }\n\n async listInstallationRepositories(input: {\n installationId: number;\n limit: number;\n cursor?: string | undefined;\n }): Promise<GithubRepositoryPage> {\n const octokit = await this.getApp().getInstallationOctokit(input.installationId);\n const page = cursorToPage(input.cursor);\n const response = await mapGithubError(() =>\n octokit.rest.apps.listReposAccessibleToInstallation({\n per_page: input.limit,\n page,\n }),\n );\n\n return {\n repositories: response.data.repositories.map(toGithubRepository),\n nextCursor: nextCursorFromLink(response.headers.link),\n };\n }\n\n async getRepository(input: {\n installationId: number;\n repositoryId: number;\n }): Promise<GithubRepository> {\n const octokit = await this.getApp().getInstallationOctokit(input.installationId);\n const response = await mapGithubError(() =>\n octokit.request('GET /repositories/{repository_id}', {\n repository_id: input.repositoryId,\n }),\n );\n\n return toGithubRepository(response.data);\n }\n\n async listRepositoryFiles(input: {\n installationId: number;\n repositoryId: number;\n ref: string;\n prefix: string;\n limit: number;\n cursor?: string | undefined;\n }): Promise<GithubFilePage> {\n const octokit = await this.getApp().getInstallationOctokit(input.installationId);\n const repository = await this.getRepository({\n installationId: input.installationId,\n repositoryId: input.repositoryId,\n });\n const owner = repository.ownerLogin;\n const repo = repository.name;\n const startPath = input.prefix.replace(TRAILING_SLASHES_RE, '');\n const offset = input.cursor ? Number.parseInt(input.cursor, 10) : 0;\n const start = Number.isNaN(offset) || offset < 0 ? 0 : offset;\n const collected: GithubFileEntry[] = [];\n const overflowLimit = start + input.limit + 1;\n\n type GetContentData = Awaited<ReturnType<typeof octokit.rest.repos.getContent>>['data'];\n\n const walk = async (path: string, depth: number): Promise<void> => {\n if (collected.length >= overflowLimit) return;\n if (depth > MAX_TREE_WALK_DEPTH) return;\n\n let data: GetContentData;\n try {\n const response = await mapGithubError(\n () =>\n octokit.rest.repos.getContent({\n owner,\n repo,\n path,\n ref: input.ref,\n }),\n 'file-not-found',\n );\n data = response.data;\n } catch (error) {\n if (error instanceof GithubIntegrationProviderError && error.reason === 'file-not-found') {\n return;\n }\n throw error;\n }\n\n if (!Array.isArray(data)) {\n if (data.type === 'file' && data.path) {\n collected.push({path: data.path, size: typeof data.size === 'number' ? data.size : null});\n }\n return;\n }\n\n const entries = [...data].sort((a, b) => (a.path ?? '').localeCompare(b.path ?? ''));\n for (const entry of entries) {\n if (collected.length >= overflowLimit) return;\n if (!entry.path) continue;\n if (entry.type === 'file') {\n collected.push({\n path: entry.path,\n size: typeof entry.size === 'number' ? entry.size : null,\n });\n } else if (entry.type === 'dir') {\n await walk(entry.path, depth + 1);\n }\n }\n };\n\n await walk(startPath, 0);\n\n const sorted = collected.sort((a, b) => a.path.localeCompare(b.path));\n const page = sorted.slice(start, start + input.limit);\n const consumed = start + page.length;\n const hasMore = consumed < sorted.length;\n\n return {\n files: page,\n nextCursor: hasMore ? String(consumed) : null,\n };\n }\n\n async fetchRepositoryFile(input: {\n installationId: number;\n repositoryId: number;\n ref: string;\n path: string;\n }): Promise<GithubFileContent> {\n const octokit = await this.getApp().getInstallationOctokit(input.installationId);\n const repository = await this.getRepository({\n installationId: input.installationId,\n repositoryId: input.repositoryId,\n });\n const response = await mapGithubError(\n () =>\n octokit.rest.repos.getContent({\n owner: repository.ownerLogin,\n repo: repository.name,\n path: input.path,\n ref: input.ref,\n }),\n 'file-not-found',\n );\n const data = response.data;\n\n if (Array.isArray(data) || data.type !== 'file') {\n throw new GithubIntegrationProviderError('file-not-found', 'GitHub path is not a file');\n }\n if (data.size > MAX_REPOSITORY_FILE_BYTES) {\n throw new GithubIntegrationProviderError(\n 'content-too-large',\n 'GitHub file content is larger than the supported limit',\n );\n }\n if (typeof data.content !== 'string' || data.encoding !== 'base64') {\n throw new GithubIntegrationProviderError(\n 'malformed-provider-response',\n 'GitHub file response did not include base64 content',\n );\n }\n\n return {\n path: data.path,\n size: data.size,\n content: Buffer.from(data.content, 'base64').toString('utf8'),\n };\n }\n\n async createInstallationAccessToken(input: {\n installationId: number;\n repositoryId: number;\n permissions?: {contents: 'read' | 'write'} | undefined;\n }): Promise<GithubInstallationAccessToken> {\n const response = await mapGithubError(() =>\n this.getApp().octokit.rest.apps.createInstallationAccessToken({\n installation_id: input.installationId,\n repository_ids: [input.repositoryId],\n permissions: input.permissions ?? {contents: 'read'},\n }),\n );\n\n if (typeof response.data.token !== 'string') {\n throw new GithubIntegrationProviderError(\n 'malformed-provider-response',\n 'GitHub installation access token response did not include a token',\n );\n }\n\n const expiresAt = new Date(response.data.expires_at);\n if (Number.isNaN(expiresAt.getTime())) {\n throw new GithubIntegrationProviderError(\n 'malformed-provider-response',\n 'GitHub installation access token response did not include a valid expiry',\n );\n }\n\n return {\n token: response.data.token,\n expiresAt,\n ...(response.data.permissions === undefined ? {} : {permissions: response.data.permissions}),\n };\n }\n\n private getApp(): App {\n if (!this.app) {\n this.app = new App({\n appId: config.GITHUB_APP_ID,\n privateKey: normalizedGithubPrivateKey(),\n Octokit: Octokit.defaults({baseUrl: normalizedGithubApiBaseUrl()}),\n });\n }\n return this.app;\n }\n}\n\nasync function mapGithubOAuthError<T>(operation: () => Promise<T>): Promise<T> {\n try {\n return await operation();\n } catch (error) {\n if (error instanceof HTTPError) {\n if (error.response.status === 429) {\n throw new GithubIntegrationProviderError(\n 'rate-limited',\n 'GitHub OAuth code exchange failed',\n retryAfterSecondsFromHeaders(error.response.headers),\n );\n }\n if (error.response.status >= 500) {\n throw new GithubIntegrationProviderError(\n 'provider-unavailable',\n 'GitHub OAuth code exchange failed',\n );\n }\n throw new GithubIntegrationProviderError(\n 'access-denied',\n 'GitHub OAuth code exchange failed',\n );\n }\n if (error instanceof TimeoutError) {\n throw new GithubIntegrationProviderError('timeout', 'GitHub OAuth request timed out');\n }\n throw error;\n }\n}\n\nexport async function mapGithubError<T>(\n operation: () => Promise<T>,\n notFoundReason:\n | 'repository-not-found'\n | 'installation-not-found'\n | 'file-not-found' = 'repository-not-found',\n): Promise<T> {\n try {\n return await operation();\n } catch (error) {\n if (error instanceof GithubIntegrationProviderError) throw error;\n if (error instanceof RequestError) {\n if (error.status === 404) {\n throw new GithubIntegrationProviderError(notFoundReason, error.message);\n }\n if (error.status === 429 || isGithubRateLimitError(error)) {\n throw new GithubIntegrationProviderError(\n 'rate-limited',\n error.message,\n retryAfterSeconds(error),\n );\n }\n if (error.status === 401 || error.status === 403) {\n throw new GithubIntegrationProviderError(\n 'access-denied',\n error.message,\n retryAfterSeconds(error),\n );\n }\n if (error.status >= 500) {\n throw new GithubIntegrationProviderError('provider-unavailable', error.message);\n }\n }\n if (error instanceof Error && error.name === 'AbortError') {\n throw new GithubIntegrationProviderError('timeout', 'GitHub request timed out');\n }\n throw error;\n }\n}\n\nfunction isGithubRateLimitError(error: RequestError): boolean {\n return error.status === 403 && error.response?.headers['x-ratelimit-remaining'] === '0';\n}\n\nfunction retryAfterSeconds(error: RequestError): number | undefined {\n return parseRetryAfterSeconds(error.response?.headers['retry-after']);\n}\n\nfunction retryAfterSecondsFromHeaders(headers: Headers): number | undefined {\n return parseRetryAfterSeconds(headers.get('retry-after'));\n}\n\nfunction parseRetryAfterSeconds(\n retryAfter: string | number | null | undefined,\n): number | undefined {\n if (!retryAfter) return undefined;\n const parsed = Number.parseInt(String(retryAfter), 10);\n return Number.isNaN(parsed) ? undefined : parsed;\n}\n\nfunction cursorToPage(cursor: string | undefined): number {\n if (!cursor) return 1;\n const page = Number.parseInt(cursor, 10);\n return Number.isNaN(page) || page < 1 ? 1 : page;\n}\n\nfunction nextCursorFromLink(link: string | undefined): string | null {\n if (!link) return null;\n const next = link.split(',').find((part) => part.includes('rel=\"next\"'));\n if (!next) return null;\n const match = next.match(NEXT_PAGE_RE);\n return match?.[1] ?? null;\n}\n\nfunction toGithubRepository(repository: {\n id: number;\n owner: {login: string};\n name: string;\n full_name: string;\n default_branch?: string | null | undefined;\n private: boolean;\n visibility?: string | undefined;\n clone_url?: string | null | undefined;\n html_url?: string | null | undefined;\n}): GithubRepository {\n if (!repository.default_branch || !repository.clone_url || !repository.html_url) {\n throw new GithubIntegrationProviderError(\n 'malformed-provider-response',\n 'GitHub repository response is missing required fields',\n );\n }\n return {\n id: repository.id,\n ownerLogin: repository.owner.login,\n name: repository.name,\n fullName: repository.full_name,\n defaultBranch: repository.default_branch,\n private: repository.private,\n visibility: repository.visibility,\n cloneUrl: repository.clone_url,\n htmlUrl: repository.html_url,\n };\n}\n"],"names":["Buffer","MAX_REPOSITORY_FILE_BYTES","ky","HTTPError","TimeoutError","App","Octokit","RequestError","config","normalizedGithubApiBaseUrl","normalizedGithubPrivateKey","GithubIntegrationProviderError","NEXT_PAGE_RE","TRAILING_SLASHES_RE","MAX_TREE_WALK_DEPTH","createGithubApiClient","OctokitGithubApiClient","exchangeOAuthCode","code","body","mapGithubOAuthError","post","headers","accept","json","client_id","GITHUB_APP_CLIENT_ID","client_secret","GITHUB_APP_CLIENT_SECRET","access_token","listUserInstallations","input","octokit","auth","userAccessToken","baseUrl","page","cursorToPage","cursor","response","mapGithubError","request","per_page","installations","data","installationIds","map","installation","id","nextCursor","nextCursorFromLink","link","getInstallation","installationId","getApp","rest","apps","installation_id","account","login","slug","type","repositorySelection","repository_selection","suspendedAt","suspended_at","Date","htmlUrl","html_url","raw","listInstallationRepositories","getInstallationOctokit","listReposAccessibleToInstallation","limit","repositories","toGithubRepository","getRepository","repository_id","repositoryId","listRepositoryFiles","repository","owner","ownerLogin","repo","name","startPath","prefix","replace","offset","Number","parseInt","start","isNaN","collected","overflowLimit","walk","path","depth","length","repos","getContent","ref","error","reason","Array","isArray","push","size","entries","sort","a","b","localeCompare","entry","sorted","slice","consumed","hasMore","files","String","fetchRepositoryFile","content","encoding","from","toString","createInstallationAccessToken","repository_ids","permissions","contents","token","expiresAt","expires_at","getTime","undefined","app","appId","GITHUB_APP_ID","privateKey","defaults","operation","status","retryAfterSecondsFromHeaders","notFoundReason","message","isGithubRateLimitError","retryAfterSeconds","Error","parseRetryAfterSeconds","get","retryAfter","parsed","next","split","find","part","includes","match","default_branch","clone_url","fullName","full_name","defaultBranch","private","visibility","cloneUrl"],"mappings":"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"}