forges 0.0.0 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENCE +21 -0
- package/README.md +72 -0
- package/dist/azure-devops/index.d.mts +2 -0
- package/dist/azure-devops/index.mjs +1 -0
- package/dist/azure-devops-DwQvL4TV.mjs +1355 -0
- package/dist/bitbucket/index.d.mts +2 -0
- package/dist/bitbucket/index.mjs +1 -0
- package/dist/bitbucket-CpcfJlYi.mjs +1171 -0
- package/dist/crypto-CQNEyYHD.mjs +156 -0
- package/dist/cursor-origin/index.d.mts +2 -0
- package/dist/cursor-origin/index.mjs +1 -0
- package/dist/cursor-origin-BhqYx2oW.mjs +980 -0
- package/dist/define-B2_4ob2i.d.mts +307 -0
- package/dist/define-D3yEXtKK.mjs +2276 -0
- package/dist/env.d.mts +46 -0
- package/dist/env.mjs +283 -0
- package/dist/fake/index.d.mts +227 -0
- package/dist/fake/index.mjs +1001 -0
- package/dist/fetch-D4pSRvvA.d.mts +1723 -0
- package/dist/forgejo/index.d.mts +2 -0
- package/dist/forgejo/index.mjs +1 -0
- package/dist/forgejo-BCefbHUN.mjs +2005 -0
- package/dist/gitea/index.d.mts +2 -0
- package/dist/gitea/index.mjs +22 -0
- package/dist/gitee/index.d.mts +2 -0
- package/dist/gitee/index.mjs +1 -0
- package/dist/gitee-CA-f7Rs8.mjs +1272 -0
- package/dist/github/index.d.mts +2 -0
- package/dist/github/index.mjs +1 -0
- package/dist/github-B0umL8_f.mjs +3259 -0
- package/dist/gitlab/index.d.mts +2 -0
- package/dist/gitlab/index.mjs +1 -0
- package/dist/gitlab-Bsr__Z0C.mjs +2378 -0
- package/dist/graphql-D7uGzZTf.mjs +366 -0
- package/dist/index-BQ6VS31s.d.mts +32 -0
- package/dist/index-C-jz9Mo3.d.mts +16 -0
- package/dist/index-C4fYzmK3.d.mts +102 -0
- package/dist/index-CQiIX4SH.d.mts +27 -0
- package/dist/index-CTj5gQzl.d.mts +35 -0
- package/dist/index-CliBWzOF.d.mts +23 -0
- package/dist/index-Cz_y-t3d.d.mts +36 -0
- package/dist/index-DEt25E1w.d.mts +77 -0
- package/dist/index-DIuS0EOy.d.mts +43 -0
- package/dist/index-uSpsMCrC.d.mts +26 -0
- package/dist/index.d.mts +41 -0
- package/dist/index.mjs +13 -0
- package/dist/kit.d.mts +183 -0
- package/dist/kit.mjs +4 -0
- package/dist/provider-CoZ7cCxp.d.mts +1439 -0
- package/dist/provider-Ed1YhkQN.mjs +169 -0
- package/dist/pushin/index.d.mts +2 -0
- package/dist/pushin/index.mjs +1 -0
- package/dist/pushin-CkxgJ2cU.mjs +343 -0
- package/dist/schema/Actor.json +96 -0
- package/dist/schema/AlertSupport.json +49 -0
- package/dist/schema/AuthKind.json +15 -0
- package/dist/schema/Branch.json +31 -0
- package/dist/schema/ChangedFile.json +58 -0
- package/dist/schema/Check.json +169 -0
- package/dist/schema/CheckRef.json +114 -0
- package/dist/schema/CheckState.json +14 -0
- package/dist/schema/ChecksSummary.json +34 -0
- package/dist/schema/CiJob.json +186 -0
- package/dist/schema/CiJobRef.json +132 -0
- package/dist/schema/CiRun.json +234 -0
- package/dist/schema/CiRunRef.json +103 -0
- package/dist/schema/CodeFragment.json +19 -0
- package/dist/schema/CodeMatch.json +128 -0
- package/dist/schema/Collaborator.json +169 -0
- package/dist/schema/Comment.json +336 -0
- package/dist/schema/CommentRef.json +158 -0
- package/dist/schema/Commit.json +307 -0
- package/dist/schema/CommitRef.json +104 -0
- package/dist/schema/CommitSignature.json +118 -0
- package/dist/schema/Comparison.json +355 -0
- package/dist/schema/Cursor.json +21 -0
- package/dist/schema/EventAction.json +33 -0
- package/dist/schema/EventDetail.json +649 -0
- package/dist/schema/EventKind.json +26 -0
- package/dist/schema/FileContent.json +73 -0
- package/dist/schema/FileStatus.json +15 -0
- package/dist/schema/ForgeCapabilities.json +737 -0
- package/dist/schema/ForgeEvent.json +789 -0
- package/dist/schema/ForgeInstance.json +7 -0
- package/dist/schema/ForgeKind.json +32 -0
- package/dist/schema/ForgeWarning.json +49 -0
- package/dist/schema/ForgeWarningCode.json +7 -0
- package/dist/schema/GetManyResult.json +699 -0
- package/dist/schema/Installation.json +131 -0
- package/dist/schema/InstallationToken.json +31 -0
- package/dist/schema/KnownForgeKind.json +19 -0
- package/dist/schema/Label.json +22 -0
- package/dist/schema/MergeMethod.json +14 -0
- package/dist/schema/Milestone.json +40 -0
- package/dist/schema/Notification.json +498 -0
- package/dist/schema/NotificationReason.json +28 -0
- package/dist/schema/NotificationRef.json +60 -0
- package/dist/schema/NotificationSubject.json +376 -0
- package/dist/schema/PerKind.json +49 -0
- package/dist/schema/PullBranches.json +125 -0
- package/dist/schema/PushCommit.json +24 -0
- package/dist/schema/Reaction.json +148 -0
- package/dist/schema/ReactionContent.json +17 -0
- package/dist/schema/ReactionSummary.json +70 -0
- package/dist/schema/Release.json +286 -0
- package/dist/schema/ReleaseAsset.json +169 -0
- package/dist/schema/ReleaseAssetRef.json +138 -0
- package/dist/schema/ReleaseRef.json +107 -0
- package/dist/schema/Repo.json +354 -0
- package/dist/schema/RepoFeatures.json +35 -0
- package/dist/schema/RepoPermissions.json +35 -0
- package/dist/schema/RepoRef.json +77 -0
- package/dist/schema/RepoRole.json +15 -0
- package/dist/schema/RepoVisibility.json +12 -0
- package/dist/schema/Review.json +371 -0
- package/dist/schema/ReviewComment.json +280 -0
- package/dist/schema/ReviewCommentInput.json +34 -0
- package/dist/schema/ReviewRef.json +158 -0
- package/dist/schema/ReviewState.json +15 -0
- package/dist/schema/Reviewer.json +133 -0
- package/dist/schema/SecurityAlert.json +219 -0
- package/dist/schema/SecurityAlertKind.json +14 -0
- package/dist/schema/SecurityAlertRef.json +122 -0
- package/dist/schema/SecuritySeverity.json +14 -0
- package/dist/schema/SourceKind.json +12 -0
- package/dist/schema/SubscriptionState.json +12 -0
- package/dist/schema/Support.json +23 -0
- package/dist/schema/Tag.json +25 -0
- package/dist/schema/TextLimits.json +18 -0
- package/dist/schema/Thread.json +653 -0
- package/dist/schema/ThreadKind.json +14 -0
- package/dist/schema/ThreadRef.json +131 -0
- package/dist/schema/ThreadStack.json +146 -0
- package/dist/schema/ThreadState.json +13 -0
- package/dist/schema/TreeEntry.json +46 -0
- package/dist/schema/TreeEntryType.json +13 -0
- package/dist/schema/UpsertCommentResult.json +355 -0
- package/dist/schema/User.json +123 -0
- package/dist/schema/Webhook.json +188 -0
- package/dist/schema/WebhookDeliveryRecord.json +173 -0
- package/dist/schema/WebhookDeliveryRef.json +135 -0
- package/dist/schema/WebhookRef.json +105 -0
- package/dist/schema/index.d.mts +4 -0
- package/dist/schema/index.mjs +3219 -0
- package/dist/tangled/index.d.mts +2 -0
- package/dist/tangled/index.mjs +1 -0
- package/dist/tangled-ChdPmiv-.mjs +1570 -0
- package/dist/testing/index.d.mts +123 -0
- package/dist/testing/index.mjs +242 -0
- package/dist/webhooks-rn9-8lxT.mjs +70 -0
- package/package.json +115 -3
- package/dist/.gitkeep +0 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { Et as ForgeKind, n as FetchLike } from "../fetch-D4pSRvvA.mjs";
|
|
2
|
+
//#region src/testing/index.d.ts
|
|
3
|
+
/** One recorded or hand-authored request and its response. */
|
|
4
|
+
export interface Fixture {
|
|
5
|
+
/** `true` when the fixture was written from documentation, not recorded. */
|
|
6
|
+
handAuthored?: boolean;
|
|
7
|
+
/** GraphQL requests share one URL, so they are matched on `operationName` and, when recorded, `variables` too. */
|
|
8
|
+
request: {
|
|
9
|
+
method: string;
|
|
10
|
+
url: string;
|
|
11
|
+
/** The GraphQL operation name, for a GraphQL request. */
|
|
12
|
+
operationName?: string;
|
|
13
|
+
/** The GraphQL variables, for a GraphQL request. */
|
|
14
|
+
variables?: Record<string, unknown>;
|
|
15
|
+
/** The JSON or text the request sent, when recorded with `requestBodies`. Replay does not match on it. */
|
|
16
|
+
body?: unknown;
|
|
17
|
+
};
|
|
18
|
+
/** A JSON body, the text of a response that is not JSON, or the base64 of one that is not text. */
|
|
19
|
+
response: {
|
|
20
|
+
status: number;
|
|
21
|
+
headers?: Record<string, string>;
|
|
22
|
+
/** The response body: parsed JSON, the text of a response that is not JSON, or base64 when `encoding` is set. */
|
|
23
|
+
body?: unknown;
|
|
24
|
+
/** Set when `body` holds the base64 of a binary response. */
|
|
25
|
+
encoding?: "base64";
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/** A request that `fixtureFetch()` received. */
|
|
29
|
+
export interface FixtureCall {
|
|
30
|
+
method: string;
|
|
31
|
+
url: string;
|
|
32
|
+
body?: string;
|
|
33
|
+
/** The GraphQL operation name, for a GraphQL request. */
|
|
34
|
+
operationName?: string;
|
|
35
|
+
/** The GraphQL variables, for a GraphQL request. */
|
|
36
|
+
variables?: Record<string, unknown>;
|
|
37
|
+
/** The `Authorization` header that the request carried. */
|
|
38
|
+
authorization?: string;
|
|
39
|
+
/** All the request headers. */
|
|
40
|
+
headers: Headers;
|
|
41
|
+
/** The fixture that answered, or `undefined` where an override did. */
|
|
42
|
+
fixture?: Fixture;
|
|
43
|
+
}
|
|
44
|
+
/** Options for {@link fixtureFetch}. */
|
|
45
|
+
export interface FixtureFetchOptions {
|
|
46
|
+
/**
|
|
47
|
+
* Serve the fixtures that share a request in the order they were recorded,
|
|
48
|
+
* repeating the last, instead of always serving the last. Use it to replay
|
|
49
|
+
* a recording that reads the same URL before and after a write.
|
|
50
|
+
*/
|
|
51
|
+
sequential?: boolean;
|
|
52
|
+
}
|
|
53
|
+
/** A `fetch` that answers from fixtures, and records the calls it received. */
|
|
54
|
+
export interface FixtureFetch {
|
|
55
|
+
/** The `fetch` to pass to a provider. */
|
|
56
|
+
fetch: FetchLike;
|
|
57
|
+
/** Every request made, in order, including ones no fixture matched. */
|
|
58
|
+
calls: FixtureCall[];
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* A `fetch` that serves `fixtures` and records every call. Requests match on
|
|
62
|
+
* method, URL (query order ignored), GraphQL `operationName` and, where the
|
|
63
|
+
* fixture has them, `variables`. `overrides`
|
|
64
|
+
* replace or add responses, keyed `METHOD url [operationName]`. An unmatched
|
|
65
|
+
* request throws.
|
|
66
|
+
* @param fixtures The recorded or hand-written requests and responses to serve.
|
|
67
|
+
* @param overrides Responses that replace or add to the fixtures.
|
|
68
|
+
* @param options How to serve fixtures that share a request.
|
|
69
|
+
* @example
|
|
70
|
+
* ```ts
|
|
71
|
+
* import { github } from 'forges'
|
|
72
|
+
* import { fixtureFetch, loadFixtures } from 'forges/testing'
|
|
73
|
+
*
|
|
74
|
+
* const { fetch, calls } = fixtureFetch(await loadFixtures(new URL('./fixtures', import.meta.url)))
|
|
75
|
+
* const provider = github({ fetch }).create()
|
|
76
|
+
* ```
|
|
77
|
+
*/
|
|
78
|
+
export declare function fixtureFetch(fixtures: Iterable<Fixture>, overrides?: Record<string, Fixture["response"]>, options?: FixtureFetchOptions): FixtureFetch;
|
|
79
|
+
/** A `fetch` that forwards to the real one and records every exchange as a fixture. */
|
|
80
|
+
export interface RecordingFetch {
|
|
81
|
+
/** The `fetch` to pass to a provider. */
|
|
82
|
+
fetch: FetchLike;
|
|
83
|
+
/** One fixture per response, in order; pass them to {@link fixtureFetch} to replay. */
|
|
84
|
+
fixtures: Fixture[];
|
|
85
|
+
}
|
|
86
|
+
/** Options for {@link recordingFetch}. */
|
|
87
|
+
export interface RecordingFetchOptions {
|
|
88
|
+
/**
|
|
89
|
+
* Record the body of each request that sends a string, parsed as JSON where
|
|
90
|
+
* it is JSON. A request body can hold a secret (a password, a webhook
|
|
91
|
+
* secret), so redact it too.
|
|
92
|
+
*/
|
|
93
|
+
requestBodies?: boolean;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Wraps `fetch` (the global one by default) and records every response as a
|
|
97
|
+
* {@link Fixture}. Request headers are never recorded, but URLs and response
|
|
98
|
+
* bodies are, and either can hold a secret (an installation token, a query
|
|
99
|
+
* token). Use `redact` to rewrite each fixture before it is kept. A body that
|
|
100
|
+
* is not UTF-8 text is kept as base64.
|
|
101
|
+
* @param fetch The `fetch` to record. Defaults to the global one.
|
|
102
|
+
* @param redact Rewrites each fixture before it is kept, to remove secrets.
|
|
103
|
+
* @param options What else to record.
|
|
104
|
+
*/
|
|
105
|
+
export declare function recordingFetch(fetch?: FetchLike, redact?: (fixture: Fixture) => Fixture, options?: RecordingFetchOptions): RecordingFetch;
|
|
106
|
+
/**
|
|
107
|
+
* Reads every `*.json` fixture in `directory`, skipping `manifest.json` and
|
|
108
|
+
* files without a `request`. Node, Bun and Deno only.
|
|
109
|
+
* @param directory The directory that holds the fixture JSON files.
|
|
110
|
+
*/
|
|
111
|
+
export declare function loadFixtures(directory: string | URL): Promise<Fixture[]>;
|
|
112
|
+
/**
|
|
113
|
+
* Builds the headers a forge would sign `body` with, merged over `headers`
|
|
114
|
+
* (the event name and delivery id, which only the caller knows). `secret` is
|
|
115
|
+
* the webhook secret, or for Cursor Origin the base64 PKCS#8 Ed25519 private
|
|
116
|
+
* key matching the provider's verification key.
|
|
117
|
+
* @param forge The forge whose signature scheme to use.
|
|
118
|
+
* @param body The raw body of the delivery.
|
|
119
|
+
* @param secret The secret that signs it.
|
|
120
|
+
* @param headers Headers to add to the signed headers.
|
|
121
|
+
*/
|
|
122
|
+
export declare function signDelivery(forge: ForgeKind, body: string, secret: string, headers?: Record<string, string>): Promise<Record<string, string>>;
|
|
123
|
+
//#endregion
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { a as sha256Hex, i as hmacSha256Hex, l as toBase64 } from "../crypto-CQNEyYHD.mjs";
|
|
2
|
+
//#region src/testing/index.ts
|
|
3
|
+
function fixtureKey(method, url, operationName, variables) {
|
|
4
|
+
const parsed = new URL(url);
|
|
5
|
+
parsed.searchParams.sort();
|
|
6
|
+
const base = `${method.toUpperCase()} ${parsed.origin}${parsed.pathname}${parsed.search}`;
|
|
7
|
+
const operation = operationName ? `${base} ${operationName}` : base;
|
|
8
|
+
return variables ? `${operation} ${JSON.stringify(variables, Object.keys(variables).sort())}` : operation;
|
|
9
|
+
}
|
|
10
|
+
function operationOf(url, body) {
|
|
11
|
+
if (!body || !url.endsWith("/graphql")) return {};
|
|
12
|
+
try {
|
|
13
|
+
const { operationName, variables } = JSON.parse(body);
|
|
14
|
+
return {
|
|
15
|
+
operationName,
|
|
16
|
+
variables
|
|
17
|
+
};
|
|
18
|
+
} catch {
|
|
19
|
+
return {};
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function callOf(input, init) {
|
|
23
|
+
const body = typeof init?.body === "string" ? init.body : void 0;
|
|
24
|
+
const headers = new Headers(init?.headers);
|
|
25
|
+
return {
|
|
26
|
+
method: (init?.method ?? "GET").toUpperCase(),
|
|
27
|
+
url: input,
|
|
28
|
+
body,
|
|
29
|
+
...operationOf(input, body),
|
|
30
|
+
authorization: headers.get("authorization") ?? void 0,
|
|
31
|
+
headers
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* A `fetch` that serves `fixtures` and records every call. Requests match on
|
|
36
|
+
* method, URL (query order ignored), GraphQL `operationName` and, where the
|
|
37
|
+
* fixture has them, `variables`. `overrides`
|
|
38
|
+
* replace or add responses, keyed `METHOD url [operationName]`. An unmatched
|
|
39
|
+
* request throws.
|
|
40
|
+
* @param fixtures The recorded or hand-written requests and responses to serve.
|
|
41
|
+
* @param overrides Responses that replace or add to the fixtures.
|
|
42
|
+
* @param options How to serve fixtures that share a request.
|
|
43
|
+
* @example
|
|
44
|
+
* ```ts
|
|
45
|
+
* import { github } from 'forges'
|
|
46
|
+
* import { fixtureFetch, loadFixtures } from 'forges/testing'
|
|
47
|
+
*
|
|
48
|
+
* const { fetch, calls } = fixtureFetch(await loadFixtures(new URL('./fixtures', import.meta.url)))
|
|
49
|
+
* const provider = github({ fetch }).create()
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
function fixtureFetch(fixtures, overrides = {}, options = {}) {
|
|
53
|
+
const responses = /* @__PURE__ */ new Map();
|
|
54
|
+
for (const fixture of fixtures) {
|
|
55
|
+
const key = fixtureKey(fixture.request.method, fixture.request.url, fixture.request.operationName, fixture.request.variables);
|
|
56
|
+
const queue = options.sequential ? responses.get(key) : void 0;
|
|
57
|
+
if (queue) queue.push({
|
|
58
|
+
response: fixture.response,
|
|
59
|
+
fixture
|
|
60
|
+
});
|
|
61
|
+
else responses.set(key, [{
|
|
62
|
+
response: fixture.response,
|
|
63
|
+
fixture
|
|
64
|
+
}]);
|
|
65
|
+
}
|
|
66
|
+
for (const [override, response] of Object.entries(overrides)) {
|
|
67
|
+
const [method = "GET", url = "", operationName] = override.split(" ");
|
|
68
|
+
responses.set(fixtureKey(method, url, operationName), [{ response }]);
|
|
69
|
+
}
|
|
70
|
+
const served = /* @__PURE__ */ new Map();
|
|
71
|
+
const take = (key) => {
|
|
72
|
+
const queue = responses.get(key);
|
|
73
|
+
if (!queue) return;
|
|
74
|
+
const index = served.get(key) ?? 0;
|
|
75
|
+
served.set(key, index + 1);
|
|
76
|
+
return queue[Math.min(index, queue.length - 1)];
|
|
77
|
+
};
|
|
78
|
+
const calls = [];
|
|
79
|
+
const fetch = async (input, init) => {
|
|
80
|
+
const call = callOf(input, init);
|
|
81
|
+
calls.push(call);
|
|
82
|
+
const match = take(fixtureKey(call.method, input, call.operationName, call.variables)) ?? take(fixtureKey(call.method, input, call.operationName));
|
|
83
|
+
if (!match) throw new Error(`No fixture for ${call.method} ${input}${call.operationName ? ` (${call.operationName})` : ""}`);
|
|
84
|
+
const { response } = match;
|
|
85
|
+
call.fixture = match.fixture;
|
|
86
|
+
const headers = new Headers(response.headers);
|
|
87
|
+
const binary = response.encoding === "base64" && typeof response.body === "string";
|
|
88
|
+
const body = response.body === void 0 ? null : binary ? Uint8Array.from(atob(response.body), (char) => char.charCodeAt(0)) : typeof response.body === "string" ? response.body : JSON.stringify(response.body);
|
|
89
|
+
if (body !== null && !headers.has("content-type")) headers.set("content-type", binary ? "application/octet-stream" : typeof response.body === "string" ? "text/plain" : "application/json");
|
|
90
|
+
const result = new Response(body, {
|
|
91
|
+
status: response.status,
|
|
92
|
+
headers
|
|
93
|
+
});
|
|
94
|
+
Object.defineProperty(result, "url", { value: input });
|
|
95
|
+
return result;
|
|
96
|
+
};
|
|
97
|
+
return {
|
|
98
|
+
fetch,
|
|
99
|
+
calls
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
const RECORDED_HEADERS = [
|
|
103
|
+
"link",
|
|
104
|
+
"etag",
|
|
105
|
+
"location",
|
|
106
|
+
"retry-after",
|
|
107
|
+
"x-ratelimit-limit",
|
|
108
|
+
"ratelimit-limit",
|
|
109
|
+
"x-ratelimit-remaining",
|
|
110
|
+
"x-ratelimit-reset",
|
|
111
|
+
"ratelimit-remaining",
|
|
112
|
+
"ratelimit-reset",
|
|
113
|
+
"x-total",
|
|
114
|
+
"x-total-count",
|
|
115
|
+
"x-total-pages",
|
|
116
|
+
"x-next-page",
|
|
117
|
+
"x-page",
|
|
118
|
+
"x-per-page",
|
|
119
|
+
"total_page",
|
|
120
|
+
"total_count"
|
|
121
|
+
];
|
|
122
|
+
/** Parsed JSON, or the text as is where it is not JSON or parsing would round an integer beyond `Number.MAX_SAFE_INTEGER`. */
|
|
123
|
+
function parsed(text) {
|
|
124
|
+
try {
|
|
125
|
+
let lossy = false;
|
|
126
|
+
const value = JSON.parse(text, (_key, item) => {
|
|
127
|
+
lossy ||= typeof item === "number" && Number.isInteger(item) && !Number.isSafeInteger(item);
|
|
128
|
+
return item;
|
|
129
|
+
});
|
|
130
|
+
return lossy ? text : value;
|
|
131
|
+
} catch {
|
|
132
|
+
return text;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function utf8(bytes) {
|
|
136
|
+
try {
|
|
137
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
138
|
+
} catch {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Wraps `fetch` (the global one by default) and records every response as a
|
|
144
|
+
* {@link Fixture}. Request headers are never recorded, but URLs and response
|
|
145
|
+
* bodies are, and either can hold a secret (an installation token, a query
|
|
146
|
+
* token). Use `redact` to rewrite each fixture before it is kept. A body that
|
|
147
|
+
* is not UTF-8 text is kept as base64.
|
|
148
|
+
* @param fetch The `fetch` to record. Defaults to the global one.
|
|
149
|
+
* @param redact Rewrites each fixture before it is kept, to remove secrets.
|
|
150
|
+
* @param options What else to record.
|
|
151
|
+
*/
|
|
152
|
+
function recordingFetch(fetch = globalThis.fetch, redact = (fixture) => fixture, options = {}) {
|
|
153
|
+
const fixtures = [];
|
|
154
|
+
const recording = async (input, init) => {
|
|
155
|
+
const call = callOf(input, init);
|
|
156
|
+
const sent = options.requestBodies && call.body !== void 0 ? parsed(call.body) : void 0;
|
|
157
|
+
const response = await fetch(input, init);
|
|
158
|
+
const bytes = new Uint8Array(await response.clone().arrayBuffer());
|
|
159
|
+
const text = utf8(bytes);
|
|
160
|
+
const body = text ? parsed(text) : void 0;
|
|
161
|
+
const headers = Object.fromEntries(RECORDED_HEADERS.flatMap((name) => response.headers.has(name) ? [[name, response.headers.get(name)]] : []));
|
|
162
|
+
fixtures.push(redact({
|
|
163
|
+
request: {
|
|
164
|
+
method: call.method,
|
|
165
|
+
url: input,
|
|
166
|
+
...call.operationName ? {
|
|
167
|
+
operationName: call.operationName,
|
|
168
|
+
variables: call.variables
|
|
169
|
+
} : {},
|
|
170
|
+
...sent === void 0 ? {} : { body: sent }
|
|
171
|
+
},
|
|
172
|
+
response: {
|
|
173
|
+
status: response.status,
|
|
174
|
+
headers,
|
|
175
|
+
...text === void 0 ? {
|
|
176
|
+
body: toBase64(bytes),
|
|
177
|
+
encoding: "base64"
|
|
178
|
+
} : body === void 0 ? {} : { body }
|
|
179
|
+
}
|
|
180
|
+
}));
|
|
181
|
+
return response;
|
|
182
|
+
};
|
|
183
|
+
return {
|
|
184
|
+
fetch: recording,
|
|
185
|
+
fixtures
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Reads every `*.json` fixture in `directory`, skipping `manifest.json` and
|
|
190
|
+
* files without a `request`. Node, Bun and Deno only.
|
|
191
|
+
* @param directory The directory that holds the fixture JSON files.
|
|
192
|
+
*/
|
|
193
|
+
async function loadFixtures(directory) {
|
|
194
|
+
const { readdir, readFile } = await import("node:fs/promises");
|
|
195
|
+
const base = typeof directory === "string" ? directory.replace(/\/?$/, "/") : new URL(directory.href.replace(/\/?$/, "/"));
|
|
196
|
+
const files = (await readdir(base)).filter((file) => file.endsWith(".json") && file !== "manifest.json").sort();
|
|
197
|
+
return (await Promise.all(files.map(async (file) => JSON.parse(await readFile(typeof base === "string" ? `${base}${file}` : new URL(file, base), "utf8"))))).filter((fixture) => fixture.request !== void 0);
|
|
198
|
+
}
|
|
199
|
+
async function signStandardWebhook(id, body, privateKeyBase64) {
|
|
200
|
+
const timestamp = String(Math.floor(Date.now() / 1e3));
|
|
201
|
+
const digest = await sha256Hex(`${id}.${timestamp}.${body}`);
|
|
202
|
+
const der = Uint8Array.from(atob(privateKeyBase64), (char) => char.charCodeAt(0));
|
|
203
|
+
const key = await crypto.subtle.importKey("pkcs8", der, { name: "Ed25519" }, false, ["sign"]);
|
|
204
|
+
const signature = new Uint8Array(await crypto.subtle.sign("Ed25519", key, new TextEncoder().encode(digest)));
|
|
205
|
+
return {
|
|
206
|
+
"webhook-id": id,
|
|
207
|
+
"webhook-timestamp": timestamp,
|
|
208
|
+
"webhook-signature": `v1ed,${toBase64(signature)}`
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
const SIGNERS = {
|
|
212
|
+
"github": async (body, secret) => ({ "x-hub-signature-256": `sha256=${await hmacSha256Hex(secret, body)}` }),
|
|
213
|
+
"gitlab": async (_body, secret) => ({ "x-gitlab-token": secret }),
|
|
214
|
+
"forgejo": async (body, secret) => ({ "x-forgejo-signature": await hmacSha256Hex(secret, body) }),
|
|
215
|
+
"gitea": async (body, secret) => ({ "x-gitea-signature": await hmacSha256Hex(secret, body) }),
|
|
216
|
+
"bitbucket": async (body, secret) => ({ "x-hub-signature": `sha256=${await hmacSha256Hex(secret, body)}` }),
|
|
217
|
+
"tangled": async (body, secret) => ({ "x-tangled-signature-256": `sha256=${await hmacSha256Hex(secret, body)}` }),
|
|
218
|
+
"gitee": async (_body, secret) => ({ "x-gitee-token": secret }),
|
|
219
|
+
"azure-devops": async (_body, secret) => ({ authorization: `Basic ${btoa(secret)}` }),
|
|
220
|
+
"cursor-origin": (body, secret) => signStandardWebhook(crypto.randomUUID(), body, secret),
|
|
221
|
+
"fake": async (body, secret) => ({ "x-fake-signature": await hmacSha256Hex(secret, body) })
|
|
222
|
+
};
|
|
223
|
+
/**
|
|
224
|
+
* Builds the headers a forge would sign `body` with, merged over `headers`
|
|
225
|
+
* (the event name and delivery id, which only the caller knows). `secret` is
|
|
226
|
+
* the webhook secret, or for Cursor Origin the base64 PKCS#8 Ed25519 private
|
|
227
|
+
* key matching the provider's verification key.
|
|
228
|
+
* @param forge The forge whose signature scheme to use.
|
|
229
|
+
* @param body The raw body of the delivery.
|
|
230
|
+
* @param secret The secret that signs it.
|
|
231
|
+
* @param headers Headers to add to the signed headers.
|
|
232
|
+
*/
|
|
233
|
+
async function signDelivery(forge, body, secret, headers = {}) {
|
|
234
|
+
const sign = SIGNERS[forge];
|
|
235
|
+
if (!sign) throw new Error(`No webhook signer for ${forge}`);
|
|
236
|
+
return {
|
|
237
|
+
...headers,
|
|
238
|
+
...await sign(body, secret)
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
//#endregion
|
|
242
|
+
export { fixtureFetch, loadFixtures, recordingFetch, signDelivery };
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { c as timingSafeEqual, i as hmacSha256Hex, n as headerValue } from "./crypto-CQNEyYHD.mjs";
|
|
2
|
+
//#region src/webhooks.ts
|
|
3
|
+
/** The first of `names` the delivery carries a non-empty value for. */
|
|
4
|
+
function firstHeader(delivery, names) {
|
|
5
|
+
for (const name of typeof names === "string" ? [names] : names) {
|
|
6
|
+
const value = headerValue(delivery.headers, name);
|
|
7
|
+
if (value) return value;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Checks a hex HMAC-SHA256 of the raw body, sent in the first of `header`
|
|
12
|
+
* present, after an optional `prefix` such as `sha256=`. The delivery's own
|
|
13
|
+
* `secret` takes precedence over the provider's.
|
|
14
|
+
*/
|
|
15
|
+
async function verifyHmacSignature(delivery, secret, { header, prefix = "" }) {
|
|
16
|
+
const signature = firstHeader(delivery, header);
|
|
17
|
+
const key = delivery.secret ?? secret;
|
|
18
|
+
if (!key || !signature) return false;
|
|
19
|
+
return timingSafeEqual(signature.toLowerCase(), `${prefix}${await hmacSha256Hex(key, delivery.body)}`);
|
|
20
|
+
}
|
|
21
|
+
/** Checks a secret sent verbatim in `header`. */
|
|
22
|
+
function verifySharedToken(delivery, secret, header) {
|
|
23
|
+
const token = headerValue(delivery.headers, header);
|
|
24
|
+
const key = delivery.secret ?? secret;
|
|
25
|
+
return Boolean(key && token && timingSafeEqual(token, key));
|
|
26
|
+
}
|
|
27
|
+
/** The kind, detail and summary of one ref update in a push: a deletion, a creation, or new commits. */
|
|
28
|
+
function refEvent(who, change) {
|
|
29
|
+
const name = change.ref.replace(/^refs\/(?:heads|tags)\//, "");
|
|
30
|
+
const refType = change.refType ?? (change.ref.startsWith("refs/tags/") ? "tag" : "branch");
|
|
31
|
+
if (change.deleted || change.created) return {
|
|
32
|
+
kind: "ref",
|
|
33
|
+
action: change.deleted ? "deleted" : "created",
|
|
34
|
+
detail: {
|
|
35
|
+
type: "ref",
|
|
36
|
+
ref: change.ref,
|
|
37
|
+
refType
|
|
38
|
+
},
|
|
39
|
+
summary: `${who} ${change.deleted ? "deleted" : "created"} ${name}`
|
|
40
|
+
};
|
|
41
|
+
const commits = change.commits ?? [];
|
|
42
|
+
const count = change.commitCount ?? commits.length;
|
|
43
|
+
return {
|
|
44
|
+
kind: "push",
|
|
45
|
+
detail: {
|
|
46
|
+
type: "push",
|
|
47
|
+
ref: change.ref,
|
|
48
|
+
before: change.before,
|
|
49
|
+
after: change.after,
|
|
50
|
+
commitCount: count,
|
|
51
|
+
forced: change.forced ?? false,
|
|
52
|
+
commits
|
|
53
|
+
},
|
|
54
|
+
summary: `${who} pushed ${count} commit(s) to ${name}`
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/** The forge's own event names for `events`, plus any `nativeEvents` passed through unchanged. */
|
|
58
|
+
function nativeEventsFor(map, events, nativeEvents) {
|
|
59
|
+
const names = new Set(nativeEvents ?? []);
|
|
60
|
+
for (const kind of events ?? []) for (const name of map[kind] ?? []) names.add(name);
|
|
61
|
+
return [...names];
|
|
62
|
+
}
|
|
63
|
+
/** The normalised kinds a subscription to `nativeEvents` covers. */
|
|
64
|
+
function eventKindsOf(map, nativeEvents) {
|
|
65
|
+
const kinds = /* @__PURE__ */ new Set();
|
|
66
|
+
for (const [kind, names] of Object.entries(map)) if (nativeEvents.includes("*") || names.some((name) => nativeEvents.includes(name))) kinds.add(kind);
|
|
67
|
+
return [...kinds];
|
|
68
|
+
}
|
|
69
|
+
//#endregion
|
|
70
|
+
export { verifyHmacSignature as a, refEvent as i, firstHeader as n, verifySharedToken as o, nativeEventsFor as r, eventKindsOf as t };
|
package/package.json
CHANGED
|
@@ -1,8 +1,120 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "forges",
|
|
3
|
-
"
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"devEngines": {
|
|
6
|
+
"runtime": {
|
|
7
|
+
"name": "node",
|
|
8
|
+
"version": "24.21.0",
|
|
9
|
+
"onFail": "download"
|
|
10
|
+
},
|
|
11
|
+
"packageManager": {
|
|
12
|
+
"name": "pnpm",
|
|
13
|
+
"version": "12.11.2",
|
|
14
|
+
"onFail": "download"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"description": "One client, every forge: issues, pull requests, notifications, checks and webhooks",
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/danielroe/forges.git"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"forge",
|
|
25
|
+
"git",
|
|
26
|
+
"github",
|
|
27
|
+
"gitlab",
|
|
28
|
+
"bitbucket",
|
|
29
|
+
"forgejo",
|
|
30
|
+
"codeberg",
|
|
31
|
+
"gitea",
|
|
32
|
+
"gitee",
|
|
33
|
+
"azure-devops",
|
|
34
|
+
"tangled",
|
|
35
|
+
"atproto",
|
|
36
|
+
"issues",
|
|
37
|
+
"pull-requests",
|
|
38
|
+
"notifications",
|
|
39
|
+
"webhooks"
|
|
40
|
+
],
|
|
41
|
+
"sideEffects": false,
|
|
42
|
+
"exports": {
|
|
43
|
+
".": "./dist/index.mjs",
|
|
44
|
+
"./azure-devops": "./dist/azure-devops/index.mjs",
|
|
45
|
+
"./bitbucket": "./dist/bitbucket/index.mjs",
|
|
46
|
+
"./cursor-origin": "./dist/cursor-origin/index.mjs",
|
|
47
|
+
"./env": "./dist/env.mjs",
|
|
48
|
+
"./fake": "./dist/fake/index.mjs",
|
|
49
|
+
"./forgejo": "./dist/forgejo/index.mjs",
|
|
50
|
+
"./gitea": "./dist/gitea/index.mjs",
|
|
51
|
+
"./gitee": "./dist/gitee/index.mjs",
|
|
52
|
+
"./github": "./dist/github/index.mjs",
|
|
53
|
+
"./gitlab": "./dist/gitlab/index.mjs",
|
|
54
|
+
"./kit": "./dist/kit.mjs",
|
|
55
|
+
"./pushin": "./dist/pushin/index.mjs",
|
|
56
|
+
"./schema": "./dist/schema/index.mjs",
|
|
57
|
+
"./tangled": "./dist/tangled/index.mjs",
|
|
58
|
+
"./testing": "./dist/testing/index.mjs",
|
|
59
|
+
"./package.json": "./package.json",
|
|
60
|
+
"./schema/*.json": "./dist/schema/*.json"
|
|
61
|
+
},
|
|
62
|
+
"types": "./dist/index.d.mts",
|
|
4
63
|
"files": [
|
|
5
64
|
"dist"
|
|
6
65
|
],
|
|
7
|
-
"
|
|
8
|
-
|
|
66
|
+
"engines": {
|
|
67
|
+
"node": ">=22.18"
|
|
68
|
+
},
|
|
69
|
+
"devDependencies": {
|
|
70
|
+
"@antfu/eslint-config": "9.5.1",
|
|
71
|
+
"@arethetypeswrong/core": "0.18.5",
|
|
72
|
+
"@octokit/openapi-webhooks-types": "12.2.0",
|
|
73
|
+
"@octokit/rest": "22.0.1",
|
|
74
|
+
"@types/node": "24.19.2",
|
|
75
|
+
"@vitest/coverage-v8": "5.0.3",
|
|
76
|
+
"eslint": "10.12.0",
|
|
77
|
+
"installed-check": "11.0.0",
|
|
78
|
+
"knip": "6.39.0",
|
|
79
|
+
"nano-staged": "1.0.2",
|
|
80
|
+
"pkg-pr-new": "0.0.91",
|
|
81
|
+
"publint": "0.3.25",
|
|
82
|
+
"rolldown": "1.2.13",
|
|
83
|
+
"simple-git-hooks": "2.14.0",
|
|
84
|
+
"ts-json-schema-generator": "2.9.0",
|
|
85
|
+
"tsdown": "0.23.0",
|
|
86
|
+
"typescript": "6.0.3",
|
|
87
|
+
"vitest": "5.0.3"
|
|
88
|
+
},
|
|
89
|
+
"simple-git-hooks": {
|
|
90
|
+
"pre-commit": "pnpm nano-staged"
|
|
91
|
+
},
|
|
92
|
+
"nano-staged": {
|
|
93
|
+
"*.{js,ts,mjs,cjs,vue,json,yml,yaml,md,.*rc}": [
|
|
94
|
+
"pnpm eslint --fix"
|
|
95
|
+
]
|
|
96
|
+
},
|
|
97
|
+
"scripts": {
|
|
98
|
+
"build": "tsdown",
|
|
99
|
+
"build:docs": "pnpm --filter forges-docs build",
|
|
100
|
+
"check:all": "pnpm lint && pnpm test && pnpm test:versions && pnpm build && pnpm test:size",
|
|
101
|
+
"dev": "vitest dev",
|
|
102
|
+
"lint": "eslint --cache .",
|
|
103
|
+
"lint:fix": "eslint --cache --fix .",
|
|
104
|
+
"test": "pnpm test:unit && pnpm test:capabilities && pnpm test:types && pnpm test:knip",
|
|
105
|
+
"test:unit": "vitest run --project unit",
|
|
106
|
+
"test:docs": "vitest run --project docs",
|
|
107
|
+
"test:capabilities": "node scripts/check-capabilities.ts",
|
|
108
|
+
"test:knip": "knip",
|
|
109
|
+
"test:links": "lychee --config lychee.toml --remap \"https://github\\.com/danielroe/forges/(?:blob|tree)/main/(.*) file://$PWD/\\$1\" --include-fragments docs/.output/public README.md CONTRIBUTING.md CODE_OF_CONDUCT.md docs/README.md docs/content examples test/fixtures",
|
|
110
|
+
"test:versions": "installed-check -d --no-workspaces",
|
|
111
|
+
"test:types": "tsc --noEmit",
|
|
112
|
+
"dev:docs": "pnpm --filter forges-docs dev",
|
|
113
|
+
"dev:playground": "pnpm --filter playground dev",
|
|
114
|
+
"record-fixtures": "node --env-file-if-exists=.env scripts/record-fixtures.ts",
|
|
115
|
+
"capability-matrix": "node scripts/capability-matrix.ts",
|
|
116
|
+
"recorded-coverage": "node scripts/recorded-coverage.ts",
|
|
117
|
+
"docs:api": "node scripts/docs-api.ts",
|
|
118
|
+
"test:size": "node scripts/bundle-size.ts"
|
|
119
|
+
}
|
|
120
|
+
}
|
package/dist/.gitkeep
DELETED
|
File without changes
|