@fragno-dev/github-app-fragment 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +16 -0
- package/README.md +163 -0
- package/bin/run.js +5 -0
- package/dist/browser/client/react.d.ts +37 -0
- package/dist/browser/client/react.d.ts.map +1 -0
- package/dist/browser/client/react.js +166 -0
- package/dist/browser/client/react.js.map +1 -0
- package/dist/browser/client/solid.d.ts +35 -0
- package/dist/browser/client/solid.d.ts.map +1 -0
- package/dist/browser/client/solid.js +136 -0
- package/dist/browser/client/solid.js.map +1 -0
- package/dist/browser/client/svelte.d.ts +30 -0
- package/dist/browser/client/svelte.d.ts.map +1 -0
- package/dist/browser/client/svelte.js +134 -0
- package/dist/browser/client/svelte.js.map +1 -0
- package/dist/browser/client/vanilla.d.ts +16 -0
- package/dist/browser/client/vanilla.d.ts.map +1 -0
- package/dist/browser/client/vanilla.js +11 -0
- package/dist/browser/client/vanilla.js.map +1 -0
- package/dist/browser/client/vue.d.ts +33 -0
- package/dist/browser/client/vue.d.ts.map +1 -0
- package/dist/browser/client/vue.js +133 -0
- package/dist/browser/client/vue.js.map +1 -0
- package/dist/browser/factory-BIj4C6PD.js +2210 -0
- package/dist/browser/factory-BIj4C6PD.js.map +1 -0
- package/dist/browser/index.d.ts +343 -0
- package/dist/browser/index.d.ts.map +1 -0
- package/dist/browser/index.js +3 -0
- package/dist/browser/types-BzeSSOQU.d.ts +660 -0
- package/dist/browser/types-BzeSSOQU.d.ts.map +1 -0
- package/dist/cli/commands/installations.js +92 -0
- package/dist/cli/commands/installations.js.map +1 -0
- package/dist/cli/commands/pulls.js +123 -0
- package/dist/cli/commands/pulls.js.map +1 -0
- package/dist/cli/commands/repositories.js +105 -0
- package/dist/cli/commands/repositories.js.map +1 -0
- package/dist/cli/commands/serve.js +187 -0
- package/dist/cli/commands/serve.js.map +1 -0
- package/dist/cli/commands/webhooks.js +122 -0
- package/dist/cli/commands/webhooks.js.map +1 -0
- package/dist/cli/github/api.js +94 -0
- package/dist/cli/github/api.js.map +1 -0
- package/dist/cli/github/definition.js +15 -0
- package/dist/cli/github/definition.js.map +1 -0
- package/dist/cli/github/factory.js +12 -0
- package/dist/cli/github/factory.js.map +1 -0
- package/dist/cli/github/repo-sync.js +33 -0
- package/dist/cli/github/repo-sync.js.map +1 -0
- package/dist/cli/github/utils.js +23 -0
- package/dist/cli/github/utils.js.map +1 -0
- package/dist/cli/github/webhook-processing.js +247 -0
- package/dist/cli/github/webhook-processing.js.map +1 -0
- package/dist/cli/index.d.ts +5 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +263 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/routes.js +718 -0
- package/dist/cli/routes.js.map +1 -0
- package/dist/cli/schema.js +47 -0
- package/dist/cli/schema.js.map +1 -0
- package/dist/cli/utils/client.js +120 -0
- package/dist/cli/utils/client.js.map +1 -0
- package/dist/cli/utils/config.js +113 -0
- package/dist/cli/utils/config.js.map +1 -0
- package/dist/cli/utils/options.js +90 -0
- package/dist/cli/utils/options.js.map +1 -0
- package/dist/cli/utils/output.js +12 -0
- package/dist/cli/utils/output.js.map +1 -0
- package/dist/node/github/api.d.ts +52 -0
- package/dist/node/github/api.d.ts.map +1 -0
- package/dist/node/github/api.js +94 -0
- package/dist/node/github/api.js.map +1 -0
- package/dist/node/github/clients.d.ts +19 -0
- package/dist/node/github/clients.d.ts.map +1 -0
- package/dist/node/github/clients.js +12 -0
- package/dist/node/github/clients.js.map +1 -0
- package/dist/node/github/definition.d.ts +33 -0
- package/dist/node/github/definition.d.ts.map +1 -0
- package/dist/node/github/definition.js +15 -0
- package/dist/node/github/definition.js.map +1 -0
- package/dist/node/github/factory.d.ts +139 -0
- package/dist/node/github/factory.d.ts.map +1 -0
- package/dist/node/github/factory.js +18 -0
- package/dist/node/github/factory.js.map +1 -0
- package/dist/node/github/repo-sync.js +33 -0
- package/dist/node/github/repo-sync.js.map +1 -0
- package/dist/node/github/types.d.ts +24 -0
- package/dist/node/github/types.d.ts.map +1 -0
- package/dist/node/github/utils.js +23 -0
- package/dist/node/github/utils.js.map +1 -0
- package/dist/node/github/webhook-processing.d.ts +15 -0
- package/dist/node/github/webhook-processing.d.ts.map +1 -0
- package/dist/node/github/webhook-processing.js +247 -0
- package/dist/node/github/webhook-processing.js.map +1 -0
- package/dist/node/index.d.ts +7 -0
- package/dist/node/index.js +6 -0
- package/dist/node/routes.d.ts +127 -0
- package/dist/node/routes.d.ts.map +1 -0
- package/dist/node/routes.js +718 -0
- package/dist/node/routes.js.map +1 -0
- package/dist/node/schema.js +47 -0
- package/dist/node/schema.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +114 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routes.js","names":[],"sources":["../../src/routes.ts"],"sourcesContent":["import type { TableToColumnValues } from \"@fragno-dev/db/query\";\nimport { FragnoId } from \"@fragno-dev/db/schema\";\nimport { z } from \"zod\";\n\nimport { defineRoutes } from \"@fragno-dev/core\";\n\nimport type { GitHubInstallationDetails, GitHubInstallationRepository } from \"./github/api\";\nimport { githubAppFragmentDefinition } from \"./github/definition\";\nimport { hasRepoChanges, toRepoCreateRecord, toRepoRecord } from \"./github/repo-sync\";\nimport {\n isRecord,\n normalizeJoinedInstallation,\n normalizeJoinedLinks,\n toExternalId,\n toStringValue,\n} from \"./github/utils\";\nimport type { WebhookProcessingPayload } from \"./github/webhook-processing\";\nimport { githubAppSchema } from \"./schema\";\n\ntype InstallationRepoRow = TableToColumnValues<\n (typeof githubAppSchema)[\"tables\"][\"installation_repo\"]\n>;\ntype RepoLinkRow = TableToColumnValues<(typeof githubAppSchema)[\"tables\"][\"repo_link\"]>;\ntype RepoId = string | FragnoId;\ntype RepoLinkId = string | FragnoId;\n\nconst toDebugSignature = (signatureHeader: string | null) => {\n if (!signatureHeader) {\n return null;\n }\n if (signatureHeader.length <= 20) {\n return signatureHeader;\n }\n return `${signatureHeader.slice(0, 12)}…${signatureHeader.slice(-8)}`;\n};\n\nconst normalizeLinkKey = (linkKey: string | null | undefined, defaultLinkKey?: string) => {\n const normalized = linkKey?.trim();\n return normalized && normalized.length > 0 ? normalized : (defaultLinkKey ?? \"default\");\n};\n\nconst parseLinkedOnly = (value: string | null) => value === \"true\" || value === \"1\";\n\nconst getHttpStatusCode = (value: unknown): number | null => {\n if (typeof value === \"number\" && Number.isFinite(value)) {\n return value;\n }\n if (typeof value === \"object\" && value && \"status\" in value) {\n const status = (value as { status?: unknown }).status;\n if (typeof status === \"number\" && Number.isFinite(status)) {\n return status;\n }\n }\n return null;\n};\n\nconst installationOutputSchema = z.object({\n id: z.string(),\n accountId: z.string(),\n accountLogin: z.string(),\n accountType: z.string(),\n status: z.string(),\n permissions: z.any(),\n events: z.any(),\n createdAt: z.date(),\n updatedAt: z.date(),\n lastWebhookAt: z.date().nullable(),\n});\n\nconst repoSummarySchema = z.object({\n id: z.string(),\n installationId: z.string(),\n ownerLogin: z.string(),\n name: z.string(),\n fullName: z.string(),\n isPrivate: z.boolean(),\n isFork: z.boolean().nullable(),\n defaultBranch: z.string().nullable(),\n removedAt: z.date().nullable(),\n updatedAt: z.date(),\n});\n\nconst repoWithLinksSchema = repoSummarySchema.extend({\n linkKeys: z.array(z.string()),\n});\n\nconst repoLinkOutputSchema = z.object({\n id: z.string(),\n repoId: z.string(),\n linkKey: z.string(),\n linkedAt: z.date(),\n});\n\nexport const githubAppRoutesFactory = defineRoutes(githubAppFragmentDefinition).create(\n ({ config, defineRoute, deps }) => {\n const api = deps.githubApiClient;\n\n return [\n defineRoute({\n method: \"POST\",\n path: \"/webhooks\",\n errorCodes: [\n \"WEBHOOK_SIGNATURE_INVALID\",\n \"WEBHOOK_DELIVERY_MISSING\",\n \"WEBHOOK_PAYLOAD_INVALID\",\n ],\n handler: async function (ctx, { empty, error }) {\n const rawBody = ctx.rawBody;\n const logWebhook =\n config.webhookDebug === true\n ? (message: string, details?: Record<string, unknown>) => {\n if (details) {\n console.log(\"[github-app-fragment webhook]\", message, details);\n } else {\n console.log(\"[github-app-fragment webhook]\", message);\n }\n }\n : undefined;\n\n logWebhook?.(\"received\", {\n hasRawBody: Boolean(rawBody),\n rawBodyBytes: rawBody ? Buffer.byteLength(rawBody, \"utf8\") : 0,\n signature: toDebugSignature(ctx.headers.get(\"x-hub-signature-256\")),\n deliveryId: ctx.headers.get(\"x-github-delivery\") ?? null,\n event: ctx.headers.get(\"x-github-event\") ?? null,\n contentType: ctx.headers.get(\"content-type\") ?? null,\n });\n\n if (!rawBody) {\n logWebhook?.(\"rejected: missing payload\");\n return error(\n { message: \"Missing webhook payload.\", code: \"WEBHOOK_PAYLOAD_INVALID\" },\n { status: 400 },\n );\n }\n\n const signatureHeader = ctx.headers.get(\"x-hub-signature-256\");\n const signatureOk = await api.verifyWebhookSignature({\n payload: rawBody,\n signatureHeader,\n });\n logWebhook?.(\"signature check\", { ok: signatureOk });\n if (!signatureOk) {\n logWebhook?.(\"rejected: invalid signature\");\n return error(\n { message: \"Invalid webhook signature.\", code: \"WEBHOOK_SIGNATURE_INVALID\" },\n { status: 401 },\n );\n }\n\n const deliveryId = ctx.headers.get(\"x-github-delivery\") ?? \"\";\n if (!deliveryId) {\n logWebhook?.(\"rejected: missing delivery id\");\n return error(\n { message: \"Missing delivery id.\", code: \"WEBHOOK_DELIVERY_MISSING\" },\n { status: 400 },\n );\n }\n\n const event = ctx.headers.get(\"x-github-event\") ?? \"\";\n if (!event) {\n logWebhook?.(\"rejected: missing event\");\n return error(\n { message: \"Missing webhook event type.\", code: \"WEBHOOK_PAYLOAD_INVALID\" },\n { status: 400 },\n );\n }\n\n let payload: unknown;\n try {\n payload = JSON.parse(rawBody);\n } catch {\n logWebhook?.(\"rejected: invalid json\");\n return error(\n { message: \"Invalid JSON payload.\", code: \"WEBHOOK_PAYLOAD_INVALID\" },\n { status: 400 },\n );\n }\n\n if (!isRecord(payload)) {\n logWebhook?.(\"rejected: payload not object\");\n return error(\n { message: \"Invalid webhook payload.\", code: \"WEBHOOK_PAYLOAD_INVALID\" },\n { status: 400 },\n );\n }\n\n const action =\n typeof payload[\"action\"] === \"string\" ? (payload[\"action\"] as string) : null;\n const installationPayload = isRecord(payload[\"installation\"])\n ? payload[\"installation\"]\n : null;\n\n const installationId = toStringValue(\n installationPayload?.[\"id\"] ?? payload[\"installation_id\"],\n );\n if (!installationId) {\n logWebhook?.(\"rejected: missing installation id\");\n return error(\n { message: \"Missing installation id.\", code: \"WEBHOOK_PAYLOAD_INVALID\" },\n { status: 400 },\n );\n }\n\n const now = new Date();\n logWebhook?.(\"accepted\", {\n deliveryId,\n event,\n action,\n installationId,\n });\n const webhookPayload: WebhookProcessingPayload = {\n deliveryId,\n event,\n action,\n installationId,\n payload,\n receivedAt: now.toISOString(),\n };\n\n await this.handlerTx()\n .mutate(({ forSchema }) => {\n const uow = forSchema(githubAppSchema);\n uow.triggerHook(\"processWebhook\", webhookPayload);\n })\n .execute();\n\n return empty(204);\n },\n }),\n defineRoute({\n method: \"GET\",\n path: \"/installations\",\n queryParameters: [\"status\"],\n outputSchema: z.array(installationOutputSchema),\n errorCodes: [\"INVALID_STATUS\"],\n handler: async function ({ query }, { json, error }) {\n const status = query.get(\"status\");\n if (status && ![\"active\", \"suspended\", \"deleted\"].includes(status)) {\n return error(\n { message: `Invalid status: ${status}`, code: \"INVALID_STATUS\" },\n { status: 400 },\n );\n }\n\n const [installations] = await this.handlerTx()\n .retrieve(({ forSchema }) => {\n const uow = forSchema(githubAppSchema);\n if (status) {\n return uow.find(\"installation\", (b) =>\n b.whereIndex(\"idx_installation_status\", (eb) => eb(\"status\", \"=\", status)),\n );\n }\n return uow.find(\"installation\", (b) =>\n b.whereIndex(\"idx_installation_status\", (eb) => eb(\"status\", \"!=\", \"\")),\n );\n })\n .execute();\n\n return json(\n installations.map((installation) => ({\n id: toExternalId(installation.id),\n accountId: installation.accountId,\n accountLogin: installation.accountLogin,\n accountType: installation.accountType,\n status: installation.status,\n permissions: installation.permissions,\n events: installation.events,\n createdAt: installation.createdAt,\n updatedAt: installation.updatedAt,\n lastWebhookAt: installation.lastWebhookAt,\n })),\n );\n },\n }),\n defineRoute({\n method: \"GET\",\n path: \"/installations/:installationId/repos\",\n queryParameters: [\"linkedOnly\", \"linkKey\"],\n outputSchema: z.array(repoWithLinksSchema),\n errorCodes: [\"INSTALLATION_NOT_FOUND\"],\n handler: async function ({ pathParams, query }, { json, error }) {\n const installationId = pathParams.installationId;\n const linkedOnly = parseLinkedOnly(query.get(\"linkedOnly\"));\n const linkKeyFilter = query.get(\"linkKey\")?.trim() ?? null;\n\n const [installation, repos] = await this.handlerTx()\n .retrieve(({ forSchema }) => {\n const uow = forSchema(githubAppSchema);\n return uow\n .findFirst(\"installation\", (b) =>\n b.whereIndex(\"uniq_installation_id\", (eb) => eb(\"id\", \"=\", installationId)),\n )\n .find(\"installation_repo\", (b) =>\n b\n .whereIndex(\"idx_installation_repo_installation\", (eb) =>\n eb(\"installationId\", \"=\", installationId),\n )\n .join((jb) => jb.links()),\n );\n })\n .execute();\n\n if (!installation) {\n return error(\n { message: \"Installation not found.\", code: \"INSTALLATION_NOT_FOUND\" },\n { status: 404 },\n );\n }\n\n const output = [];\n for (const repo of repos) {\n if (repo.removedAt !== null) {\n continue;\n }\n const repoId = toExternalId(repo.id);\n if (!repoId) {\n continue;\n }\n const linkEntries = normalizeJoinedLinks(repo.links);\n const linkKeys = linkEntries\n .filter((link) => !linkKeyFilter || link.linkKey === linkKeyFilter)\n .map((link) => link.linkKey);\n if (linkedOnly && linkKeys.length === 0) {\n continue;\n }\n output.push({\n id: repoId,\n installationId,\n ownerLogin: repo.ownerLogin ?? \"\",\n name: repo.name ?? \"\",\n fullName: repo.fullName ?? \"\",\n isPrivate: Boolean(repo.isPrivate),\n isFork: repo.isFork ?? null,\n defaultBranch: repo.defaultBranch ?? null,\n removedAt: repo.removedAt ?? null,\n updatedAt: repo.updatedAt,\n linkKeys,\n });\n }\n\n return json(output);\n },\n }),\n defineRoute({\n method: \"GET\",\n path: \"/repositories/linked\",\n queryParameters: [\"linkKey\"],\n outputSchema: z.array(repoWithLinksSchema),\n handler: async function ({ query }, { json }) {\n const linkKeyFilter = query.get(\"linkKey\")?.trim() ?? null;\n\n const [repos] = await this.handlerTx()\n .retrieve(({ forSchema }) => {\n const uow = forSchema(githubAppSchema);\n return uow.find(\"installation_repo\", (b) =>\n b\n .whereIndex(\"idx_installation_repo_full_name\", (eb) => eb(\"fullName\", \"!=\", \"\"))\n .join((jb) => jb.installation().links()),\n );\n })\n .execute();\n\n const output = [];\n for (const repo of repos) {\n if (repo.removedAt !== null) {\n continue;\n }\n const repoId = toExternalId(repo.id);\n if (!repoId) {\n continue;\n }\n const installation = normalizeJoinedInstallation(repo.installation);\n if (!installation || installation.status !== \"active\") {\n continue;\n }\n const linkEntries = normalizeJoinedLinks(repo.links);\n const linkKeys = linkEntries\n .filter((link) => !linkKeyFilter || link.linkKey === linkKeyFilter)\n .map((link) => link.linkKey);\n if (linkKeys.length === 0) {\n continue;\n }\n output.push({\n id: repoId,\n installationId: toExternalId(installation.id),\n ownerLogin: repo.ownerLogin ?? \"\",\n name: repo.name ?? \"\",\n fullName: repo.fullName ?? \"\",\n isPrivate: Boolean(repo.isPrivate),\n isFork: repo.isFork ?? null,\n defaultBranch: repo.defaultBranch ?? null,\n removedAt: repo.removedAt ?? null,\n updatedAt: repo.updatedAt,\n linkKeys,\n });\n }\n\n return json(output);\n },\n }),\n defineRoute({\n method: \"POST\",\n path: \"/repositories/link\",\n inputSchema: z.object({\n installationId: z.string(),\n repoId: z.string(),\n linkKey: z.string().optional(),\n }),\n outputSchema: z.object({\n link: repoLinkOutputSchema,\n repo: repoSummarySchema,\n }),\n errorCodes: [\n \"INSTALLATION_NOT_FOUND\",\n \"INSTALLATION_INACTIVE\",\n \"REPO_NOT_FOUND\",\n \"REPO_REMOVED\",\n ],\n handler: async function ({ input }, { json, error }) {\n const values = await input.valid();\n const linkKey = normalizeLinkKey(values.linkKey, config.defaultLinkKey);\n\n const [installation, repos] = await this.handlerTx()\n .retrieve(({ forSchema }) => {\n const uow = forSchema(githubAppSchema);\n return uow\n .findFirst(\"installation\", (b) =>\n b.whereIndex(\"uniq_installation_id\", (eb) =>\n eb(\"id\", \"=\", values.installationId),\n ),\n )\n .find(\"installation_repo\", (b) =>\n b\n .whereIndex(\"idx_installation_repo_installation\", (eb) =>\n eb(\"installationId\", \"=\", values.installationId),\n )\n .join((jb) => jb.links()),\n );\n })\n .execute();\n\n if (!installation) {\n return error(\n { message: \"Installation not found.\", code: \"INSTALLATION_NOT_FOUND\" },\n { status: 404 },\n );\n }\n\n if (installation.status !== \"active\") {\n return error(\n { message: \"Installation is not active.\", code: \"INSTALLATION_INACTIVE\" },\n { status: 409 },\n );\n }\n\n const repo = repos.find((record) => toExternalId(record.id) === values.repoId);\n if (!repo) {\n return error(\n { message: \"Repository not found.\", code: \"REPO_NOT_FOUND\" },\n { status: 404 },\n );\n }\n\n if (repo.removedAt !== null) {\n return error(\n { message: \"Repository has been removed.\", code: \"REPO_REMOVED\" },\n { status: 409 },\n );\n }\n\n const linkEntries = normalizeJoinedLinks(repo.links);\n const existingLink = linkEntries.find((link) => link.linkKey === linkKey);\n\n if (existingLink) {\n return json({\n link: {\n id: toExternalId(existingLink.id),\n repoId: values.repoId,\n linkKey: existingLink.linkKey,\n linkedAt: existingLink.linkedAt,\n },\n repo: {\n id: values.repoId,\n installationId: values.installationId,\n ownerLogin: repo.ownerLogin ?? \"\",\n name: repo.name ?? \"\",\n fullName: repo.fullName ?? \"\",\n isPrivate: Boolean(repo.isPrivate),\n isFork: repo.isFork ?? null,\n defaultBranch: repo.defaultBranch ?? null,\n removedAt: repo.removedAt ?? null,\n updatedAt: repo.updatedAt,\n },\n });\n }\n\n const linkedAt = new Date();\n const linkId = await this.handlerTx()\n .mutate(({ forSchema }) => {\n const uow = forSchema(githubAppSchema);\n return uow.create(\"repo_link\", {\n repoId: values.repoId,\n linkKey,\n linkedAt,\n });\n })\n .execute();\n\n return json({\n link: {\n id: toExternalId(linkId),\n repoId: values.repoId,\n linkKey,\n linkedAt,\n },\n repo: {\n id: values.repoId,\n installationId: values.installationId,\n ownerLogin: repo.ownerLogin ?? \"\",\n name: repo.name ?? \"\",\n fullName: repo.fullName ?? \"\",\n isPrivate: Boolean(repo.isPrivate),\n isFork: repo.isFork ?? null,\n defaultBranch: repo.defaultBranch ?? null,\n removedAt: repo.removedAt ?? null,\n updatedAt: repo.updatedAt,\n },\n });\n },\n }),\n defineRoute({\n method: \"POST\",\n path: \"/repositories/unlink\",\n inputSchema: z.object({\n repoId: z.string(),\n linkKey: z.string().optional(),\n }),\n outputSchema: z.object({ ok: z.literal(true) }),\n errorCodes: [\n \"REPO_NOT_FOUND\",\n \"INSTALLATION_NOT_FOUND\",\n \"INSTALLATION_INACTIVE\",\n \"LINK_NOT_FOUND\",\n ],\n handler: async function ({ input }, { json, error }) {\n const values = await input.valid();\n const linkKey = normalizeLinkKey(values.linkKey, config.defaultLinkKey);\n\n const [repos] = await this.handlerTx()\n .retrieve(({ forSchema }) => {\n const uow = forSchema(githubAppSchema);\n return uow.find(\"installation_repo\", (b) =>\n b\n .whereIndex(\"idx_installation_repo_full_name\", (eb) => eb(\"fullName\", \"!=\", \"\"))\n .join((jb) => jb.installation().links()),\n );\n })\n .execute();\n\n const repo = repos.find((record) => toExternalId(record.id) === values.repoId);\n if (!repo) {\n return error(\n { message: \"Repository not found.\", code: \"REPO_NOT_FOUND\" },\n { status: 404 },\n );\n }\n\n const installation = normalizeJoinedInstallation(repo.installation);\n\n if (!installation) {\n return error(\n { message: \"Installation not found.\", code: \"INSTALLATION_NOT_FOUND\" },\n { status: 404 },\n );\n }\n\n if (installation.status !== \"active\") {\n return error(\n { message: \"Installation is not active.\", code: \"INSTALLATION_INACTIVE\" },\n { status: 409 },\n );\n }\n\n const linkEntries = normalizeJoinedLinks(repo.links);\n const link = linkEntries.find((entry) => entry.linkKey === linkKey);\n\n if (!link) {\n return error({ message: \"Link not found.\", code: \"LINK_NOT_FOUND\" }, { status: 404 });\n }\n\n await this.handlerTx()\n .mutate(({ forSchema }) => {\n const uow = forSchema(githubAppSchema);\n uow.delete(\"repo_link\", link.id as RepoLinkId);\n })\n .execute();\n\n return json({ ok: true });\n },\n }),\n defineRoute({\n method: \"GET\",\n path: \"/repositories/:owner/:repo/pulls\",\n queryParameters: [\"state\", \"perPage\", \"page\"],\n outputSchema: z.object({\n pulls: z.array(z.any()),\n pageInfo: z.object({\n page: z.number(),\n perPage: z.number(),\n }),\n }),\n errorCodes: [\n \"INVALID_STATE\",\n \"INVALID_PER_PAGE\",\n \"INVALID_PAGE\",\n \"REPO_NOT_FOUND\",\n \"REPO_REMOVED\",\n \"REPO_NOT_LINKED\",\n \"INSTALLATION_NOT_FOUND\",\n \"INSTALLATION_INACTIVE\",\n \"GITHUB_API_ERROR\",\n ],\n handler: async function ({ pathParams, query }, { json, error }) {\n const state = query.get(\"state\");\n if (state && ![\"open\", \"closed\", \"all\"].includes(state)) {\n return error(\n { message: `Invalid state: ${state}`, code: \"INVALID_STATE\" },\n { status: 400 },\n );\n }\n const stateFilter = (state ?? undefined) as \"open\" | \"closed\" | \"all\" | undefined;\n\n const perPageRaw = query.get(\"perPage\");\n const perPage = perPageRaw ? Number.parseInt(perPageRaw, 10) : 30;\n if (!Number.isFinite(perPage) || perPage <= 0 || perPage > 100) {\n return error(\n { message: \"perPage must be between 1 and 100.\", code: \"INVALID_PER_PAGE\" },\n { status: 400 },\n );\n }\n\n const pageRaw = query.get(\"page\");\n const page = pageRaw ? Number.parseInt(pageRaw, 10) : 1;\n if (!Number.isFinite(page) || page <= 0) {\n return error(\n { message: \"page must be a positive number.\", code: \"INVALID_PAGE\" },\n { status: 400 },\n );\n }\n\n const fullName = `${pathParams.owner}/${pathParams.repo}`;\n const [repos] = await this.handlerTx()\n .retrieve(({ forSchema }) => {\n const uow = forSchema(githubAppSchema);\n return uow.find(\"installation_repo\", (b) =>\n b\n .whereIndex(\"idx_installation_repo_full_name\", (eb) =>\n eb(\"fullName\", \"=\", fullName),\n )\n .join((jb) => jb.installation().links()),\n );\n })\n .execute();\n\n const repo = repos[0];\n if (!repo) {\n return error(\n { message: \"Repository not found.\", code: \"REPO_NOT_FOUND\" },\n { status: 404 },\n );\n }\n\n if (repo.removedAt !== null) {\n return error(\n { message: \"Repository has been removed.\", code: \"REPO_REMOVED\" },\n { status: 409 },\n );\n }\n\n const installation = normalizeJoinedInstallation(repo.installation);\n\n if (!installation) {\n return error(\n { message: \"Installation not found.\", code: \"INSTALLATION_NOT_FOUND\" },\n { status: 404 },\n );\n }\n\n if (installation.status !== \"active\") {\n return error(\n { message: \"Installation is not active.\", code: \"INSTALLATION_INACTIVE\" },\n { status: 409 },\n );\n }\n\n const installationId = toExternalId(installation.id);\n\n const linkEntries = normalizeJoinedLinks(repo.links);\n const linked = linkEntries.length > 0;\n\n if (!linked) {\n return error(\n { message: \"Repository is not linked.\", code: \"REPO_NOT_LINKED\" },\n { status: 403 },\n );\n }\n\n let pulls: unknown[];\n try {\n const installationOctokit = await api.app.getInstallationOctokit(\n api.resolveInstallationId(installationId),\n );\n const response = await installationOctokit.request(\"GET /repos/{owner}/{repo}/pulls\", {\n owner: pathParams.owner,\n repo: pathParams.repo,\n state: stateFilter,\n per_page: perPage,\n page,\n });\n pulls = response.data;\n } catch (err) {\n const message = err instanceof Error ? err.message : \"GitHub API request failed.\";\n return error({ message, code: \"GITHUB_API_ERROR\" }, { status: 502 });\n }\n\n return json({ pulls, pageInfo: { page, perPage } });\n },\n }),\n defineRoute({\n method: \"POST\",\n path: \"/repositories/:owner/:repo/pulls/:number/reviews\",\n inputSchema: z.object({\n event: z.enum([\"APPROVE\", \"REQUEST_CHANGES\", \"COMMENT\"]).optional(),\n body: z.string().optional(),\n comments: z.array(z.any()).optional(),\n commitId: z.string().optional(),\n }),\n outputSchema: z.object({ review: z.any() }),\n errorCodes: [\n \"INVALID_PULL_NUMBER\",\n \"REPO_NOT_FOUND\",\n \"REPO_REMOVED\",\n \"REPO_NOT_LINKED\",\n \"INSTALLATION_NOT_FOUND\",\n \"INSTALLATION_INACTIVE\",\n \"GITHUB_API_ERROR\",\n ],\n handler: async function ({ pathParams, input }, { json, error }) {\n const values = await input.valid();\n const pullNumber = Number.parseInt(pathParams.number, 10);\n if (!Number.isFinite(pullNumber) || pullNumber <= 0) {\n return error(\n { message: \"Invalid pull request number.\", code: \"INVALID_PULL_NUMBER\" },\n { status: 400 },\n );\n }\n\n const fullName = `${pathParams.owner}/${pathParams.repo}`;\n const [repos] = await this.handlerTx()\n .retrieve(({ forSchema }) => {\n const uow = forSchema(githubAppSchema);\n return uow.find(\"installation_repo\", (b) =>\n b\n .whereIndex(\"idx_installation_repo_full_name\", (eb) =>\n eb(\"fullName\", \"=\", fullName),\n )\n .join((jb) => jb.installation().links()),\n );\n })\n .execute();\n\n const repo = repos[0];\n if (!repo) {\n return error(\n { message: \"Repository not found.\", code: \"REPO_NOT_FOUND\" },\n { status: 404 },\n );\n }\n\n if (repo.removedAt !== null) {\n return error(\n { message: \"Repository has been removed.\", code: \"REPO_REMOVED\" },\n { status: 409 },\n );\n }\n\n const installation = normalizeJoinedInstallation(repo.installation);\n\n if (!installation) {\n return error(\n { message: \"Installation not found.\", code: \"INSTALLATION_NOT_FOUND\" },\n { status: 404 },\n );\n }\n\n if (installation.status !== \"active\") {\n return error(\n { message: \"Installation is not active.\", code: \"INSTALLATION_INACTIVE\" },\n { status: 409 },\n );\n }\n\n const installationId = toExternalId(installation.id);\n\n const linkEntries = normalizeJoinedLinks(repo.links);\n const linked = linkEntries.length > 0;\n\n if (!linked) {\n return error(\n { message: \"Repository is not linked.\", code: \"REPO_NOT_LINKED\" },\n { status: 403 },\n );\n }\n\n let review: unknown;\n try {\n const installationOctokit = await api.app.getInstallationOctokit(\n api.resolveInstallationId(installationId),\n );\n const response = await installationOctokit.request(\n \"POST /repos/{owner}/{repo}/pulls/{pull_number}/reviews\",\n {\n owner: pathParams.owner,\n repo: pathParams.repo,\n pull_number: pullNumber,\n event: values.event,\n body: values.body,\n comments: values.comments,\n commit_id: values.commitId,\n },\n );\n review = response.data;\n } catch (err) {\n const message = err instanceof Error ? err.message : \"GitHub API request failed.\";\n return error({ message, code: \"GITHUB_API_ERROR\" }, { status: 502 });\n }\n\n return json({ review });\n },\n }),\n defineRoute({\n method: \"POST\",\n path: \"/installations/:installationId/sync\",\n outputSchema: z.object({\n added: z.number(),\n removed: z.number(),\n updated: z.number(),\n }),\n errorCodes: [\"GITHUB_API_ERROR\", \"INSTALLATION_NOT_FOUND\"],\n handler: async function ({ pathParams }, { json, error }) {\n const installationId = pathParams.installationId;\n let bootstrapInstallation: GitHubInstallationDetails | null = null;\n\n const [existingInstallation, existingRepos] = await this.handlerTx()\n .retrieve(({ forSchema }) => {\n const uow = forSchema(githubAppSchema);\n return uow\n .findFirst(\"installation\", (b) =>\n b.whereIndex(\"uniq_installation_id\", (eb) => eb(\"id\", \"=\", installationId)),\n )\n .find(\"installation_repo\", (b) =>\n b\n .whereIndex(\"idx_installation_repo_installation\", (eb) =>\n eb(\"installationId\", \"=\", installationId),\n )\n .join((jb) => jb.links()),\n );\n })\n .execute();\n\n if (!existingInstallation) {\n try {\n bootstrapInstallation = await api.getInstallation(installationId);\n } catch (err) {\n if (getHttpStatusCode(err) === 404) {\n return error(\n { message: \"Installation not found.\", code: \"INSTALLATION_NOT_FOUND\" },\n { status: 404 },\n );\n }\n const message = err instanceof Error ? err.message : \"GitHub API request failed.\";\n return error({ message, code: \"GITHUB_API_ERROR\" }, { status: 502 });\n }\n }\n\n let response: { repositories: GitHubInstallationRepository[] };\n try {\n response = await api.listInstallationRepos(installationId);\n } catch (err) {\n const message = err instanceof Error ? err.message : \"GitHub API request failed.\";\n return error({ message, code: \"GITHUB_API_ERROR\" }, { status: 502 });\n }\n\n const repos = response.repositories ?? [];\n const now = new Date();\n\n const existingById = new Map<string, InstallationRepoRow>();\n for (const repo of existingRepos) {\n const id = toExternalId(repo.id);\n if (id) {\n existingById.set(id, repo);\n }\n }\n\n const repoLinksByRepoId = new Map<string, RepoLinkRow[]>();\n for (const repo of existingRepos) {\n const repoId = toExternalId(repo.id);\n if (!repoId) {\n continue;\n }\n const linkEntries = normalizeJoinedLinks(repo.links);\n if (linkEntries.length === 0) {\n continue;\n }\n repoLinksByRepoId.set(repoId, linkEntries);\n }\n\n let added = 0;\n let updated = 0;\n let removed = 0;\n\n type RepoUpdateData = Omit<ReturnType<typeof toRepoRecord>, \"id\">;\n const creates: Array<ReturnType<typeof toRepoRecord>> = [];\n const updates: Array<{ id: RepoId; data: Partial<RepoUpdateData> }> = [];\n const removals: Array<RepoId> = [];\n const linksToDelete: Array<RepoLinkId> = [];\n\n const seen = new Set<string>();\n\n for (const repo of repos) {\n const record = toRepoRecord(installationId, repo, now);\n seen.add(record.id);\n const existing = existingById.get(record.id);\n\n if (!existing) {\n added += 1;\n creates.push(record);\n continue;\n }\n\n if (hasRepoChanges(existing, record)) {\n updated += 1;\n updates.push({\n id: existing.id as RepoId,\n data: {\n installationId: record.installationId,\n ownerLogin: record.ownerLogin,\n name: record.name,\n fullName: record.fullName,\n isPrivate: record.isPrivate,\n ...(record.isFork !== undefined ? { isFork: record.isFork } : {}),\n ...(record.defaultBranch !== undefined\n ? { defaultBranch: record.defaultBranch }\n : {}),\n removedAt: null,\n updatedAt: now,\n },\n });\n }\n }\n\n for (const [repoId, repo] of existingById.entries()) {\n if (seen.has(repoId)) {\n continue;\n }\n if (repo.removedAt === null) {\n removed += 1;\n removals.push(repo.id as RepoId);\n const links = repoLinksByRepoId.get(repoId);\n if (links) {\n for (const link of links) {\n linksToDelete.push(link.id as RepoLinkId);\n }\n }\n }\n }\n\n if (\n bootstrapInstallation ||\n creates.length > 0 ||\n updates.length > 0 ||\n removals.length > 0 ||\n linksToDelete.length > 0\n ) {\n await this.handlerTx()\n .mutate(({ forSchema }) => {\n const uow = forSchema(githubAppSchema);\n\n if (bootstrapInstallation) {\n uow.create(\"installation\", {\n id: bootstrapInstallation.id,\n accountId: bootstrapInstallation.accountId,\n accountLogin: bootstrapInstallation.accountLogin,\n accountType: bootstrapInstallation.accountType,\n status: bootstrapInstallation.status,\n permissions: bootstrapInstallation.permissions,\n events: bootstrapInstallation.events,\n });\n }\n\n for (const record of creates) {\n uow.create(\"installation_repo\", toRepoCreateRecord(record));\n }\n\n for (const update of updates) {\n uow.update(\"installation_repo\", update.id, (b) => b.set(update.data));\n }\n\n for (const id of removals) {\n uow.update(\"installation_repo\", id, (b) =>\n b.set({ removedAt: now, updatedAt: now }),\n );\n }\n\n for (const id of linksToDelete) {\n uow.delete(\"repo_link\", id);\n }\n })\n .execute();\n }\n\n return json({ added, removed, updated });\n },\n }),\n ];\n },\n);\n"],"mappings":"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"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { column, idColumn, referenceColumn, schema } from "@fragno-dev/db/schema";
|
|
2
|
+
|
|
3
|
+
//#region src/schema.ts
|
|
4
|
+
const githubAppSchema = schema("github-app-fragment", (s) => {
|
|
5
|
+
return s.addTable("installation", (t) => {
|
|
6
|
+
return t.addColumn("id", idColumn()).addColumn("accountId", column("string")).addColumn("accountLogin", column("string")).addColumn("accountType", column("string")).addColumn("status", column("string")).addColumn("permissions", column("json")).addColumn("events", column("json")).addColumn("createdAt", column("timestamp").defaultTo((b) => b.now())).addColumn("updatedAt", column("timestamp").defaultTo((b) => b.now())).addColumn("lastWebhookAt", column("timestamp").nullable()).createIndex("idx_installation_account_login", ["accountLogin"]).createIndex("idx_installation_status", ["status"]).createIndex("uniq_installation_id", ["id"], { unique: true });
|
|
7
|
+
}).addTable("installation_repo", (t) => {
|
|
8
|
+
return t.addColumn("id", idColumn()).addColumn("installationId", referenceColumn()).addColumn("ownerLogin", column("string")).addColumn("name", column("string")).addColumn("fullName", column("string")).addColumn("isPrivate", column("bool")).addColumn("isFork", column("bool").nullable()).addColumn("defaultBranch", column("string").nullable()).addColumn("removedAt", column("timestamp").nullable()).addColumn("updatedAt", column("timestamp").defaultTo((b) => b.now())).createIndex("idx_installation_repo_installation", ["installationId"]).createIndex("idx_installation_repo_full_name", ["fullName"]);
|
|
9
|
+
}).addTable("repo_link", (t) => {
|
|
10
|
+
return t.addColumn("id", idColumn()).addColumn("repoId", referenceColumn()).addColumn("linkKey", column("string")).addColumn("linkedAt", column("timestamp").defaultTo((b) => b.now())).createIndex("uniq_repo_link_repo_id_link_key", ["repoId", "linkKey"], { unique: true });
|
|
11
|
+
}).addReference("installation", {
|
|
12
|
+
type: "one",
|
|
13
|
+
from: {
|
|
14
|
+
table: "installation_repo",
|
|
15
|
+
column: "installationId"
|
|
16
|
+
},
|
|
17
|
+
to: {
|
|
18
|
+
table: "installation",
|
|
19
|
+
column: "id"
|
|
20
|
+
}
|
|
21
|
+
}).addReference("links", {
|
|
22
|
+
type: "many",
|
|
23
|
+
from: {
|
|
24
|
+
table: "installation_repo",
|
|
25
|
+
column: "id"
|
|
26
|
+
},
|
|
27
|
+
to: {
|
|
28
|
+
table: "repo_link",
|
|
29
|
+
column: "repoId"
|
|
30
|
+
},
|
|
31
|
+
foreignKey: false
|
|
32
|
+
}).addReference("repo", {
|
|
33
|
+
type: "one",
|
|
34
|
+
from: {
|
|
35
|
+
table: "repo_link",
|
|
36
|
+
column: "repoId"
|
|
37
|
+
},
|
|
38
|
+
to: {
|
|
39
|
+
table: "installation_repo",
|
|
40
|
+
column: "id"
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
//#endregion
|
|
46
|
+
export { githubAppSchema };
|
|
47
|
+
//# sourceMappingURL=schema.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema.js","names":[],"sources":["../../src/schema.ts"],"sourcesContent":["import { column, idColumn, referenceColumn, schema } from \"@fragno-dev/db/schema\";\n\nexport const githubAppSchema = schema(\"github-app-fragment\", (s) => {\n return s\n .addTable(\"installation\", (t) => {\n return t\n .addColumn(\"id\", idColumn())\n .addColumn(\"accountId\", column(\"string\"))\n .addColumn(\"accountLogin\", column(\"string\"))\n .addColumn(\"accountType\", column(\"string\"))\n .addColumn(\"status\", column(\"string\"))\n .addColumn(\"permissions\", column(\"json\"))\n .addColumn(\"events\", column(\"json\"))\n .addColumn(\n \"createdAt\",\n column(\"timestamp\").defaultTo((b) => b.now()),\n )\n .addColumn(\n \"updatedAt\",\n column(\"timestamp\").defaultTo((b) => b.now()),\n )\n .addColumn(\"lastWebhookAt\", column(\"timestamp\").nullable())\n .createIndex(\"idx_installation_account_login\", [\"accountLogin\"])\n .createIndex(\"idx_installation_status\", [\"status\"])\n .createIndex(\"uniq_installation_id\", [\"id\"], { unique: true });\n })\n .addTable(\"installation_repo\", (t) => {\n return t\n .addColumn(\"id\", idColumn())\n .addColumn(\"installationId\", referenceColumn())\n .addColumn(\"ownerLogin\", column(\"string\"))\n .addColumn(\"name\", column(\"string\"))\n .addColumn(\"fullName\", column(\"string\"))\n .addColumn(\"isPrivate\", column(\"bool\"))\n .addColumn(\"isFork\", column(\"bool\").nullable())\n .addColumn(\"defaultBranch\", column(\"string\").nullable())\n .addColumn(\"removedAt\", column(\"timestamp\").nullable())\n .addColumn(\n \"updatedAt\",\n column(\"timestamp\").defaultTo((b) => b.now()),\n )\n .createIndex(\"idx_installation_repo_installation\", [\"installationId\"])\n .createIndex(\"idx_installation_repo_full_name\", [\"fullName\"]);\n })\n .addTable(\"repo_link\", (t) => {\n return (\n t\n .addColumn(\"id\", idColumn())\n .addColumn(\"repoId\", referenceColumn())\n // Namespaces a repo link so the same repository can be linked for multiple contexts.\n .addColumn(\"linkKey\", column(\"string\"))\n .addColumn(\n \"linkedAt\",\n column(\"timestamp\").defaultTo((b) => b.now()),\n )\n .createIndex(\"uniq_repo_link_repo_id_link_key\", [\"repoId\", \"linkKey\"], {\n unique: true,\n })\n );\n })\n .addReference(\"installation\", {\n type: \"one\",\n from: { table: \"installation_repo\", column: \"installationId\" },\n to: { table: \"installation\", column: \"id\" },\n })\n .addReference(\"links\", {\n // A repo is considered authorized for PR routes when at least one link exists.\n type: \"many\",\n from: { table: \"installation_repo\", column: \"id\" },\n to: { table: \"repo_link\", column: \"repoId\" },\n foreignKey: false,\n })\n .addReference(\"repo\", {\n type: \"one\",\n from: { table: \"repo_link\", column: \"repoId\" },\n to: { table: \"installation_repo\", column: \"id\" },\n });\n});\n"],"mappings":";;;AAEA,MAAa,kBAAkB,OAAO,wBAAwB,MAAM;AAClE,QAAO,EACJ,SAAS,iBAAiB,MAAM;AAC/B,SAAO,EACJ,UAAU,MAAM,UAAU,CAAC,CAC3B,UAAU,aAAa,OAAO,SAAS,CAAC,CACxC,UAAU,gBAAgB,OAAO,SAAS,CAAC,CAC3C,UAAU,eAAe,OAAO,SAAS,CAAC,CAC1C,UAAU,UAAU,OAAO,SAAS,CAAC,CACrC,UAAU,eAAe,OAAO,OAAO,CAAC,CACxC,UAAU,UAAU,OAAO,OAAO,CAAC,CACnC,UACC,aACA,OAAO,YAAY,CAAC,WAAW,MAAM,EAAE,KAAK,CAAC,CAC9C,CACA,UACC,aACA,OAAO,YAAY,CAAC,WAAW,MAAM,EAAE,KAAK,CAAC,CAC9C,CACA,UAAU,iBAAiB,OAAO,YAAY,CAAC,UAAU,CAAC,CAC1D,YAAY,kCAAkC,CAAC,eAAe,CAAC,CAC/D,YAAY,2BAA2B,CAAC,SAAS,CAAC,CAClD,YAAY,wBAAwB,CAAC,KAAK,EAAE,EAAE,QAAQ,MAAM,CAAC;GAChE,CACD,SAAS,sBAAsB,MAAM;AACpC,SAAO,EACJ,UAAU,MAAM,UAAU,CAAC,CAC3B,UAAU,kBAAkB,iBAAiB,CAAC,CAC9C,UAAU,cAAc,OAAO,SAAS,CAAC,CACzC,UAAU,QAAQ,OAAO,SAAS,CAAC,CACnC,UAAU,YAAY,OAAO,SAAS,CAAC,CACvC,UAAU,aAAa,OAAO,OAAO,CAAC,CACtC,UAAU,UAAU,OAAO,OAAO,CAAC,UAAU,CAAC,CAC9C,UAAU,iBAAiB,OAAO,SAAS,CAAC,UAAU,CAAC,CACvD,UAAU,aAAa,OAAO,YAAY,CAAC,UAAU,CAAC,CACtD,UACC,aACA,OAAO,YAAY,CAAC,WAAW,MAAM,EAAE,KAAK,CAAC,CAC9C,CACA,YAAY,sCAAsC,CAAC,iBAAiB,CAAC,CACrE,YAAY,mCAAmC,CAAC,WAAW,CAAC;GAC/D,CACD,SAAS,cAAc,MAAM;AAC5B,SACE,EACG,UAAU,MAAM,UAAU,CAAC,CAC3B,UAAU,UAAU,iBAAiB,CAAC,CAEtC,UAAU,WAAW,OAAO,SAAS,CAAC,CACtC,UACC,YACA,OAAO,YAAY,CAAC,WAAW,MAAM,EAAE,KAAK,CAAC,CAC9C,CACA,YAAY,mCAAmC,CAAC,UAAU,UAAU,EAAE,EACrE,QAAQ,MACT,CAAC;GAEN,CACD,aAAa,gBAAgB;EAC5B,MAAM;EACN,MAAM;GAAE,OAAO;GAAqB,QAAQ;GAAkB;EAC9D,IAAI;GAAE,OAAO;GAAgB,QAAQ;GAAM;EAC5C,CAAC,CACD,aAAa,SAAS;EAErB,MAAM;EACN,MAAM;GAAE,OAAO;GAAqB,QAAQ;GAAM;EAClD,IAAI;GAAE,OAAO;GAAa,QAAQ;GAAU;EAC5C,YAAY;EACb,CAAC,CACD,aAAa,QAAQ;EACpB,MAAM;EACN,MAAM;GAAE,OAAO;GAAa,QAAQ;GAAU;EAC9C,IAAI;GAAE,OAAO;GAAqB,QAAQ;GAAM;EACjD,CAAC;EACJ"}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { Readable } from "node:stream";
|
|
2
|
+
|
|
3
|
+
//#region src/cli/utils/client.ts
|
|
4
|
+
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
5
|
+
const buildUrl = (baseUrl, path, query) => {
|
|
6
|
+
const base = new URL(baseUrl);
|
|
7
|
+
base.pathname = `${base.pathname.endsWith("/") ? base.pathname.slice(0, -1) : base.pathname}${path.startsWith("/") ? path : `/${path}`}`;
|
|
8
|
+
if (query) for (const [key, value] of Object.entries(query)) {
|
|
9
|
+
if (value === void 0) continue;
|
|
10
|
+
base.searchParams.set(key, String(value));
|
|
11
|
+
}
|
|
12
|
+
return base.toString();
|
|
13
|
+
};
|
|
14
|
+
const shouldRetry = (response) => {
|
|
15
|
+
if (response.status >= 500) return true;
|
|
16
|
+
return response.status === 429;
|
|
17
|
+
};
|
|
18
|
+
async function fetchWithRetry(url, init, config) {
|
|
19
|
+
let attempt = 0;
|
|
20
|
+
let lastError;
|
|
21
|
+
while (attempt <= config.retries) {
|
|
22
|
+
const controller = new AbortController();
|
|
23
|
+
const timeoutId = setTimeout(() => controller.abort(), config.timeoutMs);
|
|
24
|
+
try {
|
|
25
|
+
const response = await fetch(url, {
|
|
26
|
+
...init,
|
|
27
|
+
signal: controller.signal
|
|
28
|
+
});
|
|
29
|
+
clearTimeout(timeoutId);
|
|
30
|
+
if (!response.ok && shouldRetry(response) && attempt < config.retries) {
|
|
31
|
+
attempt += 1;
|
|
32
|
+
await delay(config.retryDelayMs);
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
return response;
|
|
36
|
+
} catch (error) {
|
|
37
|
+
clearTimeout(timeoutId);
|
|
38
|
+
lastError = error;
|
|
39
|
+
if (attempt >= config.retries) throw error;
|
|
40
|
+
attempt += 1;
|
|
41
|
+
await delay(config.retryDelayMs);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
throw lastError ?? /* @__PURE__ */ new Error("Request failed");
|
|
45
|
+
}
|
|
46
|
+
async function fetchWithTimeout(url, init, timeoutMs) {
|
|
47
|
+
const controller = new AbortController();
|
|
48
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
49
|
+
try {
|
|
50
|
+
return await fetch(url, {
|
|
51
|
+
...init,
|
|
52
|
+
signal: controller.signal
|
|
53
|
+
});
|
|
54
|
+
} finally {
|
|
55
|
+
clearTimeout(timeoutId);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const safeJsonParse = (text) => {
|
|
59
|
+
try {
|
|
60
|
+
return JSON.parse(text);
|
|
61
|
+
} catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
const readErrorPayload = async (response) => {
|
|
66
|
+
const text = await response.text();
|
|
67
|
+
const parsed = text ? safeJsonParse(text) : void 0;
|
|
68
|
+
const errorPayload = parsed;
|
|
69
|
+
return {
|
|
70
|
+
text,
|
|
71
|
+
parsed,
|
|
72
|
+
message: errorPayload?.message ?? text ?? response.statusText,
|
|
73
|
+
code: errorPayload?.code
|
|
74
|
+
};
|
|
75
|
+
};
|
|
76
|
+
async function requestResponse(config, options) {
|
|
77
|
+
const url = buildUrl(config.baseUrl, options.path, options.query);
|
|
78
|
+
const mergedHeaders = new Headers(config.headers ?? {});
|
|
79
|
+
if (options.headers) {
|
|
80
|
+
const extra = new Headers(options.headers);
|
|
81
|
+
for (const [key, value] of extra.entries()) mergedHeaders.set(key, value);
|
|
82
|
+
}
|
|
83
|
+
let payload;
|
|
84
|
+
if (options.json && options.body !== void 0) {
|
|
85
|
+
mergedHeaders.set("content-type", "application/json");
|
|
86
|
+
payload = JSON.stringify(options.body);
|
|
87
|
+
} else payload = options.body;
|
|
88
|
+
const init = {
|
|
89
|
+
method: options.method ?? "GET",
|
|
90
|
+
headers: mergedHeaders,
|
|
91
|
+
body: payload
|
|
92
|
+
};
|
|
93
|
+
if (payload && typeof payload === "object" && (payload instanceof Readable || "getReader" in payload)) init.duplex = "half";
|
|
94
|
+
const response = options.retry !== false ? await fetchWithRetry(url, init, config) : await fetchWithTimeout(url, init, config.timeoutMs);
|
|
95
|
+
if (!response.ok) {
|
|
96
|
+
const { message, code } = await readErrorPayload(response);
|
|
97
|
+
const error = /* @__PURE__ */ new Error(`Request failed: ${response.status} ${message}${code ? ` (${code})` : ""}`);
|
|
98
|
+
if (code) error.code = code;
|
|
99
|
+
throw error;
|
|
100
|
+
}
|
|
101
|
+
return response;
|
|
102
|
+
}
|
|
103
|
+
async function requestJson(config, options) {
|
|
104
|
+
const text = await (await requestResponse(config, {
|
|
105
|
+
...options,
|
|
106
|
+
json: true
|
|
107
|
+
})).text();
|
|
108
|
+
const parsed = text ? safeJsonParse(text) : void 0;
|
|
109
|
+
if (parsed === void 0) return;
|
|
110
|
+
if (parsed === null && text.trim() !== "null") throw new Error(`Expected JSON response from ${options.path}`);
|
|
111
|
+
return parsed;
|
|
112
|
+
}
|
|
113
|
+
const createClient = (config) => ({
|
|
114
|
+
request: (options) => requestResponse(config, options),
|
|
115
|
+
requestJson: (options) => requestJson(config, options)
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
//#endregion
|
|
119
|
+
export { createClient };
|
|
120
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","names":[],"sources":["../../../src/cli/utils/client.ts"],"sourcesContent":["import { Readable } from \"node:stream\";\n\nexport type ClientConfig = {\n baseUrl: string;\n headers?: HeadersInit;\n timeoutMs: number;\n retries: number;\n retryDelayMs: number;\n};\n\ntype RequestOptions = {\n path: string;\n method?: string;\n query?: Record<string, string | number | boolean | undefined>;\n body?: BodyInit | null | unknown;\n json?: boolean;\n headers?: HeadersInit;\n retry?: boolean;\n};\n\ntype JsonRequestOptions = Omit<RequestOptions, \"body\" | \"json\"> & { body?: unknown };\n\nconst delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));\n\nconst buildUrl = (\n baseUrl: string,\n path: string,\n query?: Record<string, string | number | boolean | undefined>,\n) => {\n const base = new URL(baseUrl);\n const basePath = base.pathname.endsWith(\"/\") ? base.pathname.slice(0, -1) : base.pathname;\n const pathPart = path.startsWith(\"/\") ? path : `/${path}`;\n base.pathname = `${basePath}${pathPart}`;\n\n if (query) {\n for (const [key, value] of Object.entries(query)) {\n if (value === undefined) {\n continue;\n }\n base.searchParams.set(key, String(value));\n }\n }\n\n return base.toString();\n};\n\nconst shouldRetry = (response: Response) => {\n if (response.status >= 500) {\n return true;\n }\n return response.status === 429;\n};\n\nasync function fetchWithRetry(\n url: string,\n init: RequestInit,\n config: Pick<ClientConfig, \"timeoutMs\" | \"retries\" | \"retryDelayMs\">,\n) {\n let attempt = 0;\n let lastError: unknown;\n\n while (attempt <= config.retries) {\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), config.timeoutMs);\n\n try {\n const response = await fetch(url, { ...init, signal: controller.signal });\n clearTimeout(timeoutId);\n\n if (!response.ok && shouldRetry(response) && attempt < config.retries) {\n attempt += 1;\n await delay(config.retryDelayMs);\n continue;\n }\n\n return response;\n } catch (error) {\n clearTimeout(timeoutId);\n lastError = error;\n if (attempt >= config.retries) {\n throw error;\n }\n attempt += 1;\n await delay(config.retryDelayMs);\n }\n }\n\n throw lastError ?? new Error(\"Request failed\");\n}\n\nasync function fetchWithTimeout(\n url: string,\n init: RequestInit,\n timeoutMs: number,\n): Promise<Response> {\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), timeoutMs);\n try {\n return await fetch(url, { ...init, signal: controller.signal });\n } finally {\n clearTimeout(timeoutId);\n }\n}\n\nconst safeJsonParse = (text: string) => {\n try {\n return JSON.parse(text) as unknown;\n } catch {\n return null;\n }\n};\n\nconst readErrorPayload = async (response: Response) => {\n const text = await response.text();\n const parsed = text ? safeJsonParse(text) : undefined;\n const errorPayload = parsed as { message?: string; code?: string } | undefined;\n return {\n text,\n parsed,\n message: errorPayload?.message ?? text ?? response.statusText,\n code: errorPayload?.code,\n };\n};\n\nasync function requestResponse(config: ClientConfig, options: RequestOptions): Promise<Response> {\n const url = buildUrl(config.baseUrl, options.path, options.query);\n const mergedHeaders = new Headers(config.headers ?? {});\n\n if (options.headers) {\n const extra = new Headers(options.headers);\n for (const [key, value] of extra.entries()) {\n mergedHeaders.set(key, value);\n }\n }\n\n let payload: BodyInit | undefined;\n\n if (options.json && options.body !== undefined) {\n mergedHeaders.set(\"content-type\", \"application/json\");\n payload = JSON.stringify(options.body);\n } else {\n payload = options.body as BodyInit | undefined;\n }\n\n const init: RequestInit & { duplex?: \"half\" } = {\n method: options.method ?? \"GET\",\n headers: mergedHeaders,\n body: payload,\n };\n\n if (\n payload &&\n typeof payload === \"object\" &&\n (payload instanceof Readable || \"getReader\" in (payload as object))\n ) {\n init.duplex = \"half\";\n }\n\n const shouldRetryRequest = options.retry !== false;\n const response = shouldRetryRequest\n ? await fetchWithRetry(url, init, config)\n : await fetchWithTimeout(url, init, config.timeoutMs);\n\n if (!response.ok) {\n const { message, code } = await readErrorPayload(response);\n const error = new Error(\n `Request failed: ${response.status} ${message}${code ? ` (${code})` : \"\"}`,\n ) as Error & { code?: string };\n if (code) {\n error.code = code;\n }\n throw error;\n }\n\n return response;\n}\n\nasync function requestJson<T>(config: ClientConfig, options: JsonRequestOptions): Promise<T> {\n const response = await requestResponse(config, { ...options, json: true });\n const text = await response.text();\n const parsed = text ? safeJsonParse(text) : undefined;\n\n if (parsed === undefined) {\n return undefined as T;\n }\n\n if (parsed === null && text.trim() !== \"null\") {\n throw new Error(`Expected JSON response from ${options.path}`);\n }\n\n return parsed as T;\n}\n\nexport type HttpClient = {\n request: (options: RequestOptions) => Promise<Response>;\n requestJson: <T>(options: JsonRequestOptions) => Promise<T>;\n};\n\nexport const createClient = (config: ClientConfig): HttpClient => ({\n request: (options) => requestResponse(config, options),\n requestJson: (options) => requestJson(config, options),\n});\n"],"mappings":"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"}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
//#region src/cli/utils/config.ts
|
|
4
|
+
const resolveString = (ctx, options) => {
|
|
5
|
+
const value = ctx.values[options.key];
|
|
6
|
+
if (value && value.trim().length > 0) return value;
|
|
7
|
+
const envKeys = Array.isArray(options.env) ? options.env : [options.env];
|
|
8
|
+
for (const envKey of envKeys) {
|
|
9
|
+
const envValue = process.env[envKey];
|
|
10
|
+
if (envValue && envValue.trim().length > 0) return envValue;
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
const resolveRequiredString = (ctx, options) => {
|
|
14
|
+
const value = resolveString(ctx, options);
|
|
15
|
+
if (!value) {
|
|
16
|
+
const envLabel = Array.isArray(options.env) ? options.env.join(" or ") : options.env;
|
|
17
|
+
throw new Error(`Missing ${options.label}. Provide --${options.key} or set ${envLabel}.`);
|
|
18
|
+
}
|
|
19
|
+
return value;
|
|
20
|
+
};
|
|
21
|
+
const parseNumber = (label, value) => {
|
|
22
|
+
if (value === void 0) return;
|
|
23
|
+
const numeric = typeof value === "number" ? value : Number(value);
|
|
24
|
+
if (!Number.isFinite(numeric)) throw new Error(`${label} must be a number`);
|
|
25
|
+
return numeric;
|
|
26
|
+
};
|
|
27
|
+
const parseBoolean = (label, value) => {
|
|
28
|
+
if (value === void 0) return;
|
|
29
|
+
if (typeof value === "boolean") return value;
|
|
30
|
+
const normalized = value.trim().toLowerCase();
|
|
31
|
+
if ([
|
|
32
|
+
"true",
|
|
33
|
+
"1",
|
|
34
|
+
"yes",
|
|
35
|
+
"y",
|
|
36
|
+
"on"
|
|
37
|
+
].includes(normalized)) return true;
|
|
38
|
+
if ([
|
|
39
|
+
"false",
|
|
40
|
+
"0",
|
|
41
|
+
"no",
|
|
42
|
+
"n",
|
|
43
|
+
"off"
|
|
44
|
+
].includes(normalized)) return false;
|
|
45
|
+
throw new Error(`${label} must be a boolean`);
|
|
46
|
+
};
|
|
47
|
+
const normalizePem = (value) => {
|
|
48
|
+
return value.trim().replace(/\r\n/g, "\n").replace(/\\n/g, "\n");
|
|
49
|
+
};
|
|
50
|
+
const resolveWebhookSecret = (ctx) => resolveRequiredString(ctx, {
|
|
51
|
+
key: "webhook-secret",
|
|
52
|
+
env: ["GITHUB_APP_WEBHOOK_SECRET", "FRAGNO_GITHUB_APP_WEBHOOK_SECRET"],
|
|
53
|
+
label: "webhook secret"
|
|
54
|
+
});
|
|
55
|
+
const resolveGitHubAppConfig = (ctx) => {
|
|
56
|
+
const appId = resolveRequiredString(ctx, {
|
|
57
|
+
key: "app-id",
|
|
58
|
+
env: "GITHUB_APP_ID",
|
|
59
|
+
label: "GitHub App ID"
|
|
60
|
+
});
|
|
61
|
+
const appSlug = resolveRequiredString(ctx, {
|
|
62
|
+
key: "app-slug",
|
|
63
|
+
env: "GITHUB_APP_SLUG",
|
|
64
|
+
label: "GitHub App slug"
|
|
65
|
+
});
|
|
66
|
+
const privateKey = resolveString(ctx, {
|
|
67
|
+
key: "private-key",
|
|
68
|
+
env: "GITHUB_APP_PRIVATE_KEY"
|
|
69
|
+
});
|
|
70
|
+
const privateKeyFile = resolveString(ctx, {
|
|
71
|
+
key: "private-key-file",
|
|
72
|
+
env: "GITHUB_APP_PRIVATE_KEY_FILE"
|
|
73
|
+
});
|
|
74
|
+
if (privateKey && privateKeyFile) throw new Error("Provide either --private-key or --private-key-file, not both.");
|
|
75
|
+
let privateKeyPem = privateKey ?? void 0;
|
|
76
|
+
if (!privateKeyPem && privateKeyFile) privateKeyPem = readFileSync(privateKeyFile, "utf-8");
|
|
77
|
+
if (!privateKeyPem) throw new Error("Missing GitHub App private key. Provide --private-key, --private-key-file, or GITHUB_APP_PRIVATE_KEY.");
|
|
78
|
+
const webhookSecret = resolveWebhookSecret(ctx);
|
|
79
|
+
const apiBaseUrl = resolveString(ctx, {
|
|
80
|
+
key: "api-base-url",
|
|
81
|
+
env: "GITHUB_APP_API_BASE_URL"
|
|
82
|
+
});
|
|
83
|
+
const apiVersion = resolveString(ctx, {
|
|
84
|
+
key: "api-version",
|
|
85
|
+
env: "GITHUB_APP_API_VERSION"
|
|
86
|
+
});
|
|
87
|
+
const webBaseUrl = resolveString(ctx, {
|
|
88
|
+
key: "web-base-url",
|
|
89
|
+
env: "GITHUB_APP_WEB_BASE_URL"
|
|
90
|
+
});
|
|
91
|
+
const defaultLinkKey = resolveString(ctx, {
|
|
92
|
+
key: "default-link-key",
|
|
93
|
+
env: "GITHUB_APP_DEFAULT_LINK_KEY"
|
|
94
|
+
});
|
|
95
|
+
const tokenCacheTtlSeconds = parseNumber("token-cache-ttl", ctx.values["token-cache-ttl"] ?? process.env["GITHUB_APP_TOKEN_CACHE_TTL_SECONDS"]);
|
|
96
|
+
const webhookDebug = parseBoolean("webhook-debug", ctx.values["webhook-debug"] ?? process.env["GITHUB_APP_WEBHOOK_DEBUG"] ?? process.env["FRAGNO_GITHUB_APP_WEBHOOK_DEBUG"]);
|
|
97
|
+
return {
|
|
98
|
+
appId,
|
|
99
|
+
appSlug,
|
|
100
|
+
privateKeyPem: normalizePem(privateKeyPem),
|
|
101
|
+
webhookSecret,
|
|
102
|
+
webhookDebug,
|
|
103
|
+
apiBaseUrl: apiBaseUrl || void 0,
|
|
104
|
+
apiVersion: apiVersion || void 0,
|
|
105
|
+
webBaseUrl: webBaseUrl || void 0,
|
|
106
|
+
defaultLinkKey: defaultLinkKey || void 0,
|
|
107
|
+
tokenCacheTtlSeconds: tokenCacheTtlSeconds || void 0
|
|
108
|
+
};
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
//#endregion
|
|
112
|
+
export { resolveGitHubAppConfig, resolveWebhookSecret };
|
|
113
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","names":[],"sources":["../../../src/cli/utils/config.ts"],"sourcesContent":["import { readFileSync } from \"node:fs\";\n\nimport type { GitHubAppFragmentConfig } from \"../../github/types.js\";\n\ntype CommandContext = { values: Record<string, unknown> };\n\ntype ResolveStringOptions = {\n key: string;\n env: string | string[];\n};\n\nconst resolveString = (ctx: CommandContext, options: ResolveStringOptions) => {\n const value = ctx.values[options.key] as string | undefined;\n if (value && value.trim().length > 0) {\n return value;\n }\n const envKeys = Array.isArray(options.env) ? options.env : [options.env];\n for (const envKey of envKeys) {\n const envValue = process.env[envKey];\n if (envValue && envValue.trim().length > 0) {\n return envValue;\n }\n }\n return undefined;\n};\n\nconst resolveRequiredString = (\n ctx: CommandContext,\n options: ResolveStringOptions & { label: string },\n) => {\n const value = resolveString(ctx, options);\n if (!value) {\n const envLabel = Array.isArray(options.env) ? options.env.join(\" or \") : options.env;\n throw new Error(`Missing ${options.label}. Provide --${options.key} or set ${envLabel}.`);\n }\n return value;\n};\n\nconst parseNumber = (label: string, value: string | number | undefined) => {\n if (value === undefined) {\n return undefined;\n }\n const numeric = typeof value === \"number\" ? value : Number(value);\n if (!Number.isFinite(numeric)) {\n throw new Error(`${label} must be a number`);\n }\n return numeric;\n};\n\nconst parseBoolean = (label: string, value: string | boolean | undefined) => {\n if (value === undefined) {\n return undefined;\n }\n if (typeof value === \"boolean\") {\n return value;\n }\n const normalized = value.trim().toLowerCase();\n if ([\"true\", \"1\", \"yes\", \"y\", \"on\"].includes(normalized)) {\n return true;\n }\n if ([\"false\", \"0\", \"no\", \"n\", \"off\"].includes(normalized)) {\n return false;\n }\n throw new Error(`${label} must be a boolean`);\n};\n\nconst normalizePem = (value: string) => {\n return value.trim().replace(/\\r\\n/g, \"\\n\").replace(/\\\\n/g, \"\\n\");\n};\n\nexport const resolveWebhookSecret = (ctx: CommandContext) =>\n resolveRequiredString(ctx, {\n key: \"webhook-secret\",\n env: [\"GITHUB_APP_WEBHOOK_SECRET\", \"FRAGNO_GITHUB_APP_WEBHOOK_SECRET\"],\n label: \"webhook secret\",\n });\n\nexport const resolveGitHubAppConfig = (ctx: CommandContext): GitHubAppFragmentConfig => {\n const appId = resolveRequiredString(ctx, {\n key: \"app-id\",\n env: \"GITHUB_APP_ID\",\n label: \"GitHub App ID\",\n });\n const appSlug = resolveRequiredString(ctx, {\n key: \"app-slug\",\n env: \"GITHUB_APP_SLUG\",\n label: \"GitHub App slug\",\n });\n\n const privateKey = resolveString(ctx, { key: \"private-key\", env: \"GITHUB_APP_PRIVATE_KEY\" });\n const privateKeyFile = resolveString(ctx, {\n key: \"private-key-file\",\n env: \"GITHUB_APP_PRIVATE_KEY_FILE\",\n });\n\n if (privateKey && privateKeyFile) {\n throw new Error(\"Provide either --private-key or --private-key-file, not both.\");\n }\n\n let privateKeyPem = privateKey ?? undefined;\n if (!privateKeyPem && privateKeyFile) {\n privateKeyPem = readFileSync(privateKeyFile, \"utf-8\");\n }\n\n if (!privateKeyPem) {\n throw new Error(\n \"Missing GitHub App private key. Provide --private-key, --private-key-file, or GITHUB_APP_PRIVATE_KEY.\",\n );\n }\n\n const webhookSecret = resolveWebhookSecret(ctx);\n\n const apiBaseUrl = resolveString(ctx, { key: \"api-base-url\", env: \"GITHUB_APP_API_BASE_URL\" });\n const apiVersion = resolveString(ctx, { key: \"api-version\", env: \"GITHUB_APP_API_VERSION\" });\n const webBaseUrl = resolveString(ctx, { key: \"web-base-url\", env: \"GITHUB_APP_WEB_BASE_URL\" });\n const defaultLinkKey = resolveString(ctx, {\n key: \"default-link-key\",\n env: \"GITHUB_APP_DEFAULT_LINK_KEY\",\n });\n\n const tokenCacheRaw =\n (ctx.values[\"token-cache-ttl\"] as number | string | undefined) ??\n process.env[\"GITHUB_APP_TOKEN_CACHE_TTL_SECONDS\"];\n const tokenCacheTtlSeconds = parseNumber(\"token-cache-ttl\", tokenCacheRaw);\n\n const webhookDebugRaw =\n (ctx.values[\"webhook-debug\"] as boolean | string | undefined) ??\n process.env[\"GITHUB_APP_WEBHOOK_DEBUG\"] ??\n process.env[\"FRAGNO_GITHUB_APP_WEBHOOK_DEBUG\"];\n const webhookDebug = parseBoolean(\"webhook-debug\", webhookDebugRaw);\n\n return {\n appId,\n appSlug,\n privateKeyPem: normalizePem(privateKeyPem),\n webhookSecret,\n webhookDebug,\n apiBaseUrl: apiBaseUrl || undefined,\n apiVersion: apiVersion || undefined,\n webBaseUrl: webBaseUrl || undefined,\n defaultLinkKey: defaultLinkKey || undefined,\n tokenCacheTtlSeconds: tokenCacheTtlSeconds || undefined,\n };\n};\n"],"mappings":"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"}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { createClient } from "./client.js";
|
|
2
|
+
|
|
3
|
+
//#region src/cli/utils/options.ts
|
|
4
|
+
const baseArgs = {
|
|
5
|
+
"base-url": {
|
|
6
|
+
type: "string",
|
|
7
|
+
short: "b",
|
|
8
|
+
description: "GitHub app fragment base URL (env: FRAGNO_GITHUB_APP_BASE_URL)"
|
|
9
|
+
},
|
|
10
|
+
header: {
|
|
11
|
+
type: "string",
|
|
12
|
+
short: "H",
|
|
13
|
+
description: "Extra HTTP header (repeatable), format: 'Name: value' (env: FRAGNO_GITHUB_APP_HEADERS)",
|
|
14
|
+
multiple: true
|
|
15
|
+
},
|
|
16
|
+
timeout: {
|
|
17
|
+
type: "number",
|
|
18
|
+
description: "Request timeout in ms (env: FRAGNO_GITHUB_APP_TIMEOUT_MS, default: 15000)"
|
|
19
|
+
},
|
|
20
|
+
retries: {
|
|
21
|
+
type: "number",
|
|
22
|
+
description: "Retry count for network/5xx/429 (env: FRAGNO_GITHUB_APP_RETRIES, default: 2)"
|
|
23
|
+
},
|
|
24
|
+
"retry-delay": {
|
|
25
|
+
type: "number",
|
|
26
|
+
description: "Delay between retries in ms (env: FRAGNO_GITHUB_APP_RETRY_DELAY_MS, default: 500)"
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
const DEFAULT_TIMEOUT_MS = 15e3;
|
|
30
|
+
const DEFAULT_RETRIES = 2;
|
|
31
|
+
const DEFAULT_RETRY_DELAY_MS = 500;
|
|
32
|
+
const parseNumberEnv = (value) => {
|
|
33
|
+
if (!value) return;
|
|
34
|
+
const parsed = Number(value);
|
|
35
|
+
if (Number.isNaN(parsed)) return;
|
|
36
|
+
return parsed;
|
|
37
|
+
};
|
|
38
|
+
const normalizeHeaderValues = (value) => {
|
|
39
|
+
if (!value) return [];
|
|
40
|
+
if (Array.isArray(value)) return value.map(String);
|
|
41
|
+
return [String(value)];
|
|
42
|
+
};
|
|
43
|
+
const parseEnvHeaders = () => {
|
|
44
|
+
const envHeaders = process.env["FRAGNO_GITHUB_APP_HEADERS"];
|
|
45
|
+
if (!envHeaders) return [];
|
|
46
|
+
return envHeaders.split(/\n|;/).map((entry) => entry.trim()).filter(Boolean);
|
|
47
|
+
};
|
|
48
|
+
const buildHeaders = (values) => {
|
|
49
|
+
const headers = new Headers();
|
|
50
|
+
for (const value of values) {
|
|
51
|
+
const index = value.indexOf(":");
|
|
52
|
+
if (index === -1) throw new Error(`Invalid header: ${value}`);
|
|
53
|
+
const name = value.slice(0, index).trim();
|
|
54
|
+
const headerValue = value.slice(index + 1).trim();
|
|
55
|
+
if (!name || !headerValue) throw new Error(`Invalid header: ${value}`);
|
|
56
|
+
headers.append(name, headerValue);
|
|
57
|
+
}
|
|
58
|
+
return headers;
|
|
59
|
+
};
|
|
60
|
+
const resolveTimeout = (ctx) => ctx.values["timeout"] ?? parseNumberEnv(process.env["FRAGNO_GITHUB_APP_TIMEOUT_MS"]) ?? DEFAULT_TIMEOUT_MS;
|
|
61
|
+
const resolveRetries = (ctx) => ctx.values["retries"] ?? parseNumberEnv(process.env["FRAGNO_GITHUB_APP_RETRIES"]) ?? DEFAULT_RETRIES;
|
|
62
|
+
const resolveRetryDelay = (ctx) => ctx.values["retry-delay"] ?? parseNumberEnv(process.env["FRAGNO_GITHUB_APP_RETRY_DELAY_MS"]) ?? DEFAULT_RETRY_DELAY_MS;
|
|
63
|
+
const resolveBaseUrl = (ctx) => {
|
|
64
|
+
const baseUrl = ctx.values["base-url"] ?? process.env["FRAGNO_GITHUB_APP_BASE_URL"];
|
|
65
|
+
if (!baseUrl) throw new Error("Missing base URL. Provide --base-url or set FRAGNO_GITHUB_APP_BASE_URL.");
|
|
66
|
+
return baseUrl.replace(/\/$/, "");
|
|
67
|
+
};
|
|
68
|
+
const createClientFromContext = (ctx) => {
|
|
69
|
+
const headerValues = normalizeHeaderValues(ctx.values["header"]).concat(parseEnvHeaders());
|
|
70
|
+
return createClient({
|
|
71
|
+
baseUrl: resolveBaseUrl(ctx),
|
|
72
|
+
headers: buildHeaders(headerValues),
|
|
73
|
+
timeoutMs: resolveTimeout(ctx),
|
|
74
|
+
retries: resolveRetries(ctx),
|
|
75
|
+
retryDelayMs: resolveRetryDelay(ctx)
|
|
76
|
+
});
|
|
77
|
+
};
|
|
78
|
+
const parseJsonValue = (label, value) => {
|
|
79
|
+
if (value === void 0) return;
|
|
80
|
+
try {
|
|
81
|
+
return JSON.parse(value);
|
|
82
|
+
} catch (error) {
|
|
83
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
84
|
+
throw new Error(`Invalid ${label} JSON: ${message}`);
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
//#endregion
|
|
89
|
+
export { baseArgs, createClientFromContext, parseJsonValue };
|
|
90
|
+
//# sourceMappingURL=options.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"options.js","names":[],"sources":["../../../src/cli/utils/options.ts"],"sourcesContent":["import { createClient, type ClientConfig } from \"./client.js\";\n\ntype CommandContext = { values: Record<string, unknown> };\n\nexport const baseArgs = {\n \"base-url\": {\n type: \"string\",\n short: \"b\",\n description: \"GitHub app fragment base URL (env: FRAGNO_GITHUB_APP_BASE_URL)\",\n },\n header: {\n type: \"string\",\n short: \"H\",\n description:\n \"Extra HTTP header (repeatable), format: 'Name: value' (env: FRAGNO_GITHUB_APP_HEADERS)\",\n multiple: true,\n },\n timeout: {\n type: \"number\",\n description: \"Request timeout in ms (env: FRAGNO_GITHUB_APP_TIMEOUT_MS, default: 15000)\",\n },\n retries: {\n type: \"number\",\n description: \"Retry count for network/5xx/429 (env: FRAGNO_GITHUB_APP_RETRIES, default: 2)\",\n },\n \"retry-delay\": {\n type: \"number\",\n description:\n \"Delay between retries in ms (env: FRAGNO_GITHUB_APP_RETRY_DELAY_MS, default: 500)\",\n },\n} as const;\n\nconst DEFAULT_TIMEOUT_MS = 15000;\nconst DEFAULT_RETRIES = 2;\nconst DEFAULT_RETRY_DELAY_MS = 500;\n\nconst parseNumberEnv = (value: string | undefined) => {\n if (!value) {\n return undefined;\n }\n const parsed = Number(value);\n if (Number.isNaN(parsed)) {\n return undefined;\n }\n return parsed;\n};\n\nconst normalizeHeaderValues = (value: unknown): string[] => {\n if (!value) {\n return [];\n }\n if (Array.isArray(value)) {\n return value.map(String);\n }\n return [String(value)];\n};\n\nconst parseEnvHeaders = () => {\n const envHeaders = process.env[\"FRAGNO_GITHUB_APP_HEADERS\"];\n if (!envHeaders) {\n return [];\n }\n return envHeaders\n .split(/\\n|;/)\n .map((entry) => entry.trim())\n .filter(Boolean);\n};\n\nconst buildHeaders = (values: string[]) => {\n const headers = new Headers();\n for (const value of values) {\n const index = value.indexOf(\":\");\n if (index === -1) {\n throw new Error(`Invalid header: ${value}`);\n }\n const name = value.slice(0, index).trim();\n const headerValue = value.slice(index + 1).trim();\n if (!name || !headerValue) {\n throw new Error(`Invalid header: ${value}`);\n }\n headers.append(name, headerValue);\n }\n return headers;\n};\n\nconst resolveTimeout = (ctx: CommandContext) =>\n (ctx.values[\"timeout\"] as number | undefined) ??\n parseNumberEnv(process.env[\"FRAGNO_GITHUB_APP_TIMEOUT_MS\"]) ??\n DEFAULT_TIMEOUT_MS;\n\nconst resolveRetries = (ctx: CommandContext) =>\n (ctx.values[\"retries\"] as number | undefined) ??\n parseNumberEnv(process.env[\"FRAGNO_GITHUB_APP_RETRIES\"]) ??\n DEFAULT_RETRIES;\n\nconst resolveRetryDelay = (ctx: CommandContext) =>\n (ctx.values[\"retry-delay\"] as number | undefined) ??\n parseNumberEnv(process.env[\"FRAGNO_GITHUB_APP_RETRY_DELAY_MS\"]) ??\n DEFAULT_RETRY_DELAY_MS;\n\nexport const resolveBaseUrl = (ctx: CommandContext) => {\n const baseUrl =\n (ctx.values[\"base-url\"] as string | undefined) ?? process.env[\"FRAGNO_GITHUB_APP_BASE_URL\"];\n if (!baseUrl) {\n throw new Error(\"Missing base URL. Provide --base-url or set FRAGNO_GITHUB_APP_BASE_URL.\");\n }\n return baseUrl.replace(/\\/$/, \"\");\n};\n\nexport const createClientFromContext = (ctx: CommandContext) => {\n const headerValues = normalizeHeaderValues(ctx.values[\"header\"]).concat(parseEnvHeaders());\n\n const clientConfig: ClientConfig = {\n baseUrl: resolveBaseUrl(ctx),\n headers: buildHeaders(headerValues),\n timeoutMs: resolveTimeout(ctx),\n retries: resolveRetries(ctx),\n retryDelayMs: resolveRetryDelay(ctx),\n };\n\n return createClient(clientConfig);\n};\n\nexport const parseJsonValue = (label: string, value: string | undefined) => {\n if (value === undefined) {\n return undefined;\n }\n try {\n return JSON.parse(value) as unknown;\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n throw new Error(`Invalid ${label} JSON: ${message}`);\n }\n};\n"],"mappings":";;;AAIA,MAAa,WAAW;CACtB,YAAY;EACV,MAAM;EACN,OAAO;EACP,aAAa;EACd;CACD,QAAQ;EACN,MAAM;EACN,OAAO;EACP,aACE;EACF,UAAU;EACX;CACD,SAAS;EACP,MAAM;EACN,aAAa;EACd;CACD,SAAS;EACP,MAAM;EACN,aAAa;EACd;CACD,eAAe;EACb,MAAM;EACN,aACE;EACH;CACF;AAED,MAAM,qBAAqB;AAC3B,MAAM,kBAAkB;AACxB,MAAM,yBAAyB;AAE/B,MAAM,kBAAkB,UAA8B;AACpD,KAAI,CAAC,MACH;CAEF,MAAM,SAAS,OAAO,MAAM;AAC5B,KAAI,OAAO,MAAM,OAAO,CACtB;AAEF,QAAO;;AAGT,MAAM,yBAAyB,UAA6B;AAC1D,KAAI,CAAC,MACH,QAAO,EAAE;AAEX,KAAI,MAAM,QAAQ,MAAM,CACtB,QAAO,MAAM,IAAI,OAAO;AAE1B,QAAO,CAAC,OAAO,MAAM,CAAC;;AAGxB,MAAM,wBAAwB;CAC5B,MAAM,aAAa,QAAQ,IAAI;AAC/B,KAAI,CAAC,WACH,QAAO,EAAE;AAEX,QAAO,WACJ,MAAM,OAAO,CACb,KAAK,UAAU,MAAM,MAAM,CAAC,CAC5B,OAAO,QAAQ;;AAGpB,MAAM,gBAAgB,WAAqB;CACzC,MAAM,UAAU,IAAI,SAAS;AAC7B,MAAK,MAAM,SAAS,QAAQ;EAC1B,MAAM,QAAQ,MAAM,QAAQ,IAAI;AAChC,MAAI,UAAU,GACZ,OAAM,IAAI,MAAM,mBAAmB,QAAQ;EAE7C,MAAM,OAAO,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM;EACzC,MAAM,cAAc,MAAM,MAAM,QAAQ,EAAE,CAAC,MAAM;AACjD,MAAI,CAAC,QAAQ,CAAC,YACZ,OAAM,IAAI,MAAM,mBAAmB,QAAQ;AAE7C,UAAQ,OAAO,MAAM,YAAY;;AAEnC,QAAO;;AAGT,MAAM,kBAAkB,QACrB,IAAI,OAAO,cACZ,eAAe,QAAQ,IAAI,gCAAgC,IAC3D;AAEF,MAAM,kBAAkB,QACrB,IAAI,OAAO,cACZ,eAAe,QAAQ,IAAI,6BAA6B,IACxD;AAEF,MAAM,qBAAqB,QACxB,IAAI,OAAO,kBACZ,eAAe,QAAQ,IAAI,oCAAoC,IAC/D;AAEF,MAAa,kBAAkB,QAAwB;CACrD,MAAM,UACH,IAAI,OAAO,eAAsC,QAAQ,IAAI;AAChE,KAAI,CAAC,QACH,OAAM,IAAI,MAAM,0EAA0E;AAE5F,QAAO,QAAQ,QAAQ,OAAO,GAAG;;AAGnC,MAAa,2BAA2B,QAAwB;CAC9D,MAAM,eAAe,sBAAsB,IAAI,OAAO,UAAU,CAAC,OAAO,iBAAiB,CAAC;AAU1F,QAAO,aAR4B;EACjC,SAAS,eAAe,IAAI;EAC5B,SAAS,aAAa,aAAa;EACnC,WAAW,eAAe,IAAI;EAC9B,SAAS,eAAe,IAAI;EAC5B,cAAc,kBAAkB,IAAI;EACrC,CAEgC;;AAGnC,MAAa,kBAAkB,OAAe,UAA8B;AAC1E,KAAI,UAAU,OACZ;AAEF,KAAI;AACF,SAAO,KAAK,MAAM,MAAM;UACjB,OAAO;EACd,MAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,MAAM;AACtE,QAAM,IAAI,MAAM,WAAW,MAAM,SAAS,UAAU"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
//#region src/cli/utils/output.ts
|
|
2
|
+
const printResult = (value) => {
|
|
3
|
+
if (value === void 0) {
|
|
4
|
+
console.log("OK");
|
|
5
|
+
return;
|
|
6
|
+
}
|
|
7
|
+
console.log(JSON.stringify(value, null, 2));
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
//#endregion
|
|
11
|
+
export { printResult };
|
|
12
|
+
//# sourceMappingURL=output.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"output.js","names":[],"sources":["../../../src/cli/utils/output.ts"],"sourcesContent":["export const printResult = (value: unknown) => {\n if (value === undefined) {\n console.log(\"OK\");\n return;\n }\n console.log(JSON.stringify(value, null, 2));\n};\n"],"mappings":";AAAA,MAAa,eAAe,UAAmB;AAC7C,KAAI,UAAU,QAAW;AACvB,UAAQ,IAAI,KAAK;AACjB;;AAEF,SAAQ,IAAI,KAAK,UAAU,OAAO,MAAM,EAAE,CAAC"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { GitHubAppFragmentConfig } from "./types.js";
|
|
2
|
+
import { App } from "octokit";
|
|
3
|
+
|
|
4
|
+
//#region src/github/api.d.ts
|
|
5
|
+
type GitHubApiClientOptions = {
|
|
6
|
+
fetch?: typeof fetch;
|
|
7
|
+
};
|
|
8
|
+
type OctokitAppInstance = InstanceType<typeof App>;
|
|
9
|
+
type GitHubOctokit = OctokitAppInstance["octokit"];
|
|
10
|
+
type GitHubAppInstance = {
|
|
11
|
+
octokit: GitHubOctokit;
|
|
12
|
+
webhooks: OctokitAppInstance["webhooks"];
|
|
13
|
+
getInstallationOctokit: (installationId: number) => Promise<GitHubOctokit>;
|
|
14
|
+
};
|
|
15
|
+
type GitHubInstallationRepository = {
|
|
16
|
+
id: number;
|
|
17
|
+
name: string;
|
|
18
|
+
full_name: string;
|
|
19
|
+
private: boolean;
|
|
20
|
+
fork?: boolean;
|
|
21
|
+
default_branch?: string | null;
|
|
22
|
+
owner: {
|
|
23
|
+
login: string;
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
type GitHubInstallationDetails = {
|
|
27
|
+
id: string;
|
|
28
|
+
accountId: string;
|
|
29
|
+
accountLogin: string;
|
|
30
|
+
accountType: string;
|
|
31
|
+
status: "active" | "suspended";
|
|
32
|
+
permissions: Record<string, unknown>;
|
|
33
|
+
events: unknown[];
|
|
34
|
+
};
|
|
35
|
+
type GitHubInstallationRepositoriesResponse = {
|
|
36
|
+
repositories: GitHubInstallationRepository[];
|
|
37
|
+
};
|
|
38
|
+
type GitHubApiClient = {
|
|
39
|
+
app: GitHubAppInstance;
|
|
40
|
+
apiVersion: string;
|
|
41
|
+
resolveInstallationId: (installationId: string) => number;
|
|
42
|
+
getInstallation: (installationId: string) => Promise<GitHubInstallationDetails>;
|
|
43
|
+
listInstallationRepos: (installationId: string) => Promise<GitHubInstallationRepositoriesResponse>;
|
|
44
|
+
verifyWebhookSignature: (options: {
|
|
45
|
+
payload: string;
|
|
46
|
+
signatureHeader: string | null;
|
|
47
|
+
}) => Promise<boolean>;
|
|
48
|
+
};
|
|
49
|
+
declare const createGitHubApiClient: (config: GitHubAppFragmentConfig, options?: GitHubApiClientOptions) => GitHubApiClient;
|
|
50
|
+
//#endregion
|
|
51
|
+
export { GitHubApiClient, GitHubAppInstance, createGitHubApiClient };
|
|
52
|
+
//# sourceMappingURL=api.d.ts.map
|