@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 @@
|
|
|
1
|
+
$ shipfox-tsc-emit
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
$ shipfox-tsc-check
|
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# @shipfox/api-integration-gitea
|
|
2
|
+
|
|
3
|
+
## 2.0.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 1b0d344: Publishes the complete API runtime closure with packed-consumer-safe internal imports and records its exact package set in application releases.
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- Updated dependencies [1b0d344]
|
|
12
|
+
- Updated dependencies [521e006]
|
|
13
|
+
- @shipfox/api-auth-context@2.0.0
|
|
14
|
+
- @shipfox/api-integration-core-dto@2.0.0
|
|
15
|
+
- @shipfox/api-integration-gitea-dto@2.0.0
|
|
16
|
+
- @shipfox/config@1.2.1
|
|
17
|
+
- @shipfox/node-drizzle@0.2.1
|
|
18
|
+
- @shipfox/node-fastify@0.2.1
|
|
19
|
+
- @shipfox/node-opentelemetry@0.5.0
|
|
20
|
+
- @shipfox/node-postgres@0.4.1
|
|
21
|
+
|
|
22
|
+
## 0.0.2
|
|
23
|
+
|
|
24
|
+
### Patch Changes
|
|
25
|
+
|
|
26
|
+
- Updated dependencies [ec75cd5]
|
|
27
|
+
- Updated dependencies [6a1fb54]
|
|
28
|
+
- @shipfox/node-drizzle@0.2.0
|
|
29
|
+
- @shipfox/node-postgres@0.4.0
|
|
30
|
+
|
|
31
|
+
## 0.0.1
|
|
32
|
+
|
|
33
|
+
### Patch Changes
|
|
34
|
+
|
|
35
|
+
- 58f51bd: Scaffold an empty `gitea` integration provider that mirrors the `github` package structure, ahead of any behavior.
|
|
36
|
+
- New `@shipfox/api-integration-gitea` + `@shipfox/api-integration-gitea-dto` packages: `createGiteaIntegrationProvider()` returns an empty provider (`{provider: 'gitea', displayName: 'Gitea', adapters: {}, routes: []}`), plus a `src/config.ts` documenting the self-hoster variables (`GITEA_BASE_URL`, `GITEA_SERVICE_USERNAME`, `GITEA_SERVICE_TOKEN`, `GITEA_WEBHOOK_SECRET`, `GITEA_WEBHOOK_TARGET_URL`, `GITEA_CHECKOUT_TTL_SECONDS`) and an empty provider database wired with the stable migrations table `__drizzle_migrations_integrations_gitea`.
|
|
37
|
+
- `@shipfox/api-integration-core`: register the Gitea provider behind `INTEGRATIONS_ENABLE_GITEA_PROVIDER` (default false). With the flag enabled, `gitea` appears in `GET /integration-providers`. Dormant scaffold; no runtime behavior yet.
|
|
38
|
+
|
|
39
|
+
- 75f2cc8: Implement the Gitea API client and source-control adapter so the `gitea` provider can read repositories and mint checkout credentials.
|
|
40
|
+
- `GiteaApiClient`: calls `GITEA_BASE_URL` with service-account Basic auth to list org repositories, get a repository, resolve a ref to a commit sha, list a recursive tree, and read base64 file content, under a bounded request timeout. Gitea failures map to a `GiteaIntegrationProviderError` carrying a `reason` (`access-denied`, `repository-not-found`, `file-not-found`, `rate-limited` with `retryAfterSeconds`, `content-too-large`, `malformed-provider-response`, `timeout`, `provider-unavailable`).
|
|
41
|
+
- `GiteaSourceControlProvider`: implements all five `SourceControlProvider` methods over the `gitea:<owner>/<repo>` external id scheme, scoping every request to the connection's own account because the service token is instance-wide. `listFiles` filters the tree by prefix and rejects a truncated tree as `too-many-files`; `fetchFile` enforces `MAX_REPOSITORY_FILE_BYTES`; `createCheckoutSpec` returns the repository's credential-free clone URL with service credentials carried separately.
|
|
42
|
+
- `createGiteaIntegrationProvider()` now exposes the adapter at `adapters.source_control`.
|
|
43
|
+
|
|
44
|
+
- 570ac69: Add the Gitea connection flow so a workspace member can link a Gitea org with a single authenticated request (no OAuth, no redirect).
|
|
45
|
+
- `@shipfox/api-integration-gitea-dto`: `createGiteaConnectionBodySchema` (`{workspace_id, org}`); the response reuses the shared `integrationConnectionDtoSchema`.
|
|
46
|
+
- `@shipfox/api-integration-gitea`: `POST /integrations/gitea/connections` (`AUTH_USER`, workspace membership) canonicalizes the org to lower case (Gitea routes org names case-insensitively, so this keeps the case-sensitive ownership lookup and unique indexes from being bypassed by a case variant), validates the org via the API, rejects an org already linked to another workspace (409), registers an org-level push webhook (`POST /orgs/{org}/hooks` with `GITEA_WEBHOOK_SECRET`/`GITEA_WEBHOOK_TARGET_URL`), and in one transaction upserts the core connection (`provider: 'gitea'`, `externalAccountId: org`) and a new `integrations_gitea_connections` row (`connection_id`/`org` unique, stored `webhook_id`). Re-connecting an already-active org is idempotent and skips a second webhook. Webhook registration itself is idempotent (an existing org hook for the target URL is reused) and compensated (a hook created before a transaction that then rolls back is deleted), so concurrent or retried connects do not leave orphaned hooks. The `GiteaApiClient` gains `organizationExists`, `createOrgPushWebhook`, and `deleteOrgWebhook`, and the provider exposes `connectionExternalUrl` pointing at the org on the Gitea instance.
|
|
47
|
+
- `@shipfox/api-integration-core`: wire the Gitea connect closure (`connectGiteaConnection`) and the cross-tenant lookup (`getExistingGiteaConnection`) into the provider, mirroring `connectGithubInstallation`.
|
|
48
|
+
|
|
49
|
+
Connecting an org creates an active connection visible in `GET /integration-connections` and registers a Gitea webhook whose id is stored.
|
|
50
|
+
|
|
51
|
+
- 857fd73: Receive Gitea push webhooks and trigger runs through the existing source-push pipeline.
|
|
52
|
+
- `@shipfox/api-integration-gitea-dto`: `giteaPushPayloadSchema` (`{ref, after, repository: {name, full_name, default_branch, owner: {username}}}`).
|
|
53
|
+
- `@shipfox/api-integration-gitea`: `POST /webhooks/integrations/gitea` (`auth: []`, `rawBodyPlugin`, standard webhook body limit). It verifies the `X-Gitea-Signature` header as hex HMAC-SHA256 of the raw body keyed by `GITEA_WEBHOOK_SECRET` (401 on mismatch), records non-`push` events for delivery dedup and returns 204, and on a `push` resolves the org (`repository.owner.username`, lower-cased to match the stored org) to its connection, drops branch deletions and unknown orgs, and publishes a normalized `SourcePushPayload` (`externalRepositoryId: gitea:<owner>/<repo>` built from `owner.username`/`repository.name` to match the source-control adapter, `refs/heads/` stripped, `headCommitSha: after`, `isDefaultBranch`). `getGiteaConnectionByOrg` gains an optional transaction executor so the lookup runs inside the publishing transaction. The webhook is registered out of band by the Gitea instance admin, so the connect flow registers nothing.
|
|
54
|
+
- `@shipfox/api-integration-core`: wire `publishSourcePush`, `recordDeliveryOnly`, `getIntegrationConnectionById`, and the core database into the Gitea provider, mirroring GitHub.
|
|
55
|
+
- `@shipfox/node-fastify`: add a shared `verifyHexHmacSignature` helper for hex HMAC-SHA256 webhook signatures.
|
|
56
|
+
- `@shipfox/api-integration-sentry`: `verifySentrySignature` now delegates to the shared helper.
|
|
57
|
+
|
|
58
|
+
A push to a connected org's repo creates a workflow run through the existing pipeline. Duplicate POSTs of the same delivery are deduped via `X-Gitea-Delivery`; note that a manual "Redeliver" in Gitea mints a new delivery id and intentionally produces a fresh run (unlike GitHub, whose redelivery reuses the original id and is suppressed).
|
|
59
|
+
|
|
60
|
+
- 0667cce: Skip publishing source pushes for non-active integration connections. Both the GitHub and Gitea push webhook handlers now treat a connection whose `lifecycleStatus` is not `active` (disabled/error) like an unknown one: the delivery is recorded for dedup but no source-push event is published, so a disabled connection no longer triggers workflow runs.
|
|
61
|
+
- 82d22e4: Make the workspace-membership gate stateless. `requireWorkspaceAccess` now lives in `@shipfox/api-auth-context` and authorizes a request purely from the verified session-token claims, replacing the `requireMembership` gate in `@shipfox/api-workspaces` that read the workspace row from the database on every workspace-scoped request. Membership and role already travel in the token, so the check needs no database access.
|
|
62
|
+
|
|
63
|
+
This removes the per-request database read and severs the runtime dependency on `@shipfox/api-workspaces` from feature modules that only needed the membership gate (integration providers, secrets, projects, agent, runners). Workspace existence and `active`-status enforcement, which no code path currently exercises, moves off the hot path; enforce it at token issuance when workspace suspension is introduced.
|
|
64
|
+
|
|
65
|
+
- Updated dependencies [34ba284]
|
|
66
|
+
- Updated dependencies [5707d6d]
|
|
67
|
+
- Updated dependencies [b9c3f32]
|
|
68
|
+
- Updated dependencies [a81b68c]
|
|
69
|
+
- Updated dependencies [115655e]
|
|
70
|
+
- Updated dependencies [c0a883c]
|
|
71
|
+
- Updated dependencies [72ce351]
|
|
72
|
+
- Updated dependencies [e47f8da]
|
|
73
|
+
- Updated dependencies [ce062a9]
|
|
74
|
+
- Updated dependencies [7b175f5]
|
|
75
|
+
- Updated dependencies [f3614ae]
|
|
76
|
+
- Updated dependencies [f92122b]
|
|
77
|
+
- Updated dependencies [f8f339a]
|
|
78
|
+
- Updated dependencies [58f51bd]
|
|
79
|
+
- Updated dependencies [570ac69]
|
|
80
|
+
- Updated dependencies [857fd73]
|
|
81
|
+
- Updated dependencies [b8e49ff]
|
|
82
|
+
- Updated dependencies [d6d4862]
|
|
83
|
+
- Updated dependencies [c0a883c]
|
|
84
|
+
- Updated dependencies [6077301]
|
|
85
|
+
- Updated dependencies [82d22e4]
|
|
86
|
+
- Updated dependencies [01be723]
|
|
87
|
+
- Updated dependencies [2933c33]
|
|
88
|
+
- Updated dependencies [6181819]
|
|
89
|
+
- @shipfox/node-fastify@0.2.0
|
|
90
|
+
- @shipfox/node-drizzle@0.1.0
|
|
91
|
+
- @shipfox/api-auth-context@0.1.0
|
|
92
|
+
- @shipfox/api-integration-core-dto@0.1.0
|
|
93
|
+
- @shipfox/node-opentelemetry@0.4.2
|
|
94
|
+
- @shipfox/node-postgres@0.3.2
|
|
95
|
+
- @shipfox/api-integration-gitea-dto@0.0.1
|
|
96
|
+
- @shipfox/config@1.2.0
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Shipfox
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Shipfox API Integration Gitea
|
|
2
|
+
|
|
3
|
+
Shipfox API Integration Gitea connects a Gitea organization to a Shipfox
|
|
4
|
+
workspace, receives organization push webhooks, and lets workflow runners check
|
|
5
|
+
out repositories through Gitea's git HTTP endpoint.
|
|
6
|
+
|
|
7
|
+
## Setup
|
|
8
|
+
|
|
9
|
+
Configure the provider in the API environment:
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
GITEA_BASE_URL=https://gitea.example.com
|
|
13
|
+
GITEA_SERVICE_USERNAME=shipfox-bot
|
|
14
|
+
GITEA_SERVICE_TOKEN=
|
|
15
|
+
GITEA_WEBHOOK_SECRET=
|
|
16
|
+
GITEA_CHECKOUT_TTL_SECONDS=300
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
`GITEA_BASE_URL` is the URL the API uses for Gitea REST calls and webhook
|
|
20
|
+
setup.
|
|
21
|
+
|
|
22
|
+
`GITEA_SERVICE_USERNAME` and `GITEA_SERVICE_TOKEN` are handed to runners as
|
|
23
|
+
checkout credentials. The checkout spec keeps these credentials separate from
|
|
24
|
+
the repository URL.
|
|
25
|
+
|
|
26
|
+
`GITEA_WEBHOOK_SECRET` must match the secret configured on the Gitea
|
|
27
|
+
organization webhook.
|
|
28
|
+
|
|
29
|
+
## Clone URL Override
|
|
30
|
+
|
|
31
|
+
By default, checkout uses the clone URL reported by Gitea for each repository.
|
|
32
|
+
Set `GITEA_CLONE_BASE_URL` when runners reach Gitea through a different scheme,
|
|
33
|
+
host, or port than the API does:
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
GITEA_BASE_URL=http://localhost:3000
|
|
37
|
+
GITEA_CLONE_BASE_URL=http://gitea:3000
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
When set, the provider rewrites only the clone URL origin and keeps the
|
|
41
|
+
repository path reported by Gitea. Repository listing, `htmlUrl`, REST API calls,
|
|
42
|
+
and webhooks continue to use `GITEA_BASE_URL`.
|
|
43
|
+
|
|
44
|
+
## Development
|
|
45
|
+
|
|
46
|
+
Run checks for this package:
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
turbo check --filter=@shipfox/api-integration-gitea
|
|
50
|
+
turbo type --filter=@shipfox/api-integration-gitea
|
|
51
|
+
turbo test --filter=@shipfox/api-integration-gitea
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Tests use Vitest and a real PostgreSQL database. Start local services before
|
|
55
|
+
running the test suite:
|
|
56
|
+
|
|
57
|
+
```sh
|
|
58
|
+
docker compose up -d
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The test environment uses the `api_test` database, set in `test/env.ts`.
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export interface GiteaRepository {
|
|
2
|
+
ownerLogin: string;
|
|
3
|
+
name: string;
|
|
4
|
+
fullName: string;
|
|
5
|
+
defaultBranch: string;
|
|
6
|
+
private: boolean;
|
|
7
|
+
cloneUrl: string;
|
|
8
|
+
htmlUrl: string;
|
|
9
|
+
}
|
|
10
|
+
export interface GiteaRepositoryPage {
|
|
11
|
+
repositories: GiteaRepository[];
|
|
12
|
+
nextCursor: string | null;
|
|
13
|
+
}
|
|
14
|
+
export interface GiteaTreeBlob {
|
|
15
|
+
path: string;
|
|
16
|
+
size: number | null;
|
|
17
|
+
}
|
|
18
|
+
export interface GiteaTree {
|
|
19
|
+
blobs: GiteaTreeBlob[];
|
|
20
|
+
truncated: boolean;
|
|
21
|
+
}
|
|
22
|
+
export interface GiteaFileContent {
|
|
23
|
+
path: string;
|
|
24
|
+
content: string;
|
|
25
|
+
size: number;
|
|
26
|
+
}
|
|
27
|
+
export interface GiteaApiClient {
|
|
28
|
+
listOrgRepositories(input: {
|
|
29
|
+
org: string;
|
|
30
|
+
limit: number;
|
|
31
|
+
cursor?: string | undefined;
|
|
32
|
+
}): Promise<GiteaRepositoryPage>;
|
|
33
|
+
getRepository(input: {
|
|
34
|
+
owner: string;
|
|
35
|
+
repo: string;
|
|
36
|
+
}): Promise<GiteaRepository>;
|
|
37
|
+
resolveRef(input: {
|
|
38
|
+
owner: string;
|
|
39
|
+
repo: string;
|
|
40
|
+
ref: string;
|
|
41
|
+
}): Promise<string>;
|
|
42
|
+
listTree(input: {
|
|
43
|
+
owner: string;
|
|
44
|
+
repo: string;
|
|
45
|
+
sha: string;
|
|
46
|
+
}): Promise<GiteaTree>;
|
|
47
|
+
fetchFileContent(input: {
|
|
48
|
+
owner: string;
|
|
49
|
+
repo: string;
|
|
50
|
+
path: string;
|
|
51
|
+
ref: string;
|
|
52
|
+
}): Promise<GiteaFileContent>;
|
|
53
|
+
organizationExists(input: {
|
|
54
|
+
org: string;
|
|
55
|
+
}): Promise<boolean>;
|
|
56
|
+
}
|
|
57
|
+
export declare function createGiteaApiClient(): GiteaApiClient;
|
|
58
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/api/client.ts"],"names":[],"mappings":"AAYA,MAAM,WAAW,eAAe;IAC9B,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,mBAAmB;IAClC,YAAY,EAAE,eAAe,EAAE,CAAC;IAChC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CACrB;AAED,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,aAAa,EAAE,CAAC;IACvB,SAAS,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,cAAc;IAC7B,mBAAmB,CAAC,KAAK,EAAE;QACzB,GAAG,EAAE,MAAM,CAAC;QACZ,KAAK,EAAE,MAAM,CAAC;QACd,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;KAC7B,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAAC;IACjC,aAAa,CAAC,KAAK,EAAE;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAC,GAAG,OAAO,CAAC,eAAe,CAAC,CAAC;IAC9E,UAAU,CAAC,KAAK,EAAE;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAC,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC/E,QAAQ,CAAC,KAAK,EAAE;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAC,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC;IAChF,gBAAgB,CAAC,KAAK,EAAE;QACtB,KAAK,EAAE,MAAM,CAAC;QACd,IAAI,EAAE,MAAM,CAAC;QACb,IAAI,EAAE,MAAM,CAAC;QACb,GAAG,EAAE,MAAM,CAAC;KACb,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAC;IAC9B,kBAAkB,CAAC,KAAK,EAAE;QAAC,GAAG,EAAE,MAAM,CAAA;KAAC,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CAC5D;AAED,wBAAgB,oBAAoB,IAAI,cAAc,CAErD"}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { Buffer } from 'node:buffer';
|
|
2
|
+
import { MAX_REPOSITORY_FILE_BYTES } from '@shipfox/api-integration-core-dto';
|
|
3
|
+
import { config } from '#config.js';
|
|
4
|
+
import { GiteaIntegrationProviderError } from '#core/errors.js';
|
|
5
|
+
const TRAILING_SLASHES_RE = /\/+$/;
|
|
6
|
+
const NEXT_PAGE_RE = /[?&]page=(\d+)/;
|
|
7
|
+
// Recursive tree listing is a single call; a tree larger than this is reported as
|
|
8
|
+
// truncated by Gitea and surfaced to the adapter as `too-many-files`.
|
|
9
|
+
const TREE_PAGE_SIZE = 1000;
|
|
10
|
+
export function createGiteaApiClient() {
|
|
11
|
+
return new HttpGiteaApiClient();
|
|
12
|
+
}
|
|
13
|
+
let HttpGiteaApiClient = class HttpGiteaApiClient {
|
|
14
|
+
async listOrgRepositories(input) {
|
|
15
|
+
const response = await this.request(`orgs/${encodeURIComponent(input.org)}/repos`, {
|
|
16
|
+
page: String(cursorToPage(input.cursor)),
|
|
17
|
+
limit: String(input.limit)
|
|
18
|
+
});
|
|
19
|
+
const data = await response.json();
|
|
20
|
+
if (!Array.isArray(data)) {
|
|
21
|
+
throw new GiteaIntegrationProviderError('malformed-provider-response', 'Gitea repository list response was not an array');
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
repositories: data.map(toGiteaRepository),
|
|
25
|
+
nextCursor: nextCursorFromLink(response.headers.get('link'))
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
async getRepository(input) {
|
|
29
|
+
const response = await this.request(`repos/${encodeURIComponent(input.owner)}/${encodeURIComponent(input.repo)}`);
|
|
30
|
+
return toGiteaRepository(await response.json());
|
|
31
|
+
}
|
|
32
|
+
async resolveRef(input) {
|
|
33
|
+
const response = await this.request(`repos/${encodeURIComponent(input.owner)}/${encodeURIComponent(input.repo)}/commits`, {
|
|
34
|
+
sha: input.ref,
|
|
35
|
+
limit: '1',
|
|
36
|
+
stat: 'false',
|
|
37
|
+
verification: 'false',
|
|
38
|
+
files: 'false'
|
|
39
|
+
});
|
|
40
|
+
const data = await response.json();
|
|
41
|
+
const head = Array.isArray(data) ? data[0] : undefined;
|
|
42
|
+
if (!isRecord(head) || typeof head.sha !== 'string') {
|
|
43
|
+
throw new GiteaIntegrationProviderError('repository-not-found', `Gitea ref ${input.ref} did not resolve to a commit`);
|
|
44
|
+
}
|
|
45
|
+
return head.sha;
|
|
46
|
+
}
|
|
47
|
+
async listTree(input) {
|
|
48
|
+
const response = await this.request(`repos/${encodeURIComponent(input.owner)}/${encodeURIComponent(input.repo)}/git/trees/${encodeURIComponent(input.sha)}`, {
|
|
49
|
+
recursive: 'true',
|
|
50
|
+
per_page: String(TREE_PAGE_SIZE)
|
|
51
|
+
});
|
|
52
|
+
const data = await response.json();
|
|
53
|
+
if (!isRecord(data)) {
|
|
54
|
+
throw new GiteaIntegrationProviderError('malformed-provider-response', 'Gitea tree response was not an object');
|
|
55
|
+
}
|
|
56
|
+
const entries = Array.isArray(data.tree) ? data.tree : [];
|
|
57
|
+
const blobs = [];
|
|
58
|
+
for (const entry of entries){
|
|
59
|
+
if (isRecord(entry) && entry.type === 'blob' && typeof entry.path === 'string') {
|
|
60
|
+
blobs.push({
|
|
61
|
+
path: entry.path,
|
|
62
|
+
size: typeof entry.size === 'number' ? entry.size : null
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
blobs,
|
|
68
|
+
truncated: data.truncated === true
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
async fetchFileContent(input) {
|
|
72
|
+
const response = await this.request(`repos/${encodeURIComponent(input.owner)}/${encodeURIComponent(input.repo)}/contents/${encodePath(input.path)}`, {
|
|
73
|
+
ref: input.ref
|
|
74
|
+
}, {
|
|
75
|
+
notFoundReason: 'file-not-found'
|
|
76
|
+
});
|
|
77
|
+
const data = await response.json();
|
|
78
|
+
// Gitea returns an array for a directory path and an object for a single file;
|
|
79
|
+
// a non-file response means the requested path is not a readable file.
|
|
80
|
+
if (!isRecord(data) || Array.isArray(data) || data.type !== 'file') {
|
|
81
|
+
throw new GiteaIntegrationProviderError('file-not-found', `Gitea path ${input.path} is not a file`);
|
|
82
|
+
}
|
|
83
|
+
// A well-formed Gitea file response always carries a numeric size; coercing a
|
|
84
|
+
// missing size to 0 would skip the limit guard below and decode unbounded content.
|
|
85
|
+
if (typeof data.size !== 'number') {
|
|
86
|
+
throw new GiteaIntegrationProviderError('malformed-provider-response', 'Gitea file response did not include a numeric size');
|
|
87
|
+
}
|
|
88
|
+
const size = data.size;
|
|
89
|
+
if (size > MAX_REPOSITORY_FILE_BYTES) {
|
|
90
|
+
throw new GiteaIntegrationProviderError('content-too-large', 'Gitea file content is larger than the supported limit');
|
|
91
|
+
}
|
|
92
|
+
if (typeof data.content !== 'string' || data.encoding !== 'base64') {
|
|
93
|
+
throw new GiteaIntegrationProviderError('malformed-provider-response', 'Gitea file response did not include base64 content');
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
path: typeof data.path === 'string' ? data.path : input.path,
|
|
97
|
+
size,
|
|
98
|
+
content: Buffer.from(data.content, 'base64').toString('utf8')
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
async organizationExists(input) {
|
|
102
|
+
const response = await this.requestRaw(`orgs/${encodeURIComponent(input.org)}`);
|
|
103
|
+
if (response.ok) return true;
|
|
104
|
+
if (response.status === 404) return false;
|
|
105
|
+
throw giteaHttpError(response);
|
|
106
|
+
}
|
|
107
|
+
async request(path, searchParams = {}, options = {}) {
|
|
108
|
+
const response = await this.requestRaw(path, {
|
|
109
|
+
searchParams
|
|
110
|
+
});
|
|
111
|
+
if (!response.ok) throw giteaHttpError(response, options.notFoundReason ?? 'repository-not-found');
|
|
112
|
+
return response;
|
|
113
|
+
}
|
|
114
|
+
async requestRaw(path, options = {}) {
|
|
115
|
+
const url = new URL(`${this.baseApiUrl()}/${path}`);
|
|
116
|
+
for (const [key, value] of Object.entries(options.searchParams ?? {})){
|
|
117
|
+
url.searchParams.set(key, value);
|
|
118
|
+
}
|
|
119
|
+
const headers = {
|
|
120
|
+
authorization: this.authHeader(),
|
|
121
|
+
accept: 'application/json'
|
|
122
|
+
};
|
|
123
|
+
const init = {
|
|
124
|
+
method: options.method ?? 'GET',
|
|
125
|
+
headers,
|
|
126
|
+
signal: AbortSignal.timeout(config.GITEA_REQUEST_TIMEOUT_MS)
|
|
127
|
+
};
|
|
128
|
+
if (options.body !== undefined) {
|
|
129
|
+
headers['content-type'] = 'application/json';
|
|
130
|
+
init.body = JSON.stringify(options.body);
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
133
|
+
return await fetch(url, init);
|
|
134
|
+
} catch (error) {
|
|
135
|
+
// A bare `fetch` has no request timeout, so an unresponsive Gitea would
|
|
136
|
+
// otherwise hold the worker open indefinitely; the AbortSignal.timeout
|
|
137
|
+
// above surfaces as a fast `timeout` (503) instead of stalling.
|
|
138
|
+
if (error instanceof Error && (error.name === 'TimeoutError' || error.name === 'AbortError')) {
|
|
139
|
+
throw new GiteaIntegrationProviderError('timeout', 'Gitea request timed out');
|
|
140
|
+
}
|
|
141
|
+
throw new GiteaIntegrationProviderError('provider-unavailable', `Gitea request failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
baseApiUrl() {
|
|
145
|
+
if (!this.cachedBaseApiUrl) {
|
|
146
|
+
this.cachedBaseApiUrl = `${config.GITEA_BASE_URL.replace(TRAILING_SLASHES_RE, '')}/api/v1`;
|
|
147
|
+
}
|
|
148
|
+
return this.cachedBaseApiUrl;
|
|
149
|
+
}
|
|
150
|
+
authHeader() {
|
|
151
|
+
if (!this.cachedAuthHeader) {
|
|
152
|
+
const credentials = `${config.GITEA_SERVICE_USERNAME}:${config.GITEA_SERVICE_TOKEN}`;
|
|
153
|
+
this.cachedAuthHeader = `Basic ${Buffer.from(credentials).toString('base64')}`;
|
|
154
|
+
}
|
|
155
|
+
return this.cachedAuthHeader;
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
function giteaHttpError(response, notFoundReason = 'repository-not-found') {
|
|
159
|
+
const status = response.status;
|
|
160
|
+
if (status === 404) {
|
|
161
|
+
return new GiteaIntegrationProviderError(notFoundReason, `Gitea responded ${status}`);
|
|
162
|
+
}
|
|
163
|
+
if (isRateLimited(response)) {
|
|
164
|
+
return new GiteaIntegrationProviderError('rate-limited', `Gitea responded ${status}`, retryAfterSeconds(response));
|
|
165
|
+
}
|
|
166
|
+
if (status === 401 || status === 403) {
|
|
167
|
+
return new GiteaIntegrationProviderError('access-denied', `Gitea responded ${status}`);
|
|
168
|
+
}
|
|
169
|
+
// Server errors and any other unexpected status mean the provider could not
|
|
170
|
+
// serve the request; surface it as unavailable rather than leaking a raw error.
|
|
171
|
+
return new GiteaIntegrationProviderError('provider-unavailable', `Gitea responded ${status}`);
|
|
172
|
+
}
|
|
173
|
+
function isRateLimited(response) {
|
|
174
|
+
if (response.status === 429) return true;
|
|
175
|
+
return response.status === 403 && response.headers.get('x-ratelimit-remaining') === '0';
|
|
176
|
+
}
|
|
177
|
+
function retryAfterSeconds(response) {
|
|
178
|
+
const retryAfter = response.headers.get('retry-after');
|
|
179
|
+
if (!retryAfter) return undefined;
|
|
180
|
+
const parsed = Number.parseInt(retryAfter, 10);
|
|
181
|
+
return Number.isNaN(parsed) ? undefined : parsed;
|
|
182
|
+
}
|
|
183
|
+
function cursorToPage(cursor) {
|
|
184
|
+
if (!cursor) return 1;
|
|
185
|
+
const page = Number.parseInt(cursor, 10);
|
|
186
|
+
return Number.isNaN(page) || page < 1 ? 1 : page;
|
|
187
|
+
}
|
|
188
|
+
function nextCursorFromLink(link) {
|
|
189
|
+
if (!link) return null;
|
|
190
|
+
const next = link.split(',').find((part)=>part.includes('rel="next"'));
|
|
191
|
+
if (!next) return null;
|
|
192
|
+
const match = next.match(NEXT_PAGE_RE);
|
|
193
|
+
return match?.[1] ?? null;
|
|
194
|
+
}
|
|
195
|
+
function encodePath(path) {
|
|
196
|
+
const segments = path.split('/');
|
|
197
|
+
// `..`/`.`/empty segments survive encodeURIComponent and would let `new URL`
|
|
198
|
+
// normalize the request path out of `/contents/` and hit other authenticated
|
|
199
|
+
// Gitea endpoints, so reject them before building the URL.
|
|
200
|
+
for (const segment of segments){
|
|
201
|
+
if (segment === '' || segment === '.' || segment === '..') {
|
|
202
|
+
throw new GiteaIntegrationProviderError('file-not-found', `Gitea path contains an invalid segment: ${path}`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return segments.map(encodeURIComponent).join('/');
|
|
206
|
+
}
|
|
207
|
+
function isRecord(value) {
|
|
208
|
+
return typeof value === 'object' && value !== null;
|
|
209
|
+
}
|
|
210
|
+
function toGiteaRepository(raw) {
|
|
211
|
+
if (!isRecord(raw) || !isRecord(raw.owner) || typeof raw.owner.login !== 'string' || typeof raw.name !== 'string' || typeof raw.full_name !== 'string' || typeof raw.default_branch !== 'string' || typeof raw.clone_url !== 'string' || typeof raw.html_url !== 'string') {
|
|
212
|
+
throw new GiteaIntegrationProviderError('malformed-provider-response', 'Gitea repository response is missing required fields');
|
|
213
|
+
}
|
|
214
|
+
return {
|
|
215
|
+
ownerLogin: raw.owner.login,
|
|
216
|
+
name: raw.name,
|
|
217
|
+
fullName: raw.full_name,
|
|
218
|
+
defaultBranch: raw.default_branch,
|
|
219
|
+
private: raw.private === true,
|
|
220
|
+
cloneUrl: raw.clone_url,
|
|
221
|
+
htmlUrl: raw.html_url
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/api/client.ts"],"sourcesContent":["import {Buffer} from 'node:buffer';\nimport {MAX_REPOSITORY_FILE_BYTES} from '@shipfox/api-integration-core-dto';\nimport {config} from '#config.js';\nimport {GiteaIntegrationProviderError} from '#core/errors.js';\n\nconst TRAILING_SLASHES_RE = /\\/+$/;\nconst NEXT_PAGE_RE = /[?&]page=(\\d+)/;\n\n// Recursive tree listing is a single call; a tree larger than this is reported as\n// truncated by Gitea and surfaced to the adapter as `too-many-files`.\nconst TREE_PAGE_SIZE = 1000;\n\nexport interface GiteaRepository {\n ownerLogin: string;\n name: string;\n fullName: string;\n defaultBranch: string;\n private: boolean;\n cloneUrl: string;\n htmlUrl: string;\n}\n\nexport interface GiteaRepositoryPage {\n repositories: GiteaRepository[];\n nextCursor: string | null;\n}\n\nexport interface GiteaTreeBlob {\n path: string;\n size: number | null;\n}\n\nexport interface GiteaTree {\n blobs: GiteaTreeBlob[];\n truncated: boolean;\n}\n\nexport interface GiteaFileContent {\n path: string;\n content: string;\n size: number;\n}\n\nexport interface GiteaApiClient {\n listOrgRepositories(input: {\n org: string;\n limit: number;\n cursor?: string | undefined;\n }): Promise<GiteaRepositoryPage>;\n getRepository(input: {owner: string; repo: string}): Promise<GiteaRepository>;\n resolveRef(input: {owner: string; repo: string; ref: string}): Promise<string>;\n listTree(input: {owner: string; repo: string; sha: string}): Promise<GiteaTree>;\n fetchFileContent(input: {\n owner: string;\n repo: string;\n path: string;\n ref: string;\n }): Promise<GiteaFileContent>;\n organizationExists(input: {org: string}): Promise<boolean>;\n}\n\nexport function createGiteaApiClient(): GiteaApiClient {\n return new HttpGiteaApiClient();\n}\n\nclass HttpGiteaApiClient implements GiteaApiClient {\n private cachedBaseApiUrl: string | undefined;\n private cachedAuthHeader: string | undefined;\n\n async listOrgRepositories(input: {\n org: string;\n limit: number;\n cursor?: string | undefined;\n }): Promise<GiteaRepositoryPage> {\n const response = await this.request(`orgs/${encodeURIComponent(input.org)}/repos`, {\n page: String(cursorToPage(input.cursor)),\n limit: String(input.limit),\n });\n const data = await response.json();\n if (!Array.isArray(data)) {\n throw new GiteaIntegrationProviderError(\n 'malformed-provider-response',\n 'Gitea repository list response was not an array',\n );\n }\n\n return {\n repositories: data.map(toGiteaRepository),\n nextCursor: nextCursorFromLink(response.headers.get('link')),\n };\n }\n\n async getRepository(input: {owner: string; repo: string}): Promise<GiteaRepository> {\n const response = await this.request(\n `repos/${encodeURIComponent(input.owner)}/${encodeURIComponent(input.repo)}`,\n );\n return toGiteaRepository(await response.json());\n }\n\n async resolveRef(input: {owner: string; repo: string; ref: string}): Promise<string> {\n const response = await this.request(\n `repos/${encodeURIComponent(input.owner)}/${encodeURIComponent(input.repo)}/commits`,\n {sha: input.ref, limit: '1', stat: 'false', verification: 'false', files: 'false'},\n );\n const data = await response.json();\n const head = Array.isArray(data) ? data[0] : undefined;\n if (!isRecord(head) || typeof head.sha !== 'string') {\n throw new GiteaIntegrationProviderError(\n 'repository-not-found',\n `Gitea ref ${input.ref} did not resolve to a commit`,\n );\n }\n return head.sha;\n }\n\n async listTree(input: {owner: string; repo: string; sha: string}): Promise<GiteaTree> {\n const response = await this.request(\n `repos/${encodeURIComponent(input.owner)}/${encodeURIComponent(input.repo)}/git/trees/${encodeURIComponent(input.sha)}`,\n {recursive: 'true', per_page: String(TREE_PAGE_SIZE)},\n );\n const data = await response.json();\n if (!isRecord(data)) {\n throw new GiteaIntegrationProviderError(\n 'malformed-provider-response',\n 'Gitea tree response was not an object',\n );\n }\n\n const entries = Array.isArray(data.tree) ? data.tree : [];\n const blobs: GiteaTreeBlob[] = [];\n for (const entry of entries) {\n if (isRecord(entry) && entry.type === 'blob' && typeof entry.path === 'string') {\n blobs.push({path: entry.path, size: typeof entry.size === 'number' ? entry.size : null});\n }\n }\n\n return {blobs, truncated: data.truncated === true};\n }\n\n async fetchFileContent(input: {\n owner: string;\n repo: string;\n path: string;\n ref: string;\n }): Promise<GiteaFileContent> {\n const response = await this.request(\n `repos/${encodeURIComponent(input.owner)}/${encodeURIComponent(input.repo)}/contents/${encodePath(input.path)}`,\n {ref: input.ref},\n {notFoundReason: 'file-not-found'},\n );\n const data = await response.json();\n\n // Gitea returns an array for a directory path and an object for a single file;\n // a non-file response means the requested path is not a readable file.\n if (!isRecord(data) || Array.isArray(data) || data.type !== 'file') {\n throw new GiteaIntegrationProviderError(\n 'file-not-found',\n `Gitea path ${input.path} is not a file`,\n );\n }\n\n // A well-formed Gitea file response always carries a numeric size; coercing a\n // missing size to 0 would skip the limit guard below and decode unbounded content.\n if (typeof data.size !== 'number') {\n throw new GiteaIntegrationProviderError(\n 'malformed-provider-response',\n 'Gitea file response did not include a numeric size',\n );\n }\n const size = data.size;\n if (size > MAX_REPOSITORY_FILE_BYTES) {\n throw new GiteaIntegrationProviderError(\n 'content-too-large',\n 'Gitea file content is larger than the supported limit',\n );\n }\n if (typeof data.content !== 'string' || data.encoding !== 'base64') {\n throw new GiteaIntegrationProviderError(\n 'malformed-provider-response',\n 'Gitea file response did not include base64 content',\n );\n }\n\n return {\n path: typeof data.path === 'string' ? data.path : input.path,\n size,\n content: Buffer.from(data.content, 'base64').toString('utf8'),\n };\n }\n\n async organizationExists(input: {org: string}): Promise<boolean> {\n const response = await this.requestRaw(`orgs/${encodeURIComponent(input.org)}`);\n if (response.ok) return true;\n if (response.status === 404) return false;\n throw giteaHttpError(response);\n }\n\n private async request(\n path: string,\n searchParams: Record<string, string> = {},\n options: {notFoundReason?: NotFoundReason} = {},\n ): Promise<Response> {\n const response = await this.requestRaw(path, {searchParams});\n if (!response.ok)\n throw giteaHttpError(response, options.notFoundReason ?? 'repository-not-found');\n return response;\n }\n\n private async requestRaw(\n path: string,\n options: {method?: string; searchParams?: Record<string, string>; body?: unknown} = {},\n ): Promise<Response> {\n const url = new URL(`${this.baseApiUrl()}/${path}`);\n for (const [key, value] of Object.entries(options.searchParams ?? {})) {\n url.searchParams.set(key, value);\n }\n\n const headers: Record<string, string> = {\n authorization: this.authHeader(),\n accept: 'application/json',\n };\n const init: RequestInit = {\n method: options.method ?? 'GET',\n headers,\n signal: AbortSignal.timeout(config.GITEA_REQUEST_TIMEOUT_MS),\n };\n if (options.body !== undefined) {\n headers['content-type'] = 'application/json';\n init.body = JSON.stringify(options.body);\n }\n\n try {\n return await fetch(url, init);\n } catch (error) {\n // A bare `fetch` has no request timeout, so an unresponsive Gitea would\n // otherwise hold the worker open indefinitely; the AbortSignal.timeout\n // above surfaces as a fast `timeout` (503) instead of stalling.\n if (\n error instanceof Error &&\n (error.name === 'TimeoutError' || error.name === 'AbortError')\n ) {\n throw new GiteaIntegrationProviderError('timeout', 'Gitea request timed out');\n }\n throw new GiteaIntegrationProviderError(\n 'provider-unavailable',\n `Gitea request failed: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n }\n\n private baseApiUrl(): string {\n if (!this.cachedBaseApiUrl) {\n this.cachedBaseApiUrl = `${config.GITEA_BASE_URL.replace(TRAILING_SLASHES_RE, '')}/api/v1`;\n }\n return this.cachedBaseApiUrl;\n }\n\n private authHeader(): string {\n if (!this.cachedAuthHeader) {\n const credentials = `${config.GITEA_SERVICE_USERNAME}:${config.GITEA_SERVICE_TOKEN}`;\n this.cachedAuthHeader = `Basic ${Buffer.from(credentials).toString('base64')}`;\n }\n return this.cachedAuthHeader;\n }\n}\n\ntype NotFoundReason = 'repository-not-found' | 'file-not-found';\n\nfunction giteaHttpError(\n response: Response,\n notFoundReason: NotFoundReason = 'repository-not-found',\n): GiteaIntegrationProviderError {\n const status = response.status;\n if (status === 404) {\n return new GiteaIntegrationProviderError(notFoundReason, `Gitea responded ${status}`);\n }\n if (isRateLimited(response)) {\n return new GiteaIntegrationProviderError(\n 'rate-limited',\n `Gitea responded ${status}`,\n retryAfterSeconds(response),\n );\n }\n if (status === 401 || status === 403) {\n return new GiteaIntegrationProviderError('access-denied', `Gitea responded ${status}`);\n }\n // Server errors and any other unexpected status mean the provider could not\n // serve the request; surface it as unavailable rather than leaking a raw error.\n return new GiteaIntegrationProviderError('provider-unavailable', `Gitea responded ${status}`);\n}\n\nfunction isRateLimited(response: Response): boolean {\n if (response.status === 429) return true;\n return response.status === 403 && response.headers.get('x-ratelimit-remaining') === '0';\n}\n\nfunction retryAfterSeconds(response: Response): number | undefined {\n const retryAfter = response.headers.get('retry-after');\n if (!retryAfter) return undefined;\n const parsed = Number.parseInt(retryAfter, 10);\n return Number.isNaN(parsed) ? undefined : parsed;\n}\n\nfunction cursorToPage(cursor: string | undefined): number {\n if (!cursor) return 1;\n const page = Number.parseInt(cursor, 10);\n return Number.isNaN(page) || page < 1 ? 1 : page;\n}\n\nfunction nextCursorFromLink(link: string | null): string | null {\n if (!link) return null;\n const next = link.split(',').find((part) => part.includes('rel=\"next\"'));\n if (!next) return null;\n const match = next.match(NEXT_PAGE_RE);\n return match?.[1] ?? null;\n}\n\nfunction encodePath(path: string): string {\n const segments = path.split('/');\n // `..`/`.`/empty segments survive encodeURIComponent and would let `new URL`\n // normalize the request path out of `/contents/` and hit other authenticated\n // Gitea endpoints, so reject them before building the URL.\n for (const segment of segments) {\n if (segment === '' || segment === '.' || segment === '..') {\n throw new GiteaIntegrationProviderError(\n 'file-not-found',\n `Gitea path contains an invalid segment: ${path}`,\n );\n }\n }\n return segments.map(encodeURIComponent).join('/');\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null;\n}\n\nfunction toGiteaRepository(raw: unknown): GiteaRepository {\n if (\n !isRecord(raw) ||\n !isRecord(raw.owner) ||\n typeof raw.owner.login !== 'string' ||\n typeof raw.name !== 'string' ||\n typeof raw.full_name !== 'string' ||\n typeof raw.default_branch !== 'string' ||\n typeof raw.clone_url !== 'string' ||\n typeof raw.html_url !== 'string'\n ) {\n throw new GiteaIntegrationProviderError(\n 'malformed-provider-response',\n 'Gitea repository response is missing required fields',\n );\n }\n\n return {\n ownerLogin: raw.owner.login,\n name: raw.name,\n fullName: raw.full_name,\n defaultBranch: raw.default_branch,\n private: raw.private === true,\n cloneUrl: raw.clone_url,\n htmlUrl: raw.html_url,\n };\n}\n"],"names":["Buffer","MAX_REPOSITORY_FILE_BYTES","config","GiteaIntegrationProviderError","TRAILING_SLASHES_RE","NEXT_PAGE_RE","TREE_PAGE_SIZE","createGiteaApiClient","HttpGiteaApiClient","listOrgRepositories","input","response","request","encodeURIComponent","org","page","String","cursorToPage","cursor","limit","data","json","Array","isArray","repositories","map","toGiteaRepository","nextCursor","nextCursorFromLink","headers","get","getRepository","owner","repo","resolveRef","sha","ref","stat","verification","files","head","undefined","isRecord","listTree","recursive","per_page","entries","tree","blobs","entry","type","path","push","size","truncated","fetchFileContent","encodePath","notFoundReason","content","encoding","from","toString","organizationExists","requestRaw","ok","status","giteaHttpError","searchParams","options","url","URL","baseApiUrl","key","value","Object","set","authorization","authHeader","accept","init","method","signal","AbortSignal","timeout","GITEA_REQUEST_TIMEOUT_MS","body","JSON","stringify","fetch","error","Error","name","message","cachedBaseApiUrl","GITEA_BASE_URL","replace","cachedAuthHeader","credentials","GITEA_SERVICE_USERNAME","GITEA_SERVICE_TOKEN","isRateLimited","retryAfterSeconds","retryAfter","parsed","Number","parseInt","isNaN","link","next","split","find","part","includes","match","segments","segment","join","raw","login","full_name","default_branch","clone_url","html_url","ownerLogin","fullName","defaultBranch","private","cloneUrl","htmlUrl"],"mappings":"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"}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare const config: Readonly<{
|
|
2
|
+
GITEA_BASE_URL: string;
|
|
3
|
+
GITEA_CLONE_BASE_URL: string | undefined;
|
|
4
|
+
GITEA_SERVICE_USERNAME: string;
|
|
5
|
+
GITEA_SERVICE_TOKEN: string;
|
|
6
|
+
GITEA_WEBHOOK_SECRET: string;
|
|
7
|
+
GITEA_CHECKOUT_TTL_SECONDS: number;
|
|
8
|
+
GITEA_REQUEST_TIMEOUT_MS: number;
|
|
9
|
+
} & import("@shipfox/config").CleanedEnvAccessors>;
|
|
10
|
+
export declare const giteaCloneBaseOrigin: URL | undefined;
|
|
11
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,MAAM;;;;;;;;kDAyBjB,CAAC;AAEH,eAAO,MAAM,oBAAoB,iBAAyD,CAAC"}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { createConfig, num, str, url } from '@shipfox/config';
|
|
2
|
+
export const config = createConfig({
|
|
3
|
+
GITEA_BASE_URL: url({
|
|
4
|
+
desc: 'Base URL of the Gitea instance the provider connects to, including the scheme (for example https://gitea.example.com). Required.'
|
|
5
|
+
}),
|
|
6
|
+
GITEA_CLONE_BASE_URL: str({
|
|
7
|
+
desc: 'Base URL runners use when cloning Gitea repositories. Leave it unset to use clone URLs reported by Gitea, or set it when runners reach Gitea through a different scheme, host, or port.',
|
|
8
|
+
default: undefined
|
|
9
|
+
}),
|
|
10
|
+
GITEA_SERVICE_USERNAME: str({
|
|
11
|
+
desc: 'Username of the Gitea service account the provider authenticates as. Required.'
|
|
12
|
+
}),
|
|
13
|
+
GITEA_SERVICE_TOKEN: str({
|
|
14
|
+
desc: 'Access token or bot password of the Gitea service account, used for REST API and git-http basic auth. Required.'
|
|
15
|
+
}),
|
|
16
|
+
GITEA_WEBHOOK_SECRET: str({
|
|
17
|
+
desc: 'Secret used to verify incoming Gitea webhooks. Must match the org webhook secret set by the instance admin. Required.'
|
|
18
|
+
}),
|
|
19
|
+
GITEA_CHECKOUT_TTL_SECONDS: num({
|
|
20
|
+
desc: 'Lifetime in seconds of the checkout credentials handed to the runner. Defaults to 300 (five minutes).',
|
|
21
|
+
default: 300
|
|
22
|
+
}),
|
|
23
|
+
GITEA_REQUEST_TIMEOUT_MS: num({
|
|
24
|
+
desc: 'How long in milliseconds the provider waits for a Gitea REST API response before giving up. A request that exceeds this fails as a timeout instead of holding an API worker open. Defaults to 10000 (ten seconds).',
|
|
25
|
+
default: 10_000
|
|
26
|
+
})
|
|
27
|
+
});
|
|
28
|
+
export const giteaCloneBaseOrigin = parseGiteaCloneBaseOrigin(config.GITEA_CLONE_BASE_URL);
|
|
29
|
+
function parseGiteaCloneBaseOrigin(value) {
|
|
30
|
+
if (!value) return undefined;
|
|
31
|
+
try {
|
|
32
|
+
return new URL(value);
|
|
33
|
+
} catch {
|
|
34
|
+
throw new Error('GITEA_CLONE_BASE_URL must be an absolute URL with a scheme, host, and optional port.');
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/config.ts"],"sourcesContent":["import {createConfig, num, str, url} from '@shipfox/config';\n\nexport const config = createConfig({\n GITEA_BASE_URL: url({\n desc: 'Base URL of the Gitea instance the provider connects to, including the scheme (for example https://gitea.example.com). Required.',\n }),\n GITEA_CLONE_BASE_URL: str({\n desc: 'Base URL runners use when cloning Gitea repositories. Leave it unset to use clone URLs reported by Gitea, or set it when runners reach Gitea through a different scheme, host, or port.',\n default: undefined,\n }),\n GITEA_SERVICE_USERNAME: str({\n desc: 'Username of the Gitea service account the provider authenticates as. Required.',\n }),\n GITEA_SERVICE_TOKEN: str({\n desc: 'Access token or bot password of the Gitea service account, used for REST API and git-http basic auth. Required.',\n }),\n GITEA_WEBHOOK_SECRET: str({\n desc: 'Secret used to verify incoming Gitea webhooks. Must match the org webhook secret set by the instance admin. Required.',\n }),\n GITEA_CHECKOUT_TTL_SECONDS: num({\n desc: 'Lifetime in seconds of the checkout credentials handed to the runner. Defaults to 300 (five minutes).',\n default: 300,\n }),\n GITEA_REQUEST_TIMEOUT_MS: num({\n desc: 'How long in milliseconds the provider waits for a Gitea REST API response before giving up. A request that exceeds this fails as a timeout instead of holding an API worker open. Defaults to 10000 (ten seconds).',\n default: 10_000,\n }),\n});\n\nexport const giteaCloneBaseOrigin = parseGiteaCloneBaseOrigin(config.GITEA_CLONE_BASE_URL);\n\nfunction parseGiteaCloneBaseOrigin(value: string | undefined): URL | undefined {\n if (!value) return undefined;\n\n try {\n return new URL(value);\n } catch {\n throw new Error(\n 'GITEA_CLONE_BASE_URL must be an absolute URL with a scheme, host, and optional port.',\n );\n }\n}\n"],"names":["createConfig","num","str","url","config","GITEA_BASE_URL","desc","GITEA_CLONE_BASE_URL","default","undefined","GITEA_SERVICE_USERNAME","GITEA_SERVICE_TOKEN","GITEA_WEBHOOK_SECRET","GITEA_CHECKOUT_TTL_SECONDS","GITEA_REQUEST_TIMEOUT_MS","giteaCloneBaseOrigin","parseGiteaCloneBaseOrigin","value","URL","Error"],"mappings":"AAAA,SAAQA,YAAY,EAAEC,GAAG,EAAEC,GAAG,EAAEC,GAAG,QAAO,kBAAkB;AAE5D,OAAO,MAAMC,SAASJ,aAAa;IACjCK,gBAAgBF,IAAI;QAClBG,MAAM;IACR;IACAC,sBAAsBL,IAAI;QACxBI,MAAM;QACNE,SAASC;IACX;IACAC,wBAAwBR,IAAI;QAC1BI,MAAM;IACR;IACAK,qBAAqBT,IAAI;QACvBI,MAAM;IACR;IACAM,sBAAsBV,IAAI;QACxBI,MAAM;IACR;IACAO,4BAA4BZ,IAAI;QAC9BK,MAAM;QACNE,SAAS;IACX;IACAM,0BAA0Bb,IAAI;QAC5BK,MAAM;QACNE,SAAS;IACX;AACF,GAAG;AAEH,OAAO,MAAMO,uBAAuBC,0BAA0BZ,OAAOG,oBAAoB,EAAE;AAE3F,SAASS,0BAA0BC,KAAyB;IAC1D,IAAI,CAACA,OAAO,OAAOR;IAEnB,IAAI;QACF,OAAO,IAAIS,IAAID;IACjB,EAAE,OAAM;QACN,MAAM,IAAIE,MACR;IAEJ;AACF"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { IntegrationConnection } from '@shipfox/api-integration-core-dto';
|
|
2
|
+
import type { GiteaApiClient } from '#api/client.js';
|
|
3
|
+
export interface ConnectGiteaConnectionInput {
|
|
4
|
+
workspaceId: string;
|
|
5
|
+
org: string;
|
|
6
|
+
displayName: string;
|
|
7
|
+
}
|
|
8
|
+
export interface HandleGiteaConnectParams {
|
|
9
|
+
gitea: GiteaApiClient;
|
|
10
|
+
workspaceId: string;
|
|
11
|
+
org: string;
|
|
12
|
+
getExistingGiteaConnection: (input: {
|
|
13
|
+
org: string;
|
|
14
|
+
}) => Promise<IntegrationConnection<'gitea'> | undefined>;
|
|
15
|
+
connectGiteaConnection: (input: ConnectGiteaConnectionInput) => Promise<IntegrationConnection<'gitea'>>;
|
|
16
|
+
}
|
|
17
|
+
export declare function handleGiteaConnect(params: HandleGiteaConnectParams): Promise<IntegrationConnection<'gitea'>>;
|
|
18
|
+
//# sourceMappingURL=connect.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connect.d.ts","sourceRoot":"","sources":["../../src/core/connect.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,qBAAqB,EAAC,MAAM,mCAAmC,CAAC;AAC7E,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,gBAAgB,CAAC;AAGnD,MAAM,WAAW,2BAA2B;IAC1C,WAAW,EAAE,MAAM,CAAC;IACpB,GAAG,EAAE,MAAM,CAAC;IACZ,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,wBAAwB;IACvC,KAAK,EAAE,cAAc,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;IACpB,GAAG,EAAE,MAAM,CAAC;IACZ,0BAA0B,EAAE,CAAC,KAAK,EAAE;QAClC,GAAG,EAAE,MAAM,CAAC;KACb,KAAK,OAAO,CAAC,qBAAqB,CAAC,OAAO,CAAC,GAAG,SAAS,CAAC,CAAC;IAC1D,sBAAsB,EAAE,CACtB,KAAK,EAAE,2BAA2B,KAC/B,OAAO,CAAC,qBAAqB,CAAC,OAAO,CAAC,CAAC,CAAC;CAC9C;AAED,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,wBAAwB,GAC/B,OAAO,CAAC,qBAAqB,CAAC,OAAO,CAAC,CAAC,CA2BzC"}
|