@shipfox/api-integration-github 12.3.0 → 12.6.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 (53) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +19 -0
  3. package/dist/api/client.d.ts.map +1 -1
  4. package/dist/api/client.js +15 -9
  5. package/dist/api/client.js.map +1 -1
  6. package/dist/api/github-octokit.d.ts +7 -0
  7. package/dist/api/github-octokit.d.ts.map +1 -0
  8. package/dist/api/github-octokit.js +32 -0
  9. package/dist/api/github-octokit.js.map +1 -0
  10. package/dist/api/installation-token-envelope.d.ts +5 -1
  11. package/dist/api/installation-token-envelope.d.ts.map +1 -1
  12. package/dist/api/installation-token-envelope.js +16 -5
  13. package/dist/api/installation-token-envelope.js.map +1 -1
  14. package/dist/api/installation-token-provider.d.ts.map +1 -1
  15. package/dist/api/installation-token-provider.js +4 -2
  16. package/dist/api/installation-token-provider.js.map +1 -1
  17. package/dist/api/shared-installation-token-cache.d.ts.map +1 -1
  18. package/dist/api/shared-installation-token-cache.js +10 -4
  19. package/dist/api/shared-installation-token-cache.js.map +1 -1
  20. package/dist/config.d.ts +1 -0
  21. package/dist/config.d.ts.map +1 -1
  22. package/dist/config.js +8 -0
  23. package/dist/config.js.map +1 -1
  24. package/dist/core/agent-tools.d.ts +11 -4
  25. package/dist/core/agent-tools.d.ts.map +1 -1
  26. package/dist/core/agent-tools.js +235 -24
  27. package/dist/core/agent-tools.js.map +1 -1
  28. package/dist/core/github-agent-tool-catalog.js +1 -1
  29. package/dist/core/github-agent-tool-catalog.js.map +1 -1
  30. package/dist/metrics/instance.d.ts +1 -0
  31. package/dist/metrics/instance.d.ts.map +1 -1
  32. package/dist/metrics/instance.js +19 -0
  33. package/dist/metrics/instance.js.map +1 -1
  34. package/dist/tsconfig.test.tsbuildinfo +1 -1
  35. package/package.json +2 -2
  36. package/src/api/client.test.ts +66 -10
  37. package/src/api/client.ts +44 -7
  38. package/src/api/github-octokit.test.ts +115 -0
  39. package/src/api/github-octokit.ts +49 -0
  40. package/src/api/installation-token-envelope.ts +24 -2
  41. package/src/api/installation-token-provider.test.ts +44 -5
  42. package/src/api/installation-token-provider.ts +5 -2
  43. package/src/api/shared-installation-token-cache.test.ts +28 -0
  44. package/src/api/shared-installation-token-cache.ts +7 -0
  45. package/src/config.ts +5 -0
  46. package/src/core/agent-tools.test.ts +1049 -8
  47. package/src/core/agent-tools.ts +388 -25
  48. package/src/core/github-agent-tool-catalog.ts +1 -1
  49. package/src/metrics/instance.ts +25 -0
  50. package/test/env.ts +1 -0
  51. package/test/fixtures/github-installation-token.ts +8 -0
  52. package/test/index.ts +4 -0
  53. package/tsconfig.build.tsbuildinfo +1 -1
@@ -1,2 +1,2 @@
1
1
  $ shipfox-swc
2
- Successfully compiled: 30 files with swc (364.39ms)
2
+ Successfully compiled: 31 files with swc (490.83ms)
package/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # @shipfox/api-integration-github
2
2
 
3
+ ## 12.6.0
4
+
5
+ ### Patch Changes
6
+
7
+ - 8aaed9e: Expose bounded error codes for deterministic GitHub agent tool failures.
8
+ - 4848ad7: Fix pending GitHub review comments by resolving the caller's pending review through the REST API.
9
+ - 594cdbe: Support both stateless and stateful GitHub App installation token formats.
10
+
11
+ ## 12.5.0
12
+
13
+ ### Patch Changes
14
+
15
+ - 420ef97: GitHub agent artifact downloads now use GitHub's required zip format and return a temporary download URL.
16
+ - f2b20af: Preserve GitHub provider rejection details and classify terminal agent-tool failures without reporting them as provider outages.
17
+ - da02bd8: Add comments to the caller's latest pending pull request review.
18
+ - 4fa3667: Resolve the latest pending pull request review before submitting or deleting it.
19
+ - Updated dependencies [f2b20af]
20
+ - @shipfox/api-integration-spi@1.1.1
21
+
3
22
  ## 12.3.0
4
23
 
5
24
  ### Patch Changes
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/api/client.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/api/client.ts"],"names":[],"mappings":"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"}
@@ -4,6 +4,8 @@ import ky, { HTTPError, TimeoutError } from 'ky';
4
4
  import { App, Octokit, RequestError } from 'octokit';
5
5
  import { config, normalizedGithubApiBaseUrl, normalizedGithubPrivateKey } from '#config.js';
6
6
  import { GithubIntegrationProviderError } from '#core/errors.js';
7
+ import { recordInstallationTokenFormat } from '#metrics/index.js';
8
+ import { getGithubInstallationOctokit, githubInstallationTokenFormatPlugin } from './github-octokit.js';
7
9
  const NEXT_PAGE_RE = /[?&]page=(\d+)/;
8
10
  const TRAILING_SLASHES_RE = /\/+$/;
9
11
  const MAX_TREE_WALK_DEPTH = 10;
@@ -66,7 +68,7 @@ let OctokitGithubApiClient = class OctokitGithubApiClient {
66
68
  };
67
69
  }
