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 CHANGED
@@ -4,7 +4,7 @@
4
4
  [![npm downloads](https://img.shields.io/npm/dm/repo-sdk)](https://www.npmjs.com/package/repo-sdk)
5
5
  [![license](https://img.shields.io/npm/l/repo-sdk)](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
 
@@ -1,4 +1,4 @@
1
- import { E as ParsedWebhookEvent, I as TokenProvider, M as RepoProvider, g as IncomingWebhookRequest } from "./types-CwXDjAbE.js";
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
  /**
@@ -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-1KJe54LW.js";
3
- import { a as headShaOrUndefined, f as HttpClient, l as filenameFromContentDisposition, n as timingSafeEqual, r as toIncomingWebhook, s as commitWebUrlBuilder, u as isRecord } from "./verify-CRVfL8D9.js";
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) => {
@@ -1,4 +1,4 @@
1
- import { E as ParsedWebhookEvent, I as TokenProvider, M as RepoProvider, g as IncomingWebhookRequest } from "./types-CwXDjAbE.js";
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-1KJe54LW.js";
3
- import { a as headShaOrUndefined, f as HttpClient, i as verifyHmacSignature, l as filenameFromContentDisposition, o as clampPerPage, r as toIncomingWebhook, s as commitWebUrlBuilder, u as isRecord } from "./verify-CRVfL8D9.js";
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 };
@@ -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 { E as ParsedWebhookEvent, I as TokenProvider, M as RepoProvider, g as IncomingWebhookRequest } from "./types-CwXDjAbE.js";
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-1KJe54LW.js";
3
- import { a as headShaOrUndefined, c as encodeRefPath, d as parseLinkNext, f as HttpClient, l as filenameFromContentDisposition, n as timingSafeEqual, o as clampPerPage, r as toIncomingWebhook, s as commitWebUrlBuilder, t as hmacSha256Hex, u as isRecord } from "./verify-CRVfL8D9.js";
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 { E as ParsedWebhookEvent, I as TokenProvider, M as RepoProvider, g as IncomingWebhookRequest } from "./types-CwXDjAbE.js";
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-1KJe54LW.js";
3
- import { a as headShaOrUndefined, c as encodeRefPath, d as parseLinkNext, f as HttpClient, i as verifyHmacSignature, l as filenameFromContentDisposition, o as clampPerPage, r as toIncomingWebhook, s as commitWebUrlBuilder, u as isRecord } from "./verify-CRVfL8D9.js";
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 { E as ParsedWebhookEvent, I as TokenProvider, M as RepoProvider, g as IncomingWebhookRequest } from "./types-CwXDjAbE.js";
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-1KJe54LW.js";
3
- import { a as headShaOrUndefined, d as parseLinkNext, f as HttpClient, l as filenameFromContentDisposition, n as timingSafeEqual, o as clampPerPage, r as toIncomingWebhook, s as commitWebUrlBuilder, u as isRecord } from "./verify-CRVfL8D9.js";
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 RefType, B as WebhookEventType, C as Namespace, D as ProviderName, E as ParsedWebhookEvent, F as Tag, I as TokenProvider, L as UpdateWebhookParams, M as RepoProvider, N as Repository, O as ProviderSearchRefsParams, P as SearchRefsParams, R as UserRef, S as ListWebhooksParams, T as Page, V as WebhookVerificationMethod, _ as ListBranchesParams, a as CloneUrl, b as ListRepositoriesParams, c as DeleteWebhookParams, d as GetCloneUrlParams, f as GetCommitParams, g as IncomingWebhookRequest, h as GitActor, i as Branch, j as RepoCapabilities, k as RefMatch, l as DownloadArchiveParams, m as GetWebhookParams, n as ArchiveFormat, o as Commit, p as GetRepositoryParams, r as AuthenticatedUser, s as CreateWebhookParams, t as Archive, u as GetAuthenticatedUserParams, v as ListCommitsParams, w as NamespaceKind, x as ListTagsParams, y as ListNamespacesParams, z as Webhook } from "./types-CwXDjAbE.js";
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: { search: async (params) => {
135
- requireNonEmpty(params.repo, "repo");
136
- if (!provider.capabilities.refSearch) fail("unsupported", `${provider.name} does not support ref search`);
137
- const types = params.types ?? ALL_REF_TYPES;
138
- if (types.length === 0) fail("validation", "At least one ref type is required");
139
- const { repo, query, signal } = params;
140
- const limit = params.limit ?? DEFAULT_REF_SEARCH_LIMIT;
141
- if (query === "") {
142
- const [branches, tags] = await Promise.all([types.includes("branch") ? withRetry(() => provider.listBranches({
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
- })) : { data: [] }, types.includes("tag") ? withRetry(() => provider.listTags({
195
+ })) : [], types.includes("commit") && COMMIT_SHA_PATTERN.test(query) ? withRetry(() => provider.getCommit({
147
196
  repo,
148
- limit,
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
- })) : { data: [] }]);
151
- return [...branches.data.map((b) => ({
152
- type: "branch",
153
- name: b.name,
154
- sha: b.sha,
155
- raw: b.raw
156
- })), ...tags.data.map((t) => ({
157
- type: "tag",
158
- name: t.name,
159
- sha: t.sha,
160
- raw: t.raw
161
- }))].slice(0, limit);
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
- const refTypes = types.filter((t) => t !== "commit");
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 bytesToBase64Url(bytes) {
3
+ function bytesToBase64(bytes) {
4
4
  let binary = "";
5
5
  for (const byte of bytes) binary += String.fromCharCode(byte);
6
- return btoa(binary).replace(/=+$/, "").replace(/\+/g, "-").replace(/\//g, "_");
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
- //#region src/webhooks/parse.ts
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 { B as WebhookEventType, C as Namespace, D as ProviderName, F as Tag, M as RepoProvider, N as Repository, i as Branch, j as RepoCapabilities, o as Commit, w as NamespaceKind, z as Webhook } from "./types-CwXDjAbE.js";
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-1KJe54LW.js";
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; the tag object rather than the commit for annotated tags on GitHub and Gitea. */
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 { RefType as A, WebhookEventType as B, Namespace as C, ProviderName as D, ParsedWebhookEvent as E, Tag as F, TokenProvider as I, UpdateWebhookParams as L, RepoProvider as M, Repository as N, ProviderSearchRefsParams as O, SearchRefsParams as P, UserRef as R, ListWebhooksParams as S, Page as T, WebhookVerificationMethod as V, ListBranchesParams as _, CloneUrl as a, ListRepositoriesParams as b, DeleteWebhookParams as c, GetCloneUrlParams as d, GetCommitParams as f, IncomingWebhookRequest as g, GitActor as h, Branch as i, RepoCapabilities as j, RefMatch as k, DownloadArchiveParams as l, GetWebhookParams as m, ArchiveFormat as n, Commit as o, GetRepositoryParams as p, AuthenticatedUser as r, CreateWebhookParams as s, Archive as t, GetAuthenticatedUserParams as u, ListCommitsParams as v, NamespaceKind as w, ListTagsParams as x, ListNamespacesParams as y, Webhook as z };
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.0",
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"