@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.
Files changed (119) hide show
  1. package/.turbo/turbo-build.log +2 -0
  2. package/.turbo/turbo-type$colon$emit.log +1 -0
  3. package/.turbo/turbo-type.log +1 -0
  4. package/CHANGELOG.md +96 -0
  5. package/LICENSE +21 -0
  6. package/README.md +61 -0
  7. package/dist/api/client.d.ts +58 -0
  8. package/dist/api/client.d.ts.map +1 -0
  9. package/dist/api/client.js +225 -0
  10. package/dist/api/client.js.map +1 -0
  11. package/dist/config.d.ts +11 -0
  12. package/dist/config.d.ts.map +1 -0
  13. package/dist/config.js +38 -0
  14. package/dist/config.js.map +1 -0
  15. package/dist/core/connect.d.ts +18 -0
  16. package/dist/core/connect.d.ts.map +1 -0
  17. package/dist/core/connect.js +32 -0
  18. package/dist/core/connect.js.map +1 -0
  19. package/dist/core/connection-url.d.ts +2 -0
  20. package/dist/core/connection-url.d.ts.map +1 -0
  21. package/dist/core/connection-url.js +8 -0
  22. package/dist/core/connection-url.js.map +1 -0
  23. package/dist/core/errors.d.ts +10 -0
  24. package/dist/core/errors.d.ts.map +1 -0
  25. package/dist/core/errors.js +17 -0
  26. package/dist/core/errors.js.map +1 -0
  27. package/dist/core/source-control.d.ts +14 -0
  28. package/dist/core/source-control.d.ts.map +1 -0
  29. package/dist/core/source-control.js +177 -0
  30. package/dist/core/source-control.js.map +1 -0
  31. package/dist/core/webhook.d.ts +38 -0
  32. package/dist/core/webhook.d.ts.map +1 -0
  33. package/dist/core/webhook.js +162 -0
  34. package/dist/core/webhook.js.map +1 -0
  35. package/dist/db/connections.d.ts +19 -0
  36. package/dist/db/connections.d.ts.map +1 -0
  37. package/dist/db/connections.js +42 -0
  38. package/dist/db/connections.js.map +1 -0
  39. package/dist/db/db.d.ts +191 -0
  40. package/dist/db/db.d.ts.map +1 -0
  41. package/dist/db/db.js +18 -0
  42. package/dist/db/db.js.map +1 -0
  43. package/dist/db/migrations.d.ts +2 -0
  44. package/dist/db/migrations.d.ts.map +1 -0
  45. package/dist/db/migrations.js +5 -0
  46. package/dist/db/migrations.js.map +1 -0
  47. package/dist/db/schema/common.d.ts +2 -0
  48. package/dist/db/schema/common.d.ts.map +1 -0
  49. package/dist/db/schema/common.js +4 -0
  50. package/dist/db/schema/common.js.map +1 -0
  51. package/dist/db/schema/connections.d.ts +97 -0
  52. package/dist/db/schema/connections.d.ts.map +1 -0
  53. package/dist/db/schema/connections.js +28 -0
  54. package/dist/db/schema/connections.js.map +1 -0
  55. package/dist/index.d.ts +39 -0
  56. package/dist/index.d.ts.map +1 -0
  57. package/dist/index.js +42 -0
  58. package/dist/index.js.map +1 -0
  59. package/dist/presentation/dto/integrations.d.ts +17 -0
  60. package/dist/presentation/dto/integrations.d.ts.map +1 -0
  61. package/dist/presentation/dto/integrations.js +11 -0
  62. package/dist/presentation/dto/integrations.js.map +1 -0
  63. package/dist/presentation/routes/connections.d.ts +13 -0
  64. package/dist/presentation/routes/connections.d.ts.map +1 -0
  65. package/dist/presentation/routes/connections.js +47 -0
  66. package/dist/presentation/routes/connections.js.map +1 -0
  67. package/dist/presentation/routes/errors.d.ts +2 -0
  68. package/dist/presentation/routes/errors.d.ts.map +1 -0
  69. package/dist/presentation/routes/errors.js +36 -0
  70. package/dist/presentation/routes/errors.js.map +1 -0
  71. package/dist/presentation/routes/webhooks.d.ts +11 -0
  72. package/dist/presentation/routes/webhooks.d.ts.map +1 -0
  73. package/dist/presentation/routes/webhooks.js +109 -0
  74. package/dist/presentation/routes/webhooks.js.map +1 -0
  75. package/dist/tsconfig.test.tsbuildinfo +1 -0
  76. package/drizzle/0000_initial.sql +10 -0
  77. package/drizzle/meta/0000_snapshot.json +96 -0
  78. package/drizzle/meta/_journal.json +13 -0
  79. package/drizzle.config.ts +7 -0
  80. package/package.json +66 -0
  81. package/src/api/client.test.ts +367 -0
  82. package/src/api/client.ts +364 -0
  83. package/src/config.ts +42 -0
  84. package/src/connection-external-url.test.ts +36 -0
  85. package/src/core/connect.test.ts +149 -0
  86. package/src/core/connect.ts +52 -0
  87. package/src/core/connection-url.ts +8 -0
  88. package/src/core/errors.ts +17 -0
  89. package/src/core/source-control-clone-url.test.ts +83 -0
  90. package/src/core/source-control.test.ts +349 -0
  91. package/src/core/source-control.ts +231 -0
  92. package/src/core/webhook.test.ts +396 -0
  93. package/src/core/webhook.ts +203 -0
  94. package/src/db/connections.test.ts +69 -0
  95. package/src/db/connections.ts +80 -0
  96. package/src/db/db.ts +18 -0
  97. package/src/db/migrations.ts +4 -0
  98. package/src/db/schema/common.ts +3 -0
  99. package/src/db/schema/connections.ts +32 -0
  100. package/src/index.test.ts +23 -0
  101. package/src/index.ts +83 -0
  102. package/src/presentation/dto/integrations.ts +14 -0
  103. package/src/presentation/routes/connections.test.ts +200 -0
  104. package/src/presentation/routes/connections.ts +63 -0
  105. package/src/presentation/routes/errors.ts +35 -0
  106. package/src/presentation/routes/webhooks.test.ts +188 -0
  107. package/src/presentation/routes/webhooks.ts +111 -0
  108. package/test/env.ts +11 -0
  109. package/test/factories/gitea-connection.ts +19 -0
  110. package/test/factories/index.ts +1 -0
  111. package/test/fixtures/gitea-webhook.ts +237 -0
  112. package/test/globalSetup.ts +14 -0
  113. package/test/index.ts +6 -0
  114. package/test/setup.ts +17 -0
  115. package/tsconfig.build.json +9 -0
  116. package/tsconfig.build.tsbuildinfo +1 -0
  117. package/tsconfig.json +3 -0
  118. package/tsconfig.test.json +9 -0
  119. 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,2 @@
1
+ export declare function giteaConnectionExternalUrl(externalAccountId: string): string;
2
+ //# sourceMappingURL=connection-url.d.ts.map
@@ -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