repo-sdk 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -8
- package/dist/azure-devops.d.ts +1 -1
- package/dist/azure-devops.js +25 -2
- package/dist/bitbucket.d.ts +1 -1
- package/dist/bitbucket.js +11 -2
- package/dist/git-http.d.ts +23 -0
- package/dist/git-http.js +317 -0
- package/dist/gitea.d.ts +1 -1
- package/dist/gitea.js +11 -2
- package/dist/github.d.ts +1 -1
- package/dist/github.js +34 -2
- package/dist/gitlab.d.ts +1 -1
- package/dist/gitlab.js +11 -2
- package/dist/index.d.ts +5 -2
- package/dist/index.js +116 -45
- package/dist/{pagination-1KJe54LW.js → pagination-CWWHSu8I.js} +9 -3
- package/dist/{verify-CRVfL8D9.js → shared-CuiOaaLD.js} +1 -61
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +13 -1
- package/dist/{types-CwXDjAbE.d.ts → types-CTeiAxMy.d.ts} +22 -4
- package/dist/verify-BBMVxz8q.js +61 -0
- package/package.json +6 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
[](https://www.npmjs.com/package/repo-sdk)
|
|
5
5
|
[](https://github.com/capawesome-team/repo-sdk/blob/main/LICENSE)
|
|
6
6
|
|
|
7
|
-
A unified, normalized, zero-dependency, edge-compatible TypeScript SDK over GitHub, GitLab, Bitbucket Cloud, Azure DevOps, and Gitea. Write your discovery, commit, tag, download, and webhook logic once against one normalized API — built on raw `fetch` and Web Crypto, so it runs on Node, Cloudflare Workers, and other Web-standard runtimes.
|
|
7
|
+
A unified, normalized, zero-dependency, edge-compatible TypeScript SDK over GitHub, GitLab, Bitbucket Cloud, Azure DevOps, and Gitea — plus generic git smart-HTTP remotes. Write your discovery, commit, tag, download, and webhook logic once against one normalized API — built on raw `fetch` and Web Crypto, so it runs on Node, Cloudflare Workers, and other Web-standard runtimes.
|
|
8
8
|
|
|
9
9
|
## Installation
|
|
10
10
|
|
|
@@ -47,13 +47,14 @@ Full documentation lives at **[repo-sdk.dev](https://repo-sdk.dev)** ([docs](htt
|
|
|
47
47
|
|
|
48
48
|
## Providers
|
|
49
49
|
|
|
50
|
-
| Provider | Import | Notes
|
|
51
|
-
| ------------ | ----------------------- |
|
|
52
|
-
| GitHub | `repo-sdk/github` | Token and GitHub App auth
|
|
53
|
-
| GitLab | `repo-sdk/gitlab` | PAT, group/project, or OAuth token
|
|
54
|
-
| Bitbucket | `repo-sdk/bitbucket` | Bitbucket Cloud
|
|
55
|
-
| Azure DevOps | `repo-sdk/azure-devops` | PAT or Entra ID
|
|
56
|
-
| Gitea | `repo-sdk/gitea` | Gitea ≥ 1.20 and Forgejo
|
|
50
|
+
| Provider | Import | Notes |
|
|
51
|
+
| ------------ | ----------------------- | ---------------------------------------------------------------- |
|
|
52
|
+
| GitHub | `repo-sdk/github` | Token and GitHub App auth |
|
|
53
|
+
| GitLab | `repo-sdk/gitlab` | PAT, group/project, or OAuth token |
|
|
54
|
+
| Bitbucket | `repo-sdk/bitbucket` | Bitbucket Cloud |
|
|
55
|
+
| Azure DevOps | `repo-sdk/azure-devops` | PAT or Entra ID |
|
|
56
|
+
| Gitea | `repo-sdk/gitea` | Gitea ≥ 1.20 and Forgejo |
|
|
57
|
+
| Generic git | `repo-sdk/git-http` | Any git smart-HTTP remote (read-only ref discovery + clone URLs) |
|
|
57
58
|
|
|
58
59
|
Self-hosted deployments — GitHub Enterprise Server, GitLab self-managed, Azure DevOps Server, and Gitea/Forgejo instances — are supported via `baseUrl`.
|
|
59
60
|
|
package/dist/azure-devops.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { O as ParsedWebhookEvent, P as RepoProvider, v as IncomingWebhookRequest, z as TokenProvider } from "./types-CTeiAxMy.js";
|
|
2
2
|
import { t as VerifyWebhookParams$1 } from "./verify-p6JsExms.js";
|
|
3
3
|
//#region src/providers/azure-devops/auth.d.ts
|
|
4
4
|
/**
|
package/dist/azure-devops.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { t as RepoError } from "./errors-Cxq_CqYv.js";
|
|
2
|
-
import { n as decodeCursor, r as encodeCursor } from "./pagination-
|
|
3
|
-
import { a as
|
|
2
|
+
import { n as decodeCursor, r as encodeCursor } from "./pagination-CWWHSu8I.js";
|
|
3
|
+
import { a as isRecord, i as filenameFromContentDisposition, n as commitWebUrlBuilder, s as HttpClient } from "./shared-CuiOaaLD.js";
|
|
4
|
+
import { a as headShaOrUndefined, n as timingSafeEqual, r as toIncomingWebhook } from "./verify-BBMVxz8q.js";
|
|
4
5
|
//#region src/providers/azure-devops/auth.ts
|
|
5
6
|
/**
|
|
6
7
|
* Username the SDK registers for a service hook's HTTP Basic auth (the secret is
|
|
@@ -198,6 +199,22 @@ function azureDevOps(options) {
|
|
|
198
199
|
}
|
|
199
200
|
return cached;
|
|
200
201
|
}
|
|
202
|
+
async function getExactRef(repo, fullName, peelTags, signal) {
|
|
203
|
+
const { data } = await jsonApi(`${repoBasePath(repo)}/refs`, {
|
|
204
|
+
query: {
|
|
205
|
+
filter: fullName,
|
|
206
|
+
...peelTags ? { peelTags: true } : {},
|
|
207
|
+
$top: 100
|
|
208
|
+
},
|
|
209
|
+
signal
|
|
210
|
+
});
|
|
211
|
+
const ref = data.value.find((candidate) => candidate.name === `refs/${fullName}`);
|
|
212
|
+
if (!ref) throw new RepoError(`Ref "refs/${fullName}" not found`, {
|
|
213
|
+
code: "not_found",
|
|
214
|
+
provider: "azure-devops"
|
|
215
|
+
});
|
|
216
|
+
return ref;
|
|
217
|
+
}
|
|
201
218
|
return {
|
|
202
219
|
name: "azure-devops",
|
|
203
220
|
capabilities: CAPABILITIES,
|
|
@@ -321,6 +338,12 @@ function azureDevOps(options) {
|
|
|
321
338
|
cursor: token ? encodeCursor("azure-devops", { token }) : void 0
|
|
322
339
|
};
|
|
323
340
|
},
|
|
341
|
+
async getBranch(params) {
|
|
342
|
+
return toBranch(await getExactRef(params.repo, `heads/${params.name}`, false, params.signal));
|
|
343
|
+
},
|
|
344
|
+
async getTag(params) {
|
|
345
|
+
return toTag(await getExactRef(params.repo, `tags/${params.name}`, true, params.signal));
|
|
346
|
+
},
|
|
324
347
|
async searchRefs(params) {
|
|
325
348
|
const refsPath = `${repoBasePath(params.repo)}/refs`;
|
|
326
349
|
return (await Promise.all(["branch", "tag"].filter((type) => params.types.includes(type)).map(async (type) => {
|
package/dist/bitbucket.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { O as ParsedWebhookEvent, P as RepoProvider, v as IncomingWebhookRequest, z as TokenProvider } from "./types-CTeiAxMy.js";
|
|
2
2
|
import { t as VerifyWebhookParams } from "./verify-p6JsExms.js";
|
|
3
3
|
//#region src/providers/bitbucket/index.d.ts
|
|
4
4
|
interface BitbucketProviderOptions {
|
package/dist/bitbucket.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { t as RepoError } from "./errors-Cxq_CqYv.js";
|
|
2
|
-
import { n as decodeCursor, r as encodeCursor, t as assertSameOriginUrl } from "./pagination-
|
|
3
|
-
import { a as
|
|
2
|
+
import { n as decodeCursor, r as encodeCursor, t as assertSameOriginUrl } from "./pagination-CWWHSu8I.js";
|
|
3
|
+
import { a as isRecord, i as filenameFromContentDisposition, n as commitWebUrlBuilder, s as HttpClient, t as clampPerPage } from "./shared-CuiOaaLD.js";
|
|
4
|
+
import { a as headShaOrUndefined, i as verifyHmacSignature, r as toIncomingWebhook } from "./verify-BBMVxz8q.js";
|
|
4
5
|
//#region src/providers/bitbucket/index.ts
|
|
5
6
|
const BASE_URL = "https://api.bitbucket.org/2.0";
|
|
6
7
|
const GIT_HOST = "bitbucket.org";
|
|
@@ -304,6 +305,14 @@ function bitbucket(options) {
|
|
|
304
305
|
cursor: nextCursor(data)
|
|
305
306
|
};
|
|
306
307
|
},
|
|
308
|
+
async getBranch(params) {
|
|
309
|
+
const { data } = await http.json(`${repoPath(params.repo)}/refs/branches/${encodeURIComponent(params.name)}`, { signal: params.signal });
|
|
310
|
+
return toBranch(data);
|
|
311
|
+
},
|
|
312
|
+
async getTag(params) {
|
|
313
|
+
const { data } = await http.json(`${repoPath(params.repo)}/refs/tags/${encodeURIComponent(params.name)}`, { signal: params.signal });
|
|
314
|
+
return toTag(data);
|
|
315
|
+
},
|
|
307
316
|
async searchRefs(params) {
|
|
308
317
|
const endpoints = {
|
|
309
318
|
branch: "branches",
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { P as RepoProvider, z as TokenProvider } from "./types-CTeiAxMy.js";
|
|
2
|
+
//#region src/providers/git-http/index.d.ts
|
|
3
|
+
interface GitHttpProviderOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Basic-auth credentials sent only after the remote answers an anonymous
|
|
6
|
+
* request with a 401 — the same flow the git CLI uses. `username` defaults
|
|
7
|
+
* to `git`; most hosts accept any non-empty username with a token as the
|
|
8
|
+
* password (Bitbucket app passwords require the real username). A
|
|
9
|
+
* `tokenProvider` mints the password per request instead (retried once on
|
|
10
|
+
* a 401).
|
|
11
|
+
*/
|
|
12
|
+
auth?: {
|
|
13
|
+
username?: string;
|
|
14
|
+
password: string;
|
|
15
|
+
} | {
|
|
16
|
+
username?: string;
|
|
17
|
+
tokenProvider: TokenProvider;
|
|
18
|
+
};
|
|
19
|
+
fetch?: typeof fetch;
|
|
20
|
+
}
|
|
21
|
+
declare function gitHttp(options?: GitHttpProviderOptions): RepoProvider;
|
|
22
|
+
//#endregion
|
|
23
|
+
export { type GitHttpProviderOptions, gitHttp };
|
package/dist/git-http.js
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import { t as RepoError } from "./errors-Cxq_CqYv.js";
|
|
2
|
+
import { a as stringToBase64, n as decodeCursor, r as encodeCursor } from "./pagination-CWWHSu8I.js";
|
|
3
|
+
import { s as HttpClient, t as clampPerPage } from "./shared-CuiOaaLD.js";
|
|
4
|
+
//#region src/providers/git-http/index.ts
|
|
5
|
+
const CAPABILITIES = {
|
|
6
|
+
userProfile: false,
|
|
7
|
+
tagDates: false,
|
|
8
|
+
repoSearch: false,
|
|
9
|
+
ownedRepoFilter: false,
|
|
10
|
+
commitUserRef: false,
|
|
11
|
+
refSearch: true,
|
|
12
|
+
webhookEvents: [],
|
|
13
|
+
webhookVerification: "none",
|
|
14
|
+
archiveFormats: []
|
|
15
|
+
};
|
|
16
|
+
const DEFAULT_USERNAME = "git";
|
|
17
|
+
const ZERO_SHA = "0".repeat(40);
|
|
18
|
+
const ADVERTISEMENT_CONTENT_TYPE = "application/x-git-upload-pack-advertisement";
|
|
19
|
+
const BRANCH_PREFIX = "refs/heads/";
|
|
20
|
+
const TAG_PREFIX = "refs/tags/";
|
|
21
|
+
function fail(code, message) {
|
|
22
|
+
throw new RepoError(message, {
|
|
23
|
+
code,
|
|
24
|
+
provider: "git-http"
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
function unsupported(feature) {
|
|
28
|
+
return new RepoError(`git-http remotes do not support ${feature}`, {
|
|
29
|
+
code: "unsupported",
|
|
30
|
+
provider: "git-http"
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Validates and normalizes a remote URL: absolute http(s), no embedded
|
|
35
|
+
* credentials, no trailing slashes. The `repo` parameter of every method is
|
|
36
|
+
* such a URL — a generic git remote has no owner/name coordinates.
|
|
37
|
+
*/
|
|
38
|
+
function normalizeRepoUrl(repo) {
|
|
39
|
+
if (/^[^/?#]+@/.test(repo) || /^(ssh|git):\/\//i.test(repo)) fail("validation", "SSH and git-protocol remotes are not supported; pass an http(s) remote URL");
|
|
40
|
+
let url;
|
|
41
|
+
try {
|
|
42
|
+
url = new URL(repo);
|
|
43
|
+
} catch {
|
|
44
|
+
fail("validation", `Invalid repository URL: expected an absolute http(s) URL`);
|
|
45
|
+
}
|
|
46
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") fail("validation", `Unsupported URL protocol "${url.protocol}"; use http(s)`);
|
|
47
|
+
if (url.username || url.password) fail("validation", "Pass credentials via the auth option, not embedded in the repository URL");
|
|
48
|
+
url.hash = "";
|
|
49
|
+
url.pathname = url.pathname.replace(/\/+$/, "");
|
|
50
|
+
return url.toString();
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Splits a smart-HTTP response into pkt-line payloads. Lengths are byte
|
|
54
|
+
* counts, so the buffer is sliced before decoding — a string-based parse
|
|
55
|
+
* would break on multi-byte ref names.
|
|
56
|
+
*/
|
|
57
|
+
function parsePktLines(bytes) {
|
|
58
|
+
const decoder = new TextDecoder();
|
|
59
|
+
const lines = [];
|
|
60
|
+
let offset = 0;
|
|
61
|
+
while (offset + 4 <= bytes.length) {
|
|
62
|
+
const header = decoder.decode(bytes.subarray(offset, offset + 4));
|
|
63
|
+
if (!/^[0-9a-f]{4}$/i.test(header)) fail("provider_error", "Malformed pkt-line in git-upload-pack advertisement");
|
|
64
|
+
const length = Number.parseInt(header, 16);
|
|
65
|
+
if (length === 0) {
|
|
66
|
+
offset += 4;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (length < 4 || offset + length > bytes.length) fail("provider_error", "Truncated pkt-line in git-upload-pack advertisement");
|
|
70
|
+
let payload = bytes.subarray(offset + 4, offset + length);
|
|
71
|
+
if (payload.at(-1) === 10) payload = payload.subarray(0, payload.length - 1);
|
|
72
|
+
lines.push(decoder.decode(payload));
|
|
73
|
+
offset += length;
|
|
74
|
+
}
|
|
75
|
+
return lines;
|
|
76
|
+
}
|
|
77
|
+
function parseAdvertisement(lines) {
|
|
78
|
+
if (lines[0] !== "# service=git-upload-pack") fail("provider_error", "Remote did not return a git-upload-pack advertisement");
|
|
79
|
+
const refs = [];
|
|
80
|
+
const peeled = /* @__PURE__ */ new Map();
|
|
81
|
+
const symrefs = /* @__PURE__ */ new Map();
|
|
82
|
+
let capabilities = [];
|
|
83
|
+
for (const line of lines.slice(1)) {
|
|
84
|
+
if (line === "") continue;
|
|
85
|
+
const nulIndex = line.indexOf("\0");
|
|
86
|
+
const record = nulIndex === -1 ? line : line.slice(0, nulIndex);
|
|
87
|
+
if (nulIndex !== -1) {
|
|
88
|
+
capabilities = line.slice(nulIndex + 1).split(" ").filter(Boolean);
|
|
89
|
+
for (const capability of capabilities) if (capability.startsWith("symref=")) {
|
|
90
|
+
const separator = capability.indexOf(":");
|
|
91
|
+
if (separator > 7) symrefs.set(capability.slice(7, separator), capability.slice(separator + 1));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const space = record.indexOf(" ");
|
|
95
|
+
if (space === -1) continue;
|
|
96
|
+
const sha = record.slice(0, space);
|
|
97
|
+
const name = record.slice(space + 1);
|
|
98
|
+
if (sha === ZERO_SHA && name === "capabilities^{}") continue;
|
|
99
|
+
if (name.endsWith("^{}")) {
|
|
100
|
+
peeled.set(name.slice(0, -3), sha);
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
refs.push({
|
|
104
|
+
name,
|
|
105
|
+
sha
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
refs,
|
|
110
|
+
peeled,
|
|
111
|
+
symrefs,
|
|
112
|
+
capabilities
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
function branchesOf(advertisement) {
|
|
116
|
+
return advertisement.refs.filter((ref) => ref.name.startsWith(BRANCH_PREFIX)).map((ref) => ({
|
|
117
|
+
name: ref.name.slice(11),
|
|
118
|
+
sha: ref.sha,
|
|
119
|
+
raw: ref
|
|
120
|
+
}));
|
|
121
|
+
}
|
|
122
|
+
function tagsOf(advertisement) {
|
|
123
|
+
return advertisement.refs.filter((ref) => ref.name.startsWith(TAG_PREFIX)).map((ref) => {
|
|
124
|
+
const peeledSha = advertisement.peeled.get(ref.name);
|
|
125
|
+
return {
|
|
126
|
+
name: ref.name.slice(10),
|
|
127
|
+
sha: peeledSha ?? ref.sha,
|
|
128
|
+
isAnnotated: peeledSha !== void 0,
|
|
129
|
+
raw: peeledSha === void 0 ? ref : {
|
|
130
|
+
...ref,
|
|
131
|
+
peeledSha
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
function mapError(_status, body) {
|
|
137
|
+
const text = typeof body === "string" ? body.trim() : "";
|
|
138
|
+
return { message: text && text.length <= 200 ? `git-http request failed: ${text}` : void 0 };
|
|
139
|
+
}
|
|
140
|
+
function gitHttp(options = {}) {
|
|
141
|
+
const fetchImpl = options.fetch ?? fetch;
|
|
142
|
+
const auth = options.auth;
|
|
143
|
+
const username = auth?.username || DEFAULT_USERNAME;
|
|
144
|
+
const authRequired = /* @__PURE__ */ new Set();
|
|
145
|
+
let lastSecret = auth && "password" in auth ? auth.password : void 0;
|
|
146
|
+
async function currentSecret(forceRefresh) {
|
|
147
|
+
if (auth === void 0 || "password" in auth) return lastSecret ?? "";
|
|
148
|
+
lastSecret = await auth.tokenProvider({ forceRefresh });
|
|
149
|
+
return lastSecret;
|
|
150
|
+
}
|
|
151
|
+
function httpFor(repoUrl) {
|
|
152
|
+
return new HttpClient({
|
|
153
|
+
provider: "git-http",
|
|
154
|
+
baseUrl: repoUrl,
|
|
155
|
+
fetchImpl,
|
|
156
|
+
authHeaders: async ({ forceRefresh }) => {
|
|
157
|
+
if (!auth) return {};
|
|
158
|
+
const hadAuth = authRequired.has(repoUrl);
|
|
159
|
+
if (!forceRefresh && !hadAuth) return {};
|
|
160
|
+
authRequired.add(repoUrl);
|
|
161
|
+
const secret = await currentSecret(forceRefresh && hadAuth);
|
|
162
|
+
return { Authorization: `Basic ${stringToBase64(`${username}:${secret}`)}` };
|
|
163
|
+
},
|
|
164
|
+
mapError,
|
|
165
|
+
secrets: () => lastSecret === void 0 ? [] : [lastSecret],
|
|
166
|
+
retryUnauthorized: auth !== void 0
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
async function fetchAdvertisement(repoUrl, signal) {
|
|
170
|
+
const response = await httpFor(repoUrl).raw(`${repoUrl}/info/refs`, {
|
|
171
|
+
query: { service: "git-upload-pack" },
|
|
172
|
+
signal
|
|
173
|
+
});
|
|
174
|
+
if (!(response.headers.get("content-type") ?? "").startsWith(ADVERTISEMENT_CONTENT_TYPE)) fail("provider_error", "Remote is not a git smart-HTTP endpoint (unexpected info/refs content type)");
|
|
175
|
+
return parseAdvertisement(parsePktLines(new Uint8Array(await response.arrayBuffer())));
|
|
176
|
+
}
|
|
177
|
+
function paginate(repoUrl, items, params) {
|
|
178
|
+
let offset = 0;
|
|
179
|
+
if (params.cursor !== void 0) {
|
|
180
|
+
const state = decodeCursor("git-http", params.cursor);
|
|
181
|
+
if (state.u !== repoUrl) fail("validation", "Pagination cursor belongs to a different repository");
|
|
182
|
+
offset = state.o;
|
|
183
|
+
}
|
|
184
|
+
const limit = clampPerPage(params.limit);
|
|
185
|
+
const data = limit === void 0 ? items.slice(offset) : items.slice(offset, offset + limit);
|
|
186
|
+
const nextOffset = offset + data.length;
|
|
187
|
+
return {
|
|
188
|
+
data,
|
|
189
|
+
cursor: nextOffset < items.length ? encodeCursor("git-http", {
|
|
190
|
+
u: repoUrl,
|
|
191
|
+
o: nextOffset
|
|
192
|
+
}) : void 0
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
return {
|
|
196
|
+
name: "git-http",
|
|
197
|
+
capabilities: CAPABILITIES,
|
|
198
|
+
async getAuthenticatedUser() {
|
|
199
|
+
throw unsupported("resolving the authenticated user");
|
|
200
|
+
},
|
|
201
|
+
async listNamespaces() {
|
|
202
|
+
throw unsupported("namespace discovery");
|
|
203
|
+
},
|
|
204
|
+
async listRepositories() {
|
|
205
|
+
throw unsupported("repository discovery");
|
|
206
|
+
},
|
|
207
|
+
async getRepository(params) {
|
|
208
|
+
const repoUrl = normalizeRepoUrl(params.repo);
|
|
209
|
+
const advertisement = await fetchAdvertisement(repoUrl, params.signal);
|
|
210
|
+
const url = new URL(repoUrl);
|
|
211
|
+
const path = url.pathname.replace(/^\/+/, "").replace(/\.git$/, "");
|
|
212
|
+
const segments = path.split("/").filter(Boolean);
|
|
213
|
+
const head = advertisement.symrefs.get("HEAD");
|
|
214
|
+
let defaultBranch = head?.startsWith(BRANCH_PREFIX) ? head.slice(11) : void 0;
|
|
215
|
+
if (defaultBranch === void 0) {
|
|
216
|
+
const headSha = advertisement.refs.find((ref) => ref.name === "HEAD")?.sha;
|
|
217
|
+
const matches = branchesOf(advertisement).filter((branch) => branch.sha === headSha);
|
|
218
|
+
if (headSha !== void 0 && matches.length === 1) defaultBranch = matches[0].name;
|
|
219
|
+
}
|
|
220
|
+
return {
|
|
221
|
+
id: repoUrl,
|
|
222
|
+
name: segments.at(-1) ?? url.host,
|
|
223
|
+
path,
|
|
224
|
+
namespace: segments.slice(0, -1).join("/"),
|
|
225
|
+
defaultBranch,
|
|
226
|
+
private: authRequired.has(repoUrl),
|
|
227
|
+
urls: {
|
|
228
|
+
web: repoUrl.replace(/\.git$/, ""),
|
|
229
|
+
cloneHttp: repoUrl
|
|
230
|
+
},
|
|
231
|
+
raw: {
|
|
232
|
+
refs: advertisement.refs,
|
|
233
|
+
peeled: Object.fromEntries(advertisement.peeled),
|
|
234
|
+
capabilities: advertisement.capabilities
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
},
|
|
238
|
+
async listCommits() {
|
|
239
|
+
throw unsupported("commit history");
|
|
240
|
+
},
|
|
241
|
+
async getCommit() {
|
|
242
|
+
throw unsupported("commit lookup");
|
|
243
|
+
},
|
|
244
|
+
async listTags(params) {
|
|
245
|
+
const repoUrl = normalizeRepoUrl(params.repo);
|
|
246
|
+
return paginate(repoUrl, tagsOf(await fetchAdvertisement(repoUrl, params.signal)), params);
|
|
247
|
+
},
|
|
248
|
+
async listBranches(params) {
|
|
249
|
+
const repoUrl = normalizeRepoUrl(params.repo);
|
|
250
|
+
return paginate(repoUrl, branchesOf(await fetchAdvertisement(repoUrl, params.signal)), params);
|
|
251
|
+
},
|
|
252
|
+
async getBranch(params) {
|
|
253
|
+
const branch = branchesOf(await fetchAdvertisement(normalizeRepoUrl(params.repo), params.signal)).find((candidate) => candidate.name === params.name);
|
|
254
|
+
if (branch === void 0) throw new RepoError(`Branch "${params.name}" not found`, {
|
|
255
|
+
code: "not_found",
|
|
256
|
+
provider: "git-http"
|
|
257
|
+
});
|
|
258
|
+
return branch;
|
|
259
|
+
},
|
|
260
|
+
async getTag(params) {
|
|
261
|
+
const tag = tagsOf(await fetchAdvertisement(normalizeRepoUrl(params.repo), params.signal)).find((candidate) => candidate.name === params.name);
|
|
262
|
+
if (tag === void 0) throw new RepoError(`Tag "${params.name}" not found`, {
|
|
263
|
+
code: "not_found",
|
|
264
|
+
provider: "git-http"
|
|
265
|
+
});
|
|
266
|
+
return tag;
|
|
267
|
+
},
|
|
268
|
+
async searchRefs(params) {
|
|
269
|
+
const advertisement = await fetchAdvertisement(normalizeRepoUrl(params.repo), params.signal);
|
|
270
|
+
const matches = [];
|
|
271
|
+
for (const type of ["branch", "tag"]) {
|
|
272
|
+
if (!params.types.includes(type)) continue;
|
|
273
|
+
const prefix = type === "branch" ? BRANCH_PREFIX : TAG_PREFIX;
|
|
274
|
+
for (const ref of advertisement.refs) {
|
|
275
|
+
if (!ref.name.startsWith(prefix)) continue;
|
|
276
|
+
const name = ref.name.slice(prefix.length);
|
|
277
|
+
if (!name.startsWith(params.query)) continue;
|
|
278
|
+
matches.push({
|
|
279
|
+
type,
|
|
280
|
+
name,
|
|
281
|
+
sha: ref.sha,
|
|
282
|
+
raw: ref
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return matches.slice(0, params.limit);
|
|
287
|
+
},
|
|
288
|
+
async downloadArchive() {
|
|
289
|
+
throw unsupported("archive downloads");
|
|
290
|
+
},
|
|
291
|
+
async getCloneUrl(params) {
|
|
292
|
+
const repoUrl = normalizeRepoUrl(params.repo);
|
|
293
|
+
if (!auth) return { url: repoUrl };
|
|
294
|
+
const url = new URL(repoUrl);
|
|
295
|
+
url.username = username;
|
|
296
|
+
url.password = await currentSecret(false);
|
|
297
|
+
return { url: url.toString() };
|
|
298
|
+
},
|
|
299
|
+
async createWebhook() {
|
|
300
|
+
throw unsupported("webhooks");
|
|
301
|
+
},
|
|
302
|
+
async listWebhooks() {
|
|
303
|
+
throw unsupported("webhooks");
|
|
304
|
+
},
|
|
305
|
+
async getWebhook() {
|
|
306
|
+
throw unsupported("webhooks");
|
|
307
|
+
},
|
|
308
|
+
async updateWebhook() {
|
|
309
|
+
throw unsupported("webhooks");
|
|
310
|
+
},
|
|
311
|
+
async deleteWebhook() {
|
|
312
|
+
throw unsupported("webhooks");
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
//#endregion
|
|
317
|
+
export { gitHttp };
|
package/dist/gitea.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { O as ParsedWebhookEvent, P as RepoProvider, v as IncomingWebhookRequest, z as TokenProvider } from "./types-CTeiAxMy.js";
|
|
2
2
|
import { t as VerifyWebhookParams } from "./verify-p6JsExms.js";
|
|
3
3
|
//#region src/providers/gitea/index.d.ts
|
|
4
4
|
interface GiteaProviderOptions {
|
package/dist/gitea.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { t as RepoError } from "./errors-Cxq_CqYv.js";
|
|
2
|
-
import { n as decodeCursor, r as encodeCursor, t as assertSameOriginUrl } from "./pagination-
|
|
3
|
-
import { a as
|
|
2
|
+
import { n as decodeCursor, r as encodeCursor, t as assertSameOriginUrl } from "./pagination-CWWHSu8I.js";
|
|
3
|
+
import { a as isRecord, i as filenameFromContentDisposition, n as commitWebUrlBuilder, o as parseLinkNext, r as encodeRefPath, s as HttpClient, t as clampPerPage } from "./shared-CuiOaaLD.js";
|
|
4
|
+
import { a as headShaOrUndefined, n as timingSafeEqual, r as toIncomingWebhook, t as hmacSha256Hex } from "./verify-BBMVxz8q.js";
|
|
4
5
|
//#region src/providers/gitea/index.ts
|
|
5
6
|
const DEFAULT_BASE_URL = "https://gitea.com/api/v1";
|
|
6
7
|
const CAPABILITIES = {
|
|
@@ -354,6 +355,14 @@ function gitea(options) {
|
|
|
354
355
|
cursor: nextCursor(response)
|
|
355
356
|
};
|
|
356
357
|
},
|
|
358
|
+
async getBranch(params) {
|
|
359
|
+
const { data } = await http.json(`${repoPath(params.repo)}/branches/${encodeRefPath(params.name)}`, { signal: params.signal });
|
|
360
|
+
return toBranch(data);
|
|
361
|
+
},
|
|
362
|
+
async getTag(params) {
|
|
363
|
+
const { data } = await http.json(`${repoPath(params.repo)}/tags/${encodeRefPath(params.name)}`, { signal: params.signal });
|
|
364
|
+
return toTag(data);
|
|
365
|
+
},
|
|
357
366
|
async searchRefs(params) {
|
|
358
367
|
const namespaces = {
|
|
359
368
|
branch: "heads",
|
package/dist/github.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { O as ParsedWebhookEvent, P as RepoProvider, v as IncomingWebhookRequest, z as TokenProvider } from "./types-CTeiAxMy.js";
|
|
2
2
|
import { t as VerifyWebhookParams } from "./verify-p6JsExms.js";
|
|
3
3
|
//#region src/providers/github/app-auth.d.ts
|
|
4
4
|
interface InstallationToken {
|
package/dist/github.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { n as codeFromStatus, t as RepoError } from "./errors-Cxq_CqYv.js";
|
|
2
|
-
import { i as bytesToBase64Url, n as decodeCursor, r as encodeCursor, t as assertSameOriginUrl } from "./pagination-
|
|
3
|
-
import { a as
|
|
2
|
+
import { i as bytesToBase64Url, n as decodeCursor, r as encodeCursor, t as assertSameOriginUrl } from "./pagination-CWWHSu8I.js";
|
|
3
|
+
import { a as isRecord, i as filenameFromContentDisposition, n as commitWebUrlBuilder, o as parseLinkNext, r as encodeRefPath, s as HttpClient, t as clampPerPage } from "./shared-CuiOaaLD.js";
|
|
4
|
+
import { a as headShaOrUndefined, i as verifyHmacSignature, r as toIncomingWebhook } from "./verify-BBMVxz8q.js";
|
|
4
5
|
//#region src/providers/github/app-auth.ts
|
|
5
6
|
const RSA_ALGORITHM_IDENTIFIER = [
|
|
6
7
|
48,
|
|
@@ -695,6 +696,37 @@ function github(options) {
|
|
|
695
696
|
cursor: nextCursor(response)
|
|
696
697
|
};
|
|
697
698
|
},
|
|
699
|
+
async getBranch(params) {
|
|
700
|
+
const { data } = await http.json(`${repoPath(params.repo)}/branches/${encodeRefPath(params.name)}`, { signal: params.signal });
|
|
701
|
+
return toBranch(data);
|
|
702
|
+
},
|
|
703
|
+
async getTag(params) {
|
|
704
|
+
const { data: ref } = await http.json(`${repoPath(params.repo)}/git/ref/tags/${encodeRefPath(params.name)}`, { signal: params.signal });
|
|
705
|
+
const name = ref.ref.replace(/^refs\/tags\//, "");
|
|
706
|
+
if (ref.object.type !== "tag") return {
|
|
707
|
+
name,
|
|
708
|
+
sha: ref.object.sha,
|
|
709
|
+
isAnnotated: false,
|
|
710
|
+
raw: ref
|
|
711
|
+
};
|
|
712
|
+
let sha = ref.object.sha;
|
|
713
|
+
for (let hops = 0; hops < 5; hops++) {
|
|
714
|
+
const { data } = await http.json(`${repoPath(params.repo)}/git/tags/${sha}`, { signal: params.signal });
|
|
715
|
+
sha = data.object.sha;
|
|
716
|
+
if (data.object.type !== "tag") return {
|
|
717
|
+
name,
|
|
718
|
+
sha,
|
|
719
|
+
message: data.message ?? void 0,
|
|
720
|
+
date: data.tagger?.date ? new Date(data.tagger.date) : void 0,
|
|
721
|
+
isAnnotated: true,
|
|
722
|
+
raw: data
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
throw new RepoError(`Tag ${name} points at a tag chain deeper than 5 objects`, {
|
|
726
|
+
code: "provider_error",
|
|
727
|
+
provider: "github"
|
|
728
|
+
});
|
|
729
|
+
},
|
|
698
730
|
async searchRefs(params) {
|
|
699
731
|
const namespaces = {
|
|
700
732
|
branch: "heads",
|
package/dist/gitlab.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { O as ParsedWebhookEvent, P as RepoProvider, v as IncomingWebhookRequest, z as TokenProvider } from "./types-CTeiAxMy.js";
|
|
2
2
|
import { t as VerifyWebhookParams } from "./verify-p6JsExms.js";
|
|
3
3
|
//#region src/providers/gitlab/index.d.ts
|
|
4
4
|
interface GitLabProviderOptions {
|
package/dist/gitlab.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { t as RepoError } from "./errors-Cxq_CqYv.js";
|
|
2
|
-
import { n as decodeCursor, r as encodeCursor, t as assertSameOriginUrl } from "./pagination-
|
|
3
|
-
import { a as
|
|
2
|
+
import { n as decodeCursor, r as encodeCursor, t as assertSameOriginUrl } from "./pagination-CWWHSu8I.js";
|
|
3
|
+
import { a as isRecord, i as filenameFromContentDisposition, n as commitWebUrlBuilder, o as parseLinkNext, s as HttpClient, t as clampPerPage } from "./shared-CuiOaaLD.js";
|
|
4
|
+
import { a as headShaOrUndefined, n as timingSafeEqual, r as toIncomingWebhook } from "./verify-BBMVxz8q.js";
|
|
4
5
|
//#region src/providers/gitlab/index.ts
|
|
5
6
|
const DEFAULT_BASE_URL = "https://gitlab.com/api/v4";
|
|
6
7
|
const CAPABILITIES = {
|
|
@@ -377,6 +378,14 @@ function gitlab(options) {
|
|
|
377
378
|
cursor: nextCursor(reqUrl(path, query), response)
|
|
378
379
|
};
|
|
379
380
|
},
|
|
381
|
+
async getBranch(params) {
|
|
382
|
+
const { data } = await http.json(`/projects/${projectId(params.repo)}/repository/branches/${encodeURIComponent(params.name)}`, { signal: params.signal });
|
|
383
|
+
return toBranch(data);
|
|
384
|
+
},
|
|
385
|
+
async getTag(params) {
|
|
386
|
+
const { data } = await http.json(`/projects/${projectId(params.repo)}/repository/tags/${encodeURIComponent(params.name)}`, { signal: params.signal });
|
|
387
|
+
return toTag(data);
|
|
388
|
+
},
|
|
380
389
|
async searchRefs(params) {
|
|
381
390
|
const base = `/projects/${projectId(params.repo)}/repository`;
|
|
382
391
|
const query = {
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { A as
|
|
1
|
+
import { A as ProviderSearchRefsParams, B as UpdateWebhookParams, C as ListTagsParams, D as Page, E as NamespaceKind, F as Repository, H as Webhook, I as ResolveRefParams, L as SearchRefsParams, M as RefType, N as RepoCapabilities, O as ParsedWebhookEvent, P as RepoProvider, R as Tag, S as ListRepositoriesParams, T as Namespace, U as WebhookEventType, V as UserRef, W as WebhookVerificationMethod, _ as GitActor, a as CloneUrl, b as ListCommitsParams, c as DeleteWebhookParams, d as GetBranchParams, f as GetCloneUrlParams, g as GetWebhookParams, h as GetTagParams, i as Branch, j as RefMatch, k as ProviderName, l as DownloadArchiveParams, m as GetRepositoryParams, n as ArchiveFormat, o as Commit, p as GetCommitParams, r as AuthenticatedUser, s as CreateWebhookParams, t as Archive, u as GetAuthenticatedUserParams, v as IncomingWebhookRequest, w as ListWebhooksParams, x as ListNamespacesParams, y as ListBranchesParams, z as TokenProvider } from "./types-CTeiAxMy.js";
|
|
2
2
|
//#region src/client.d.ts
|
|
3
3
|
interface RetryOptions {
|
|
4
4
|
rateLimit?: boolean;
|
|
@@ -33,13 +33,16 @@ interface RepoClient {
|
|
|
33
33
|
tags: {
|
|
34
34
|
list(params: ListTagsParams): Promise<Page<Tag>>;
|
|
35
35
|
listAll(params: Omit<ListTagsParams, 'cursor'>): AsyncGenerator<Tag, void>;
|
|
36
|
+
get(params: GetTagParams): Promise<Tag>;
|
|
36
37
|
};
|
|
37
38
|
branches: {
|
|
38
39
|
list(params: ListBranchesParams): Promise<Page<Branch>>;
|
|
39
40
|
listAll(params: Omit<ListBranchesParams, 'cursor'>): AsyncGenerator<Branch, void>;
|
|
41
|
+
get(params: GetBranchParams): Promise<Branch>;
|
|
40
42
|
};
|
|
41
43
|
refs: {
|
|
42
44
|
search(params: SearchRefsParams): Promise<RefMatch[]>;
|
|
45
|
+
resolve(params: ResolveRefParams): Promise<RefMatch>;
|
|
43
46
|
};
|
|
44
47
|
webhooks: {
|
|
45
48
|
create(params: CreateWebhookParams): Promise<Webhook>;
|
|
@@ -89,4 +92,4 @@ declare class RepoError extends Error {
|
|
|
89
92
|
constructor(message: string, options: RepoErrorOptions);
|
|
90
93
|
}
|
|
91
94
|
//#endregion
|
|
92
|
-
export { type Archive, type ArchiveFormat, type AuthenticatedUser, type Branch, type CloneUrl, type Commit, type CreateClientOptions, type CreateWebhookParams, type DeleteWebhookParams, type DetectWebhookProviderInput, type DownloadArchiveParams, type GetAuthenticatedUserParams, type GetCloneUrlParams, type GetCommitParams, type GetRepositoryParams, type GetWebhookParams, type GitActor, type IncomingWebhookRequest, type ListBranchesParams, type ListCommitsParams, type ListNamespacesParams, type ListRepositoriesParams, type ListTagsParams, type ListWebhooksParams, type Namespace, type NamespaceKind, type Page, type ParsedWebhookEvent, type ProviderName, type ProviderSearchRefsParams, type RefMatch, type RefType, type RepoCapabilities, type RepoClient, RepoError, type RepoErrorCode, type RepoErrorOptions, type RepoProvider, type Repository, type RetryOptions, type SearchRefsParams, type Tag, type TokenProvider, type UpdateWebhookParams, type UserRef, type Webhook, type WebhookEventType, type WebhookVerificationMethod, createClient, detectWebhookProvider };
|
|
95
|
+
export { type Archive, type ArchiveFormat, type AuthenticatedUser, type Branch, type CloneUrl, type Commit, type CreateClientOptions, type CreateWebhookParams, type DeleteWebhookParams, type DetectWebhookProviderInput, type DownloadArchiveParams, type GetAuthenticatedUserParams, type GetBranchParams, type GetCloneUrlParams, type GetCommitParams, type GetRepositoryParams, type GetTagParams, type GetWebhookParams, type GitActor, type IncomingWebhookRequest, type ListBranchesParams, type ListCommitsParams, type ListNamespacesParams, type ListRepositoriesParams, type ListTagsParams, type ListWebhooksParams, type Namespace, type NamespaceKind, type Page, type ParsedWebhookEvent, type ProviderName, type ProviderSearchRefsParams, type RefMatch, type RefType, type RepoCapabilities, type RepoClient, RepoError, type RepoErrorCode, type RepoErrorOptions, type RepoProvider, type Repository, type ResolveRefParams, type RetryOptions, type SearchRefsParams, type Tag, type TokenProvider, type UpdateWebhookParams, type UserRef, type Webhook, type WebhookEventType, type WebhookVerificationMethod, createClient, detectWebhookProvider };
|
package/dist/index.js
CHANGED
|
@@ -10,6 +10,30 @@ const ALL_REF_TYPES = [
|
|
|
10
10
|
"commit"
|
|
11
11
|
];
|
|
12
12
|
const COMMIT_SHA_PATTERN = /^[0-9a-f]{4,40}$/i;
|
|
13
|
+
function branchMatch(branch) {
|
|
14
|
+
return {
|
|
15
|
+
type: "branch",
|
|
16
|
+
name: branch.name,
|
|
17
|
+
sha: branch.sha,
|
|
18
|
+
raw: branch.raw
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function tagMatch(tag) {
|
|
22
|
+
return {
|
|
23
|
+
type: "tag",
|
|
24
|
+
name: tag.name,
|
|
25
|
+
sha: tag.sha,
|
|
26
|
+
raw: tag.raw
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function commitMatch(commit) {
|
|
30
|
+
return {
|
|
31
|
+
type: "commit",
|
|
32
|
+
name: commit.sha,
|
|
33
|
+
sha: commit.sha,
|
|
34
|
+
raw: commit.raw
|
|
35
|
+
};
|
|
36
|
+
}
|
|
13
37
|
function createClient(options) {
|
|
14
38
|
const { provider } = options;
|
|
15
39
|
const retryRateLimit = options.retry?.rateLimit ?? true;
|
|
@@ -112,6 +136,11 @@ function createClient(options) {
|
|
|
112
136
|
}
|
|
113
137
|
},
|
|
114
138
|
tags: {
|
|
139
|
+
get: async (params) => {
|
|
140
|
+
requireNonEmpty(params.repo, "repo");
|
|
141
|
+
requireNonEmpty(params.name, "name");
|
|
142
|
+
return withRetry(() => provider.getTag(params));
|
|
143
|
+
},
|
|
115
144
|
list: async (params) => {
|
|
116
145
|
requireNonEmpty(params.repo, "repo");
|
|
117
146
|
return withRetry(() => provider.listTags(params));
|
|
@@ -122,6 +151,11 @@ function createClient(options) {
|
|
|
122
151
|
})
|
|
123
152
|
},
|
|
124
153
|
branches: {
|
|
154
|
+
get: async (params) => {
|
|
155
|
+
requireNonEmpty(params.repo, "repo");
|
|
156
|
+
requireNonEmpty(params.name, "name");
|
|
157
|
+
return withRetry(() => provider.getBranch(params));
|
|
158
|
+
},
|
|
125
159
|
list: async (params) => {
|
|
126
160
|
requireNonEmpty(params.repo, "repo");
|
|
127
161
|
return withRetry(() => provider.listBranches(params));
|
|
@@ -131,57 +165,94 @@ function createClient(options) {
|
|
|
131
165
|
return provider.listBranches(p);
|
|
132
166
|
})
|
|
133
167
|
},
|
|
134
|
-
refs: {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
168
|
+
refs: {
|
|
169
|
+
search: async (params) => {
|
|
170
|
+
requireNonEmpty(params.repo, "repo");
|
|
171
|
+
if (!provider.capabilities.refSearch) fail("unsupported", `${provider.name} does not support ref search`);
|
|
172
|
+
const types = params.types ?? ALL_REF_TYPES;
|
|
173
|
+
if (types.length === 0) fail("validation", "At least one ref type is required");
|
|
174
|
+
const { repo, query, signal } = params;
|
|
175
|
+
const limit = params.limit ?? DEFAULT_REF_SEARCH_LIMIT;
|
|
176
|
+
if (query === "") {
|
|
177
|
+
const [branches, tags] = await Promise.all([types.includes("branch") ? withRetry(() => provider.listBranches({
|
|
178
|
+
repo,
|
|
179
|
+
limit,
|
|
180
|
+
signal
|
|
181
|
+
})) : { data: [] }, types.includes("tag") ? withRetry(() => provider.listTags({
|
|
182
|
+
repo,
|
|
183
|
+
limit,
|
|
184
|
+
signal
|
|
185
|
+
})) : { data: [] }]);
|
|
186
|
+
return [...branches.data.map(branchMatch), ...tags.data.map(tagMatch)].slice(0, limit);
|
|
187
|
+
}
|
|
188
|
+
const refTypes = types.filter((t) => t !== "commit");
|
|
189
|
+
const [refMatches, commitMatches] = await Promise.all([refTypes.length > 0 ? withRetry(() => provider.searchRefs({
|
|
143
190
|
repo,
|
|
191
|
+
query,
|
|
192
|
+
types: refTypes,
|
|
144
193
|
limit,
|
|
145
194
|
signal
|
|
146
|
-
})) :
|
|
195
|
+
})) : [], types.includes("commit") && COMMIT_SHA_PATTERN.test(query) ? withRetry(() => provider.getCommit({
|
|
147
196
|
repo,
|
|
148
|
-
|
|
197
|
+
ref: query,
|
|
198
|
+
signal
|
|
199
|
+
})).then((commit) => [commitMatch(commit)], (error) => {
|
|
200
|
+
if (error instanceof RepoError && (error.code === "not_found" || error.code === "validation" || error.code === "unsupported")) return [];
|
|
201
|
+
throw error;
|
|
202
|
+
}) : []]);
|
|
203
|
+
return [...refMatches, ...commitMatches].slice(0, limit);
|
|
204
|
+
},
|
|
205
|
+
resolve: async (params) => {
|
|
206
|
+
requireNonEmpty(params.repo, "repo");
|
|
207
|
+
requireNonEmpty(params.ref, "ref");
|
|
208
|
+
const { repo, ref, type, signal } = params;
|
|
209
|
+
if (ref === "HEAD" && (type === void 0 || type === "branch")) {
|
|
210
|
+
const defaultBranch = (await withRetry(() => provider.getRepository({
|
|
211
|
+
repo,
|
|
212
|
+
signal
|
|
213
|
+
}))).defaultBranch;
|
|
214
|
+
if (defaultBranch === void 0) throw new RepoError("Cannot resolve HEAD: repository has no default branch", {
|
|
215
|
+
code: "not_found",
|
|
216
|
+
provider: provider.name
|
|
217
|
+
});
|
|
218
|
+
return branchMatch(await withRetry(() => provider.getBranch({
|
|
219
|
+
repo,
|
|
220
|
+
name: defaultBranch,
|
|
221
|
+
signal
|
|
222
|
+
})));
|
|
223
|
+
}
|
|
224
|
+
if (type === "commit") return commitMatch(await withRetry(() => provider.getCommit({
|
|
225
|
+
repo,
|
|
226
|
+
ref,
|
|
149
227
|
signal
|
|
150
|
-
}))
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
name:
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
228
|
+
})));
|
|
229
|
+
const [tagResult, branchResult] = await Promise.allSettled([type === "branch" ? void 0 : withRetry(() => provider.getTag({
|
|
230
|
+
repo,
|
|
231
|
+
name: ref,
|
|
232
|
+
signal
|
|
233
|
+
})), type === "tag" ? void 0 : withRetry(() => provider.getBranch({
|
|
234
|
+
repo,
|
|
235
|
+
name: ref,
|
|
236
|
+
signal
|
|
237
|
+
}))]);
|
|
238
|
+
const isMiss = (result) => result.status === "rejected" && result.reason instanceof RepoError && result.reason.code === "not_found";
|
|
239
|
+
if (tagResult.status === "fulfilled") {
|
|
240
|
+
if (tagResult.value !== void 0) return tagMatch(tagResult.value);
|
|
241
|
+
} else if (!isMiss(tagResult)) throw tagResult.reason;
|
|
242
|
+
if (branchResult.status === "fulfilled") {
|
|
243
|
+
if (branchResult.value !== void 0) return branchMatch(branchResult.value);
|
|
244
|
+
} else if (!isMiss(branchResult)) throw branchResult.reason;
|
|
245
|
+
if (type === void 0 && COMMIT_SHA_PATTERN.test(ref)) return commitMatch(await withRetry(() => provider.getCommit({
|
|
246
|
+
repo,
|
|
247
|
+
ref,
|
|
248
|
+
signal
|
|
249
|
+
})));
|
|
250
|
+
throw new RepoError(`Ref not found: ${ref}`, {
|
|
251
|
+
code: "not_found",
|
|
252
|
+
provider: provider.name
|
|
253
|
+
});
|
|
162
254
|
}
|
|
163
|
-
|
|
164
|
-
const [refMatches, commitMatches] = await Promise.all([refTypes.length > 0 ? withRetry(() => provider.searchRefs({
|
|
165
|
-
repo,
|
|
166
|
-
query,
|
|
167
|
-
types: refTypes,
|
|
168
|
-
limit,
|
|
169
|
-
signal
|
|
170
|
-
})) : [], types.includes("commit") && COMMIT_SHA_PATTERN.test(query) ? withRetry(() => provider.getCommit({
|
|
171
|
-
repo,
|
|
172
|
-
ref: query,
|
|
173
|
-
signal
|
|
174
|
-
})).then((commit) => [{
|
|
175
|
-
type: "commit",
|
|
176
|
-
name: commit.sha,
|
|
177
|
-
sha: commit.sha,
|
|
178
|
-
raw: commit.raw
|
|
179
|
-
}], (error) => {
|
|
180
|
-
if (error instanceof RepoError && (error.code === "not_found" || error.code === "validation")) return [];
|
|
181
|
-
throw error;
|
|
182
|
-
}) : []]);
|
|
183
|
-
return [...refMatches, ...commitMatches].slice(0, limit);
|
|
184
|
-
} },
|
|
255
|
+
},
|
|
185
256
|
webhooks: {
|
|
186
257
|
create: async (params) => {
|
|
187
258
|
requireNonEmpty(params.repo, "repo");
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import { t as RepoError } from "./errors-Cxq_CqYv.js";
|
|
2
2
|
//#region src/base64.ts
|
|
3
|
-
function
|
|
3
|
+
function bytesToBase64(bytes) {
|
|
4
4
|
let binary = "";
|
|
5
5
|
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
6
|
-
return btoa(binary)
|
|
6
|
+
return btoa(binary);
|
|
7
|
+
}
|
|
8
|
+
function bytesToBase64Url(bytes) {
|
|
9
|
+
return bytesToBase64(bytes).replace(/=+$/, "").replace(/\+/g, "-").replace(/\//g, "_");
|
|
10
|
+
}
|
|
11
|
+
function stringToBase64(text) {
|
|
12
|
+
return bytesToBase64(new TextEncoder().encode(text));
|
|
7
13
|
}
|
|
8
14
|
function stringToBase64Url(text) {
|
|
9
15
|
return bytesToBase64Url(new TextEncoder().encode(text));
|
|
@@ -64,4 +70,4 @@ function assertSameOriginUrl(provider, baseUrl, url) {
|
|
|
64
70
|
return url;
|
|
65
71
|
}
|
|
66
72
|
//#endregion
|
|
67
|
-
export { bytesToBase64Url as i, decodeCursor as n, encodeCursor as r, assertSameOriginUrl as t };
|
|
73
|
+
export { stringToBase64 as a, bytesToBase64Url as i, decodeCursor as n, encodeCursor as r, assertSameOriginUrl as t };
|
|
@@ -102,64 +102,4 @@ function filenameFromContentDisposition(header) {
|
|
|
102
102
|
return header.match(/filename="?([^";]+)"?/i)?.[1]?.trim();
|
|
103
103
|
}
|
|
104
104
|
//#endregion
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* A push that deletes its ref reports an all-zero object id; normalize that to
|
|
108
|
-
* undefined so `headCommitSha` is only set when the ref still points at a commit.
|
|
109
|
-
*/
|
|
110
|
-
function headShaOrUndefined(sha) {
|
|
111
|
-
if (!sha || /^0+$/.test(sha)) return void 0;
|
|
112
|
-
return sha;
|
|
113
|
-
}
|
|
114
|
-
//#endregion
|
|
115
|
-
//#region src/webhooks/verify.ts
|
|
116
|
-
const encoder = new TextEncoder();
|
|
117
|
-
async function hmacSha256Hex(secret, payload) {
|
|
118
|
-
const key = await crypto.subtle.importKey("raw", encoder.encode(secret), {
|
|
119
|
-
name: "HMAC",
|
|
120
|
-
hash: "SHA-256"
|
|
121
|
-
}, false, ["sign"]);
|
|
122
|
-
const data = typeof payload === "string" ? encoder.encode(payload) : payload;
|
|
123
|
-
const signature = await crypto.subtle.sign("HMAC", key, data);
|
|
124
|
-
return [...new Uint8Array(signature)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
125
|
-
}
|
|
126
|
-
function timingSafeEqual(a, b) {
|
|
127
|
-
const bytesA = encoder.encode(a);
|
|
128
|
-
const bytesB = encoder.encode(b);
|
|
129
|
-
const length = Math.max(bytesA.length, bytesB.length);
|
|
130
|
-
let mismatch = bytesA.length === bytesB.length ? 0 : 1;
|
|
131
|
-
for (let index = 0; index < length; index++) mismatch |= (bytesA[index] ?? 0) ^ (bytesB[index] ?? 0);
|
|
132
|
-
return mismatch === 0;
|
|
133
|
-
}
|
|
134
|
-
/**
|
|
135
|
-
* Shared HMAC webhook verification: compares the incoming signature header
|
|
136
|
-
* against `${scheme}=<hmac-sha256(secret, body)>` in constant time. Providers
|
|
137
|
-
* differ only in the header name and (rarely) the scheme prefix.
|
|
138
|
-
*/
|
|
139
|
-
async function verifyHmacSignature(params, options) {
|
|
140
|
-
if (!params.secret) return false;
|
|
141
|
-
const incoming = await toIncomingWebhook("request" in params ? params.request : params);
|
|
142
|
-
const signature = incoming.headers[options.header.toLowerCase()];
|
|
143
|
-
if (!signature) return false;
|
|
144
|
-
return timingSafeEqual(signature, `${options.scheme ?? "sha256"}=${await hmacSha256Hex(params.secret, incoming.body)}`);
|
|
145
|
-
}
|
|
146
|
-
async function toIncomingWebhook(input) {
|
|
147
|
-
if (input instanceof Request) {
|
|
148
|
-
const headers = {};
|
|
149
|
-
input.headers.forEach((value, key) => {
|
|
150
|
-
headers[key.toLowerCase()] = value;
|
|
151
|
-
});
|
|
152
|
-
return {
|
|
153
|
-
headers,
|
|
154
|
-
body: await input.clone().text()
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
|
-
const headers = {};
|
|
158
|
-
for (const [key, value] of Object.entries(input.headers)) headers[key.toLowerCase()] = value;
|
|
159
|
-
return {
|
|
160
|
-
headers,
|
|
161
|
-
body: input.body
|
|
162
|
-
};
|
|
163
|
-
}
|
|
164
|
-
//#endregion
|
|
165
|
-
export { headShaOrUndefined as a, encodeRefPath as c, parseLinkNext as d, HttpClient as f, verifyHmacSignature as i, filenameFromContentDisposition as l, timingSafeEqual as n, clampPerPage as o, toIncomingWebhook as r, commitWebUrlBuilder as s, hmacSha256Hex as t, isRecord as u };
|
|
105
|
+
export { isRecord as a, filenameFromContentDisposition as i, commitWebUrlBuilder as n, parseLinkNext as o, encodeRefPath as r, HttpClient as s, clampPerPage as t };
|
package/dist/testing.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { E as NamespaceKind, F as Repository, H as Webhook, N as RepoCapabilities, P as RepoProvider, R as Tag, T as Namespace, U as WebhookEventType, i as Branch, k as ProviderName, o as Commit } from "./types-CTeiAxMy.js";
|
|
2
2
|
//#region src/providers/testing/index.d.ts
|
|
3
3
|
interface InMemoryNamespaceSeed {
|
|
4
4
|
id: string;
|
package/dist/testing.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t as RepoError } from "./errors-Cxq_CqYv.js";
|
|
2
|
-
import { n as decodeCursor, r as encodeCursor } from "./pagination-
|
|
2
|
+
import { n as decodeCursor, r as encodeCursor } from "./pagination-CWWHSu8I.js";
|
|
3
3
|
//#region src/providers/testing/index.ts
|
|
4
4
|
/**
|
|
5
5
|
* Small on purpose so that consumers exercise cursor-based pagination.
|
|
@@ -220,6 +220,18 @@ function createInMemoryProvider(seed = {}, options = {}) {
|
|
|
220
220
|
requireRepository(params.repo);
|
|
221
221
|
return Promise.resolve(paginate(state.branches.get(params.repo) ?? [], params.cursor));
|
|
222
222
|
},
|
|
223
|
+
getBranch(params) {
|
|
224
|
+
requireRepository(params.repo);
|
|
225
|
+
const branch = (state.branches.get(params.repo) ?? []).find((candidate) => candidate.name === params.name);
|
|
226
|
+
if (!branch) throw notFound(`Unknown branch "${params.name}" in repository: ${params.repo}`);
|
|
227
|
+
return Promise.resolve(branch);
|
|
228
|
+
},
|
|
229
|
+
getTag(params) {
|
|
230
|
+
requireRepository(params.repo);
|
|
231
|
+
const tag = (state.tags.get(params.repo) ?? []).find((candidate) => candidate.name === params.name);
|
|
232
|
+
if (!tag) throw notFound(`Unknown tag "${params.name}" in repository: ${params.repo}`);
|
|
233
|
+
return Promise.resolve(tag);
|
|
234
|
+
},
|
|
223
235
|
searchRefs(params) {
|
|
224
236
|
requireRepository(params.repo);
|
|
225
237
|
const matches = [];
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
//#region src/types.d.ts
|
|
2
|
-
type ProviderName = 'github' | 'gitlab' | 'bitbucket' | 'azure-devops' | 'gitea';
|
|
2
|
+
type ProviderName = 'github' | 'gitlab' | 'bitbucket' | 'azure-devops' | 'gitea' | 'git-http';
|
|
3
3
|
type WebhookEventType = 'push' | 'tag_push' | 'release';
|
|
4
4
|
type ArchiveFormat = 'zip' | 'tar.gz';
|
|
5
5
|
type NamespaceKind = 'user' | 'organization' | 'group' | 'workspace' | 'project';
|
|
6
|
-
type WebhookVerificationMethod = 'hmac-sha256' | 'shared-token' | 'basic-auth';
|
|
6
|
+
type WebhookVerificationMethod = 'hmac-sha256' | 'shared-token' | 'basic-auth' | 'none';
|
|
7
7
|
/**
|
|
8
8
|
* Mints a bearer token for each request. `forceRefresh` is true only on the
|
|
9
9
|
* single retry after a 401 — bypass any cache and mint a fresh token then.
|
|
@@ -93,7 +93,7 @@ type RefType = 'branch' | 'tag' | 'commit';
|
|
|
93
93
|
interface RefMatch {
|
|
94
94
|
type: RefType;
|
|
95
95
|
name: string;
|
|
96
|
-
/** SHA the ref points to
|
|
96
|
+
/** SHA the ref points to. In `refs.search` results on GitHub and Gitea, annotated tags carry the tag object SHA rather than the commit; `refs.resolve` and `tags.get` always return the peeled commit SHA. */
|
|
97
97
|
sha: string;
|
|
98
98
|
raw: unknown;
|
|
99
99
|
}
|
|
@@ -178,6 +178,20 @@ interface ListBranchesParams extends BaseParams {
|
|
|
178
178
|
cursor?: string;
|
|
179
179
|
limit?: number;
|
|
180
180
|
}
|
|
181
|
+
interface GetBranchParams extends BaseParams {
|
|
182
|
+
repo: string;
|
|
183
|
+
name: string;
|
|
184
|
+
}
|
|
185
|
+
interface GetTagParams extends BaseParams {
|
|
186
|
+
repo: string;
|
|
187
|
+
name: string;
|
|
188
|
+
}
|
|
189
|
+
interface ResolveRefParams extends BaseParams {
|
|
190
|
+
repo: string;
|
|
191
|
+
ref: string;
|
|
192
|
+
/** Restrict resolution to one ref type; when omitted, branches and tags are tried (tag wins on collision) before falling back to commit SHAs. */
|
|
193
|
+
type?: RefType;
|
|
194
|
+
}
|
|
181
195
|
interface SearchRefsParams extends BaseParams {
|
|
182
196
|
repo: string;
|
|
183
197
|
/** Prefix to match ref names against; an empty string lists the first refs unfiltered. */
|
|
@@ -250,6 +264,10 @@ interface RepoProvider {
|
|
|
250
264
|
getCommit(params: GetCommitParams): Promise<Commit>;
|
|
251
265
|
listTags(params: ListTagsParams): Promise<Page<Tag>>;
|
|
252
266
|
listBranches(params: ListBranchesParams): Promise<Page<Branch>>;
|
|
267
|
+
/** Exact-match a branch by name; throws `not_found` on a miss. */
|
|
268
|
+
getBranch(params: GetBranchParams): Promise<Branch>;
|
|
269
|
+
/** Exact-match a tag by name, peeled to the commit SHA for annotated tags; throws `not_found` on a miss. */
|
|
270
|
+
getTag(params: GetTagParams): Promise<Tag>;
|
|
253
271
|
/** Prefix-match refs of the requested types, branches before tags, at most `limit` results. */
|
|
254
272
|
searchRefs(params: ProviderSearchRefsParams): Promise<RefMatch[]>;
|
|
255
273
|
downloadArchive(params: DownloadArchiveParams): Promise<Archive>;
|
|
@@ -261,4 +279,4 @@ interface RepoProvider {
|
|
|
261
279
|
deleteWebhook(params: DeleteWebhookParams): Promise<void>;
|
|
262
280
|
}
|
|
263
281
|
//#endregion
|
|
264
|
-
export {
|
|
282
|
+
export { ProviderSearchRefsParams as A, UpdateWebhookParams as B, ListTagsParams as C, Page as D, NamespaceKind as E, Repository as F, Webhook as H, ResolveRefParams as I, SearchRefsParams as L, RefType as M, RepoCapabilities as N, ParsedWebhookEvent as O, RepoProvider as P, Tag as R, ListRepositoriesParams as S, Namespace as T, WebhookEventType as U, UserRef as V, WebhookVerificationMethod as W, GitActor as _, CloneUrl as a, ListCommitsParams as b, DeleteWebhookParams as c, GetBranchParams as d, GetCloneUrlParams as f, GetWebhookParams as g, GetTagParams as h, Branch as i, RefMatch as j, ProviderName as k, DownloadArchiveParams as l, GetRepositoryParams as m, ArchiveFormat as n, Commit as o, GetCommitParams as p, AuthenticatedUser as r, CreateWebhookParams as s, Archive as t, GetAuthenticatedUserParams as u, IncomingWebhookRequest as v, ListWebhooksParams as w, ListNamespacesParams as x, ListBranchesParams as y, TokenProvider as z };
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
//#region src/webhooks/parse.ts
|
|
2
|
+
/**
|
|
3
|
+
* A push that deletes its ref reports an all-zero object id; normalize that to
|
|
4
|
+
* undefined so `headCommitSha` is only set when the ref still points at a commit.
|
|
5
|
+
*/
|
|
6
|
+
function headShaOrUndefined(sha) {
|
|
7
|
+
if (!sha || /^0+$/.test(sha)) return void 0;
|
|
8
|
+
return sha;
|
|
9
|
+
}
|
|
10
|
+
//#endregion
|
|
11
|
+
//#region src/webhooks/verify.ts
|
|
12
|
+
const encoder = new TextEncoder();
|
|
13
|
+
async function hmacSha256Hex(secret, payload) {
|
|
14
|
+
const key = await crypto.subtle.importKey("raw", encoder.encode(secret), {
|
|
15
|
+
name: "HMAC",
|
|
16
|
+
hash: "SHA-256"
|
|
17
|
+
}, false, ["sign"]);
|
|
18
|
+
const data = typeof payload === "string" ? encoder.encode(payload) : payload;
|
|
19
|
+
const signature = await crypto.subtle.sign("HMAC", key, data);
|
|
20
|
+
return [...new Uint8Array(signature)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
21
|
+
}
|
|
22
|
+
function timingSafeEqual(a, b) {
|
|
23
|
+
const bytesA = encoder.encode(a);
|
|
24
|
+
const bytesB = encoder.encode(b);
|
|
25
|
+
const length = Math.max(bytesA.length, bytesB.length);
|
|
26
|
+
let mismatch = bytesA.length === bytesB.length ? 0 : 1;
|
|
27
|
+
for (let index = 0; index < length; index++) mismatch |= (bytesA[index] ?? 0) ^ (bytesB[index] ?? 0);
|
|
28
|
+
return mismatch === 0;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Shared HMAC webhook verification: compares the incoming signature header
|
|
32
|
+
* against `${scheme}=<hmac-sha256(secret, body)>` in constant time. Providers
|
|
33
|
+
* differ only in the header name and (rarely) the scheme prefix.
|
|
34
|
+
*/
|
|
35
|
+
async function verifyHmacSignature(params, options) {
|
|
36
|
+
if (!params.secret) return false;
|
|
37
|
+
const incoming = await toIncomingWebhook("request" in params ? params.request : params);
|
|
38
|
+
const signature = incoming.headers[options.header.toLowerCase()];
|
|
39
|
+
if (!signature) return false;
|
|
40
|
+
return timingSafeEqual(signature, `${options.scheme ?? "sha256"}=${await hmacSha256Hex(params.secret, incoming.body)}`);
|
|
41
|
+
}
|
|
42
|
+
async function toIncomingWebhook(input) {
|
|
43
|
+
if (input instanceof Request) {
|
|
44
|
+
const headers = {};
|
|
45
|
+
input.headers.forEach((value, key) => {
|
|
46
|
+
headers[key.toLowerCase()] = value;
|
|
47
|
+
});
|
|
48
|
+
return {
|
|
49
|
+
headers,
|
|
50
|
+
body: await input.clone().text()
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
const headers = {};
|
|
54
|
+
for (const [key, value] of Object.entries(input.headers)) headers[key.toLowerCase()] = value;
|
|
55
|
+
return {
|
|
56
|
+
headers,
|
|
57
|
+
body: input.body
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
//#endregion
|
|
61
|
+
export { headShaOrUndefined as a, verifyHmacSignature as i, timingSafeEqual as n, toIncomingWebhook as r, hmacSha256Hex as t };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "repo-sdk",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Unified TypeScript SDK for git providers — GitHub, GitLab, Bitbucket, Azure DevOps, Gitea",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Robin Genz <mail@robingenz.dev>",
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
"azure-devops",
|
|
18
18
|
"gitea",
|
|
19
19
|
"forgejo",
|
|
20
|
+
"git-http",
|
|
20
21
|
"sdk",
|
|
21
22
|
"version-control",
|
|
22
23
|
"webhooks"
|
|
@@ -54,6 +55,10 @@
|
|
|
54
55
|
"types": "./dist/gitea.d.ts",
|
|
55
56
|
"default": "./dist/gitea.js"
|
|
56
57
|
},
|
|
58
|
+
"./git-http": {
|
|
59
|
+
"types": "./dist/git-http.d.ts",
|
|
60
|
+
"default": "./dist/git-http.js"
|
|
61
|
+
},
|
|
57
62
|
"./testing": {
|
|
58
63
|
"types": "./dist/testing.d.ts",
|
|
59
64
|
"default": "./dist/testing.js"
|