repo-sdk 0.2.1 → 0.2.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.
Files changed (3) hide show
  1. package/README.md +142 -19
  2. package/dist/github.js +23 -15
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -4,7 +4,16 @@
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 — 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.
7
+ A unified, normalized, zero-dependency, edge-compatible TypeScript SDK over [GitHub](https://github.com), [GitLab](https://gitlab.com), [Bitbucket Cloud](https://bitbucket.org), [Azure DevOps](https://azure.microsoft.com/products/devops), and [Gitea](https://about.gitea.com) — plus generic git smart-HTTP remotes. Write your repository, commit, branch, tag, ref-resolution, 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
+
9
+ ## Features
10
+
11
+ - **One API, eight namespaces** — users, namespaces, repos, commits, branches, tags, refs, and webhooks behind a single interface.
12
+ - **Capability gating** — providers differ. The client reports what the active provider supports and throws `unsupported` instead of silently dropping an option.
13
+ - **Ref resolution that follows git** — `refs.resolve` accepts a branch, a tag, a fully-qualified `refs/…`, `HEAD`, or an abbreviated SHA and returns one normalized match, with tags shadowing branches exactly like `git rev-parse`.
14
+ - **Webhook verify & parse** — standalone per-provider helpers that take a Web-standard `Request`. No client, no credentials. `detectWebhookProvider` routes a shared endpoint to the right pair.
15
+ - **Zero dependencies** — `fetch` and Web Crypto only. No `node:*` imports, so it runs on Cloudflare Workers without `nodejs_compat` — including RS256 JWT signing for GitHub App auth.
16
+ - **Tree-shakable and typed** — each provider ships on its own subpath, so unused providers never reach your bundle. Every model is normalized (IDs as strings, dates as `Date`, refs fully qualified) and keeps the untouched provider payload on `raw`.
8
17
 
9
18
  ## Installation
10
19
 
@@ -14,6 +23,8 @@ npm install repo-sdk
14
23
 
15
24
  ## Quickstart
16
25
 
26
+ Create a client from the package root and a provider factory from its subpath.
27
+
17
28
  ```ts
18
29
  import { createClient } from 'repo-sdk';
19
30
  import { github } from 'repo-sdk/github';
@@ -21,42 +32,154 @@ import { github } from 'repo-sdk/github';
21
32
  const client = createClient({
22
33
  provider: github({ auth: { token: process.env.GITHUB_TOKEN! } }),
23
34
  });
35
+ ```
36
+
37
+ Resolve a ref — a branch, tag, SHA, or `HEAD` — to a single commit, and page through history.
24
38
 
25
- // Resolve a ref (branch, tag, or SHA) to a single commit.
39
+ ```ts
26
40
  const head = await client.commits.get({ repo: 'capawesome-team/repo-sdk', ref: 'main' });
27
41
  console.log(head.sha, head.message);
28
42
 
29
- // List commits on a branch.
30
- const { data: commits } = await client.commits.list({
43
+ const { data: commits, cursor } = await client.commits.list({
31
44
  repo: 'capawesome-team/repo-sdk',
32
45
  ref: 'main',
33
46
  limit: 20,
34
47
  });
35
48
  ```
36
49
 
37
- Switching providers means swapping the `provider` — the rest of your code stays the same.
50
+ Find out what a user-supplied ref actually is before you act on it.
38
51
 
39
- ## Documentation
52
+ ```ts
53
+ const match = await client.refs.resolve({ repo: 'capawesome-team/repo-sdk', ref: 'v1.0.0' });
54
+ match.type; // 'branch' | 'tag' | 'commit'
55
+ match.ref; // 'refs/tags/v1.0.0'
56
+ match.sha; // peeled to the commit SHA for annotated tags
57
+ ```
40
58
 
41
- Full documentation lives at **[repo-sdk.dev](https://repo-sdk.dev)** ([docs](https://repo-sdk.dev/docs)):
59
+ Download the code at that ref, as a stream or as a credential-bearing clone URL for the `git` CLI.
42
60
 
43
- - [Quickstart](https://repo-sdk.dev/docs/quickstart)
44
- - [Authentication](https://repo-sdk.dev/docs/authentication)
45
- - [Guides](https://repo-sdk.dev/docs/guides)
46
- - [API Reference](https://repo-sdk.dev/docs/reference)
61
+ ```ts
62
+ const archive = await client.repos.downloadArchive({
63
+ repo: 'capawesome-team/repo-sdk',
64
+ ref: match.sha,
65
+ format: 'tar.gz',
66
+ });
67
+ await new Response(archive.stream).arrayBuffer();
68
+
69
+ const { url } = await client.repos.getCloneUrl({ repo: 'capawesome-team/repo-sdk' });
70
+ ```
71
+
72
+ Verify and handle the webhook. The helpers take the `Request` itself — no client, no credentials.
73
+
74
+ ```ts
75
+ import { parseWebhookEvent, verifyWebhook } from 'repo-sdk/github';
76
+
77
+ export async function POST(request: Request): Promise<Response> {
78
+ if (!(await verifyWebhook({ request, secret: process.env.WEBHOOK_SECRET! }))) {
79
+ return new Response('invalid signature', { status: 401 });
80
+ }
81
+
82
+ const event = await parseWebhookEvent(request);
83
+
84
+ switch (event.type) {
85
+ case 'push':
86
+ // event.headCommitSha is undefined when the push deleted the ref.
87
+ break;
88
+ case 'tag_push':
89
+ // event.ref is always fully qualified — refs/tags/v1.0.0.
90
+ break;
91
+ case 'release':
92
+ break;
93
+ }
94
+
95
+ return new Response(null, { status: 204 });
96
+ }
97
+ ```
98
+
99
+ ## API at a glance
100
+
101
+ | Namespace | Methods |
102
+ | ------------ | -------------------------------------------------------------- |
103
+ | `users` | `me` |
104
+ | `namespaces` | `list` · `listAll` |
105
+ | `repos` | `list` · `listAll` · `get` · `downloadArchive` · `getCloneUrl` |
106
+ | `commits` | `list` · `listAll` · `get` |
107
+ | `branches` | `list` · `listAll` · `get` |
108
+ | `tags` | `list` · `listAll` · `get` |
109
+ | `refs` | `resolve` · `search` |
110
+ | `webhooks` | `create` · `list` · `get` · `update` · `delete` |
111
+
112
+ Every list returns an opaque cursor and has a `listAll` async generator that walks the pages for you. Cursors are provider-tagged and origin-checked, so a forged one cannot redirect an authenticated request. Every method accepts a `signal` for cancellation. Rate-limited requests are retried once when the provider's `Retry-After` fits the budget (10 seconds by default, configurable via `retry`).
113
+
114
+ Webhook payload helpers — `verifyWebhook` and `parseWebhookEvent` — are exported from the provider subpaths and take no credential beyond the shared secret, so they run in a handler with no client at all. `detectWebhookProvider` from the core entry identifies the sender when one endpoint serves several providers.
47
115
 
48
116
  ## Providers
49
117
 
50
118
  | Provider | Import | Notes |
51
119
  | ------------ | ----------------------- | ---------------------------------------------------------------- |
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) |
120
+ | GitHub | `repo-sdk/github` | Personal access token or GitHub App installation auth. |
121
+ | GitLab | `repo-sdk/gitlab` | Personal, group, project, or OAuth token. |
122
+ | Bitbucket | `repo-sdk/bitbucket` | Bitbucket Cloud. Atlassian API token or access token. |
123
+ | Azure DevOps | `repo-sdk/azure-devops` | Organization-scoped. PAT or Entra ID token. |
124
+ | Gitea | `repo-sdk/gitea` | Gitea ≥ 1.20 and Forgejo. |
125
+ | Generic git | `repo-sdk/git-http` | Any git smart-HTTP remote. Ref discovery and clone URLs only. |
126
+ | Testing | `repo-sdk/testing` | Seedable in-memory provider for your test suite. No credentials. |
58
127
 
59
- Self-hosted deployments — GitHub Enterprise Server, GitLab self-managed, Azure DevOps Server, and Gitea/Forgejo instances — are supported via `baseUrl`.
128
+ Every factory accepts an injectable `fetch`, and every token can be a static string or a `tokenProvider` that mints one per request and is re-invoked once on a 401. Self-hosted deployments — GitHub Enterprise Server, GitLab self-managed, Azure DevOps Server, and Gitea/Forgejo — are supported via `baseUrl`. Providers do not support the same feature set — `client.capabilities` tells you what the active one can do, and the full breakdown lives in the [capability matrix](https://repo-sdk.dev/docs/reference/capability-matrix).
129
+
130
+ ## Testing
131
+
132
+ `repo-sdk/testing` ships a seedable in-memory provider you hand to `createClient` like any other — no account, no tokens, no `fetch` stubs, no throwaway repositories.
133
+
134
+ ```ts
135
+ import { createClient } from 'repo-sdk';
136
+ import { createInMemoryProvider } from 'repo-sdk/testing';
137
+
138
+ const client = createClient({
139
+ provider: createInMemoryProvider({
140
+ repositories: { 'acme/app': { name: 'app', namespace: 'acme', defaultBranch: 'main' } },
141
+ branches: { 'acme/app': [{ name: 'main', sha: 'c1' }] },
142
+ commits: { 'acme/app': [{ sha: 'c1', message: 'Initial commit' }] },
143
+ }),
144
+ });
145
+ ```
146
+
147
+ It reports every capability enabled by default, and a second options argument simulates a specific provider's identity and capability gaps — so code that branches on `client.capabilities` is testable. Its page size is deliberately small, so your cursor pagination gets exercised, and `provider.state` exposes the data as plain maps for assertions.
148
+
149
+ ## Errors
150
+
151
+ Every failure is a `RepoError` with a closed `code` union: `unauthorized`, `forbidden`, `not_found`, `rate_limited`, `validation`, `unsupported`, `provider_error`, `network_error`.
152
+
153
+ ```ts
154
+ import { RepoError } from 'repo-sdk';
155
+
156
+ try {
157
+ await client.repos.downloadArchive({ repo: 'acme/app', ref: 'main', format: 'tar.gz' });
158
+ } catch (error) {
159
+ if (error instanceof RepoError && error.code === 'unsupported') {
160
+ // Azure DevOps serves zip only — fall back instead of failing the job.
161
+ }
162
+ }
163
+ ```
164
+
165
+ Errors also carry `provider`, `status`, `retryAfter` and `retryable`; `cause` is the underlying JS `Error`. Token values are redacted from every message, so a leaked stack trace cannot leak a credential. The one deliberate exception is `repos.getCloneUrl`, which returns a URL with the credential embedded because that is what `git clone` needs — treat it as a secret.
166
+
167
+ ## Runtime support
168
+
169
+ Node.js ≥ 20, Cloudflare Workers (without `nodejs_compat`), Deno, Bun, Vercel Edge, and any other runtime with `fetch`, Web Crypto and `TextEncoder`. ESM only.
170
+
171
+ ## Documentation
172
+
173
+ Full documentation lives at **[repo-sdk.dev](https://repo-sdk.dev)** ([docs](https://repo-sdk.dev/docs)):
174
+
175
+ - [Quickstart](https://repo-sdk.dev/docs/quickstart)
176
+ - [Concepts](https://repo-sdk.dev/docs/concepts/client-and-providers) — clients, repositories and namespaces, capabilities, pagination, errors
177
+ - [Authentication](https://repo-sdk.dev/docs/authentication) — tokens, GitHub Apps, Entra ID, and the scopes each provider needs
178
+ - [Guides](https://repo-sdk.dev/docs/guides/resolving-refs) — resolving refs, commits, branches, tags, downloading code, managing and receiving webhooks
179
+ - [Testing](https://repo-sdk.dev/docs/testing)
180
+ - [Capability matrix](https://repo-sdk.dev/docs/reference/capability-matrix)
181
+ - [Provider support](https://repo-sdk.dev/docs/reference/provider-support)
182
+ - [Error codes](https://repo-sdk.dev/docs/reference/error-codes)
60
183
 
61
184
  ## Development
62
185
 
@@ -69,7 +192,7 @@ npm install
69
192
  | Script | Description |
70
193
  | -------------------- | --------------------------------------------------------------- |
71
194
  | `npm run build` | Build with tsdown |
72
- | `npm test` | Run the unit + contract test suite |
195
+ | `npm test` | Run the unit + provider contract test suite |
73
196
  | `npm run test:watch` | Run tests in watch mode |
74
197
  | `npm run test:live` | Gated live provider tests (env vars documented in CONTRIBUTING) |
75
198
  | `npm run typecheck` | Type-check without emitting |
package/dist/github.js CHANGED
@@ -580,6 +580,17 @@ function github(options) {
580
580
  };
581
581
  }
582
582
  if (params.namespace) {
583
+ if (tokenSource.kind === "app") {
584
+ const namespaceLower = params.namespace.toLowerCase();
585
+ const { data, response } = await http.json("/installation/repositories", {
586
+ query: { per_page: perPage },
587
+ signal: params.signal
588
+ });
589
+ return {
590
+ data: reposFromBody(data).filter((repo) => repo.owner.login.toLowerCase() === namespaceLower).map(mapRepository),
591
+ cursor: nextCursor(response)
592
+ };
593
+ }
583
594
  try {
584
595
  const { data, response } = await http.json(`/orgs/${encodeURIComponent(params.namespace)}/repos`, {
585
596
  query: { per_page: perPage },
@@ -592,21 +603,18 @@ function github(options) {
592
603
  } catch (error) {
593
604
  if (!(error instanceof RepoError && error.code === "not_found")) throw error;
594
605
  }
595
- if (tokenSource.kind === "token") {
596
- const login = await getLogin(params.signal);
597
- if (params.namespace.toLowerCase() === login.toLowerCase()) {
598
- const { data, response } = await http.json("/user/repos", {
599
- query: {
600
- affiliation: "owner",
601
- per_page: perPage
602
- },
603
- signal: params.signal
604
- });
605
- return {
606
- data: data.map(mapRepository),
607
- cursor: nextCursor(response)
608
- };
609
- }
606
+ if ((await getLogin(params.signal)).toLowerCase() === params.namespace.toLowerCase()) {
607
+ const { data, response } = await http.json("/user/repos", {
608
+ query: {
609
+ affiliation: "owner",
610
+ per_page: perPage
611
+ },
612
+ signal: params.signal
613
+ });
614
+ return {
615
+ data: data.map(mapRepository),
616
+ cursor: nextCursor(response)
617
+ };
610
618
  }
611
619
  const { data, response } = await http.json(`/users/${encodeURIComponent(params.namespace)}/repos`, {
612
620
  query: { per_page: perPage },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "repo-sdk",
3
- "version": "0.2.1",
3
+ "version": "0.2.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>",
@@ -22,7 +22,7 @@
22
22
  "version-control",
23
23
  "webhooks"
24
24
  ],
25
- "homepage": "https://github.com/capawesome-team/repo-sdk",
25
+ "homepage": "https://repo-sdk.dev",
26
26
  "repository": {
27
27
  "type": "git",
28
28
  "url": "https://github.com/capawesome-team/repo-sdk"