68
70
  async listInstallationRepositories(input) {
69
- const octokit = await this.getApp().getInstallationOctokit(input.installationId);
71
+ const octokit = await mapGithubError(()=>getGithubInstallationOctokit(this.getApp(), input.installationId));
70
72
  const page = cursorToPage(input.cursor);
71
73
  const response = await mapGithubError(()=>octokit.rest.apps.listReposAccessibleToInstallation({
72
74
  per_page: input.limit,
@@ -78,14 +80,14 @@ let OctokitGithubApiClient = class OctokitGithubApiClient {
78
80
  };
79
81
  }
80
82
  async getRepository(input) {
81
- const octokit = await this.getApp().getInstallationOctokit(input.installationId);
83
+ const octokit = await mapGithubError(()=>getGithubInstallationOctokit(this.getApp(), input.installationId));
82
84
  const response = await mapGithubError(()=>octokit.request('GET /repositories/{repository_id}', {
83
85
  repository_id: input.repositoryId
84
86
  }));
85
87
  return toGithubRepository(response.data);
86
88
  }
87
89
  async listRepositoryFiles(input) {
88
- const octokit = await this.getApp().getInstallationOctokit(input.installationId);
90
+ const octokit = await mapGithubError(()=>getGithubInstallationOctokit(this.getApp(), input.installationId));
89
91
  const repository = await this.getRepository({
90
92
  installationId: input.installationId,
91
93
  repositoryId: input.repositoryId
@@ -151,7 +153,7 @@ let OctokitGithubApiClient = class OctokitGithubApiClient {
151
153
  };
152
154
  }
153
155
  async fetchRepositoryFile(input) {
154
- const octokit = await this.getApp().getInstallationOctokit(input.installationId);
156
+ const octokit = await mapGithubError(()=>getGithubInstallationOctokit(this.getApp(), input.installationId));
155
157
  const repository = await this.getRepository({
156
158
  installationId: input.installationId,
157
159
  repositoryId: input.repositoryId
@@ -191,6 +193,7 @@ let OctokitGithubApiClient = class OctokitGithubApiClient {
191
193
  if (typeof response.data.token !== 'string') {
192
194
  throw new GithubIntegrationProviderError('malformed-provider-response', 'GitHub installation access token response did not include a token');
193
195
  }
196
+ recordInstallationTokenFormat(response.data.token);
194
197
  const expiresAt = new Date(response.data.expires_at);
195
198
  if (Number.isNaN(expiresAt.getTime())) {
196
199
  throw new GithubIntegrationProviderError('malformed-provider-response', 'GitHub installation access token response did not include a valid expiry');
@@ -208,7 +211,7 @@ let OctokitGithubApiClient = class OctokitGithubApiClient {
208
211
  this.app = new App({
209
212
  appId: config.GITHUB_APP_ID,
210
213
  privateKey: normalizedGithubPrivateKey(),
211
- Octokit: Octokit.defaults({
214
+ Octokit: Octokit.plugin(githubInstallationTokenFormatPlugin).defaults({
212
215
  baseUrl: normalizedGithubApiBaseUrl()
213
216
  })
214
217
  });
@@ -242,16 +245,19 @@ export async function mapGithubError(operation, notFoundReason = 'repository-not
242
245
  if (error instanceof GithubIntegrationProviderError) throw error;
243
246
  if (error instanceof RequestError) {
244
247
  if (error.status === 404) {
245
- throw new GithubIntegrationProviderError(notFoundReason, error.message);
248
+ throw new GithubIntegrationProviderError(notFoundReason, error.message, undefined, error.status);
246
249
  }
247
250
  if (error.status === 429 || isGithubRateLimitError(error)) {
248
- throw new GithubIntegrationProviderError('rate-limited', error.message, retryAfterSeconds(error));
251
+ throw new GithubIntegrationProviderError('rate-limited', error.message, retryAfterSeconds(error), error.status);
249
252
  }
250
253
  if (error.status === 401 || error.status === 403) {
251
- throw new GithubIntegrationProviderError('access-denied', error.message, retryAfterSeconds(error));
254
+ throw new GithubIntegrationProviderError('access-denied', error.message, retryAfterSeconds(error), error.status);
252
255
  }
253
256
  if (error.status >= 500) {
254
- throw new GithubIntegrationProviderError('provider-unavailable', error.message);
257
+ throw new GithubIntegrationProviderError('provider-unavailable', error.message, undefined, error.status);
258
+ }
259
+ if (error.status >= 400) {
260
+ throw new GithubIntegrationProviderError('provider-rejected', error.message, undefined, error.status);
255
261
  }
256
262
  }
257
263
  if (error instanceof Error && error.name === 'AbortError') {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/api/client.ts"],"sourcesContent":["import {Buffer} from 'node:buffer';\nimport {MAX_REPOSITORY_FILE_BYTES} from '@shipfox/api-integration-spi';\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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"}
1
+ {"version":3,"sources":["../../src/api/client.ts"],"sourcesContent":["import {Buffer} from 'node:buffer';\nimport {MAX_REPOSITORY_FILE_BYTES} from '@shipfox/api-integration-spi';\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';\nimport {recordInstallationTokenFormat} from '#metrics/index.js';\nimport {\n getGithubInstallationOctokit,\n githubInstallationTokenFormatPlugin,\n} from './github-octokit.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 mapGithubError(() =>\n getGithubInstallationOctokit(this.getApp(), input.installationId),\n );\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 mapGithubError(() =>\n getGithubInstallationOctokit(this.getApp(), input.installationId),\n );\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 mapGithubError(() =>\n getGithubInstallationOctokit(this.getApp(), input.installationId),\n );\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 mapGithubError(() =>\n getGithubInstallationOctokit(this.getApp(), input.installationId),\n );\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 recordInstallationTokenFormat(response.data.token);\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.plugin(githubInstallationTokenFormatPlugin).defaults({\n baseUrl: normalizedGithubApiBaseUrl(),\n }),\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(\n notFoundReason,\n error.message,\n undefined,\n error.status,\n );\n }\n if (error.status === 429 || isGithubRateLimitError(error)) {\n throw new GithubIntegrationProviderError(\n 'rate-limited',\n error.message,\n retryAfterSeconds(error),\n error.status,\n );\n }\n if (error.status === 401 || error.status === 403) {\n throw new GithubIntegrationProviderError(\n 'access-denied',\n error.message,\n retryAfterSeconds(error),\n error.status,\n );\n }\n if (error.status >= 500) {\n throw new GithubIntegrationProviderError(\n 'provider-unavailable',\n error.message,\n undefined,\n error.status,\n );\n }\n if (error.status >= 400) {\n throw new GithubIntegrationProviderError(\n 'provider-rejected',\n error.message,\n undefined,\n error.status,\n );\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","recordInstallationTokenFormat","getGithubInstallationOctokit","githubInstallationTokenFormatPlugin","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","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","plugin","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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"}
@@ -0,0 +1,7 @@
1
+ import { type App, Octokit } from 'octokit';
2
+ type GithubInstallationTokenFormatOverride = 'enabled' | 'disabled' | undefined;
3
+ export declare function createGithubInstallationTokenFormatPlugin(override: GithubInstallationTokenFormatOverride): Parameters<typeof Octokit.plugin>[0];
4
+ export declare const githubInstallationTokenFormatPlugin: Parameters<typeof Octokit.plugin>[0];
5
+ export declare function getGithubInstallationOctokit(app: App, installationId: number, baseUrl?: string): Promise<Octokit>;
6
+ export {};
7
+ //# sourceMappingURL=github-octokit.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"github-octokit.d.ts","sourceRoot":"","sources":["../../src/api/github-octokit.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,KAAK,GAAG,EAAE,OAAO,EAAC,MAAM,SAAS,CAAC;AAM1C,KAAK,qCAAqC,GAAG,SAAS,GAAG,UAAU,GAAG,SAAS,CAAC;AAEhF,wBAAgB,yCAAyC,CACvD,QAAQ,EAAE,qCAAqC,GAC9C,UAAU,CAAC,OAAO,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CActC;AAED,eAAO,MAAM,mCAAmC,EAAE,UAAU,CAAC,OAAO,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CACQ,CAAC;AAE9F,wBAAsB,4BAA4B,CAChD,GAAG,EAAE,GAAG,EACR,cAAc,EAAE,MAAM,EACtB,OAAO,SAA+B,GACrC,OAAO,CAAC,OAAO,CAAC,CAelB"}
@@ -0,0 +1,32 @@
1
+ import { Octokit } from 'octokit';
2
+ import { config, normalizedGithubApiBaseUrl } from '#config.js';
3
+ import { GithubIntegrationProviderError } from '#core/errors.js';
4
+ import { recordInstallationTokenFormat } from '#metrics/index.js';
5
+ const INSTALLATION_TOKEN_REQUEST_PATH = /\/app\/installations\/[^/]+\/access_tokens(?:\?|$)/u;
6
+ export function createGithubInstallationTokenFormatPlugin(override) {
7
+ return (octokit)=>{
8
+ octokit.hook.before('request', (options)=>{
9
+ if (!override || options.method !== 'POST' || !INSTALLATION_TOKEN_REQUEST_PATH.test(options.url)) {
10
+ return;
11
+ }
12
+ options.headers['x-github-stateless-s2s-token'] = override;
13
+ });
14
+ };
15
+ }
16
+ export const githubInstallationTokenFormatPlugin = createGithubInstallationTokenFormatPlugin(config.GITHUB_INSTALLATION_TOKEN_FORMAT_OVERRIDE);
17
+ export async function getGithubInstallationOctokit(app, installationId, baseUrl = normalizedGithubApiBaseUrl()) {
18
+ const authentication = await app.octokit.auth({
19
+ type: 'installation',
20
+ installationId
21
+ });
22
+ if (typeof authentication.token !== 'string') {
23
+ throw new GithubIntegrationProviderError('malformed-provider-response', 'GitHub installation authentication did not include a token');
24
+ }
25
+ recordInstallationTokenFormat(authentication.token);
26
+ return new Octokit({
27
+ auth: authentication.token,
28
+ baseUrl
29
+ });
30
+ }
31
+
32
+ //# sourceMappingURL=github-octokit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/api/github-octokit.ts"],"sourcesContent":["import {type App, Octokit} from 'octokit';\nimport {config, normalizedGithubApiBaseUrl} from '#config.js';\nimport {GithubIntegrationProviderError} from '#core/errors.js';\nimport {recordInstallationTokenFormat} from '#metrics/index.js';\n\nconst INSTALLATION_TOKEN_REQUEST_PATH = /\\/app\\/installations\\/[^/]+\\/access_tokens(?:\\?|$)/u;\ntype GithubInstallationTokenFormatOverride = 'enabled' | 'disabled' | undefined;\n\nexport function createGithubInstallationTokenFormatPlugin(\n override: GithubInstallationTokenFormatOverride,\n): Parameters<typeof Octokit.plugin>[0] {\n return (octokit) => {\n octokit.hook.before('request', (options) => {\n if (\n !override ||\n options.method !== 'POST' ||\n !INSTALLATION_TOKEN_REQUEST_PATH.test(options.url)\n ) {\n return;\n }\n\n options.headers['x-github-stateless-s2s-token'] = override;\n });\n };\n}\n\nexport const githubInstallationTokenFormatPlugin: Parameters<typeof Octokit.plugin>[0] =\n createGithubInstallationTokenFormatPlugin(config.GITHUB_INSTALLATION_TOKEN_FORMAT_OVERRIDE);\n\nexport async function getGithubInstallationOctokit(\n app: App,\n installationId: number,\n baseUrl = normalizedGithubApiBaseUrl(),\n): Promise<Octokit> {\n const authentication = (await app.octokit.auth({\n type: 'installation',\n installationId,\n })) as {token?: unknown};\n\n if (typeof authentication.token !== 'string') {\n throw new GithubIntegrationProviderError(\n 'malformed-provider-response',\n 'GitHub installation authentication did not include a token',\n );\n }\n\n recordInstallationTokenFormat(authentication.token);\n return new Octokit({auth: authentication.token, baseUrl});\n}\n"],"names":["Octokit","config","normalizedGithubApiBaseUrl","GithubIntegrationProviderError","recordInstallationTokenFormat","INSTALLATION_TOKEN_REQUEST_PATH","createGithubInstallationTokenFormatPlugin","override","octokit","hook","before","options","method","test","url","headers","githubInstallationTokenFormatPlugin","GITHUB_INSTALLATION_TOKEN_FORMAT_OVERRIDE","getGithubInstallationOctokit","app","installationId","baseUrl","authentication","auth","type","token"],"mappings":"AAAA,SAAkBA,OAAO,QAAO,UAAU;AAC1C,SAAQC,MAAM,EAAEC,0BAA0B,QAAO,aAAa;AAC9D,SAAQC,8BAA8B,QAAO,kBAAkB;AAC/D,SAAQC,6BAA6B,QAAO,oBAAoB;AAEhE,MAAMC,kCAAkC;AAGxC,OAAO,SAASC,0CACdC,QAA+C;IAE/C,OAAO,CAACC;QACNA,QAAQC,IAAI,CAACC,MAAM,CAAC,WAAW,CAACC;YAC9B,IACE,CAACJ,YACDI,QAAQC,MAAM,KAAK,UACnB,CAACP,gCAAgCQ,IAAI,CAACF,QAAQG,GAAG,GACjD;gBACA;YACF;YAEAH,QAAQI,OAAO,CAAC,+BAA+B,GAAGR;QACpD;IACF;AACF;AAEA,OAAO,MAAMS,sCACXV,0CAA0CL,OAAOgB,yCAAyC,EAAE;AAE9F,OAAO,eAAeC,6BACpBC,GAAQ,EACRC,cAAsB,EACtBC,UAAUnB,4BAA4B;IAEtC,MAAMoB,iBAAkB,MAAMH,IAAIX,OAAO,CAACe,IAAI,CAAC;QAC7CC,MAAM;QACNJ;IACF;IAEA,IAAI,OAAOE,eAAeG,KAAK,KAAK,UAAU;QAC5C,MAAM,IAAItB,+BACR,+BACA;IAEJ;IAEAC,8BAA8BkB,eAAeG,KAAK;IAClD,OAAO,IAAIzB,QAAQ;QAACuB,MAAMD,eAAeG,KAAK;QAAEJ;IAAO;AACzD"}
@@ -11,6 +11,10 @@ export interface InstallationTokenEnvelope {
11
11
  permissions?: Record<string, 'read' | 'write' | 'admin'> | undefined;
12
12
  backoffUntil?: Date | undefined;
13
13
  backoffReason?: IntegrationProviderErrorReason | undefined;
14
+ backoffError?: {
15
+ message: string;
16
+ status?: number | undefined;
17
+ } | undefined;
14
18
  }
15
19
  export type MintErrorClass = 'transient' | 'terminal';
16
20
  export interface ClassifiedMintError {
@@ -31,6 +35,6 @@ export declare function backoffActive(envelope: InstallationTokenEnvelope | unde
31
35
  export declare function classifyMintError(error: unknown): ClassifiedMintError;
32
36
  export declare function mintErrorClassForReason(reason: IntegrationProviderErrorReason): MintErrorClass;
33
37
  export declare function backoffMs(classified: ClassifiedMintError): number;
34
- export declare function providerErrorFromBackoff(reason: IntegrationProviderErrorReason, retryAfterMs: number): GithubIntegrationProviderError;
38
+ export declare function providerErrorFromBackoff(reason: IntegrationProviderErrorReason, retryAfterMs: number, backoffError?: InstallationTokenEnvelope['backoffError']): GithubIntegrationProviderError;
35
39
  export declare function toProviderError(error: unknown): GithubIntegrationProviderError;
36
40
  //# sourceMappingURL=installation-token-envelope.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"installation-token-envelope.d.ts","sourceRoot":"","sources":["../../src/api/installation-token-envelope.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,8BAA8B,EACpC,MAAM,8BAA8B,CAAC;AAEtC,OAAO,EAAC,8BAA8B,EAAC,MAAM,iBAAiB,CAAC;AAE/D,eAAO,MAAM,uBAAuB,QAAgB,CAAC;AACrD,eAAO,MAAM,wBAAwB,QAAY,CAAC;AAClD,eAAO,MAAM,wBAAwB,QAAY,CAAC;AAClD,eAAO,MAAM,wBAAwB,QAAgB,CAAC;AACtD,eAAO,MAAM,mBAAmB,QAAiB,CAAC;AAqClD,MAAM,WAAW,yBAAyB;IACxC,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,SAAS,CAAC,EAAE,IAAI,GAAG,SAAS,CAAC;IAC7B,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,GAAG,OAAO,CAAC,GAAG,SAAS,CAAC;IACrE,YAAY,CAAC,EAAE,IAAI,GAAG,SAAS,CAAC;IAChC,aAAa,CAAC,EAAE,8BAA8B,GAAG,SAAS,CAAC;CAC5D;AAED,MAAM,MAAM,cAAc,GAAG,WAAW,GAAG,UAAU,CAAC;AAEtD,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,cAAc,CAAC;IACtB,MAAM,EAAE,8BAA8B,CAAC;IACvC,iBAAiB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACxC;AAED,wBAAgB,gCAAgC,CAAC,cAAc,EAAE,MAAM,GAAG,MAAM,CAE/E;AAED,wBAAgB,+BAA+B,CAAC,QAAQ,EAAE,yBAAyB,GAAG,MAAM,CAU3F;AAED,wBAAgB,8BAA8B,CAAC,GAAG,EAAE,MAAM,GAAG,yBAAyB,GAAG,SAAS,CAkBjG;AAED,wBAAgB,MAAM,CACpB,QAAQ,EAAE,yBAAyB,GAAG,SAAS,EAC/C,GAAG,EAAE,IAAI,GACR,QAAQ,IAAI,yBAAyB,GAAG;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,IAAI,CAAA;CAAC,CAM1E;AAED,wBAAgB,YAAY,CAAC,SAAS,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,GAAG,OAAO,CAEhE;AAED,wBAAgB,UAAU,CAAC,SAAS,EAAE,IAAI,GAAG,SAAS,EAAE,GAAG,EAAE,IAAI,GAAG,OAAO,CAE1E;AAED,wBAAgB,aAAa,CAAC,QAAQ,EAAE,yBAAyB,GAAG,SAAS,EAAE,GAAG,EAAE,IAAI,GAAG,OAAO,CAMjG;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,mBAAmB,CAUrE;AAED,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,8BAA8B,GAAG,cAAc,CAE9F;AAED,wBAAgB,SAAS,CAAC,UAAU,EAAE,mBAAmB,GAAG,MAAM,CAKjE;AAED,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,8BAA8B,EACtC,YAAY,EAAE,MAAM,GACnB,8BAA8B,CAMhC;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,8BAA8B,CAS9E"}
1
+ {"version":3,"file":"installation-token-envelope.d.ts","sourceRoot":"","sources":["../../src/api/installation-token-envelope.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,8BAA8B,EACpC,MAAM,8BAA8B,CAAC;AAEtC,OAAO,EAAC,8BAA8B,EAAC,MAAM,iBAAiB,CAAC;AAE/D,eAAO,MAAM,uBAAuB,QAAgB,CAAC;AACrD,eAAO,MAAM,wBAAwB,QAAY,CAAC;AAClD,eAAO,MAAM,wBAAwB,QAAY,CAAC;AAClD,eAAO,MAAM,wBAAwB,QAAgB,CAAC;AACtD,eAAO,MAAM,mBAAmB,QAAiB,CAAC;AA4ClD,MAAM,WAAW,yBAAyB;IACxC,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,SAAS,CAAC,EAAE,IAAI,GAAG,SAAS,CAAC;IAC7B,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,GAAG,OAAO,CAAC,GAAG,SAAS,CAAC;IACrE,YAAY,CAAC,EAAE,IAAI,GAAG,SAAS,CAAC;IAChC,aAAa,CAAC,EAAE,8BAA8B,GAAG,SAAS,CAAC;IAC3D,YAAY,CAAC,EACT;QACE,OAAO,EAAE,MAAM,CAAC;QAChB,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;KAC7B,GACD,SAAS,CAAC;CACf;AAED,MAAM,MAAM,cAAc,GAAG,WAAW,GAAG,UAAU,CAAC;AAEtD,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,cAAc,CAAC;IACtB,MAAM,EAAE,8BAA8B,CAAC;IACvC,iBAAiB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACxC;AAED,wBAAgB,gCAAgC,CAAC,cAAc,EAAE,MAAM,GAAG,MAAM,CAE/E;AAED,wBAAgB,+BAA+B,CAAC,QAAQ,EAAE,yBAAyB,GAAG,MAAM,CAW3F;AAED,wBAAgB,8BAA8B,CAAC,GAAG,EAAE,MAAM,GAAG,yBAAyB,GAAG,SAAS,CAmBjG;AAED,wBAAgB,MAAM,CACpB,QAAQ,EAAE,yBAAyB,GAAG,SAAS,EAC/C,GAAG,EAAE,IAAI,GACR,QAAQ,IAAI,yBAAyB,GAAG;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,IAAI,CAAA;CAAC,CAM1E;AAED,wBAAgB,YAAY,CAAC,SAAS,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,GAAG,OAAO,CAEhE;AAED,wBAAgB,UAAU,CAAC,SAAS,EAAE,IAAI,GAAG,SAAS,EAAE,GAAG,EAAE,IAAI,GAAG,OAAO,CAE1E;AAED,wBAAgB,aAAa,CAAC,QAAQ,EAAE,yBAAyB,GAAG,SAAS,EAAE,GAAG,EAAE,IAAI,GAAG,OAAO,CAMjG;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,mBAAmB,CAUrE;AAED,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,8BAA8B,GAAG,cAAc,CAE9F;AAED,wBAAgB,SAAS,CAAC,UAAU,EAAE,mBAAmB,GAAG,MAAM,CAKjE;AAED,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,8BAA8B,EACtC,YAAY,EAAE,MAAM,EACpB,YAAY,CAAC,EAAE,yBAAyB,CAAC,cAAc,CAAC,GACvD,8BAA8B,CAOhC;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,8BAA8B,CAc9E"}
@@ -14,14 +14,20 @@ const providerErrorReasons = [
14
14
  'rate-limited',
15
15
  'timeout',
16
16
  'provider-unavailable',
17
+ 'provider-rejected',
17
18
  'malformed-provider-response',
18
19
  'content-too-large',
19
20
  'too-many-files'
20
21
  ];
21
22
  const providerErrorReasonSchema = z.enum(providerErrorReasons);
23
+ const backoffErrorSchema = z.object({
24
+ message: z.string(),
25
+ status: z.number().int().optional()
26
+ });
22
27
  const terminalMintErrorReasons = new Set([
23
28
  'access-denied',
24
29
  'installation-not-found',
30
+ 'provider-rejected',
25
31
  'malformed-provider-response'
26
32
  ]);
27
33
  const providerErrorReasonSchemaCoversUnion = {};
@@ -35,7 +41,8 @@ const installationTokenEnvelopeSchema = z.object({
35
41
  'admin'
36
42
  ])).optional(),
37
43
  backoffUntil: z.string().datetime().optional(),
38
- backoffReason: providerErrorReasonSchema.optional()
44
+ backoffReason: providerErrorReasonSchema.optional(),
45
+ backoffError: backoffErrorSchema.optional()
39
46
  });
40
47
  export function githubInstallationTokenNamespace(installationId) {
41
48
  return `system/github/installation-token/${installationId}`;
@@ -56,6 +63,9 @@ export function encodeInstallationTokenEnvelope(envelope) {
56
63
  },
57
64
  ...envelope.backoffReason !== undefined && {
58
65
  backoffReason: envelope.backoffReason
66
+ },
67
+ ...envelope.backoffError !== undefined && {
68
+ backoffError: envelope.backoffError
59
69
  }
60
70
  });
61
71
  }
@@ -73,7 +83,8 @@ export function parseInstallationTokenEnvelope(raw) {
73
83
  expiresAt: result.data.expiresAt ? new Date(result.data.expiresAt) : undefined,
74
84
  permissions: result.data.permissions,
75
85
  backoffUntil: result.data.backoffUntil ? new Date(result.data.backoffUntil) : undefined,
76
- backoffReason: result.data.backoffReason
86
+ backoffReason: result.data.backoffReason,
87
+ backoffError: result.data.backoffError
77
88
  };
78
89
  }
79
90
  export function usable(envelope, now) {
@@ -109,13 +120,13 @@ export function backoffMs(classified) {
109
120
  const retryAfterMs = (classified.retryAfterSeconds ?? 0) * 1000;
110
121
  return Math.min(TRANSIENT_BACKOFF_MAX_MS, Math.max(TRANSIENT_BACKOFF_MIN_MS, retryAfterMs));
111
122
  }
112
- export function providerErrorFromBackoff(reason, retryAfterMs) {
113
- return new GithubIntegrationProviderError(reason, `GitHub installation token mint is backed off after ${reason}`, Math.max(1, Math.ceil(retryAfterMs / 1000)));
123
+ export function providerErrorFromBackoff(reason, retryAfterMs, backoffError) {
124
+ return new GithubIntegrationProviderError(reason, backoffError?.message ?? `GitHub installation token mint is backed off after ${reason}`, Math.max(1, Math.ceil(retryAfterMs / 1000)), backoffError?.status);
114
125
  }
115
126
  export function toProviderError(error) {
116
127
  if (error instanceof GithubIntegrationProviderError) return error;
117
128
  if (error instanceof IntegrationProviderError) {
118
- return new GithubIntegrationProviderError(error.reason, error.message, error.retryAfterSeconds);
129
+ return new GithubIntegrationProviderError(error.reason, error.message, error.retryAfterSeconds, error.status);
119
130
  }
120
131
  return new GithubIntegrationProviderError('provider-unavailable', error instanceof Error ? error.message : 'GitHub installation token mint failed');
121
132
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/api/installation-token-envelope.ts"],"sourcesContent":["import {\n IntegrationProviderError,\n type IntegrationProviderErrorReason,\n} from '@shipfox/api-integration-spi';\nimport {z} from 'zod';\nimport {GithubIntegrationProviderError} from '#core/errors.js';\n\nexport const TOKEN_REFRESH_MARGIN_MS = 5 * 60 * 1000;\nexport const TOKEN_VALIDITY_BUFFER_MS = 60 * 1000;\nexport const TRANSIENT_BACKOFF_MIN_MS = 30 * 1000;\nexport const TRANSIENT_BACKOFF_MAX_MS = 5 * 60 * 1000;\nexport const TERMINAL_BACKOFF_MS = 15 * 60 * 1000;\n\nconst providerErrorReasons = [\n 'repository-not-found',\n 'installation-not-found',\n 'file-not-found',\n 'access-denied',\n 'rate-limited',\n 'timeout',\n 'provider-unavailable',\n 'malformed-provider-response',\n 'content-too-large',\n 'too-many-files',\n] as const satisfies readonly IntegrationProviderErrorReason[];\n\nconst providerErrorReasonSchema = z.enum(providerErrorReasons);\nconst terminalMintErrorReasons = new Set<IntegrationProviderErrorReason>([\n 'access-denied',\n 'installation-not-found',\n 'malformed-provider-response',\n]);\n\ntype MissingProviderErrorReason = Exclude<\n IntegrationProviderErrorReason,\n (typeof providerErrorReasons)[number]\n>;\nconst providerErrorReasonSchemaCoversUnion: Record<MissingProviderErrorReason, never> = {};\nvoid providerErrorReasonSchemaCoversUnion;\n\nconst installationTokenEnvelopeSchema = z.object({\n token: z.string().min(1).optional(),\n expiresAt: z.string().datetime().optional(),\n permissions: z.record(z.string(), z.enum(['read', 'write', 'admin'])).optional(),\n backoffUntil: z.string().datetime().optional(),\n backoffReason: providerErrorReasonSchema.optional(),\n});\n\nexport interface InstallationTokenEnvelope {\n token?: string | undefined;\n expiresAt?: Date | undefined;\n permissions?: Record<string, 'read' | 'write' | 'admin'> | undefined;\n backoffUntil?: Date | undefined;\n backoffReason?: IntegrationProviderErrorReason | undefined;\n}\n\nexport type MintErrorClass = 'transient' | 'terminal';\n\nexport interface ClassifiedMintError {\n class: MintErrorClass;\n reason: IntegrationProviderErrorReason;\n retryAfterSeconds?: number | undefined;\n}\n\nexport function githubInstallationTokenNamespace(installationId: number): string {\n return `system/github/installation-token/${installationId}`;\n}\n\nexport function encodeInstallationTokenEnvelope(envelope: InstallationTokenEnvelope): string {\n return JSON.stringify({\n ...(envelope.token !== undefined && {token: envelope.token}),\n ...(envelope.expiresAt !== undefined && {expiresAt: envelope.expiresAt.toISOString()}),\n ...(envelope.permissions !== undefined && {permissions: envelope.permissions}),\n ...(envelope.backoffUntil !== undefined && {\n backoffUntil: envelope.backoffUntil.toISOString(),\n }),\n ...(envelope.backoffReason !== undefined && {backoffReason: envelope.backoffReason}),\n });\n}\n\nexport function parseInstallationTokenEnvelope(raw: string): InstallationTokenEnvelope | undefined {\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n return undefined;\n }\n\n const result = installationTokenEnvelopeSchema.safeParse(parsed);\n if (!result.success) return undefined;\n\n return {\n token: result.data.token,\n expiresAt: result.data.expiresAt ? new Date(result.data.expiresAt) : undefined,\n permissions: result.data.permissions,\n backoffUntil: result.data.backoffUntil ? new Date(result.data.backoffUntil) : undefined,\n backoffReason: result.data.backoffReason,\n };\n}\n\nexport function usable(\n envelope: InstallationTokenEnvelope | undefined,\n now: Date,\n): envelope is InstallationTokenEnvelope & {token: string; expiresAt: Date} {\n return (\n envelope?.token !== undefined &&\n envelope.expiresAt !== undefined &&\n !needsRefresh(envelope.expiresAt, now)\n );\n}\n\nexport function needsRefresh(expiresAt: Date, now: Date): boolean {\n return expiresAt.getTime() <= now.getTime() + TOKEN_REFRESH_MARGIN_MS;\n}\n\nexport function stillValid(expiresAt: Date | undefined, now: Date): boolean {\n return expiresAt !== undefined && expiresAt.getTime() > now.getTime() + TOKEN_VALIDITY_BUFFER_MS;\n}\n\nexport function backoffActive(envelope: InstallationTokenEnvelope | undefined, now: Date): boolean {\n return (\n envelope?.backoffUntil !== undefined &&\n envelope.backoffReason !== undefined &&\n envelope.backoffUntil.getTime() > now.getTime()\n );\n}\n\nexport function classifyMintError(error: unknown): ClassifiedMintError {\n if (error instanceof IntegrationProviderError) {\n return {\n reason: error.reason,\n retryAfterSeconds: error.retryAfterSeconds,\n class: mintErrorClassForReason(error.reason),\n };\n }\n\n return {reason: 'provider-unavailable', class: 'transient'};\n}\n\nexport function mintErrorClassForReason(reason: IntegrationProviderErrorReason): MintErrorClass {\n return terminalMintErrorReasons.has(reason) ? 'terminal' : 'transient';\n}\n\nexport function backoffMs(classified: ClassifiedMintError): number {\n if (classified.class === 'terminal') return TERMINAL_BACKOFF_MS;\n\n const retryAfterMs = (classified.retryAfterSeconds ?? 0) * 1000;\n return Math.min(TRANSIENT_BACKOFF_MAX_MS, Math.max(TRANSIENT_BACKOFF_MIN_MS, retryAfterMs));\n}\n\nexport function providerErrorFromBackoff(\n reason: IntegrationProviderErrorReason,\n retryAfterMs: number,\n): GithubIntegrationProviderError {\n return new GithubIntegrationProviderError(\n reason,\n `GitHub installation token mint is backed off after ${reason}`,\n Math.max(1, Math.ceil(retryAfterMs / 1000)),\n );\n}\n\nexport function toProviderError(error: unknown): GithubIntegrationProviderError {\n if (error instanceof GithubIntegrationProviderError) return error;\n if (error instanceof IntegrationProviderError) {\n return new GithubIntegrationProviderError(error.reason, error.message, error.retryAfterSeconds);\n }\n return new GithubIntegrationProviderError(\n 'provider-unavailable',\n error instanceof Error ? error.message : 'GitHub installation token mint failed',\n );\n}\n"],"names":["IntegrationProviderError","z","GithubIntegrationProviderError","TOKEN_REFRESH_MARGIN_MS","TOKEN_VALIDITY_BUFFER_MS","TRANSIENT_BACKOFF_MIN_MS","TRANSIENT_BACKOFF_MAX_MS","TERMINAL_BACKOFF_MS","providerErrorReasons","providerErrorReasonSchema","enum","terminalMintErrorReasons","Set","providerErrorReasonSchemaCoversUnion","installationTokenEnvelopeSchema","object","token","string","min","optional","expiresAt","datetime","permissions","record","backoffUntil","backoffReason","githubInstallationTokenNamespace","installationId","encodeInstallationTokenEnvelope","envelope","JSON","stringify","undefined","toISOString","parseInstallationTokenEnvelope","raw","parsed","parse","result","safeParse","success","data","Date","usable","now","needsRefresh","getTime","stillValid","backoffActive","classifyMintError","error","reason","retryAfterSeconds","class","mintErrorClassForReason","has","backoffMs","classified","retryAfterMs","Math","max","providerErrorFromBackoff","ceil","toProviderError","message","Error"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/api/installation-token-envelope.ts"],"sourcesContent":["import {\n IntegrationProviderError,\n type IntegrationProviderErrorReason,\n} from '@shipfox/api-integration-spi';\nimport {z} from 'zod';\nimport {GithubIntegrationProviderError} from '#core/errors.js';\n\nexport const TOKEN_REFRESH_MARGIN_MS = 5 * 60 * 1000;\nexport const TOKEN_VALIDITY_BUFFER_MS = 60 * 1000;\nexport const TRANSIENT_BACKOFF_MIN_MS = 30 * 1000;\nexport const TRANSIENT_BACKOFF_MAX_MS = 5 * 60 * 1000;\nexport const TERMINAL_BACKOFF_MS = 15 * 60 * 1000;\n\nconst providerErrorReasons = [\n 'repository-not-found',\n 'installation-not-found',\n 'file-not-found',\n 'access-denied',\n 'rate-limited',\n 'timeout',\n 'provider-unavailable',\n 'provider-rejected',\n 'malformed-provider-response',\n 'content-too-large',\n 'too-many-files',\n] as const satisfies readonly IntegrationProviderErrorReason[];\n\nconst providerErrorReasonSchema = z.enum(providerErrorReasons);\nconst backoffErrorSchema = z.object({\n message: z.string(),\n status: z.number().int().optional(),\n});\nconst terminalMintErrorReasons = new Set<IntegrationProviderErrorReason>([\n 'access-denied',\n 'installation-not-found',\n 'provider-rejected',\n 'malformed-provider-response',\n]);\n\ntype MissingProviderErrorReason = Exclude<\n IntegrationProviderErrorReason,\n (typeof providerErrorReasons)[number]\n>;\nconst providerErrorReasonSchemaCoversUnion: Record<MissingProviderErrorReason, never> = {};\nvoid providerErrorReasonSchemaCoversUnion;\n\nconst installationTokenEnvelopeSchema = z.object({\n token: z.string().min(1).optional(),\n expiresAt: z.string().datetime().optional(),\n permissions: z.record(z.string(), z.enum(['read', 'write', 'admin'])).optional(),\n backoffUntil: z.string().datetime().optional(),\n backoffReason: providerErrorReasonSchema.optional(),\n backoffError: backoffErrorSchema.optional(),\n});\n\nexport interface InstallationTokenEnvelope {\n token?: string | undefined;\n expiresAt?: Date | undefined;\n permissions?: Record<string, 'read' | 'write' | 'admin'> | undefined;\n backoffUntil?: Date | undefined;\n backoffReason?: IntegrationProviderErrorReason | undefined;\n backoffError?:\n | {\n message: string;\n status?: number | undefined;\n }\n | undefined;\n}\n\nexport type MintErrorClass = 'transient' | 'terminal';\n\nexport interface ClassifiedMintError {\n class: MintErrorClass;\n reason: IntegrationProviderErrorReason;\n retryAfterSeconds?: number | undefined;\n}\n\nexport function githubInstallationTokenNamespace(installationId: number): string {\n return `system/github/installation-token/${installationId}`;\n}\n\nexport function encodeInstallationTokenEnvelope(envelope: InstallationTokenEnvelope): string {\n return JSON.stringify({\n ...(envelope.token !== undefined && {token: envelope.token}),\n ...(envelope.expiresAt !== undefined && {expiresAt: envelope.expiresAt.toISOString()}),\n ...(envelope.permissions !== undefined && {permissions: envelope.permissions}),\n ...(envelope.backoffUntil !== undefined && {\n backoffUntil: envelope.backoffUntil.toISOString(),\n }),\n ...(envelope.backoffReason !== undefined && {backoffReason: envelope.backoffReason}),\n ...(envelope.backoffError !== undefined && {backoffError: envelope.backoffError}),\n });\n}\n\nexport function parseInstallationTokenEnvelope(raw: string): InstallationTokenEnvelope | undefined {\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n return undefined;\n }\n\n const result = installationTokenEnvelopeSchema.safeParse(parsed);\n if (!result.success) return undefined;\n\n return {\n token: result.data.token,\n expiresAt: result.data.expiresAt ? new Date(result.data.expiresAt) : undefined,\n permissions: result.data.permissions,\n backoffUntil: result.data.backoffUntil ? new Date(result.data.backoffUntil) : undefined,\n backoffReason: result.data.backoffReason,\n backoffError: result.data.backoffError,\n };\n}\n\nexport function usable(\n envelope: InstallationTokenEnvelope | undefined,\n now: Date,\n): envelope is InstallationTokenEnvelope & {token: string; expiresAt: Date} {\n return (\n envelope?.token !== undefined &&\n envelope.expiresAt !== undefined &&\n !needsRefresh(envelope.expiresAt, now)\n );\n}\n\nexport function needsRefresh(expiresAt: Date, now: Date): boolean {\n return expiresAt.getTime() <= now.getTime() + TOKEN_REFRESH_MARGIN_MS;\n}\n\nexport function stillValid(expiresAt: Date | undefined, now: Date): boolean {\n return expiresAt !== undefined && expiresAt.getTime() > now.getTime() + TOKEN_VALIDITY_BUFFER_MS;\n}\n\nexport function backoffActive(envelope: InstallationTokenEnvelope | undefined, now: Date): boolean {\n return (\n envelope?.backoffUntil !== undefined &&\n envelope.backoffReason !== undefined &&\n envelope.backoffUntil.getTime() > now.getTime()\n );\n}\n\nexport function classifyMintError(error: unknown): ClassifiedMintError {\n if (error instanceof IntegrationProviderError) {\n return {\n reason: error.reason,\n retryAfterSeconds: error.retryAfterSeconds,\n class: mintErrorClassForReason(error.reason),\n };\n }\n\n return {reason: 'provider-unavailable', class: 'transient'};\n}\n\nexport function mintErrorClassForReason(reason: IntegrationProviderErrorReason): MintErrorClass {\n return terminalMintErrorReasons.has(reason) ? 'terminal' : 'transient';\n}\n\nexport function backoffMs(classified: ClassifiedMintError): number {\n if (classified.class === 'terminal') return TERMINAL_BACKOFF_MS;\n\n const retryAfterMs = (classified.retryAfterSeconds ?? 0) * 1000;\n return Math.min(TRANSIENT_BACKOFF_MAX_MS, Math.max(TRANSIENT_BACKOFF_MIN_MS, retryAfterMs));\n}\n\nexport function providerErrorFromBackoff(\n reason: IntegrationProviderErrorReason,\n retryAfterMs: number,\n backoffError?: InstallationTokenEnvelope['backoffError'],\n): GithubIntegrationProviderError {\n return new GithubIntegrationProviderError(\n reason,\n backoffError?.message ?? `GitHub installation token mint is backed off after ${reason}`,\n Math.max(1, Math.ceil(retryAfterMs / 1000)),\n backoffError?.status,\n );\n}\n\nexport function toProviderError(error: unknown): GithubIntegrationProviderError {\n if (error instanceof GithubIntegrationProviderError) return error;\n if (error instanceof IntegrationProviderError) {\n return new GithubIntegrationProviderError(\n error.reason,\n error.message,\n error.retryAfterSeconds,\n error.status,\n );\n }\n return new GithubIntegrationProviderError(\n 'provider-unavailable',\n error instanceof Error ? error.message : 'GitHub installation token mint failed',\n );\n}\n"],"names":["IntegrationProviderError","z","GithubIntegrationProviderError","TOKEN_REFRESH_MARGIN_MS","TOKEN_VALIDITY_BUFFER_MS","TRANSIENT_BACKOFF_MIN_MS","TRANSIENT_BACKOFF_MAX_MS","TERMINAL_BACKOFF_MS","providerErrorReasons","providerErrorReasonSchema","enum","backoffErrorSchema","object","message","string","status","number","int","optional","terminalMintErrorReasons","Set","providerErrorReasonSchemaCoversUnion","installationTokenEnvelopeSchema","token","min","expiresAt","datetime","permissions","record","backoffUntil","backoffReason","backoffError","githubInstallationTokenNamespace","installationId","encodeInstallationTokenEnvelope","envelope","JSON","stringify","undefined","toISOString","parseInstallationTokenEnvelope","raw","parsed","parse","result","safeParse","success","data","Date","usable","now","needsRefresh","getTime","stillValid","backoffActive","classifyMintError","error","reason","retryAfterSeconds","class","mintErrorClassForReason","has","backoffMs","classified","retryAfterMs","Math","max","providerErrorFromBackoff","ceil","toProviderError","Error"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"installation-token-provider.d.ts","sourceRoot":"","sources":["../../src/api/installation-token-provider.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,8BAA8B,EAAC,MAAM,8BAA8B,CAAC;AAOjF,OAAO,EAAC,KAAK,6BAA6B,EAAiB,MAAM,aAAa,CAAC;AAK/E,OAAO,EACL,KAAK,sBAAsB,EAC3B,KAAK,4BAA4B,EAEjC,KAAK,mCAAmC,EACzC,MAAM,sCAAsC,CAAC;AAE9C,MAAM,WAAW,+BAA+B;IAC9C,0BAA0B,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,6BAA6B,CAAC,CAAC;CAC5F;AAED,MAAM,WAAW,sCAAsC;IACrD,KAAK,CAAC,EAAE,sBAAsB,GAAG,SAAS,CAAC;IAC3C,4BAA4B,CAAC,EAAE,8BAA8B,GAAG,SAAS,CAAC;IAC1E,WAAW,CAAC,EAAE,4BAA4B,GAAG,SAAS,CAAC;IACvD,QAAQ,CAAC,EAAE,mCAAmC,CAAC,UAAU,CAAC,GAAG,SAAS,CAAC;IACvE,GAAG,CAAC,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,SAAS,CAAC;CAChC;AAED,wBAAgB,qCAAqC,CACnD,OAAO,GAAE,sCAA2C,GACnD,+BAA+B,CAEjC;AAmLD,wBAAgB,mCAAmC,CAAC,MAAM,EAAE;IAC1D,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,CAAC,MAAM,EAAE;QAAC,WAAW,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAC,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;CACtF,GAAG,OAAO,CAAC,MAAM,CAAC,CAKlB"}
1
+ {"version":3,"file":"installation-token-provider.d.ts","sourceRoot":"","sources":["../../src/api/installation-token-provider.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,8BAA8B,EAAC,MAAM,8BAA8B,CAAC;AAOjF,OAAO,EAAC,KAAK,6BAA6B,EAAiB,MAAM,aAAa,CAAC;AAM/E,OAAO,EACL,KAAK,sBAAsB,EAC3B,KAAK,4BAA4B,EAEjC,KAAK,mCAAmC,EACzC,MAAM,sCAAsC,CAAC;AAE9C,MAAM,WAAW,+BAA+B;IAC9C,0BAA0B,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,6BAA6B,CAAC,CAAC;CAC5F;AAED,MAAM,WAAW,sCAAsC;IACrD,KAAK,CAAC,EAAE,sBAAsB,GAAG,SAAS,CAAC;IAC3C,4BAA4B,CAAC,EAAE,8BAA8B,GAAG,SAAS,CAAC;IAC1E,WAAW,CAAC,EAAE,4BAA4B,GAAG,SAAS,CAAC;IACvD,QAAQ,CAAC,EAAE,mCAAmC,CAAC,UAAU,CAAC,GAAG,SAAS,CAAC;IACvE,GAAG,CAAC,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,SAAS,CAAC;CAChC;AAED,wBAAgB,qCAAqC,CACnD,OAAO,GAAE,sCAA2C,GACnD,+BAA+B,CAEjC;AAqLD,wBAAgB,mCAAmC,CAAC,MAAM,EAAE;IAC1D,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,CAAC,MAAM,EAAE;QAAC,WAAW,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAC,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;CACtF,GAAG,OAAO,CAAC,MAAM,CAAC,CAKlB"}
@@ -3,8 +3,9 @@ import { config, normalizedGithubApiBaseUrl, normalizedGithubPrivateKey } from '
3
3
  import { GithubIntegrationProviderError } from '#core/errors.js';
4
4
  import { withInstallationTokenLock } from '#db/installation-token-lock.js';
5
5
  import { getGithubInstallationByInstallationId } from '#db/installations.js';
6
- import { recordInstallationTokenLookup } from '#metrics/index.js';
6
+ import { recordInstallationTokenFormat, recordInstallationTokenLookup } from '#metrics/index.js';
7
7
  import { mapGithubError } from './client.js';
8
+ import { githubInstallationTokenFormatPlugin } from './github-octokit.js';
8
9
  import { githubInstallationTokenNamespace, TOKEN_REFRESH_MARGIN_MS } from './installation-token-envelope.js';
9
10
  import { SharedInstallationTokenCache } from './shared-installation-token-cache.js';
10
11
  export function createGithubInstallationTokenProvider(options = {}) {
@@ -24,6 +25,7 @@ let OctokitGithubInstallationTokenProvider = class OctokitGithubInstallationToke
24
25
  if (typeof response.data.token !== 'string') {
25
26
  throw new GithubIntegrationProviderError('malformed-provider-response', 'GitHub installation access token response did not include a token');
26
27
  }
28
+ recordInstallationTokenFormat(response.data.token);
27
29
  const expiresAt = new Date(response.data.expires_at);
28
30
  if (Number.isNaN(expiresAt.getTime())) {
29
31
  throw new GithubIntegrationProviderError('malformed-provider-response', 'GitHub installation access token response did not include a valid expiry');
@@ -41,7 +43,7 @@ let OctokitGithubInstallationTokenProvider = class OctokitGithubInstallationToke
41
43
  this.app = new App({
42
44
  appId: config.GITHUB_APP_ID,
43
45
  privateKey: normalizedGithubPrivateKey(),
44
- Octokit: Octokit.defaults({
46
+ Octokit: Octokit.plugin(githubInstallationTokenFormatPlugin).defaults({
45
47
  baseUrl: normalizedGithubApiBaseUrl(),
46
48
  throttle: {
47
49
  onRateLimit: (_retryAfter, _options, _octokit, retryCount)=>retryCount === 0,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/api/installation-token-provider.ts"],"sourcesContent":["import type {GetIntegrationConnectionByIdFn} from '@shipfox/api-integration-spi';\nimport {App, Octokit} from 'octokit';\nimport {config, normalizedGithubApiBaseUrl, normalizedGithubPrivateKey} from '#config.js';\nimport {GithubIntegrationProviderError} from '#core/errors.js';\nimport {withInstallationTokenLock} from '#db/installation-token-lock.js';\nimport {getGithubInstallationByInstallationId} from '#db/installations.js';\nimport {recordInstallationTokenLookup} from '#metrics/index.js';\nimport {type GithubInstallationAccessToken, mapGithubError} from './client.js';\nimport {\n githubInstallationTokenNamespace,\n TOKEN_REFRESH_MARGIN_MS,\n} from './installation-token-envelope.js';\nimport {\n type InstallationTokenCache,\n type InstallationTokenSecretStore,\n SharedInstallationTokenCache,\n type SharedInstallationTokenCacheOptions,\n} from './shared-installation-token-cache.js';\n\nexport interface GithubInstallationTokenProvider {\n getInstallationAccessToken(installationId: number): Promise<GithubInstallationAccessToken>;\n}\n\nexport interface GithubInstallationTokenProviderOptions {\n cache?: InstallationTokenCache | undefined;\n getIntegrationConnectionById?: GetIntegrationConnectionByIdFn | undefined;\n secretStore?: InstallationTokenSecretStore | undefined;\n withLock?: SharedInstallationTokenCacheOptions['withLock'] | undefined;\n now?: (() => Date) | undefined;\n}\n\nexport function createGithubInstallationTokenProvider(\n options: GithubInstallationTokenProviderOptions = {},\n): GithubInstallationTokenProvider {\n return new OctokitGithubInstallationTokenProvider(createInstallationTokenCache(options));\n}\n\nclass OctokitGithubInstallationTokenProvider implements GithubInstallationTokenProvider {\n private app: App | undefined;\n\n constructor(\n private readonly cache: InstallationTokenCache = new InMemoryInstallationTokenCache(),\n ) {}\n\n getInstallationAccessToken(installationId: number): Promise<GithubInstallationAccessToken> {\n return this.cache.getOrMint(installationId, () =>\n this.mintInstallationAccessToken(installationId),\n );\n }\n\n private async mintInstallationAccessToken(\n installationId: number,\n ): Promise<GithubInstallationAccessToken> {\n const response = await mapGithubError(\n () =>\n this.getApp().octokit.rest.apps.createInstallationAccessToken({\n installation_id: installationId,\n }),\n 'installation-not-found',\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({\n baseUrl: normalizedGithubApiBaseUrl(),\n throttle: {\n onRateLimit: (\n _retryAfter: number,\n _options: unknown,\n _octokit: unknown,\n retryCount: number,\n ) => retryCount === 0,\n onSecondaryRateLimit: (\n _retryAfter: number,\n _options: unknown,\n _octokit: unknown,\n retryCount: number,\n ) => retryCount === 0,\n },\n }),\n });\n }\n return this.app;\n }\n}\n\nclass InMemoryInstallationTokenCache implements InstallationTokenCache {\n private readonly tokens = new Map<number, GithubInstallationAccessToken>();\n private readonly inFlightMints = new Map<number, Promise<GithubInstallationAccessToken>>();\n\n constructor(\n private readonly options: {\n refreshMarginMs: number;\n now: () => Date;\n } = {\n refreshMarginMs: TOKEN_REFRESH_MARGIN_MS,\n now: () => new Date(),\n },\n ) {}\n\n getOrMint(\n installationId: number,\n mint: () => Promise<GithubInstallationAccessToken>,\n ): Promise<GithubInstallationAccessToken> {\n const cached = this.tokens.get(installationId);\n if (cached && !this.isInsideRefreshMargin(cached.expiresAt)) {\n recordInstallationTokenLookup('ram-hit');\n return Promise.resolve(cached);\n }\n\n const inFlightMint = this.inFlightMints.get(installationId);\n if (inFlightMint) return inFlightMint;\n\n const freshToken = mint()\n .then((token) => {\n this.tokens.set(installationId, token);\n return token;\n })\n .finally(() => {\n this.inFlightMints.delete(installationId);\n });\n this.inFlightMints.set(installationId, freshToken);\n return freshToken;\n }\n\n private isInsideRefreshMargin(expiresAt: Date): boolean {\n return expiresAt.getTime() <= this.options.now().getTime() + this.options.refreshMarginMs;\n }\n}\n\nclass TieredInstallationTokenCache implements InstallationTokenCache {\n constructor(\n private readonly ram: InstallationTokenCache,\n private readonly shared: InstallationTokenCache,\n ) {}\n\n getOrMint(\n installationId: number,\n mint: () => Promise<GithubInstallationAccessToken>,\n ): Promise<GithubInstallationAccessToken> {\n return this.ram.getOrMint(installationId, () => this.shared.getOrMint(installationId, mint));\n }\n}\n\nfunction createInstallationTokenCache(\n options: GithubInstallationTokenProviderOptions,\n): InstallationTokenCache {\n if (options.cache) return options.cache;\n\n const ram = new InMemoryInstallationTokenCache({\n refreshMarginMs: TOKEN_REFRESH_MARGIN_MS,\n now: options.now ?? (() => new Date()),\n });\n if (!options.getIntegrationConnectionById || !options.secretStore) return ram;\n\n const shared = new SharedInstallationTokenCache({\n secretStore: options.secretStore,\n withLock: options.withLock ?? withInstallationTokenLock,\n resolveWorkspaceId: createGithubInstallationWorkspaceResolver(\n options.getIntegrationConnectionById,\n ),\n now: options.now,\n });\n return new TieredInstallationTokenCache(ram, shared);\n}\n\nfunction createGithubInstallationWorkspaceResolver(\n getIntegrationConnectionById: GetIntegrationConnectionByIdFn,\n) {\n return async (installationId: number): Promise<string> => {\n const installation = await getGithubInstallationByInstallationId(String(installationId));\n if (!installation) {\n throw new GithubIntegrationProviderError(\n 'installation-not-found',\n `GitHub installation not found: ${installationId}`,\n );\n }\n\n const connection = await getIntegrationConnectionById(installation.connectionId);\n if (!connection) {\n throw new GithubIntegrationProviderError(\n 'installation-not-found',\n `GitHub installation connection not found: ${installation.connectionId}`,\n );\n }\n return connection.workspaceId;\n };\n}\n\nexport function deleteGithubInstallationTokenSecret(params: {\n workspaceId: string;\n installationId: number;\n deleteSecrets: (params: {workspaceId: string; namespace: string}) => Promise<number>;\n}): Promise<number> {\n return params.deleteSecrets({\n workspaceId: params.workspaceId,\n namespace: githubInstallationTokenNamespace(params.installationId),\n });\n}\n"],"names":["App","Octokit","config","normalizedGithubApiBaseUrl","normalizedGithubPrivateKey","GithubIntegrationProviderError","withInstallationTokenLock","getGithubInstallationByInstallationId","recordInstallationTokenLookup","mapGithubError","githubInstallationTokenNamespace","TOKEN_REFRESH_MARGIN_MS","SharedInstallationTokenCache","createGithubInstallationTokenProvider","options","OctokitGithubInstallationTokenProvider","createInstallationTokenCache","cache","InMemoryInstallationTokenCache","getInstallationAccessToken","installationId","getOrMint","mintInstallationAccessToken","response","getApp","octokit","rest","apps","createInstallationAccessToken","installation_id","data","token","expiresAt","Date","expires_at","Number","isNaN","getTime","permissions","undefined","app","appId","GITHUB_APP_ID","privateKey","defaults","baseUrl","throttle","onRateLimit","_retryAfter","_options","_octokit","retryCount","onSecondaryRateLimit","refreshMarginMs","now","tokens","Map","inFlightMints","mint","cached","get","isInsideRefreshMargin","Promise","resolve","inFlightMint","freshToken","then","set","finally","delete","TieredInstallationTokenCache","ram","shared","getIntegrationConnectionById","secretStore","withLock","resolveWorkspaceId","createGithubInstallationWorkspaceResolver","installation","String","connection","connectionId","workspaceId","deleteGithubInstallationTokenSecret","params","deleteSecrets","namespace"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/api/installation-token-provider.ts"],"sourcesContent":["import type {GetIntegrationConnectionByIdFn} from '@shipfox/api-integration-spi';\nimport {App, Octokit} from 'octokit';\nimport {config, normalizedGithubApiBaseUrl, normalizedGithubPrivateKey} from '#config.js';\nimport {GithubIntegrationProviderError} from '#core/errors.js';\nimport {withInstallationTokenLock} from '#db/installation-token-lock.js';\nimport {getGithubInstallationByInstallationId} from '#db/installations.js';\nimport {recordInstallationTokenFormat, recordInstallationTokenLookup} from '#metrics/index.js';\nimport {type GithubInstallationAccessToken, mapGithubError} from './client.js';\nimport {githubInstallationTokenFormatPlugin} from './github-octokit.js';\nimport {\n githubInstallationTokenNamespace,\n TOKEN_REFRESH_MARGIN_MS,\n} from './installation-token-envelope.js';\nimport {\n type InstallationTokenCache,\n type InstallationTokenSecretStore,\n SharedInstallationTokenCache,\n type SharedInstallationTokenCacheOptions,\n} from './shared-installation-token-cache.js';\n\nexport interface GithubInstallationTokenProvider {\n getInstallationAccessToken(installationId: number): Promise<GithubInstallationAccessToken>;\n}\n\nexport interface GithubInstallationTokenProviderOptions {\n cache?: InstallationTokenCache | undefined;\n getIntegrationConnectionById?: GetIntegrationConnectionByIdFn | undefined;\n secretStore?: InstallationTokenSecretStore | undefined;\n withLock?: SharedInstallationTokenCacheOptions['withLock'] | undefined;\n now?: (() => Date) | undefined;\n}\n\nexport function createGithubInstallationTokenProvider(\n options: GithubInstallationTokenProviderOptions = {},\n): GithubInstallationTokenProvider {\n return new OctokitGithubInstallationTokenProvider(createInstallationTokenCache(options));\n}\n\nclass OctokitGithubInstallationTokenProvider implements GithubInstallationTokenProvider {\n private app: App | undefined;\n\n constructor(\n private readonly cache: InstallationTokenCache = new InMemoryInstallationTokenCache(),\n ) {}\n\n getInstallationAccessToken(installationId: number): Promise<GithubInstallationAccessToken> {\n return this.cache.getOrMint(installationId, () =>\n this.mintInstallationAccessToken(installationId),\n );\n }\n\n private async mintInstallationAccessToken(\n installationId: number,\n ): Promise<GithubInstallationAccessToken> {\n const response = await mapGithubError(\n () =>\n this.getApp().octokit.rest.apps.createInstallationAccessToken({\n installation_id: installationId,\n }),\n 'installation-not-found',\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 recordInstallationTokenFormat(response.data.token);\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.plugin(githubInstallationTokenFormatPlugin).defaults({\n baseUrl: normalizedGithubApiBaseUrl(),\n throttle: {\n onRateLimit: (\n _retryAfter: number,\n _options: unknown,\n _octokit: unknown,\n retryCount: number,\n ) => retryCount === 0,\n onSecondaryRateLimit: (\n _retryAfter: number,\n _options: unknown,\n _octokit: unknown,\n retryCount: number,\n ) => retryCount === 0,\n },\n }),\n });\n }\n return this.app;\n }\n}\n\nclass InMemoryInstallationTokenCache implements InstallationTokenCache {\n private readonly tokens = new Map<number, GithubInstallationAccessToken>();\n private readonly inFlightMints = new Map<number, Promise<GithubInstallationAccessToken>>();\n\n constructor(\n private readonly options: {\n refreshMarginMs: number;\n now: () => Date;\n } = {\n refreshMarginMs: TOKEN_REFRESH_MARGIN_MS,\n now: () => new Date(),\n },\n ) {}\n\n getOrMint(\n installationId: number,\n mint: () => Promise<GithubInstallationAccessToken>,\n ): Promise<GithubInstallationAccessToken> {\n const cached = this.tokens.get(installationId);\n if (cached && !this.isInsideRefreshMargin(cached.expiresAt)) {\n recordInstallationTokenLookup('ram-hit');\n return Promise.resolve(cached);\n }\n\n const inFlightMint = this.inFlightMints.get(installationId);\n if (inFlightMint) return inFlightMint;\n\n const freshToken = mint()\n .then((token) => {\n this.tokens.set(installationId, token);\n return token;\n })\n .finally(() => {\n this.inFlightMints.delete(installationId);\n });\n this.inFlightMints.set(installationId, freshToken);\n return freshToken;\n }\n\n private isInsideRefreshMargin(expiresAt: Date): boolean {\n return expiresAt.getTime() <= this.options.now().getTime() + this.options.refreshMarginMs;\n }\n}\n\nclass TieredInstallationTokenCache implements InstallationTokenCache {\n constructor(\n private readonly ram: InstallationTokenCache,\n private readonly shared: InstallationTokenCache,\n ) {}\n\n getOrMint(\n installationId: number,\n mint: () => Promise<GithubInstallationAccessToken>,\n ): Promise<GithubInstallationAccessToken> {\n return this.ram.getOrMint(installationId, () => this.shared.getOrMint(installationId, mint));\n }\n}\n\nfunction createInstallationTokenCache(\n options: GithubInstallationTokenProviderOptions,\n): InstallationTokenCache {\n if (options.cache) return options.cache;\n\n const ram = new InMemoryInstallationTokenCache({\n refreshMarginMs: TOKEN_REFRESH_MARGIN_MS,\n now: options.now ?? (() => new Date()),\n });\n if (!options.getIntegrationConnectionById || !options.secretStore) return ram;\n\n const shared = new SharedInstallationTokenCache({\n secretStore: options.secretStore,\n withLock: options.withLock ?? withInstallationTokenLock,\n resolveWorkspaceId: createGithubInstallationWorkspaceResolver(\n options.getIntegrationConnectionById,\n ),\n now: options.now,\n });\n return new TieredInstallationTokenCache(ram, shared);\n}\n\nfunction createGithubInstallationWorkspaceResolver(\n getIntegrationConnectionById: GetIntegrationConnectionByIdFn,\n) {\n return async (installationId: number): Promise<string> => {\n const installation = await getGithubInstallationByInstallationId(String(installationId));\n if (!installation) {\n throw new GithubIntegrationProviderError(\n 'installation-not-found',\n `GitHub installation not found: ${installationId}`,\n );\n }\n\n const connection = await getIntegrationConnectionById(installation.connectionId);\n if (!connection) {\n throw new GithubIntegrationProviderError(\n 'installation-not-found',\n `GitHub installation connection not found: ${installation.connectionId}`,\n );\n }\n return connection.workspaceId;\n };\n}\n\nexport function deleteGithubInstallationTokenSecret(params: {\n workspaceId: string;\n installationId: number;\n deleteSecrets: (params: {workspaceId: string; namespace: string}) => Promise<number>;\n}): Promise<number> {\n return params.deleteSecrets({\n workspaceId: params.workspaceId,\n namespace: githubInstallationTokenNamespace(params.installationId),\n });\n}\n"],"names":["App","Octokit","config","normalizedGithubApiBaseUrl","normalizedGithubPrivateKey","GithubIntegrationProviderError","withInstallationTokenLock","getGithubInstallationByInstallationId","recordInstallationTokenFormat","recordInstallationTokenLookup","mapGithubError","githubInstallationTokenFormatPlugin","githubInstallationTokenNamespace","TOKEN_REFRESH_MARGIN_MS","SharedInstallationTokenCache","createGithubInstallationTokenProvider","options","OctokitGithubInstallationTokenProvider","createInstallationTokenCache","cache","InMemoryInstallationTokenCache","getInstallationAccessToken","installationId","getOrMint","mintInstallationAccessToken","response","getApp","octokit","rest","apps","createInstallationAccessToken","installation_id","data","token","expiresAt","Date","expires_at","Number","isNaN","getTime","permissions","undefined","app","appId","GITHUB_APP_ID","privateKey","plugin","defaults","baseUrl","throttle","onRateLimit","_retryAfter","_options","_octokit","retryCount","onSecondaryRateLimit","refreshMarginMs","now","tokens","Map","inFlightMints","mint","cached","get","isInsideRefreshMargin","Promise","resolve","inFlightMint","freshToken","then","set","finally","delete","TieredInstallationTokenCache","ram","shared","getIntegrationConnectionById","secretStore","withLock","resolveWorkspaceId","createGithubInstallationWorkspaceResolver","installation","String","connection","connectionId","workspaceId","deleteGithubInstallationTokenSecret","params","deleteSecrets","namespace"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"shared-installation-token-cache.d.ts","sourceRoot":"","sources":["../../src/api/shared-installation-token-cache.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAC,6BAA6B,EAAC,MAAM,aAAa,CAAC;AAC/D,OAAO,EAIL,KAAK,yBAAyB,EAO/B,MAAM,kCAAkC,CAAC;AAE1C,MAAM,WAAW,sBAAsB;IACrC,SAAS,CACP,cAAc,EAAE,MAAM,EACtB,IAAI,EAAE,MAAM,OAAO,CAAC,6BAA6B,CAAC,GACjD,OAAO,CAAC,6BAA6B,CAAC,CAAC;CAC3C;AAED,MAAM,MAAM,2BAA2B,CAAC,CAAC,IAAI;IAAC,QAAQ,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,CAAC,CAAA;CAAC,GAAG;IAAC,QAAQ,EAAE,KAAK,CAAA;CAAC,CAAC;AAE5F,MAAM,WAAW,4BAA4B;IAC3C,IAAI,CAAC,WAAW,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC1E,KAAK,CACH,WAAW,EAAE,MAAM,EACnB,cAAc,EAAE,MAAM,EACtB,QAAQ,EAAE,yBAAyB,GAClC,OAAO,CAAC,IAAI,CAAC,CAAC;CAClB;AAED,MAAM,WAAW,mCAAmC;IAClD,WAAW,EAAE,4BAA4B,CAAC;IAC1C,QAAQ,EAAE,CAAC,CAAC,EACV,cAAc,EAAE,MAAM,EACtB,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,KACjB,OAAO,CAAC,2BAA2B,CAAC,CAAC,CAAC,CAAC,CAAC;IAC7C,kBAAkB,EAAE,CAAC,cAAc,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAChE,GAAG,CAAC,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,SAAS,CAAC;IAC/B,KAAK,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,SAAS,CAAC;IACpD,YAAY,CAAC,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC;IACpC,mBAAmB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACzC,aAAa,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACpC;AAMD,qBAAa,4BAA6B,YAAW,sBAAsB;IAQ7D,OAAO,CAAC,QAAQ,CAAC,OAAO;IAPpC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAiE;IAC9F,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAa;IACjC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAgC;IACtD,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAW;IACxC,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAS;IAC7C,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IAEvC,YAA6B,OAAO,EAAE,mCAAmC,EAMxE;IAEK,SAAS,CACb,cAAc,EAAE,MAAM,EACtB,IAAI,EAAE,MAAM,OAAO,CAAC,6BAA6B,CAAC,GACjD,OAAO,CAAC,6BAA6B,CAAC,CAcxC;YAEa,aAAa;YA4Gb,gBAAgB;YA0ChB,UAAU;YAcV,YAAY;YAcZ,aAAa;YAQb,kBAAkB;CAYjC"}
1
+ {"version":3,"file":"shared-installation-token-cache.d.ts","sourceRoot":"","sources":["../../src/api/shared-installation-token-cache.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAC,6BAA6B,EAAC,MAAM,aAAa,CAAC;AAC/D,OAAO,EAIL,KAAK,yBAAyB,EAO/B,MAAM,kCAAkC,CAAC;AAE1C,MAAM,WAAW,sBAAsB;IACrC,SAAS,CACP,cAAc,EAAE,MAAM,EACtB,IAAI,EAAE,MAAM,OAAO,CAAC,6BAA6B,CAAC,GACjD,OAAO,CAAC,6BAA6B,CAAC,CAAC;CAC3C;AAED,MAAM,MAAM,2BAA2B,CAAC,CAAC,IAAI;IAAC,QAAQ,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,CAAC,CAAA;CAAC,GAAG;IAAC,QAAQ,EAAE,KAAK,CAAA;CAAC,CAAC;AAE5F,MAAM,WAAW,4BAA4B;IAC3C,IAAI,CAAC,WAAW,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC1E,KAAK,CACH,WAAW,EAAE,MAAM,EACnB,cAAc,EAAE,MAAM,EACtB,QAAQ,EAAE,yBAAyB,GAClC,OAAO,CAAC,IAAI,CAAC,CAAC;CAClB;AAED,MAAM,WAAW,mCAAmC;IAClD,WAAW,EAAE,4BAA4B,CAAC;IAC1C,QAAQ,EAAE,CAAC,CAAC,EACV,cAAc,EAAE,MAAM,EACtB,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,KACjB,OAAO,CAAC,2BAA2B,CAAC,CAAC,CAAC,CAAC,CAAC;IAC7C,kBAAkB,EAAE,CAAC,cAAc,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAChE,GAAG,CAAC,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,SAAS,CAAC;IAC/B,KAAK,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,SAAS,CAAC;IACpD,YAAY,CAAC,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC;IACpC,mBAAmB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACzC,aAAa,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACpC;AAMD,qBAAa,4BAA6B,YAAW,sBAAsB;IAQ7D,OAAO,CAAC,QAAQ,CAAC,OAAO;IAPpC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAiE;IAC9F,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAa;IACjC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAgC;IACtD,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAW;IACxC,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAS;IAC7C,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IAEvC,YAA6B,OAAO,EAAE,mCAAmC,EAMxE;IAEK,SAAS,CACb,cAAc,EAAE,MAAM,EACtB,IAAI,EAAE,MAAM,OAAO,CAAC,6BAA6B,CAAC,GACjD,OAAO,CAAC,6BAA6B,CAAC,CAcxC;YAEa,aAAa;YAiHb,gBAAgB;YA4ChB,UAAU;YAcV,YAAY;YAcZ,aAAa;YAQb,kBAAkB;CAYjC"}