@shipfox/api-integration-gitea 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +2 -0
- package/.turbo/turbo-type$colon$emit.log +1 -0
- package/.turbo/turbo-type.log +1 -0
- package/CHANGELOG.md +96 -0
- package/LICENSE +21 -0
- package/README.md +61 -0
- package/dist/api/client.d.ts +58 -0
- package/dist/api/client.d.ts.map +1 -0
- package/dist/api/client.js +225 -0
- package/dist/api/client.js.map +1 -0
- package/dist/config.d.ts +11 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +38 -0
- package/dist/config.js.map +1 -0
- package/dist/core/connect.d.ts +18 -0
- package/dist/core/connect.d.ts.map +1 -0
- package/dist/core/connect.js +32 -0
- package/dist/core/connect.js.map +1 -0
- package/dist/core/connection-url.d.ts +2 -0
- package/dist/core/connection-url.d.ts.map +1 -0
- package/dist/core/connection-url.js +8 -0
- package/dist/core/connection-url.js.map +1 -0
- package/dist/core/errors.d.ts +10 -0
- package/dist/core/errors.d.ts.map +1 -0
- package/dist/core/errors.js +17 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/source-control.d.ts +14 -0
- package/dist/core/source-control.d.ts.map +1 -0
- package/dist/core/source-control.js +177 -0
- package/dist/core/source-control.js.map +1 -0
- package/dist/core/webhook.d.ts +38 -0
- package/dist/core/webhook.d.ts.map +1 -0
- package/dist/core/webhook.js +162 -0
- package/dist/core/webhook.js.map +1 -0
- package/dist/db/connections.d.ts +19 -0
- package/dist/db/connections.d.ts.map +1 -0
- package/dist/db/connections.js +42 -0
- package/dist/db/connections.js.map +1 -0
- package/dist/db/db.d.ts +191 -0
- package/dist/db/db.d.ts.map +1 -0
- package/dist/db/db.js +18 -0
- package/dist/db/db.js.map +1 -0
- package/dist/db/migrations.d.ts +2 -0
- package/dist/db/migrations.d.ts.map +1 -0
- package/dist/db/migrations.js +5 -0
- package/dist/db/migrations.js.map +1 -0
- package/dist/db/schema/common.d.ts +2 -0
- package/dist/db/schema/common.d.ts.map +1 -0
- package/dist/db/schema/common.js +4 -0
- package/dist/db/schema/common.js.map +1 -0
- package/dist/db/schema/connections.d.ts +97 -0
- package/dist/db/schema/connections.d.ts.map +1 -0
- package/dist/db/schema/connections.js +28 -0
- package/dist/db/schema/connections.js.map +1 -0
- package/dist/index.d.ts +39 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +42 -0
- package/dist/index.js.map +1 -0
- package/dist/presentation/dto/integrations.d.ts +17 -0
- package/dist/presentation/dto/integrations.d.ts.map +1 -0
- package/dist/presentation/dto/integrations.js +11 -0
- package/dist/presentation/dto/integrations.js.map +1 -0
- package/dist/presentation/routes/connections.d.ts +13 -0
- package/dist/presentation/routes/connections.d.ts.map +1 -0
- package/dist/presentation/routes/connections.js +47 -0
- package/dist/presentation/routes/connections.js.map +1 -0
- package/dist/presentation/routes/errors.d.ts +2 -0
- package/dist/presentation/routes/errors.d.ts.map +1 -0
- package/dist/presentation/routes/errors.js +36 -0
- package/dist/presentation/routes/errors.js.map +1 -0
- package/dist/presentation/routes/webhooks.d.ts +11 -0
- package/dist/presentation/routes/webhooks.d.ts.map +1 -0
- package/dist/presentation/routes/webhooks.js +109 -0
- package/dist/presentation/routes/webhooks.js.map +1 -0
- package/dist/tsconfig.test.tsbuildinfo +1 -0
- package/drizzle/0000_initial.sql +10 -0
- package/drizzle/meta/0000_snapshot.json +96 -0
- package/drizzle/meta/_journal.json +13 -0
- package/drizzle.config.ts +7 -0
- package/package.json +66 -0
- package/src/api/client.test.ts +367 -0
- package/src/api/client.ts +364 -0
- package/src/config.ts +42 -0
- package/src/connection-external-url.test.ts +36 -0
- package/src/core/connect.test.ts +149 -0
- package/src/core/connect.ts +52 -0
- package/src/core/connection-url.ts +8 -0
- package/src/core/errors.ts +17 -0
- package/src/core/source-control-clone-url.test.ts +83 -0
- package/src/core/source-control.test.ts +349 -0
- package/src/core/source-control.ts +231 -0
- package/src/core/webhook.test.ts +396 -0
- package/src/core/webhook.ts +203 -0
- package/src/db/connections.test.ts +69 -0
- package/src/db/connections.ts +80 -0
- package/src/db/db.ts +18 -0
- package/src/db/migrations.ts +4 -0
- package/src/db/schema/common.ts +3 -0
- package/src/db/schema/connections.ts +32 -0
- package/src/index.test.ts +23 -0
- package/src/index.ts +83 -0
- package/src/presentation/dto/integrations.ts +14 -0
- package/src/presentation/routes/connections.test.ts +200 -0
- package/src/presentation/routes/connections.ts +63 -0
- package/src/presentation/routes/errors.ts +35 -0
- package/src/presentation/routes/webhooks.test.ts +188 -0
- package/src/presentation/routes/webhooks.ts +111 -0
- package/test/env.ts +11 -0
- package/test/factories/gitea-connection.ts +19 -0
- package/test/factories/index.ts +1 -0
- package/test/fixtures/gitea-webhook.ts +237 -0
- package/test/globalSetup.ts +14 -0
- package/test/index.ts +6 -0
- package/test/setup.ts +17 -0
- package/tsconfig.build.json +9 -0
- package/tsconfig.build.tsbuildinfo +1 -0
- package/tsconfig.json +3 -0
- package/tsconfig.test.json +9 -0
- package/vitest.config.ts +12 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { GiteaOrgAlreadyLinkedError, GiteaOrganizationNotFoundError } from './errors.js';
|
|
2
|
+
export async function handleGiteaConnect(params) {
|
|
3
|
+
// Gitea resolves org names case-insensitively, but the ownership lookup and the
|
|
4
|
+
// unique indexes that back the cross-tenant guard compare the stored `org`
|
|
5
|
+
// case-sensitively. Canonicalize once here so a case variant ("Acme" vs "acme")
|
|
6
|
+
// cannot slip past getExistingGiteaConnection and link an org another workspace
|
|
7
|
+
// already owns. The source-control scope guard already lowercases both sides.
|
|
8
|
+
const org = params.org.toLowerCase();
|
|
9
|
+
if (!await params.gitea.organizationExists({
|
|
10
|
+
org
|
|
11
|
+
})) {
|
|
12
|
+
throw new GiteaOrganizationNotFoundError(org);
|
|
13
|
+
}
|
|
14
|
+
const existing = await params.getExistingGiteaConnection({
|
|
15
|
+
org
|
|
16
|
+
});
|
|
17
|
+
if (existing && existing.workspaceId !== params.workspaceId) {
|
|
18
|
+
throw new GiteaOrgAlreadyLinkedError(org);
|
|
19
|
+
}
|
|
20
|
+
if (existing && existing.lifecycleStatus === 'active') {
|
|
21
|
+
return existing;
|
|
22
|
+
}
|
|
23
|
+
// The read-only service account cannot manage org hooks; the instance admin
|
|
24
|
+
// provisions push delivery out of band.
|
|
25
|
+
return await params.connectGiteaConnection({
|
|
26
|
+
workspaceId: params.workspaceId,
|
|
27
|
+
org,
|
|
28
|
+
displayName: `Gitea ${org}`
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
//# sourceMappingURL=connect.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/core/connect.ts"],"sourcesContent":["import type {IntegrationConnection} from '@shipfox/api-integration-core-dto';\nimport type {GiteaApiClient} from '#api/client.js';\nimport {GiteaOrgAlreadyLinkedError, GiteaOrganizationNotFoundError} from './errors.js';\n\nexport interface ConnectGiteaConnectionInput {\n workspaceId: string;\n org: string;\n displayName: string;\n}\n\nexport interface HandleGiteaConnectParams {\n gitea: GiteaApiClient;\n workspaceId: string;\n org: string;\n getExistingGiteaConnection: (input: {\n org: string;\n }) => Promise<IntegrationConnection<'gitea'> | undefined>;\n connectGiteaConnection: (\n input: ConnectGiteaConnectionInput,\n ) => Promise<IntegrationConnection<'gitea'>>;\n}\n\nexport async function handleGiteaConnect(\n params: HandleGiteaConnectParams,\n): Promise<IntegrationConnection<'gitea'>> {\n // Gitea resolves org names case-insensitively, but the ownership lookup and the\n // unique indexes that back the cross-tenant guard compare the stored `org`\n // case-sensitively. Canonicalize once here so a case variant (\"Acme\" vs \"acme\")\n // cannot slip past getExistingGiteaConnection and link an org another workspace\n // already owns. The source-control scope guard already lowercases both sides.\n const org = params.org.toLowerCase();\n\n if (!(await params.gitea.organizationExists({org}))) {\n throw new GiteaOrganizationNotFoundError(org);\n }\n\n const existing = await params.getExistingGiteaConnection({org});\n if (existing && existing.workspaceId !== params.workspaceId) {\n throw new GiteaOrgAlreadyLinkedError(org);\n }\n if (existing && existing.lifecycleStatus === 'active') {\n return existing;\n }\n\n // The read-only service account cannot manage org hooks; the instance admin\n // provisions push delivery out of band.\n return await params.connectGiteaConnection({\n workspaceId: params.workspaceId,\n org,\n displayName: `Gitea ${org}`,\n });\n}\n"],"names":["GiteaOrgAlreadyLinkedError","GiteaOrganizationNotFoundError","handleGiteaConnect","params","org","toLowerCase","gitea","organizationExists","existing","getExistingGiteaConnection","workspaceId","lifecycleStatus","connectGiteaConnection","displayName"],"mappings":"AAEA,SAAQA,0BAA0B,EAAEC,8BAA8B,QAAO,cAAc;AAoBvF,OAAO,eAAeC,mBACpBC,MAAgC;IAEhC,gFAAgF;IAChF,2EAA2E;IAC3E,gFAAgF;IAChF,gFAAgF;IAChF,8EAA8E;IAC9E,MAAMC,MAAMD,OAAOC,GAAG,CAACC,WAAW;IAElC,IAAI,CAAE,MAAMF,OAAOG,KAAK,CAACC,kBAAkB,CAAC;QAACH;IAAG,IAAK;QACnD,MAAM,IAAIH,+BAA+BG;IAC3C;IAEA,MAAMI,WAAW,MAAML,OAAOM,0BAA0B,CAAC;QAACL;IAAG;IAC7D,IAAII,YAAYA,SAASE,WAAW,KAAKP,OAAOO,WAAW,EAAE;QAC3D,MAAM,IAAIV,2BAA2BI;IACvC;IACA,IAAII,YAAYA,SAASG,eAAe,KAAK,UAAU;QACrD,OAAOH;IACT;IAEA,4EAA4E;IAC5E,wCAAwC;IACxC,OAAO,MAAML,OAAOS,sBAAsB,CAAC;QACzCF,aAAaP,OAAOO,WAAW;QAC/BN;QACAS,aAAa,CAAC,MAAM,EAAET,KAAK;IAC7B;AACF"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connection-url.d.ts","sourceRoot":"","sources":["../../src/core/connection-url.ts"],"names":[],"mappings":"AAIA,wBAAgB,0BAA0B,CAAC,iBAAiB,EAAE,MAAM,GAAG,MAAM,CAG5E"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { config } from '#config.js';
|
|
2
|
+
const TRAILING_SLASHES_RE = /\/+$/;
|
|
3
|
+
export function giteaConnectionExternalUrl(externalAccountId) {
|
|
4
|
+
const base = config.GITEA_BASE_URL.replace(TRAILING_SLASHES_RE, '');
|
|
5
|
+
return `${base}/${encodeURIComponent(externalAccountId)}`;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
//# sourceMappingURL=connection-url.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/core/connection-url.ts"],"sourcesContent":["import {config} from '#config.js';\n\nconst TRAILING_SLASHES_RE = /\\/+$/;\n\nexport function giteaConnectionExternalUrl(externalAccountId: string): string {\n const base = config.GITEA_BASE_URL.replace(TRAILING_SLASHES_RE, '');\n return `${base}/${encodeURIComponent(externalAccountId)}`;\n}\n"],"names":["config","TRAILING_SLASHES_RE","giteaConnectionExternalUrl","externalAccountId","base","GITEA_BASE_URL","replace","encodeURIComponent"],"mappings":"AAAA,SAAQA,MAAM,QAAO,aAAa;AAElC,MAAMC,sBAAsB;AAE5B,OAAO,SAASC,2BAA2BC,iBAAyB;IAClE,MAAMC,OAAOJ,OAAOK,cAAc,CAACC,OAAO,CAACL,qBAAqB;IAChE,OAAO,GAAGG,KAAK,CAAC,EAAEG,mBAAmBJ,oBAAoB;AAC3D"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { IntegrationProviderError } from '@shipfox/api-integration-core-dto';
|
|
2
|
+
export declare class GiteaIntegrationProviderError extends IntegrationProviderError {
|
|
3
|
+
}
|
|
4
|
+
export declare class GiteaOrganizationNotFoundError extends Error {
|
|
5
|
+
constructor(org: string);
|
|
6
|
+
}
|
|
7
|
+
export declare class GiteaOrgAlreadyLinkedError extends Error {
|
|
8
|
+
constructor(org: string);
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/core/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,wBAAwB,EAAC,MAAM,mCAAmC,CAAC;AAE3E,qBAAa,6BAA8B,SAAQ,wBAAwB;CAAG;AAE9E,qBAAa,8BAA+B,SAAQ,KAAK;gBAC3C,GAAG,EAAE,MAAM;CAIxB;AAED,qBAAa,0BAA2B,SAAQ,KAAK;gBACvC,GAAG,EAAE,MAAM;CAIxB"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { IntegrationProviderError } from '@shipfox/api-integration-core-dto';
|
|
2
|
+
export class GiteaIntegrationProviderError extends IntegrationProviderError {
|
|
3
|
+
}
|
|
4
|
+
export class GiteaOrganizationNotFoundError extends Error {
|
|
5
|
+
constructor(org){
|
|
6
|
+
super(`Gitea organization not found: ${org}`);
|
|
7
|
+
this.name = 'GiteaOrganizationNotFoundError';
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
export class GiteaOrgAlreadyLinkedError extends Error {
|
|
11
|
+
constructor(org){
|
|
12
|
+
super(`Gitea organization is already linked to another Shipfox workspace: ${org}`);
|
|
13
|
+
this.name = 'GiteaOrgAlreadyLinkedError';
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/core/errors.ts"],"sourcesContent":["import {IntegrationProviderError} from '@shipfox/api-integration-core-dto';\n\nexport class GiteaIntegrationProviderError extends IntegrationProviderError {}\n\nexport class GiteaOrganizationNotFoundError extends Error {\n constructor(org: string) {\n super(`Gitea organization not found: ${org}`);\n this.name = 'GiteaOrganizationNotFoundError';\n }\n}\n\nexport class GiteaOrgAlreadyLinkedError extends Error {\n constructor(org: string) {\n super(`Gitea organization is already linked to another Shipfox workspace: ${org}`);\n this.name = 'GiteaOrgAlreadyLinkedError';\n }\n}\n"],"names":["IntegrationProviderError","GiteaIntegrationProviderError","GiteaOrganizationNotFoundError","Error","org","name","GiteaOrgAlreadyLinkedError"],"mappings":"AAAA,SAAQA,wBAAwB,QAAO,oCAAoC;AAE3E,OAAO,MAAMC,sCAAsCD;AAA0B;AAE7E,OAAO,MAAME,uCAAuCC;IAClD,YAAYC,GAAW,CAAE;QACvB,KAAK,CAAC,CAAC,8BAA8B,EAAEA,KAAK;QAC5C,IAAI,CAACC,IAAI,GAAG;IACd;AACF;AAEA,OAAO,MAAMC,mCAAmCH;IAC9C,YAAYC,GAAW,CAAE;QACvB,KAAK,CAAC,CAAC,mEAAmE,EAAEA,KAAK;QACjF,IAAI,CAACC,IAAI,GAAG;IACd;AACF"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type CheckoutSpec, type CreateCheckoutSpecInput, type FetchFileInput, type FilePage, type FileSnapshot, type IntegrationConnection, type ListFilesInput, type ListRepositoriesInput, type RepositoryPage, type RepositorySnapshot, type ResolveRepositoryInput, type SourceControlProvider } from '@shipfox/api-integration-core-dto';
|
|
2
|
+
import type { GiteaApiClient } from '#api/client.js';
|
|
3
|
+
type GiteaIntegrationConnection = IntegrationConnection<'gitea'>;
|
|
4
|
+
export declare class GiteaSourceControlProvider implements SourceControlProvider<GiteaIntegrationConnection> {
|
|
5
|
+
private readonly gitea;
|
|
6
|
+
constructor(gitea: GiteaApiClient);
|
|
7
|
+
listRepositories(input: ListRepositoriesInput<GiteaIntegrationConnection>): Promise<RepositoryPage>;
|
|
8
|
+
resolveRepository(input: ResolveRepositoryInput<GiteaIntegrationConnection>): Promise<RepositorySnapshot>;
|
|
9
|
+
listFiles(input: ListFilesInput<GiteaIntegrationConnection>): Promise<FilePage>;
|
|
10
|
+
fetchFile(input: FetchFileInput<GiteaIntegrationConnection>): Promise<FileSnapshot>;
|
|
11
|
+
createCheckoutSpec(input: CreateCheckoutSpecInput<GiteaIntegrationConnection>): Promise<CheckoutSpec>;
|
|
12
|
+
}
|
|
13
|
+
export {};
|
|
14
|
+
//# sourceMappingURL=source-control.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"source-control.d.ts","sourceRoot":"","sources":["../../src/core/source-control.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,KAAK,YAAY,EACjB,KAAK,uBAAuB,EAC5B,KAAK,cAAc,EACnB,KAAK,QAAQ,EACb,KAAK,YAAY,EACjB,KAAK,qBAAqB,EAC1B,KAAK,cAAc,EACnB,KAAK,qBAAqB,EAG1B,KAAK,cAAc,EACnB,KAAK,kBAAkB,EAEvB,KAAK,sBAAsB,EAC3B,KAAK,qBAAqB,EAC3B,MAAM,mCAAmC,CAAC;AAE3C,OAAO,KAAK,EAAC,cAAc,EAAkB,MAAM,gBAAgB,CAAC;AAIpE,KAAK,0BAA0B,GAAG,qBAAqB,CAAC,OAAO,CAAC,CAAC;AAMjE,qBAAa,0BACX,YAAW,qBAAqB,CAAC,0BAA0B,CAAC;IAEhD,OAAO,CAAC,QAAQ,CAAC,KAAK;gBAAL,KAAK,EAAE,cAAc;IAE5C,gBAAgB,CACpB,KAAK,EAAE,qBAAqB,CAAC,0BAA0B,CAAC,GACvD,OAAO,CAAC,cAAc,CAAC;IAqCpB,iBAAiB,CACrB,KAAK,EAAE,sBAAsB,CAAC,0BAA0B,CAAC,GACxD,OAAO,CAAC,kBAAkB,CAAC;IAQxB,SAAS,CAAC,KAAK,EAAE,cAAc,CAAC,0BAA0B,CAAC,GAAG,OAAO,CAAC,QAAQ,CAAC;IA4B/E,SAAS,CAAC,KAAK,EAAE,cAAc,CAAC,0BAA0B,CAAC,GAAG,OAAO,CAAC,YAAY,CAAC;IAwBnF,kBAAkB,CACtB,KAAK,EAAE,uBAAuB,CAAC,0BAA0B,CAAC,GACzD,OAAO,CAAC,YAAY,CAAC;CAsBzB"}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { Buffer } from 'node:buffer';
|
|
2
|
+
import { buildProviderRepositoryId, MAX_REPOSITORY_FILE_BYTES, parseProviderRepositoryId } from '@shipfox/api-integration-core-dto';
|
|
3
|
+
import { giteaProviderKind } from '@shipfox/api-integration-gitea-dto';
|
|
4
|
+
import { config, giteaCloneBaseOrigin } from '#config.js';
|
|
5
|
+
import { GiteaIntegrationProviderError } from './errors.js';
|
|
6
|
+
const TRAILING_SLASHES_RE = /\/+$/;
|
|
7
|
+
const SEARCH_PAGE_SIZE = 100;
|
|
8
|
+
const SEARCH_MAX_PAGES_PER_REQUEST = 5;
|
|
9
|
+
export class GiteaSourceControlProvider {
|
|
10
|
+
constructor(gitea){
|
|
11
|
+
this.gitea = gitea;
|
|
12
|
+
}
|
|
13
|
+
async listRepositories(input) {
|
|
14
|
+
const org = input.connection.externalAccountId;
|
|
15
|
+
const needle = input.search?.trim().toLowerCase();
|
|
16
|
+
if (!needle) {
|
|
17
|
+
const page = await this.gitea.listOrgRepositories({
|
|
18
|
+
org,
|
|
19
|
+
limit: input.limit,
|
|
20
|
+
cursor: input.cursor
|
|
21
|
+
});
|
|
22
|
+
return {
|
|
23
|
+
repositories: page.repositories.map(toRepositorySnapshot),
|
|
24
|
+
nextCursor: page.nextCursor
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
const matches = [];
|
|
28
|
+
let cursor = input.cursor;
|
|
29
|
+
let pagesScanned = 0;
|
|
30
|
+
while(matches.length < input.limit && pagesScanned < SEARCH_MAX_PAGES_PER_REQUEST){
|
|
31
|
+
const page = await this.gitea.listOrgRepositories({
|
|
32
|
+
org,
|
|
33
|
+
limit: SEARCH_PAGE_SIZE,
|
|
34
|
+
cursor
|
|
35
|
+
});
|
|
36
|
+
pagesScanned += 1;
|
|
37
|
+
for (const repo of page.repositories){
|
|
38
|
+
if (repo.fullName.toLowerCase().includes(needle)) {
|
|
39
|
+
matches.push(toRepositorySnapshot(repo));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
cursor = page.nextCursor ?? undefined;
|
|
43
|
+
if (!cursor) break;
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
repositories: matches.slice(0, input.limit),
|
|
47
|
+
nextCursor: cursor ?? null
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
async resolveRepository(input) {
|
|
51
|
+
const { owner, repo } = parseGiteaRepositoryLocator(input.externalRepositoryId, input.connection.externalAccountId);
|
|
52
|
+
return toRepositorySnapshot(await this.gitea.getRepository({
|
|
53
|
+
owner,
|
|
54
|
+
repo
|
|
55
|
+
}));
|
|
56
|
+
}
|
|
57
|
+
async listFiles(input) {
|
|
58
|
+
const { owner, repo } = parseGiteaRepositoryLocator(input.externalRepositoryId, input.connection.externalAccountId);
|
|
59
|
+
const sha = await this.gitea.resolveRef({
|
|
60
|
+
owner,
|
|
61
|
+
repo,
|
|
62
|
+
ref: input.ref
|
|
63
|
+
});
|
|
64
|
+
const tree = await this.gitea.listTree({
|
|
65
|
+
owner,
|
|
66
|
+
repo,
|
|
67
|
+
sha
|
|
68
|
+
});
|
|
69
|
+
if (tree.truncated) {
|
|
70
|
+
throw new GiteaIntegrationProviderError('too-many-files', `Gitea repository tree for ${input.externalRepositoryId} is too large to enumerate`);
|
|
71
|
+
}
|
|
72
|
+
const prefix = input.prefix.replace(TRAILING_SLASHES_RE, '');
|
|
73
|
+
const matched = tree.blobs.filter((blob)=>prefixMatches(blob.path, prefix)).sort((a, b)=>a.path.localeCompare(b.path));
|
|
74
|
+
const offset = parseOffset(input.cursor);
|
|
75
|
+
const page = matched.slice(offset, offset + input.limit);
|
|
76
|
+
const consumed = offset + page.length;
|
|
77
|
+
return {
|
|
78
|
+
files: page.map((blob)=>({
|
|
79
|
+
path: blob.path,
|
|
80
|
+
type: 'file',
|
|
81
|
+
size: blob.size
|
|
82
|
+
})),
|
|
83
|
+
nextCursor: consumed < matched.length ? String(consumed) : null
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
async fetchFile(input) {
|
|
87
|
+
const { owner, repo } = parseGiteaRepositoryLocator(input.externalRepositoryId, input.connection.externalAccountId);
|
|
88
|
+
const file = await this.gitea.fetchFileContent({
|
|
89
|
+
owner,
|
|
90
|
+
repo,
|
|
91
|
+
path: input.path,
|
|
92
|
+
ref: input.ref
|
|
93
|
+
});
|
|
94
|
+
if (file.size > MAX_REPOSITORY_FILE_BYTES || Buffer.byteLength(file.content, 'utf8') > MAX_REPOSITORY_FILE_BYTES) {
|
|
95
|
+
throw new GiteaIntegrationProviderError('content-too-large', 'Gitea file content is larger than the supported limit');
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
path: file.path,
|
|
99
|
+
ref: input.ref,
|
|
100
|
+
content: file.content
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
async createCheckoutSpec(input) {
|
|
104
|
+
const { owner, repo } = parseGiteaRepositoryLocator(input.externalRepositoryId, input.connection.externalAccountId);
|
|
105
|
+
const repository = await this.gitea.getRepository({
|
|
106
|
+
owner,
|
|
107
|
+
repo
|
|
108
|
+
});
|
|
109
|
+
const ref = input.ref?.trim() || repository.defaultBranch;
|
|
110
|
+
return {
|
|
111
|
+
repositoryUrl: createCheckoutRepositoryUrl(repository.cloneUrl),
|
|
112
|
+
ref,
|
|
113
|
+
// Gitea has no per-repo, auto-expiring token like a GitHub App installation
|
|
114
|
+
// token, so checkout reuses the long-lived service credential. `expiresAt`
|
|
115
|
+
// is the runner's lease/refresh window, not the token's real expiry: this
|
|
116
|
+
// credential does not actually expire and stays valid if it leaks.
|
|
117
|
+
credentials: {
|
|
118
|
+
username: config.GITEA_SERVICE_USERNAME,
|
|
119
|
+
token: config.GITEA_SERVICE_TOKEN,
|
|
120
|
+
expiresAt: new Date(Date.now() + config.GITEA_CHECKOUT_TTL_SECONDS * 1000)
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function createCheckoutRepositoryUrl(cloneUrl) {
|
|
126
|
+
if (!giteaCloneBaseOrigin) return cloneUrl;
|
|
127
|
+
const repositoryUrl = new URL(cloneUrl);
|
|
128
|
+
repositoryUrl.protocol = giteaCloneBaseOrigin.protocol;
|
|
129
|
+
repositoryUrl.host = giteaCloneBaseOrigin.host;
|
|
130
|
+
return repositoryUrl.toString();
|
|
131
|
+
}
|
|
132
|
+
function toRepositorySnapshot(repository) {
|
|
133
|
+
return {
|
|
134
|
+
externalRepositoryId: buildProviderRepositoryId(giteaProviderKind, `${repository.ownerLogin}/${repository.name}`),
|
|
135
|
+
owner: repository.ownerLogin,
|
|
136
|
+
name: repository.name,
|
|
137
|
+
fullName: repository.fullName,
|
|
138
|
+
defaultBranch: repository.defaultBranch,
|
|
139
|
+
visibility: toRepositoryVisibility(repository),
|
|
140
|
+
cloneUrl: repository.cloneUrl,
|
|
141
|
+
htmlUrl: repository.htmlUrl
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
function toRepositoryVisibility(repository) {
|
|
145
|
+
return repository.private ? 'private' : 'public';
|
|
146
|
+
}
|
|
147
|
+
function parseGiteaRepositoryLocator(externalRepositoryId, expectedOwner) {
|
|
148
|
+
const value = parseProviderRepositoryId(externalRepositoryId, giteaProviderKind);
|
|
149
|
+
const separatorIndex = value.indexOf('/');
|
|
150
|
+
const owner = separatorIndex > 0 ? value.slice(0, separatorIndex) : '';
|
|
151
|
+
const repo = separatorIndex > 0 ? value.slice(separatorIndex + 1) : '';
|
|
152
|
+
if (!owner || !repo || repo.includes('/')) {
|
|
153
|
+
throw new GiteaIntegrationProviderError('repository-not-found', `Gitea repository id ${externalRepositoryId} must follow the form ${giteaProviderKind}:<owner>/<repo>`);
|
|
154
|
+
}
|
|
155
|
+
// The service token is instance-wide, so the adapter must scope every request
|
|
156
|
+
// to the connection's own account itself; without this an external id naming
|
|
157
|
+
// another org would read its private repos and mint checkout credentials for
|
|
158
|
+
// them. Reported as not-found so it does not confirm an out-of-scope repo.
|
|
159
|
+
if (owner.toLowerCase() !== expectedOwner.toLowerCase()) {
|
|
160
|
+
throw new GiteaIntegrationProviderError('repository-not-found', `Gitea repository id ${externalRepositoryId} is not in the ${expectedOwner} account`);
|
|
161
|
+
}
|
|
162
|
+
return {
|
|
163
|
+
owner,
|
|
164
|
+
repo
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
function prefixMatches(path, prefix) {
|
|
168
|
+
if (!prefix) return true;
|
|
169
|
+
return path === prefix || path.startsWith(`${prefix}/`);
|
|
170
|
+
}
|
|
171
|
+
function parseOffset(cursor) {
|
|
172
|
+
if (!cursor) return 0;
|
|
173
|
+
const offset = Number.parseInt(cursor, 10);
|
|
174
|
+
return Number.isNaN(offset) || offset < 0 ? 0 : offset;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
//# sourceMappingURL=source-control.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/core/source-control.ts"],"sourcesContent":["import {Buffer} from 'node:buffer';\nimport {\n buildProviderRepositoryId,\n type CheckoutSpec,\n type CreateCheckoutSpecInput,\n type FetchFileInput,\n type FilePage,\n type FileSnapshot,\n type IntegrationConnection,\n type ListFilesInput,\n type ListRepositoriesInput,\n MAX_REPOSITORY_FILE_BYTES,\n parseProviderRepositoryId,\n type RepositoryPage,\n type RepositorySnapshot,\n type RepositoryVisibility,\n type ResolveRepositoryInput,\n type SourceControlProvider,\n} from '@shipfox/api-integration-core-dto';\nimport {giteaProviderKind} from '@shipfox/api-integration-gitea-dto';\nimport type {GiteaApiClient, GiteaRepository} from '#api/client.js';\nimport {config, giteaCloneBaseOrigin} from '#config.js';\nimport {GiteaIntegrationProviderError} from './errors.js';\n\ntype GiteaIntegrationConnection = IntegrationConnection<'gitea'>;\n\nconst TRAILING_SLASHES_RE = /\\/+$/;\nconst SEARCH_PAGE_SIZE = 100;\nconst SEARCH_MAX_PAGES_PER_REQUEST = 5;\n\nexport class GiteaSourceControlProvider\n implements SourceControlProvider<GiteaIntegrationConnection>\n{\n constructor(private readonly gitea: GiteaApiClient) {}\n\n async listRepositories(\n input: ListRepositoriesInput<GiteaIntegrationConnection>,\n ): Promise<RepositoryPage> {\n const org = input.connection.externalAccountId;\n const needle = input.search?.trim().toLowerCase();\n\n if (!needle) {\n const page = await this.gitea.listOrgRepositories({\n org,\n limit: input.limit,\n cursor: input.cursor,\n });\n return {\n repositories: page.repositories.map(toRepositorySnapshot),\n nextCursor: page.nextCursor,\n };\n }\n\n const matches: RepositorySnapshot[] = [];\n let cursor = input.cursor;\n let pagesScanned = 0;\n while (matches.length < input.limit && pagesScanned < SEARCH_MAX_PAGES_PER_REQUEST) {\n const page = await this.gitea.listOrgRepositories({org, limit: SEARCH_PAGE_SIZE, cursor});\n pagesScanned += 1;\n for (const repo of page.repositories) {\n if (repo.fullName.toLowerCase().includes(needle)) {\n matches.push(toRepositorySnapshot(repo));\n }\n }\n cursor = page.nextCursor ?? undefined;\n if (!cursor) break;\n }\n\n return {\n repositories: matches.slice(0, input.limit),\n nextCursor: cursor ?? null,\n };\n }\n\n async resolveRepository(\n input: ResolveRepositoryInput<GiteaIntegrationConnection>,\n ): Promise<RepositorySnapshot> {\n const {owner, repo} = parseGiteaRepositoryLocator(\n input.externalRepositoryId,\n input.connection.externalAccountId,\n );\n return toRepositorySnapshot(await this.gitea.getRepository({owner, repo}));\n }\n\n async listFiles(input: ListFilesInput<GiteaIntegrationConnection>): Promise<FilePage> {\n const {owner, repo} = parseGiteaRepositoryLocator(\n input.externalRepositoryId,\n input.connection.externalAccountId,\n );\n const sha = await this.gitea.resolveRef({owner, repo, ref: input.ref});\n const tree = await this.gitea.listTree({owner, repo, sha});\n if (tree.truncated) {\n throw new GiteaIntegrationProviderError(\n 'too-many-files',\n `Gitea repository tree for ${input.externalRepositoryId} is too large to enumerate`,\n );\n }\n\n const prefix = input.prefix.replace(TRAILING_SLASHES_RE, '');\n const matched = tree.blobs\n .filter((blob) => prefixMatches(blob.path, prefix))\n .sort((a, b) => a.path.localeCompare(b.path));\n const offset = parseOffset(input.cursor);\n const page = matched.slice(offset, offset + input.limit);\n const consumed = offset + page.length;\n\n return {\n files: page.map((blob) => ({path: blob.path, type: 'file', size: blob.size})),\n nextCursor: consumed < matched.length ? String(consumed) : null,\n };\n }\n\n async fetchFile(input: FetchFileInput<GiteaIntegrationConnection>): Promise<FileSnapshot> {\n const {owner, repo} = parseGiteaRepositoryLocator(\n input.externalRepositoryId,\n input.connection.externalAccountId,\n );\n const file = await this.gitea.fetchFileContent({owner, repo, path: input.path, ref: input.ref});\n\n if (\n file.size > MAX_REPOSITORY_FILE_BYTES ||\n Buffer.byteLength(file.content, 'utf8') > MAX_REPOSITORY_FILE_BYTES\n ) {\n throw new GiteaIntegrationProviderError(\n 'content-too-large',\n 'Gitea file content is larger than the supported limit',\n );\n }\n\n return {\n path: file.path,\n ref: input.ref,\n content: file.content,\n };\n }\n\n async createCheckoutSpec(\n input: CreateCheckoutSpecInput<GiteaIntegrationConnection>,\n ): Promise<CheckoutSpec> {\n const {owner, repo} = parseGiteaRepositoryLocator(\n input.externalRepositoryId,\n input.connection.externalAccountId,\n );\n const repository = await this.gitea.getRepository({owner, repo});\n const ref = input.ref?.trim() || repository.defaultBranch;\n\n return {\n repositoryUrl: createCheckoutRepositoryUrl(repository.cloneUrl),\n ref,\n // Gitea has no per-repo, auto-expiring token like a GitHub App installation\n // token, so checkout reuses the long-lived service credential. `expiresAt`\n // is the runner's lease/refresh window, not the token's real expiry: this\n // credential does not actually expire and stays valid if it leaks.\n credentials: {\n username: config.GITEA_SERVICE_USERNAME,\n token: config.GITEA_SERVICE_TOKEN,\n expiresAt: new Date(Date.now() + config.GITEA_CHECKOUT_TTL_SECONDS * 1000),\n },\n };\n }\n}\n\nfunction createCheckoutRepositoryUrl(cloneUrl: string): string {\n if (!giteaCloneBaseOrigin) return cloneUrl;\n\n const repositoryUrl = new URL(cloneUrl);\n\n repositoryUrl.protocol = giteaCloneBaseOrigin.protocol;\n repositoryUrl.host = giteaCloneBaseOrigin.host;\n\n return repositoryUrl.toString();\n}\n\nfunction toRepositorySnapshot(repository: GiteaRepository): RepositorySnapshot {\n return {\n externalRepositoryId: buildProviderRepositoryId(\n giteaProviderKind,\n `${repository.ownerLogin}/${repository.name}`,\n ),\n owner: repository.ownerLogin,\n name: repository.name,\n fullName: repository.fullName,\n defaultBranch: repository.defaultBranch,\n visibility: toRepositoryVisibility(repository),\n cloneUrl: repository.cloneUrl,\n htmlUrl: repository.htmlUrl,\n };\n}\n\nfunction toRepositoryVisibility(repository: GiteaRepository): RepositoryVisibility {\n return repository.private ? 'private' : 'public';\n}\n\nfunction parseGiteaRepositoryLocator(\n externalRepositoryId: string,\n expectedOwner: string,\n): {owner: string; repo: string} {\n const value = parseProviderRepositoryId(externalRepositoryId, giteaProviderKind);\n const separatorIndex = value.indexOf('/');\n const owner = separatorIndex > 0 ? value.slice(0, separatorIndex) : '';\n const repo = separatorIndex > 0 ? value.slice(separatorIndex + 1) : '';\n if (!owner || !repo || repo.includes('/')) {\n throw new GiteaIntegrationProviderError(\n 'repository-not-found',\n `Gitea repository id ${externalRepositoryId} must follow the form ${giteaProviderKind}:<owner>/<repo>`,\n );\n }\n // The service token is instance-wide, so the adapter must scope every request\n // to the connection's own account itself; without this an external id naming\n // another org would read its private repos and mint checkout credentials for\n // them. Reported as not-found so it does not confirm an out-of-scope repo.\n if (owner.toLowerCase() !== expectedOwner.toLowerCase()) {\n throw new GiteaIntegrationProviderError(\n 'repository-not-found',\n `Gitea repository id ${externalRepositoryId} is not in the ${expectedOwner} account`,\n );\n }\n\n return {owner, repo};\n}\n\nfunction prefixMatches(path: string, prefix: string): boolean {\n if (!prefix) return true;\n return path === prefix || path.startsWith(`${prefix}/`);\n}\n\nfunction parseOffset(cursor: string | undefined): number {\n if (!cursor) return 0;\n const offset = Number.parseInt(cursor, 10);\n return Number.isNaN(offset) || offset < 0 ? 0 : offset;\n}\n"],"names":["Buffer","buildProviderRepositoryId","MAX_REPOSITORY_FILE_BYTES","parseProviderRepositoryId","giteaProviderKind","config","giteaCloneBaseOrigin","GiteaIntegrationProviderError","TRAILING_SLASHES_RE","SEARCH_PAGE_SIZE","SEARCH_MAX_PAGES_PER_REQUEST","GiteaSourceControlProvider","gitea","listRepositories","input","org","connection","externalAccountId","needle","search","trim","toLowerCase","page","listOrgRepositories","limit","cursor","repositories","map","toRepositorySnapshot","nextCursor","matches","pagesScanned","length","repo","fullName","includes","push","undefined","slice","resolveRepository","owner","parseGiteaRepositoryLocator","externalRepositoryId","getRepository","listFiles","sha","resolveRef","ref","tree","listTree","truncated","prefix","replace","matched","blobs","filter","blob","prefixMatches","path","sort","a","b","localeCompare","offset","parseOffset","consumed","files","type","size","String","fetchFile","file","fetchFileContent","byteLength","content","createCheckoutSpec","repository","defaultBranch","repositoryUrl","createCheckoutRepositoryUrl","cloneUrl","credentials","username","GITEA_SERVICE_USERNAME","token","GITEA_SERVICE_TOKEN","expiresAt","Date","now","GITEA_CHECKOUT_TTL_SECONDS","URL","protocol","host","toString","ownerLogin","name","visibility","toRepositoryVisibility","htmlUrl","private","expectedOwner","value","separatorIndex","indexOf","startsWith","Number","parseInt","isNaN"],"mappings":"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"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { type GetIntegrationConnectionByIdFn, type IntegrationTx, type PublishSourcePushFn, type RecordDeliveryOnlyFn } from '@shipfox/api-integration-core-dto';
|
|
2
|
+
import { type GiteaPushPayloadDto } from '@shipfox/api-integration-gitea-dto';
|
|
3
|
+
export declare class GiteaWebhookMalformedJsonError extends Error {
|
|
4
|
+
constructor(options: {
|
|
5
|
+
cause: unknown;
|
|
6
|
+
});
|
|
7
|
+
}
|
|
8
|
+
export declare class GiteaWebhookMalformedPushPayloadError extends Error {
|
|
9
|
+
readonly issues: unknown;
|
|
10
|
+
constructor(issues: unknown);
|
|
11
|
+
}
|
|
12
|
+
export interface HandleGiteaWebhookParams {
|
|
13
|
+
tx: IntegrationTx;
|
|
14
|
+
deliveryId: string;
|
|
15
|
+
event: string;
|
|
16
|
+
rawBody: string;
|
|
17
|
+
publishSourcePush: PublishSourcePushFn;
|
|
18
|
+
recordDeliveryOnly: RecordDeliveryOnlyFn;
|
|
19
|
+
getIntegrationConnectionById: GetIntegrationConnectionByIdFn;
|
|
20
|
+
}
|
|
21
|
+
export interface HandleGiteaPushParams {
|
|
22
|
+
tx: IntegrationTx;
|
|
23
|
+
deliveryId: string;
|
|
24
|
+
payload: GiteaPushPayloadDto;
|
|
25
|
+
rawPayload: unknown;
|
|
26
|
+
publishSourcePush: PublishSourcePushFn;
|
|
27
|
+
recordDeliveryOnly: RecordDeliveryOnlyFn;
|
|
28
|
+
getIntegrationConnectionById: GetIntegrationConnectionByIdFn;
|
|
29
|
+
}
|
|
30
|
+
export type HandleGiteaPushOutcome = 'published' | 'duplicate' | 'deleted' | 'unknown-org' | 'inactive-connection';
|
|
31
|
+
export type HandleGiteaWebhookOutcome = HandleGiteaPushOutcome | 'recorded-only';
|
|
32
|
+
export declare function handleGiteaWebhook(params: HandleGiteaWebhookParams): Promise<{
|
|
33
|
+
outcome: HandleGiteaWebhookOutcome;
|
|
34
|
+
}>;
|
|
35
|
+
export declare function handleGiteaPush(params: HandleGiteaPushParams): Promise<{
|
|
36
|
+
outcome: HandleGiteaPushOutcome;
|
|
37
|
+
}>;
|
|
38
|
+
//# sourceMappingURL=webhook.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webhook.d.ts","sourceRoot":"","sources":["../../src/core/webhook.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,8BAA8B,EACnC,KAAK,aAAa,EAClB,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EAE1B,MAAM,mCAAmC,CAAC;AAC3C,OAAO,EACL,KAAK,mBAAmB,EAGzB,MAAM,oCAAoC,CAAC;AAQ5C,qBAAa,8BAA+B,SAAQ,KAAK;gBAC3C,OAAO,EAAE;QAAC,KAAK,EAAE,OAAO,CAAA;KAAC;CAItC;AAED,qBAAa,qCAAsC,SAAQ,KAAK;aAClC,MAAM,EAAE,OAAO;gBAAf,MAAM,EAAE,OAAO;CAI5C;AAED,MAAM,WAAW,wBAAwB;IACvC,EAAE,EAAE,aAAa,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,iBAAiB,EAAE,mBAAmB,CAAC;IACvC,kBAAkB,EAAE,oBAAoB,CAAC;IACzC,4BAA4B,EAAE,8BAA8B,CAAC;CAC9D;AAED,MAAM,WAAW,qBAAqB;IACpC,EAAE,EAAE,aAAa,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,mBAAmB,CAAC;IAC7B,UAAU,EAAE,OAAO,CAAC;IACpB,iBAAiB,EAAE,mBAAmB,CAAC;IACvC,kBAAkB,EAAE,oBAAoB,CAAC;IACzC,4BAA4B,EAAE,8BAA8B,CAAC;CAC9D;AAED,MAAM,MAAM,sBAAsB,GAC9B,WAAW,GACX,WAAW,GACX,SAAS,GACT,aAAa,GACb,qBAAqB,CAAC;AAC1B,MAAM,MAAM,yBAAyB,GAAG,sBAAsB,GAAG,eAAe,CAAC;AAMjF,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,wBAAwB,GAC/B,OAAO,CAAC;IAAC,OAAO,EAAE,yBAAyB,CAAA;CAAC,CAAC,CAoB/C;AAED,wBAAsB,eAAe,CACnC,MAAM,EAAE,qBAAqB,GAC5B,OAAO,CAAC;IAAC,OAAO,EAAE,sBAAsB,CAAA;CAAC,CAAC,CAuF5C"}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { buildProviderRepositoryId } from '@shipfox/api-integration-core-dto';
|
|
2
|
+
import { giteaProviderKind, giteaPushPayloadSchema } from '@shipfox/api-integration-gitea-dto';
|
|
3
|
+
import { logger } from '@shipfox/node-opentelemetry';
|
|
4
|
+
import { getGiteaConnectionByOrg } from '#db/connections.js';
|
|
5
|
+
const REFS_HEADS_PREFIX = 'refs/heads/';
|
|
6
|
+
// Gitea sends a `push` webhook for a branch deletion with `after` set to this all-zero SHA.
|
|
7
|
+
const DELETED_BRANCH_SHA = '0'.repeat(40);
|
|
8
|
+
export class GiteaWebhookMalformedJsonError extends Error {
|
|
9
|
+
constructor(options){
|
|
10
|
+
super('Gitea webhook payload is not valid JSON', {
|
|
11
|
+
cause: options.cause
|
|
12
|
+
});
|
|
13
|
+
this.name = 'GiteaWebhookMalformedJsonError';
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export class GiteaWebhookMalformedPushPayloadError extends Error {
|
|
17
|
+
constructor(issues){
|
|
18
|
+
super('Gitea webhook push payload failed schema validation'), this.issues = issues;
|
|
19
|
+
this.name = 'GiteaWebhookMalformedPushPayloadError';
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function isBranchDeletion(after) {
|
|
23
|
+
return after === DELETED_BRANCH_SHA;
|
|
24
|
+
}
|
|
25
|
+
export async function handleGiteaWebhook(params) {
|
|
26
|
+
if (params.event !== 'push') {
|
|
27
|
+
await params.recordDeliveryOnly({
|
|
28
|
+
tx: params.tx,
|
|
29
|
+
provider: giteaProviderKind,
|
|
30
|
+
deliveryId: params.deliveryId
|
|
31
|
+
});
|
|
32
|
+
return {
|
|
33
|
+
outcome: 'recorded-only'
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
const { payload, rawPayload } = parseGiteaPushPayload(params.rawBody);
|
|
37
|
+
return handleGiteaPush({
|
|
38
|
+
tx: params.tx,
|
|
39
|
+
deliveryId: params.deliveryId,
|
|
40
|
+
payload,
|
|
41
|
+
rawPayload,
|
|
42
|
+
publishSourcePush: params.publishSourcePush,
|
|
43
|
+
recordDeliveryOnly: params.recordDeliveryOnly,
|
|
44
|
+
getIntegrationConnectionById: params.getIntegrationConnectionById
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
export async function handleGiteaPush(params) {
|
|
48
|
+
// Dropping deletes before `publishSourcePush` avoids project and trigger fan-out.
|
|
49
|
+
if (isBranchDeletion(params.payload.after)) {
|
|
50
|
+
return {
|
|
51
|
+
outcome: 'deleted'
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
const owner = params.payload.repository.owner.username;
|
|
55
|
+
// Gitea org routes are case-insensitive, and the connect flow stores them lower-cased.
|
|
56
|
+
const giteaConnection = await getGiteaConnectionByOrg(owner.toLowerCase(), {
|
|
57
|
+
tx: params.tx
|
|
58
|
+
});
|
|
59
|
+
if (!giteaConnection) {
|
|
60
|
+
logger().warn({
|
|
61
|
+
deliveryId: params.deliveryId,
|
|
62
|
+
org: owner
|
|
63
|
+
}, 'gitea webhook: unknown org, dropping');
|
|
64
|
+
await params.recordDeliveryOnly({
|
|
65
|
+
tx: params.tx,
|
|
66
|
+
provider: giteaProviderKind,
|
|
67
|
+
deliveryId: params.deliveryId
|
|
68
|
+
});
|
|
69
|
+
return {
|
|
70
|
+
outcome: 'unknown-org'
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
const connection = await params.getIntegrationConnectionById(giteaConnection.connectionId, {
|
|
74
|
+
tx: params.tx
|
|
75
|
+
});
|
|
76
|
+
if (!connection) {
|
|
77
|
+
logger().warn({
|
|
78
|
+
deliveryId: params.deliveryId,
|
|
79
|
+
org: owner,
|
|
80
|
+
connectionId: giteaConnection.connectionId
|
|
81
|
+
}, 'gitea webhook: org has no connection, dropping');
|
|
82
|
+
await params.recordDeliveryOnly({
|
|
83
|
+
tx: params.tx,
|
|
84
|
+
provider: giteaProviderKind,
|
|
85
|
+
deliveryId: params.deliveryId
|
|
86
|
+
});
|
|
87
|
+
return {
|
|
88
|
+
outcome: 'unknown-org'
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
if (connection.lifecycleStatus !== 'active') {
|
|
92
|
+
const logContext = {
|
|
93
|
+
deliveryId: params.deliveryId,
|
|
94
|
+
org: owner,
|
|
95
|
+
connectionId: connection.id,
|
|
96
|
+
workspaceId: connection.workspaceId,
|
|
97
|
+
lifecycleStatus: connection.lifecycleStatus
|
|
98
|
+
};
|
|
99
|
+
// `disabled` is an expected steady state; only `error` is anomalous.
|
|
100
|
+
if (connection.lifecycleStatus === 'error') {
|
|
101
|
+
logger().warn(logContext, 'gitea webhook: connection in error state, dropping');
|
|
102
|
+
} else {
|
|
103
|
+
logger().info(logContext, 'gitea webhook: connection disabled, dropping');
|
|
104
|
+
}
|
|
105
|
+
await params.recordDeliveryOnly({
|
|
106
|
+
tx: params.tx,
|
|
107
|
+
provider: giteaProviderKind,
|
|
108
|
+
deliveryId: params.deliveryId
|
|
109
|
+
});
|
|
110
|
+
return {
|
|
111
|
+
outcome: 'inactive-connection'
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
const ref = stripRefsHeads(params.payload.ref);
|
|
115
|
+
const defaultBranch = params.payload.repository.default_branch;
|
|
116
|
+
const push = {
|
|
117
|
+
// Match the source-control adapter's owner/name repository id.
|
|
118
|
+
externalRepositoryId: buildProviderRepositoryId(giteaProviderKind, `${owner}/${params.payload.repository.name}`),
|
|
119
|
+
ref,
|
|
120
|
+
headCommitSha: params.payload.after,
|
|
121
|
+
defaultBranch,
|
|
122
|
+
isDefaultBranch: ref === defaultBranch
|
|
123
|
+
};
|
|
124
|
+
const result = await params.publishSourcePush({
|
|
125
|
+
tx: params.tx,
|
|
126
|
+
provider: giteaProviderKind,
|
|
127
|
+
source: connection.slug,
|
|
128
|
+
workspaceId: connection.workspaceId,
|
|
129
|
+
connectionId: connection.id,
|
|
130
|
+
connectionName: connection.displayName,
|
|
131
|
+
deliveryId: params.deliveryId,
|
|
132
|
+
receivedAt: new Date().toISOString(),
|
|
133
|
+
rawPayload: params.rawPayload,
|
|
134
|
+
push
|
|
135
|
+
});
|
|
136
|
+
return {
|
|
137
|
+
outcome: result.published ? 'published' : 'duplicate'
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
function stripRefsHeads(ref) {
|
|
141
|
+
return ref.startsWith(REFS_HEADS_PREFIX) ? ref.slice(REFS_HEADS_PREFIX.length) : ref;
|
|
142
|
+
}
|
|
143
|
+
function parseGiteaPushPayload(rawBody) {
|
|
144
|
+
let parsedJson;
|
|
145
|
+
try {
|
|
146
|
+
parsedJson = JSON.parse(rawBody);
|
|
147
|
+
} catch (error) {
|
|
148
|
+
throw new GiteaWebhookMalformedJsonError({
|
|
149
|
+
cause: error
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
const validated = giteaPushPayloadSchema.safeParse(parsedJson);
|
|
153
|
+
if (!validated.success) {
|
|
154
|
+
throw new GiteaWebhookMalformedPushPayloadError(validated.error.issues);
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
payload: validated.data,
|
|
158
|
+
rawPayload: parsedJson
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
//# sourceMappingURL=webhook.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/core/webhook.ts"],"sourcesContent":["import {\n buildProviderRepositoryId,\n type GetIntegrationConnectionByIdFn,\n type IntegrationTx,\n type PublishSourcePushFn,\n type RecordDeliveryOnlyFn,\n type SourcePushPayload,\n} from '@shipfox/api-integration-core-dto';\nimport {\n type GiteaPushPayloadDto,\n giteaProviderKind,\n giteaPushPayloadSchema,\n} from '@shipfox/api-integration-gitea-dto';\nimport {logger} from '@shipfox/node-opentelemetry';\nimport {getGiteaConnectionByOrg} from '#db/connections.js';\n\nconst REFS_HEADS_PREFIX = 'refs/heads/';\n// Gitea sends a `push` webhook for a branch deletion with `after` set to this all-zero SHA.\nconst DELETED_BRANCH_SHA = '0'.repeat(40);\n\nexport class GiteaWebhookMalformedJsonError extends Error {\n constructor(options: {cause: unknown}) {\n super('Gitea webhook payload is not valid JSON', {cause: options.cause});\n this.name = 'GiteaWebhookMalformedJsonError';\n }\n}\n\nexport class GiteaWebhookMalformedPushPayloadError extends Error {\n constructor(public readonly issues: unknown) {\n super('Gitea webhook push payload failed schema validation');\n this.name = 'GiteaWebhookMalformedPushPayloadError';\n }\n}\n\nexport interface HandleGiteaWebhookParams {\n tx: IntegrationTx;\n deliveryId: string;\n event: string;\n rawBody: string;\n publishSourcePush: PublishSourcePushFn;\n recordDeliveryOnly: RecordDeliveryOnlyFn;\n getIntegrationConnectionById: GetIntegrationConnectionByIdFn;\n}\n\nexport interface HandleGiteaPushParams {\n tx: IntegrationTx;\n deliveryId: string;\n payload: GiteaPushPayloadDto;\n rawPayload: unknown;\n publishSourcePush: PublishSourcePushFn;\n recordDeliveryOnly: RecordDeliveryOnlyFn;\n getIntegrationConnectionById: GetIntegrationConnectionByIdFn;\n}\n\nexport type HandleGiteaPushOutcome =\n | 'published'\n | 'duplicate'\n | 'deleted'\n | 'unknown-org'\n | 'inactive-connection';\nexport type HandleGiteaWebhookOutcome = HandleGiteaPushOutcome | 'recorded-only';\n\nfunction isBranchDeletion(after: string): boolean {\n return after === DELETED_BRANCH_SHA;\n}\n\nexport async function handleGiteaWebhook(\n params: HandleGiteaWebhookParams,\n): Promise<{outcome: HandleGiteaWebhookOutcome}> {\n if (params.event !== 'push') {\n await params.recordDeliveryOnly({\n tx: params.tx,\n provider: giteaProviderKind,\n deliveryId: params.deliveryId,\n });\n return {outcome: 'recorded-only'};\n }\n\n const {payload, rawPayload} = parseGiteaPushPayload(params.rawBody);\n return handleGiteaPush({\n tx: params.tx,\n deliveryId: params.deliveryId,\n payload,\n rawPayload,\n publishSourcePush: params.publishSourcePush,\n recordDeliveryOnly: params.recordDeliveryOnly,\n getIntegrationConnectionById: params.getIntegrationConnectionById,\n });\n}\n\nexport async function handleGiteaPush(\n params: HandleGiteaPushParams,\n): Promise<{outcome: HandleGiteaPushOutcome}> {\n // Dropping deletes before `publishSourcePush` avoids project and trigger fan-out.\n if (isBranchDeletion(params.payload.after)) {\n return {outcome: 'deleted'};\n }\n\n const owner = params.payload.repository.owner.username;\n // Gitea org routes are case-insensitive, and the connect flow stores them lower-cased.\n const giteaConnection = await getGiteaConnectionByOrg(owner.toLowerCase(), {tx: params.tx});\n if (!giteaConnection) {\n logger().warn(\n {deliveryId: params.deliveryId, org: owner},\n 'gitea webhook: unknown org, dropping',\n );\n await params.recordDeliveryOnly({\n tx: params.tx,\n provider: giteaProviderKind,\n deliveryId: params.deliveryId,\n });\n return {outcome: 'unknown-org'};\n }\n\n const connection = await params.getIntegrationConnectionById(giteaConnection.connectionId, {\n tx: params.tx,\n });\n if (!connection) {\n logger().warn(\n {deliveryId: params.deliveryId, org: owner, connectionId: giteaConnection.connectionId},\n 'gitea webhook: org has no connection, dropping',\n );\n await params.recordDeliveryOnly({\n tx: params.tx,\n provider: giteaProviderKind,\n deliveryId: params.deliveryId,\n });\n return {outcome: 'unknown-org'};\n }\n\n if (connection.lifecycleStatus !== 'active') {\n const logContext = {\n deliveryId: params.deliveryId,\n org: owner,\n connectionId: connection.id,\n workspaceId: connection.workspaceId,\n lifecycleStatus: connection.lifecycleStatus,\n };\n // `disabled` is an expected steady state; only `error` is anomalous.\n if (connection.lifecycleStatus === 'error') {\n logger().warn(logContext, 'gitea webhook: connection in error state, dropping');\n } else {\n logger().info(logContext, 'gitea webhook: connection disabled, dropping');\n }\n await params.recordDeliveryOnly({\n tx: params.tx,\n provider: giteaProviderKind,\n deliveryId: params.deliveryId,\n });\n return {outcome: 'inactive-connection'};\n }\n\n const ref = stripRefsHeads(params.payload.ref);\n const defaultBranch = params.payload.repository.default_branch;\n const push: SourcePushPayload = {\n // Match the source-control adapter's owner/name repository id.\n externalRepositoryId: buildProviderRepositoryId(\n giteaProviderKind,\n `${owner}/${params.payload.repository.name}`,\n ),\n ref,\n headCommitSha: params.payload.after,\n defaultBranch,\n isDefaultBranch: ref === defaultBranch,\n };\n const result = await params.publishSourcePush({\n tx: params.tx,\n provider: giteaProviderKind,\n source: connection.slug,\n workspaceId: connection.workspaceId,\n connectionId: connection.id,\n connectionName: connection.displayName,\n deliveryId: params.deliveryId,\n receivedAt: new Date().toISOString(),\n rawPayload: params.rawPayload,\n push,\n });\n\n return {outcome: result.published ? 'published' : 'duplicate'};\n}\n\nfunction stripRefsHeads(ref: string): string {\n return ref.startsWith(REFS_HEADS_PREFIX) ? ref.slice(REFS_HEADS_PREFIX.length) : ref;\n}\n\nfunction parseGiteaPushPayload(rawBody: string): {\n payload: GiteaPushPayloadDto;\n rawPayload: unknown;\n} {\n let parsedJson: unknown;\n try {\n parsedJson = JSON.parse(rawBody);\n } catch (error) {\n throw new GiteaWebhookMalformedJsonError({cause: error});\n }\n\n const validated = giteaPushPayloadSchema.safeParse(parsedJson);\n if (!validated.success) {\n throw new GiteaWebhookMalformedPushPayloadError(validated.error.issues);\n }\n\n return {payload: validated.data, rawPayload: parsedJson};\n}\n"],"names":["buildProviderRepositoryId","giteaProviderKind","giteaPushPayloadSchema","logger","getGiteaConnectionByOrg","REFS_HEADS_PREFIX","DELETED_BRANCH_SHA","repeat","GiteaWebhookMalformedJsonError","Error","options","cause","name","GiteaWebhookMalformedPushPayloadError","issues","isBranchDeletion","after","handleGiteaWebhook","params","event","recordDeliveryOnly","tx","provider","deliveryId","outcome","payload","rawPayload","parseGiteaPushPayload","rawBody","handleGiteaPush","publishSourcePush","getIntegrationConnectionById","owner","repository","username","giteaConnection","toLowerCase","warn","org","connection","connectionId","lifecycleStatus","logContext","id","workspaceId","info","ref","stripRefsHeads","defaultBranch","default_branch","push","externalRepositoryId","headCommitSha","isDefaultBranch","result","source","slug","connectionName","displayName","receivedAt","Date","toISOString","published","startsWith","slice","length","parsedJson","JSON","parse","error","validated","safeParse","success","data"],"mappings":"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"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface GiteaConnection {
|
|
2
|
+
id: string;
|
|
3
|
+
connectionId: string;
|
|
4
|
+
org: string;
|
|
5
|
+
createdAt: Date;
|
|
6
|
+
updatedAt: Date;
|
|
7
|
+
}
|
|
8
|
+
export interface UpsertGiteaConnectionParams {
|
|
9
|
+
connectionId: string;
|
|
10
|
+
org: string;
|
|
11
|
+
}
|
|
12
|
+
export declare function upsertGiteaConnection(params: UpsertGiteaConnectionParams, options?: {
|
|
13
|
+
tx?: unknown;
|
|
14
|
+
}): Promise<GiteaConnection>;
|
|
15
|
+
export declare function getGiteaConnectionByOrg(org: string, options?: {
|
|
16
|
+
tx?: unknown;
|
|
17
|
+
}): Promise<GiteaConnection | undefined>;
|
|
18
|
+
export declare function getGiteaConnectionByConnectionId(connectionId: string): Promise<GiteaConnection | undefined>;
|
|
19
|
+
//# sourceMappingURL=connections.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connections.d.ts","sourceRoot":"","sources":["../../src/db/connections.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,YAAY,EAAE,MAAM,CAAC;IACrB,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,IAAI,CAAC;IAChB,SAAS,EAAE,IAAI,CAAC;CACjB;AAED,MAAM,WAAW,2BAA2B;IAC1C,YAAY,EAAE,MAAM,CAAC;IACrB,GAAG,EAAE,MAAM,CAAC;CACb;AAKD,wBAAsB,qBAAqB,CACzC,MAAM,EAAE,2BAA2B,EACnC,OAAO,GAAE;IAAC,EAAE,CAAC,EAAE,OAAO,CAAA;CAAM,GAC3B,OAAO,CAAC,eAAe,CAAC,CA2B1B;AAED,wBAAsB,uBAAuB,CAC3C,GAAG,EAAE,MAAM,EACX,OAAO,GAAE;IAAC,EAAE,CAAC,EAAE,OAAO,CAAA;CAAM,GAC3B,OAAO,CAAC,eAAe,GAAG,SAAS,CAAC,CAUtC;AAED,wBAAsB,gCAAgC,CACpD,YAAY,EAAE,MAAM,GACnB,OAAO,CAAC,eAAe,GAAG,SAAS,CAAC,CAStC"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { eq } from 'drizzle-orm';
|
|
2
|
+
import { GiteaOrgAlreadyLinkedError } from '#core/errors.js';
|
|
3
|
+
import { db } from './db.js';
|
|
4
|
+
import { giteaConnections, toGiteaConnection } from './schema/connections.js';
|
|
5
|
+
export async function upsertGiteaConnection(params, options = {}) {
|
|
6
|
+
const executor = options.tx ?? db();
|
|
7
|
+
const now = new Date();
|
|
8
|
+
const [row] = await executor.insert(giteaConnections).values({
|
|
9
|
+
connectionId: params.connectionId,
|
|
10
|
+
org: params.org
|
|
11
|
+
}).onConflictDoUpdate({
|
|
12
|
+
target: giteaConnections.org,
|
|
13
|
+
// TOCTOU guard: only (re)point this org at the connection that already
|
|
14
|
+
// owns it. A concurrent connect of the same org to a different workspace
|
|
15
|
+
// inserts its own connection row, so its connectionId differs here; the
|
|
16
|
+
// predicate is false, Postgres updates nothing, and the empty RETURNING
|
|
17
|
+
// below rolls the losing transaction back instead of silently repointing
|
|
18
|
+
// the org (cross-tenant connection takeover).
|
|
19
|
+
setWhere: eq(giteaConnections.connectionId, params.connectionId),
|
|
20
|
+
set: {
|
|
21
|
+
connectionId: params.connectionId,
|
|
22
|
+
updatedAt: now
|
|
23
|
+
}
|
|
24
|
+
}).returning();
|
|
25
|
+
if (!row) throw new GiteaOrgAlreadyLinkedError(params.org);
|
|
26
|
+
return toGiteaConnection(row);
|
|
27
|
+
}
|
|
28
|
+
export async function getGiteaConnectionByOrg(org, options = {}) {
|
|
29
|
+
const executor = options.tx ?? db();
|
|
30
|
+
const rows = await executor.select().from(giteaConnections).where(eq(giteaConnections.org, org)).limit(1);
|
|
31
|
+
const row = rows[0];
|
|
32
|
+
if (!row) return undefined;
|
|
33
|
+
return toGiteaConnection(row);
|
|
34
|
+
}
|
|
35
|
+
export async function getGiteaConnectionByConnectionId(connectionId) {
|
|
36
|
+
const rows = await db().select().from(giteaConnections).where(eq(giteaConnections.connectionId, connectionId)).limit(1);
|
|
37
|
+
const row = rows[0];
|
|
38
|
+
if (!row) return undefined;
|
|
39
|
+
return toGiteaConnection(row);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
//# sourceMappingURL=connections.js.map
|