repo-sdk 0.1.0 → 0.1.2
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 +148 -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-DfiRluvJ.d.ts} +28 -5
- 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 { B as TokenProvider, F as RepoProvider, O as ParsedWebhookEvent, v as IncomingWebhookRequest } from "./types-DfiRluvJ.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 { B as TokenProvider, F as RepoProvider, O as ParsedWebhookEvent, v as IncomingWebhookRequest } from "./types-DfiRluvJ.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 { B as TokenProvider, F as RepoProvider } from "./types-DfiRluvJ.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 { B as TokenProvider, F as RepoProvider, O as ParsedWebhookEvent, v as IncomingWebhookRequest } from "./types-DfiRluvJ.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 { B as TokenProvider, F as RepoProvider, O as ParsedWebhookEvent, v as IncomingWebhookRequest } from "./types-DfiRluvJ.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 { B as TokenProvider, F as RepoProvider, O as ParsedWebhookEvent, v as IncomingWebhookRequest } from "./types-DfiRluvJ.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 ProviderRefMatch, B as TokenProvider, C as ListTagsParams, D as Page, E as NamespaceKind, F as RepoProvider, G as WebhookVerificationMethod, H as UserRef, I as Repository, L as ResolveRefParams, M as RefMatch, N as RefType, O as ParsedWebhookEvent, P as RepoCapabilities, R as SearchRefsParams, S as ListRepositoriesParams, T as Namespace, U as Webhook, V as UpdateWebhookParams, W as WebhookEventType, _ 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 ProviderSearchRefsParams, 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 Tag } from "./types-DfiRluvJ.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 ProviderRefMatch, 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,40 @@ const ALL_REF_TYPES = [
|
|
|
10
10
|
"commit"
|
|
11
11
|
];
|
|
12
12
|
const COMMIT_SHA_PATTERN = /^[0-9a-f]{4,40}$/i;
|
|
13
|
+
const FULL_COMMIT_SHA_PATTERN = /^[0-9a-f]{40}$/i;
|
|
14
|
+
const BRANCH_REF_PREFIX = "refs/heads/";
|
|
15
|
+
const TAG_REF_PREFIX = "refs/tags/";
|
|
16
|
+
function qualifyRef(match) {
|
|
17
|
+
const ref = match.type === "branch" ? `${BRANCH_REF_PREFIX}${match.name}` : match.type === "tag" ? `${TAG_REF_PREFIX}${match.name}` : match.sha;
|
|
18
|
+
return {
|
|
19
|
+
...match,
|
|
20
|
+
ref
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function branchMatch(branch) {
|
|
24
|
+
return qualifyRef({
|
|
25
|
+
type: "branch",
|
|
26
|
+
name: branch.name,
|
|
27
|
+
sha: branch.sha,
|
|
28
|
+
raw: branch.raw
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
function tagMatch(tag) {
|
|
32
|
+
return qualifyRef({
|
|
33
|
+
type: "tag",
|
|
34
|
+
name: tag.name,
|
|
35
|
+
sha: tag.sha,
|
|
36
|
+
raw: tag.raw
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
function commitMatch(commit) {
|
|
40
|
+
return qualifyRef({
|
|
41
|
+
type: "commit",
|
|
42
|
+
name: commit.sha,
|
|
43
|
+
sha: commit.sha,
|
|
44
|
+
raw: commit.raw
|
|
45
|
+
});
|
|
46
|
+
}
|
|
13
47
|
function createClient(options) {
|
|
14
48
|
const { provider } = options;
|
|
15
49
|
const retryRateLimit = options.retry?.rateLimit ?? true;
|
|
@@ -112,6 +146,11 @@ function createClient(options) {
|
|
|
112
146
|
}
|
|
113
147
|
},
|
|
114
148
|
tags: {
|
|
149
|
+
get: async (params) => {
|
|
150
|
+
requireNonEmpty(params.repo, "repo");
|
|
151
|
+
requireNonEmpty(params.name, "name");
|
|
152
|
+
return withRetry(() => provider.getTag(params));
|
|
153
|
+
},
|
|
115
154
|
list: async (params) => {
|
|
116
155
|
requireNonEmpty(params.repo, "repo");
|
|
117
156
|
return withRetry(() => provider.listTags(params));
|
|
@@ -122,6 +161,11 @@ function createClient(options) {
|
|
|
122
161
|
})
|
|
123
162
|
},
|
|
124
163
|
branches: {
|
|
164
|
+
get: async (params) => {
|
|
165
|
+
requireNonEmpty(params.repo, "repo");
|
|
166
|
+
requireNonEmpty(params.name, "name");
|
|
167
|
+
return withRetry(() => provider.getBranch(params));
|
|
168
|
+
},
|
|
125
169
|
list: async (params) => {
|
|
126
170
|
requireNonEmpty(params.repo, "repo");
|
|
127
171
|
return withRetry(() => provider.listBranches(params));
|
|
@@ -131,57 +175,116 @@ function createClient(options) {
|
|
|
131
175
|
return provider.listBranches(p);
|
|
132
176
|
})
|
|
133
177
|
},
|
|
134
|
-
refs: {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
178
|
+
refs: {
|
|
179
|
+
search: async (params) => {
|
|
180
|
+
requireNonEmpty(params.repo, "repo");
|
|
181
|
+
if (!provider.capabilities.refSearch) fail("unsupported", `${provider.name} does not support ref search`);
|
|
182
|
+
const types = params.types ?? ALL_REF_TYPES;
|
|
183
|
+
if (types.length === 0) fail("validation", "At least one ref type is required");
|
|
184
|
+
const { repo, query, signal } = params;
|
|
185
|
+
const limit = params.limit ?? DEFAULT_REF_SEARCH_LIMIT;
|
|
186
|
+
if (query === "") {
|
|
187
|
+
const [branches, tags] = await Promise.all([types.includes("branch") ? withRetry(() => provider.listBranches({
|
|
188
|
+
repo,
|
|
189
|
+
limit,
|
|
190
|
+
signal
|
|
191
|
+
})) : { data: [] }, types.includes("tag") ? withRetry(() => provider.listTags({
|
|
192
|
+
repo,
|
|
193
|
+
limit,
|
|
194
|
+
signal
|
|
195
|
+
})) : { data: [] }]);
|
|
196
|
+
return [...branches.data.map(branchMatch), ...tags.data.map(tagMatch)].slice(0, limit);
|
|
197
|
+
}
|
|
198
|
+
const refTypes = types.filter((t) => t !== "commit");
|
|
199
|
+
const [refMatches, commitMatches] = await Promise.all([refTypes.length > 0 ? withRetry(() => provider.searchRefs({
|
|
143
200
|
repo,
|
|
201
|
+
query,
|
|
202
|
+
types: refTypes,
|
|
144
203
|
limit,
|
|
145
204
|
signal
|
|
146
|
-
}))
|
|
205
|
+
})).then((matches) => matches.map(qualifyRef)) : [], types.includes("commit") && COMMIT_SHA_PATTERN.test(query) ? withRetry(() => provider.getCommit({
|
|
147
206
|
repo,
|
|
148
|
-
|
|
207
|
+
ref: query,
|
|
208
|
+
signal
|
|
209
|
+
})).then((commit) => [commitMatch(commit)], (error) => {
|
|
210
|
+
if (error instanceof RepoError && (error.code === "not_found" || error.code === "validation" || error.code === "unsupported")) return [];
|
|
211
|
+
throw error;
|
|
212
|
+
}) : []]);
|
|
213
|
+
return [...refMatches, ...commitMatches].slice(0, limit);
|
|
214
|
+
},
|
|
215
|
+
resolve: async (params) => {
|
|
216
|
+
requireNonEmpty(params.repo, "repo");
|
|
217
|
+
requireNonEmpty(params.ref, "ref");
|
|
218
|
+
const { repo, signal } = params;
|
|
219
|
+
let { ref, type } = params;
|
|
220
|
+
const impliedType = ref.startsWith(BRANCH_REF_PREFIX) ? "branch" : ref.startsWith(TAG_REF_PREFIX) ? "tag" : void 0;
|
|
221
|
+
if (impliedType !== void 0) {
|
|
222
|
+
if (type !== void 0 && type !== impliedType) fail("validation", `Fully-qualified ref '${ref}' contradicts type '${type}'`);
|
|
223
|
+
type = impliedType;
|
|
224
|
+
ref = ref.slice(impliedType === "branch" ? 11 : 10);
|
|
225
|
+
requireNonEmpty(ref, "ref");
|
|
226
|
+
}
|
|
227
|
+
if (impliedType === void 0 && ref === "HEAD" && (type === void 0 || type === "branch")) {
|
|
228
|
+
const defaultBranch = (await withRetry(() => provider.getRepository({
|
|
229
|
+
repo,
|
|
230
|
+
signal
|
|
231
|
+
}))).defaultBranch;
|
|
232
|
+
if (defaultBranch === void 0) throw new RepoError("Cannot resolve HEAD: repository has no default branch", {
|
|
233
|
+
code: "not_found",
|
|
234
|
+
provider: provider.name
|
|
235
|
+
});
|
|
236
|
+
return branchMatch(await withRetry(() => provider.getBranch({
|
|
237
|
+
repo,
|
|
238
|
+
name: defaultBranch,
|
|
239
|
+
signal
|
|
240
|
+
})));
|
|
241
|
+
}
|
|
242
|
+
if (type === "commit") return commitMatch(await withRetry(() => provider.getCommit({
|
|
243
|
+
repo,
|
|
244
|
+
ref,
|
|
149
245
|
signal
|
|
150
|
-
}))
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
}
|
|
246
|
+
})));
|
|
247
|
+
let commitProbeError;
|
|
248
|
+
if (type === void 0 && FULL_COMMIT_SHA_PATTERN.test(ref)) try {
|
|
249
|
+
return commitMatch(await withRetry(() => provider.getCommit({
|
|
250
|
+
repo,
|
|
251
|
+
ref,
|
|
252
|
+
signal
|
|
253
|
+
})));
|
|
254
|
+
} catch (error) {
|
|
255
|
+
if (error instanceof RepoError && (error.code === "not_found" || error.code === "validation" || error.code === "unsupported")) commitProbeError = error;
|
|
256
|
+
else throw error;
|
|
257
|
+
}
|
|
258
|
+
const [tagResult, branchResult] = await Promise.allSettled([type === "branch" ? void 0 : withRetry(() => provider.getTag({
|
|
259
|
+
repo,
|
|
260
|
+
name: ref,
|
|
261
|
+
signal
|
|
262
|
+
})), type === "tag" ? void 0 : withRetry(() => provider.getBranch({
|
|
263
|
+
repo,
|
|
264
|
+
name: ref,
|
|
265
|
+
signal
|
|
266
|
+
}))]);
|
|
267
|
+
const isMiss = (result) => result.status === "rejected" && result.reason instanceof RepoError && result.reason.code === "not_found";
|
|
268
|
+
if (tagResult.status === "fulfilled") {
|
|
269
|
+
if (tagResult.value !== void 0) return tagMatch(tagResult.value);
|
|
270
|
+
} else if (!isMiss(tagResult)) throw tagResult.reason;
|
|
271
|
+
if (branchResult.status === "fulfilled") {
|
|
272
|
+
if (branchResult.value !== void 0) return branchMatch(branchResult.value);
|
|
273
|
+
} else if (!isMiss(branchResult)) throw branchResult.reason;
|
|
274
|
+
if (type === void 0 && COMMIT_SHA_PATTERN.test(ref)) {
|
|
275
|
+
if (commitProbeError !== void 0) throw commitProbeError;
|
|
276
|
+
return commitMatch(await withRetry(() => provider.getCommit({
|
|
277
|
+
repo,
|
|
278
|
+
ref,
|
|
279
|
+
signal
|
|
280
|
+
})));
|
|
281
|
+
}
|
|
282
|
+
throw new RepoError(`Ref not found: ${ref}`, {
|
|
283
|
+
code: "not_found",
|
|
284
|
+
provider: provider.name
|
|
285
|
+
});
|
|
162
286
|
}
|
|
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
|
-
} },
|
|
287
|
+
},
|
|
185
288
|
webhooks: {
|
|
186
289
|
create: async (params) => {
|
|
187
290
|
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 RepoProvider, I as Repository, P as RepoCapabilities, T as Namespace, U as Webhook, W as WebhookEventType, i as Branch, k as ProviderName, o as Commit, z as Tag } from "./types-DfiRluvJ.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,10 +93,14 @@ type RefType = 'branch' | 'tag' | 'commit';
|
|
|
93
93
|
interface RefMatch {
|
|
94
94
|
type: RefType;
|
|
95
95
|
name: string;
|
|
96
|
-
/**
|
|
96
|
+
/** Fully-qualified form — `refs/heads/<name>` for branches, `refs/tags/<name>` for tags, the SHA itself for commits. Any `ref` value round-trips through `refs.resolve`. */
|
|
97
|
+
ref: string;
|
|
98
|
+
/** 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
99
|
sha: string;
|
|
98
100
|
raw: unknown;
|
|
99
101
|
}
|
|
102
|
+
/** `RefMatch` as produced by provider adapters, before the client derives the fully-qualified `ref`. */
|
|
103
|
+
type ProviderRefMatch = Omit<RefMatch, 'ref'>;
|
|
100
104
|
interface Webhook {
|
|
101
105
|
id: string;
|
|
102
106
|
url: string;
|
|
@@ -178,6 +182,21 @@ interface ListBranchesParams extends BaseParams {
|
|
|
178
182
|
cursor?: string;
|
|
179
183
|
limit?: number;
|
|
180
184
|
}
|
|
185
|
+
interface GetBranchParams extends BaseParams {
|
|
186
|
+
repo: string;
|
|
187
|
+
name: string;
|
|
188
|
+
}
|
|
189
|
+
interface GetTagParams extends BaseParams {
|
|
190
|
+
repo: string;
|
|
191
|
+
name: string;
|
|
192
|
+
}
|
|
193
|
+
interface ResolveRefParams extends BaseParams {
|
|
194
|
+
repo: string;
|
|
195
|
+
/** A branch or tag name, a fully-qualified ref (`refs/heads/…`, `refs/tags/…`), a 4–40 hex commit SHA, or `HEAD`. */
|
|
196
|
+
ref: string;
|
|
197
|
+
/** Restrict resolution to one ref type; when omitted, a full 40-hex `ref` is tried as a commit first, then branches and tags (tag wins on collision), then abbreviated commit SHAs. A fully-qualified `ref` implies its type; passing a contradicting `type` throws a validation error. */
|
|
198
|
+
type?: RefType;
|
|
199
|
+
}
|
|
181
200
|
interface SearchRefsParams extends BaseParams {
|
|
182
201
|
repo: string;
|
|
183
202
|
/** Prefix to match ref names against; an empty string lists the first refs unfiltered. */
|
|
@@ -250,8 +269,12 @@ interface RepoProvider {
|
|
|
250
269
|
getCommit(params: GetCommitParams): Promise<Commit>;
|
|
251
270
|
listTags(params: ListTagsParams): Promise<Page<Tag>>;
|
|
252
271
|
listBranches(params: ListBranchesParams): Promise<Page<Branch>>;
|
|
272
|
+
/** Exact-match a branch by name; throws `not_found` on a miss. */
|
|
273
|
+
getBranch(params: GetBranchParams): Promise<Branch>;
|
|
274
|
+
/** Exact-match a tag by name, peeled to the commit SHA for annotated tags; throws `not_found` on a miss. */
|
|
275
|
+
getTag(params: GetTagParams): Promise<Tag>;
|
|
253
276
|
/** Prefix-match refs of the requested types, branches before tags, at most `limit` results. */
|
|
254
|
-
searchRefs(params: ProviderSearchRefsParams): Promise<
|
|
277
|
+
searchRefs(params: ProviderSearchRefsParams): Promise<ProviderRefMatch[]>;
|
|
255
278
|
downloadArchive(params: DownloadArchiveParams): Promise<Archive>;
|
|
256
279
|
getCloneUrl(params: GetCloneUrlParams): Promise<CloneUrl>;
|
|
257
280
|
createWebhook(params: CreateWebhookParams): Promise<Webhook>;
|
|
@@ -261,4 +284,4 @@ interface RepoProvider {
|
|
|
261
284
|
deleteWebhook(params: DeleteWebhookParams): Promise<void>;
|
|
262
285
|
}
|
|
263
286
|
//#endregion
|
|
264
|
-
export {
|
|
287
|
+
export { ProviderRefMatch as A, TokenProvider as B, ListTagsParams as C, Page as D, NamespaceKind as E, RepoProvider as F, WebhookVerificationMethod as G, UserRef as H, Repository as I, ResolveRefParams as L, RefMatch as M, RefType as N, ParsedWebhookEvent as O, RepoCapabilities as P, SearchRefsParams as R, ListRepositoriesParams as S, Namespace as T, Webhook as U, UpdateWebhookParams as V, WebhookEventType 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, ProviderSearchRefsParams 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, Tag 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.2",
|
|
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"
|