@effected/github 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/GitCommit.js ADDED
@@ -0,0 +1,183 @@
1
+ import { GitHubClient } from "./GitHubClient.js";
2
+ import { Repo } from "./Repo.js";
3
+ import { Context, Effect, Layer, Schema } from "effect";
4
+
5
+ //#region src/GitCommit.ts
6
+ /**
7
+ * A blob's file mode, as the Git Database API spells it.
8
+ *
9
+ * @public
10
+ */
11
+ const FileMode = Schema.Literals([
12
+ "100644",
13
+ "100755",
14
+ "120000"
15
+ ]);
16
+ /**
17
+ * A file to write in a commit.
18
+ *
19
+ * @public
20
+ */
21
+ var FileContent = class extends Schema.TaggedClass()("FileContent", {
22
+ /** Repository-relative path. */
23
+ path: Schema.NonEmptyString,
24
+ /** The file's new contents. */
25
+ content: Schema.String,
26
+ /** Defaults to a regular file. */
27
+ mode: Schema.optionalKey(FileMode)
28
+ }) {};
29
+ /**
30
+ * A file to remove in a commit.
31
+ *
32
+ * @public
33
+ */
34
+ var FileDeletion = class extends Schema.TaggedClass()("FileDeletion", {
35
+ /** Repository-relative path. */
36
+ path: Schema.NonEmptyString }) {};
37
+ /**
38
+ * One change in a commit.
39
+ *
40
+ * @public
41
+ */
42
+ const FileChange = Schema.Union([FileContent, FileDeletion]);
43
+ /**
44
+ * A commit, projected to the three fields callers actually use.
45
+ *
46
+ * @remarks
47
+ * `treeSha` is here because two surveyed call sites dropped to a raw octokit
48
+ * cast for it alone, both with the comment *"the Git Data API's `base_tree`
49
+ * wants a tree SHA, not a commit SHA"* — the same eight lines written twice, in
50
+ * two files, for one string.
51
+ *
52
+ * @public
53
+ */
54
+ var CommitRef = class extends Schema.Class("CommitRef")({
55
+ /** The commit's own sha. */
56
+ sha: Schema.String,
57
+ /** The tree the commit points at — what `baseTree` wants. */
58
+ treeSha: Schema.String,
59
+ /** Parent commit shas, in order. */
60
+ parents: Schema.Array(Schema.String)
61
+ }) {};
62
+ /**
63
+ * Commits, trees and blobs.
64
+ *
65
+ * @public
66
+ */
67
+ var GitCommit = class GitCommit extends Context.Service()("@effected/github/GitCommit") {
68
+ static layer = Layer.effect(this, Effect.map(GitHubClient, (client) => make(client)));
69
+ /** An in-memory double; unstubbed members die naming themselves. */
70
+ static makeTest = (overrides = {}) => ({
71
+ get: overrides.get ?? (() => unstubbed("get")),
72
+ createTree: overrides.createTree ?? (() => unstubbed("createTree")),
73
+ createCommit: overrides.createCommit ?? (() => unstubbed("createCommit")),
74
+ commitFiles: overrides.commitFiles ?? (() => unstubbed("commitFiles"))
75
+ });
76
+ /** {@link GitCommit.makeTest} behind a `Layer`. */
77
+ static layerTest = (overrides = {}) => Layer.succeed(GitCommit, GitCommit.makeTest(overrides));
78
+ };
79
+ const unstubbed = (member) => {
80
+ throw new Error(`GitCommit.makeTest: ${member}() was called but not stubbed — pass an override.`);
81
+ };
82
+ /** The Git Database tree entry for one change. */
83
+ const treeEntry = (change) => change._tag === "FileContent" ? {
84
+ path: change.path,
85
+ mode: change.mode ?? "100644",
86
+ type: "blob",
87
+ content: change.content
88
+ } : {
89
+ path: change.path,
90
+ mode: "100644",
91
+ type: "blob",
92
+ sha: null
93
+ };
94
+ const make = (client) => {
95
+ const get = Effect.fn("GitCommit.get")(function* (sha) {
96
+ const { owner, repo } = yield* Repo;
97
+ yield* Effect.annotateCurrentSpan({
98
+ owner,
99
+ repo,
100
+ sha
101
+ });
102
+ const commit = yield* client.request("GET /repos/{owner}/{repo}/git/commits/{commit_sha}", {
103
+ owner,
104
+ repo,
105
+ commit_sha: sha
106
+ });
107
+ return CommitRef.make({
108
+ sha: commit.sha,
109
+ treeSha: commit.tree.sha,
110
+ parents: commit.parents.map((parent) => parent.sha)
111
+ });
112
+ });
113
+ const createTree = Effect.fn("GitCommit.createTree")(function* (options) {
114
+ const { owner, repo } = yield* Repo;
115
+ yield* Effect.annotateCurrentSpan({
116
+ owner,
117
+ repo,
118
+ changes: options.changes.length
119
+ });
120
+ return (yield* client.request("POST /repos/{owner}/{repo}/git/trees", {
121
+ owner,
122
+ repo,
123
+ tree: options.changes.map(treeEntry),
124
+ ...options.baseTree !== void 0 ? { base_tree: options.baseTree } : {}
125
+ })).sha;
126
+ });
127
+ const createCommit = Effect.fn("GitCommit.createCommit")(function* (options) {
128
+ const { owner, repo } = yield* Repo;
129
+ yield* Effect.annotateCurrentSpan({
130
+ owner,
131
+ repo,
132
+ tree: options.tree
133
+ });
134
+ return (yield* client.request("POST /repos/{owner}/{repo}/git/commits", {
135
+ owner,
136
+ repo,
137
+ message: options.message,
138
+ tree: options.tree,
139
+ parents: [...options.parents]
140
+ })).sha;
141
+ });
142
+ return {
143
+ get,
144
+ createTree,
145
+ createCommit,
146
+ commitFiles: Effect.fn("GitCommit.commitFiles")(function* (options) {
147
+ const { owner, repo } = yield* Repo;
148
+ const short = options.branch.replace(/^refs\/heads\//, "").replace(/^heads\//, "");
149
+ yield* Effect.annotateCurrentSpan({
150
+ owner,
151
+ repo,
152
+ branch: short,
153
+ changes: options.changes.length
154
+ });
155
+ const ref = yield* client.request("GET /repos/{owner}/{repo}/git/ref/{ref}", {
156
+ owner,
157
+ repo,
158
+ ref: `heads/${short}`
159
+ });
160
+ const head = yield* get(ref.object.sha);
161
+ const tree = yield* createTree({
162
+ changes: options.changes,
163
+ baseTree: head.treeSha
164
+ });
165
+ const commit = yield* createCommit({
166
+ message: options.message,
167
+ tree,
168
+ parents: [head.sha]
169
+ });
170
+ yield* client.request("PATCH /repos/{owner}/{repo}/git/refs/{ref}", {
171
+ owner,
172
+ repo,
173
+ ref: `heads/${short}`,
174
+ sha: commit,
175
+ force: false
176
+ });
177
+ return commit;
178
+ })
179
+ };
180
+ };
181
+
182
+ //#endregion
183
+ export { CommitRef, FileChange, FileContent, FileDeletion, FileMode, GitCommit };
package/GitHubApp.js ADDED
@@ -0,0 +1,363 @@
1
+ import { GitHubError } from "./GitHubError.js";
2
+ import { GitHubGraphQLError } from "./GraphQL.js";
3
+ import { GitHubClient, makeClientShape } from "./GitHubClient.js";
4
+ import { Clock, Context, DateTime, Duration, Effect, Layer, Option, Redacted, Ref, Schema, Stream } from "effect";
5
+ import githubAppJwt from "universal-github-app-jwt";
6
+
7
+ //#region src/GitHubApp.ts
8
+ /**
9
+ * A GitHub App call failed.
10
+ *
11
+ * @remarks
12
+ * Distinct from `GitHubError` because "I could not obtain credentials" and "the
13
+ * API call failed" are different problems with different fixes: the first is a
14
+ * misconfigured app, a wrong private key or a missing installation; the second
15
+ * is the request that used the credentials.
16
+ *
17
+ * @public
18
+ */
19
+ var GitHubAppError = class GitHubAppError extends Schema.TaggedErrorClass()("GitHubAppError", {
20
+ /** Which step failed. */
21
+ kind: Schema.Literals([
22
+ "jwt",
23
+ "token",
24
+ "revoke",
25
+ "identity",
26
+ "installation"
27
+ ]),
28
+ /** Human-readable cause. */
29
+ reason: Schema.String,
30
+ /** The underlying failure, when there is one. */
31
+ cause: Schema.optionalKey(Schema.Defect())
32
+ }) {
33
+ get message() {
34
+ return `GitHub App ${this.kind} failed: ${this.reason}`;
35
+ }
36
+ /** @internal */
37
+ static of(kind, reason, cause) {
38
+ return new GitHubAppError({
39
+ kind,
40
+ reason,
41
+ ...cause !== void 0 ? { cause } : {}
42
+ });
43
+ }
44
+ };
45
+ /**
46
+ * An installation access token and what GitHub said about it.
47
+ *
48
+ * @remarks
49
+ * Encodable on purpose. `@effected/github-actions` persists one across the
50
+ * `pre`/`main`/`post` process boundary through `GITHUB_STATE`, and
51
+ * `Schema.encodeUnknownEffect` produces JSON with the token as a plain string
52
+ * and `expiresAt` as an ISO instant. A `Redacted` cannot survive serialization
53
+ * by design, so masking the encoded value is the caller's job — Actions calls
54
+ * `::add-mask::`.
55
+ *
56
+ * @public
57
+ */
58
+ var InstallationToken = class extends Schema.Class("InstallationToken")({
59
+ /** The token. Decodes to `Redacted`, encodes back to the raw string. */
60
+ token: Schema.RedactedFromValue(Schema.String),
61
+ /** When GitHub will stop accepting it — about an hour out. */
62
+ expiresAt: Schema.DateTimeUtcFromString,
63
+ /** The installation it is scoped to. */
64
+ installationId: Schema.Int,
65
+ /** The permissions GitHub actually granted, which may be narrower than requested. */
66
+ permissions: Schema.Record(Schema.String, Schema.String),
67
+ /** The app's slug, when identity was resolved. */
68
+ appSlug: Schema.optionalKey(Schema.String),
69
+ /** The app's bot user id, when identity was resolved. */
70
+ appUserId: Schema.optionalKey(Schema.Int),
71
+ /** The app's display name, when identity was resolved. */
72
+ appName: Schema.optionalKey(Schema.String)
73
+ }) {
74
+ /**
75
+ * Whether this token is spent, `skew` before its stated expiry.
76
+ *
77
+ * @remarks
78
+ * Modelled **and enforced**. The package this replaces persisted `expiresAt`
79
+ * and read it nowhere, so a long `main` phase that outlived the hour simply
80
+ * started answering 401 with no explanation.
81
+ */
82
+ isExpired(nowMillis, skew = DEFAULT_SKEW) {
83
+ return DateTime.toEpochMillis(this.expiresAt) - Duration.toMillis(skew) <= nowMillis;
84
+ }
85
+ /** The committer identity a commit made with this token should carry. */
86
+ botIdentity() {
87
+ return this.appSlug === void 0 ? BotIdentity.githubActions : BotIdentity.forApp({
88
+ appSlug: this.appSlug,
89
+ ...this.appUserId !== void 0 ? { appUserId: this.appUserId } : {}
90
+ });
91
+ }
92
+ };
93
+ /** Re-mint a minute before GitHub would start refusing the token. */
94
+ const DEFAULT_SKEW = Duration.seconds(60);
95
+ /**
96
+ * Who a bot commits as.
97
+ *
98
+ * @remarks
99
+ * A **pure class**, not a service member. The package this replaces put
100
+ * `botIdentity(source?)` on the `GitHubApp` service shape as a plain synchronous
101
+ * method, which makes it required in every `Layer.mock` and silently degrades
102
+ * every partial double to a full implementation.
103
+ *
104
+ * @public
105
+ */
106
+ var BotIdentity = class BotIdentity extends Schema.Class("BotIdentity")({
107
+ /** The git author/committer name, e.g. `"my-app[bot]"`. */
108
+ name: Schema.String,
109
+ /** The no-reply address GitHub attributes to that account. */
110
+ email: Schema.String
111
+ }) {
112
+ /** The identity for an app, given whatever of its identity is known. */
113
+ static forApp(source) {
114
+ const name = `${source.appSlug}[bot]`;
115
+ return BotIdentity.make({
116
+ name,
117
+ email: source.appUserId === void 0 ? `${name}@users.noreply.github.com` : `${source.appUserId}+${name}@users.noreply.github.com`
118
+ });
119
+ }
120
+ /** The well-known identity of the `github-actions` bot. */
121
+ static githubActions = BotIdentity.make({
122
+ name: "github-actions[bot]",
123
+ email: "41898282+github-actions[bot]@users.noreply.github.com"
124
+ });
125
+ };
126
+ /**
127
+ * What GitHub knows about the app itself.
128
+ *
129
+ * @public
130
+ */
131
+ var AppIdentity = class extends Schema.Class("AppIdentity")({
132
+ /** The URL slug, e.g. `"my-app"`. */
133
+ slug: Schema.String,
134
+ /** The display name. */
135
+ name: Schema.String,
136
+ /** The bot user's numeric id, when it could be resolved. */
137
+ userId: Schema.optionalKey(Schema.Int)
138
+ }) {
139
+ /** The committer identity for this app. */
140
+ botIdentity() {
141
+ return BotIdentity.forApp({
142
+ appSlug: this.slug,
143
+ ...this.userId !== void 0 ? { appUserId: this.userId } : {}
144
+ });
145
+ }
146
+ };
147
+ /**
148
+ * One installation of the app.
149
+ *
150
+ * @public
151
+ */
152
+ var Installation = class extends Schema.Class("Installation")({
153
+ /** The installation id, which is what a token is minted against. */
154
+ id: Schema.Int,
155
+ /** The account the app is installed on, when GitHub reported one. */
156
+ account: Schema.optionalKey(Schema.String)
157
+ }) {};
158
+ /**
159
+ * GitHub App authentication: mint, revoke and identify.
160
+ *
161
+ * @remarks
162
+ * **This is the only module in the package that imports a JWT signer**, which is
163
+ * what makes the tree-shaking invariant structural rather than aspirational: a
164
+ * consumer that authenticates with a token it already holds imports
165
+ * `GitHubClient` and never reaches this module or its dependency.
166
+ *
167
+ * That constraint is also why the App-authenticated **client** layer lives here
168
+ * as {@link GitHubApp.clientLayer} rather than as a third static on
169
+ * `GitHubClient`: statics on one class share one module, and putting it there
170
+ * would make every token-only consumer link the signer. The kit has this shape
171
+ * already — `@effected/workspaces` ships `localExecLayer`, which builds
172
+ * `@effected/commands`' service, for the same reason.
173
+ *
174
+ * The JWT signer is `universal-github-app-jwt` — zero dependencies, and
175
+ * `@octokit/auth-app`'s own JWT dependency. Taking it directly rather than
176
+ * taking `auth-app` leaves behind roughly half a megabyte of OAuth app, user and
177
+ * device-flow machinery that this package never calls.
178
+ *
179
+ * @public
180
+ */
181
+ var GitHubApp = class GitHubApp extends Context.Service()("@effected/github/GitHubApp") {
182
+ /** The default transport. Bind it once; layers are memoized by reference. */
183
+ static layer = Layer.effect(this, makeApp({}));
184
+ /**
185
+ * A transport with custom settings.
186
+ *
187
+ * @remarks
188
+ * Parameterized, so **bind the result to a `const`** and reuse it. Calling
189
+ * this at two provide sites builds two instances, because layers are
190
+ * memoized by reference.
191
+ */
192
+ static layerWith = (options) => Layer.effect(GitHubApp, makeApp(options));
193
+ /**
194
+ * A {@link GitHubClient} authenticated as an app installation.
195
+ *
196
+ * @remarks
197
+ * The token's lifetime is the layer's scope: it is minted on build and
198
+ * **revoked on release**, best-effort, so a workflow does not leave live
199
+ * credentials behind. It is also **re-minted automatically** a minute before
200
+ * it expires, which the package this replaces did not do — it persisted
201
+ * `expiresAt` and read it nowhere, so a `main` phase outliving the hour
202
+ * started answering 401 with nothing explaining why.
203
+ *
204
+ * A failure to obtain credentials surfaces to the caller as a
205
+ * `GitHubError { kind: "unauthorized" }` carrying the `GitHubAppError` as its
206
+ * cause: from a request's point of view, "could not authenticate" is an
207
+ * authorization failure, and widening every method's error channel to say so
208
+ * would tax every caller for a case only this layer can produce.
209
+ */
210
+ static clientLayer = (request, options = {}) => Layer.effect(GitHubClient, Effect.flatMap(GitHubApp, (app) => makeRotatingClient(app, request, options))).pipe(Layer.provide(GitHubApp.layerWith(options)));
211
+ /** An in-memory double; unstubbed members die naming themselves. */
212
+ static makeTest = (overrides = {}) => ({
213
+ token: overrides.token ?? (() => unstubbed("token")),
214
+ scopedToken: overrides.scopedToken ?? (() => unstubbed("scopedToken")),
215
+ revoke: overrides.revoke ?? (() => unstubbed("revoke")),
216
+ identity: overrides.identity ?? (() => unstubbed("identity")),
217
+ installations: overrides.installations ?? (() => unstubbed("installations"))
218
+ });
219
+ /** {@link GitHubApp.makeTest} behind a `Layer`. */
220
+ static layerTest = (overrides = {}) => Layer.succeed(GitHubApp, GitHubApp.makeTest(overrides));
221
+ };
222
+ const unstubbed = (member) => {
223
+ throw new Error(`GitHubApp.makeTest: ${member}() was called but not stubbed — pass an override.`);
224
+ };
225
+ /** Mint an app JWT. The only cryptography in this package, and it is a leaf call. */
226
+ const mintJwt = (credentials) => Effect.tryPromise({
227
+ try: () => githubAppJwt({
228
+ id: credentials.appId,
229
+ privateKey: Redacted.value(credentials.privateKey)
230
+ }),
231
+ catch: (error) => GitHubAppError.of("jwt", error instanceof Error ? error.message : "could not sign the app JWT", error)
232
+ }).pipe(Effect.map((result) => Redacted.make(result.token)));
233
+ /** A client speaking as the app itself. */
234
+ const asApp = (credentials, options) => Effect.flatMap(mintJwt(credentials), (jwt) => makeClientShape({
235
+ ...options,
236
+ token: jwt
237
+ }));
238
+ /** A client speaking as a holder of `token`, or as nobody when there is none. */
239
+ const asBearer = (token, options) => makeClientShape({
240
+ ...options,
241
+ token: token ?? Redacted.make("")
242
+ });
243
+ const appFailure = (kind) => (error) => Effect.fail(GitHubAppError.of(kind, error.reason, error));
244
+ function makeApp(options) {
245
+ return Effect.sync(() => {
246
+ const installations = Effect.fn("GitHubApp.installations")(function* (credentials) {
247
+ return (yield* (yield* asApp(credentials, options)).paginate("GET /app/installations", {}).pipe(Effect.catch(appFailure("installation")))).map((entry) => Installation.make({
248
+ id: entry.id,
249
+ ...entry.account !== null && entry.account !== void 0 && "login" in entry.account ? { account: entry.account.login } : {}
250
+ }));
251
+ });
252
+ const resolveInstallationId = (request) => request.installationId !== void 0 ? Effect.succeed(request.installationId) : Effect.gen(function* () {
253
+ const all = yield* installations(request);
254
+ if (request.owner !== void 0) {
255
+ const wanted = request.owner.toLowerCase();
256
+ const match = all.find((entry) => entry.account?.toLowerCase() === wanted);
257
+ if (match !== void 0) return match.id;
258
+ return yield* Effect.fail(GitHubAppError.of("installation", `the app is not installed on ${request.owner} (installed on: ${all.map((entry) => entry.account ?? entry.id).join(", ") || "nothing"})`));
259
+ }
260
+ const only = all[0];
261
+ if (all.length === 1 && only !== void 0) return only.id;
262
+ return yield* Effect.fail(GitHubAppError.of("installation", all.length === 0 ? "the app has no installations" : `the app has ${all.length} installations; pass installationId or owner`));
263
+ });
264
+ const token = Effect.fn("GitHubApp.token")(function* (request) {
265
+ const installationId = yield* resolveInstallationId(request);
266
+ const minted = yield* (yield* asApp(request, options)).request("POST /app/installations/{installation_id}/access_tokens", { installation_id: installationId }).pipe(Effect.catch(appFailure("token")));
267
+ return yield* Schema.decodeUnknownEffect(InstallationToken)({
268
+ token: minted.token,
269
+ expiresAt: minted.expires_at,
270
+ installationId,
271
+ permissions: normalizePermissions(minted.permissions)
272
+ }).pipe(Effect.catchTag("SchemaError", (error) => Effect.fail(GitHubAppError.of("token", "GitHub returned an unexpected token payload", error))));
273
+ });
274
+ const revoke = Effect.fn("GitHubApp.revoke")(function* (value) {
275
+ yield* (yield* asBearer(value, options)).request("DELETE /installation/token", {}).pipe(Effect.catch(appFailure("revoke")));
276
+ });
277
+ const scopedToken = (request) => Effect.acquireRelease(token(request), (minted) => Effect.ignore(revoke(minted.token)));
278
+ return {
279
+ token,
280
+ scopedToken,
281
+ revoke,
282
+ identity: Effect.fn("GitHubApp.identity")(function* (request) {
283
+ const app = yield* (yield* asApp(request, options)).request("GET /app", {}).pipe(Effect.catch(appFailure("identity")));
284
+ if (app === null) return yield* Effect.fail(GitHubAppError.of("identity", "GET /app returned no app"));
285
+ const slug = app.slug ?? "";
286
+ const name = app.name;
287
+ const user = yield* (yield* asBearer(request.installationToken, options)).request("GET /users/{username}", { username: `${slug}[bot]` }).pipe(Effect.option);
288
+ return AppIdentity.make({
289
+ slug,
290
+ name,
291
+ ...Option.isSome(user) ? { userId: user.value.id } : {}
292
+ });
293
+ }),
294
+ installations
295
+ };
296
+ });
297
+ }
298
+ /** GitHub's permission values are strings; anything else is not a permission. */
299
+ const normalizePermissions = (raw) => {
300
+ if (typeof raw !== "object" || raw === null) return {};
301
+ const out = {};
302
+ for (const [key, value] of Object.entries(raw)) if (typeof value === "string") out[key] = value;
303
+ return out;
304
+ };
305
+ /**
306
+ * A client shape that re-mints its installation token before it expires.
307
+ *
308
+ * @remarks
309
+ * The rotation is invisible to a caller: each member resolves the current
310
+ * client first, and "current" means "minted, and not within a minute of
311
+ * expiry". Rotating revokes the token it replaces, so at most one live token
312
+ * exists at a time and the scope's release revokes the last of them.
313
+ */
314
+ const makeRotatingClient = (app, request, options) => Effect.gen(function* () {
315
+ const held = yield* Ref.make(Option.none());
316
+ const revokeHeld = Effect.flatMap(Ref.get(held), (current) => Option.isSome(current) ? Effect.ignore(app.revoke(current.value.token.token)) : Effect.void);
317
+ const rotate = Effect.gen(function* () {
318
+ yield* revokeHeld;
319
+ const minted = yield* app.token(request);
320
+ const client = yield* makeClientShape({
321
+ ...options,
322
+ token: minted.token
323
+ });
324
+ yield* Ref.set(held, Option.some({
325
+ token: minted,
326
+ client
327
+ }));
328
+ return client;
329
+ });
330
+ yield* rotate;
331
+ yield* Effect.addFinalizer(() => revokeHeld);
332
+ /** The live client, re-minting first if the held token is spent. */
333
+ const fresh = Effect.gen(function* () {
334
+ const now = yield* Clock.currentTimeMillis;
335
+ const state = yield* Ref.get(held);
336
+ if (Option.isSome(state) && !state.value.token.isExpired(now)) return state.value.client;
337
+ return yield* rotate;
338
+ });
339
+ const current = fresh.pipe(Effect.catchTag("GitHubAppError", (error) => Effect.fail(new GitHubError({
340
+ kind: "unauthorized",
341
+ operation: "GitHubApp.clientLayer",
342
+ reason: error.reason,
343
+ cause: error
344
+ }))));
345
+ const currentForGraphQL = fresh.pipe(Effect.catchTag("GitHubAppError", (error) => Effect.fail(new GitHubGraphQLError({
346
+ kind: "unauthorized",
347
+ operation: "GitHubApp.clientLayer",
348
+ reason: error.reason,
349
+ errors: [],
350
+ cause: error
351
+ }))));
352
+ return {
353
+ request: (route, params) => Effect.flatMap(current, (client) => client.request(route, params)),
354
+ requestDecoded: (route, params, schema) => Effect.flatMap(current, (client) => client.requestDecoded(route, params, schema)),
355
+ paginate: (route, params, pageOptions) => Effect.flatMap(current, (client) => client.paginate(route, params, pageOptions)),
356
+ paginateStream: (route, params, pageOptions) => Stream.unwrap(Effect.map(current, (client) => client.paginateStream(route, params, pageOptions))),
357
+ graphql: (document, variables) => Effect.flatMap(currentForGraphQL, (client) => client.graphql(document, variables)),
358
+ rateLimit: Effect.flatMap(Ref.get(held), (state) => Option.isSome(state) ? state.value.client.rateLimit : Effect.succeed(Option.none()))
359
+ };
360
+ });
361
+
362
+ //#endregion
363
+ export { AppIdentity, BotIdentity, GitHubApp, GitHubAppError, Installation, InstallationToken };