@effected/github 0.15.1 → 0.16.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/index.d.ts CHANGED
@@ -1,9 +1,160 @@
1
1
  import { BareLineReference, CLOSING_KEYWORDS, ClosingKeyword, IssueReference, harvestIssueReferences, parseBareLineReference } from "@effected/github-references";
2
- import { Config, Context, Duration, Effect, Layer, Option, Redacted, Result, Schedule, Schema, Scope, Stream } from "effect";
2
+ import { JwksResolver, JwtError } from "@effected/jwt";
3
+ import { Cause, Config, Context, Duration, Effect, Layer, Option, Redacted, Result, Schedule, Schema, Scope, Stream } from "effect";
3
4
  import { PaginatingEndpoints } from "@octokit/plugin-paginate-rest";
4
5
  import { Endpoints, RequestHeaders } from "@octokit/types";
5
6
  import { SemVer } from "@effected/semver";
6
7
  //#endregion
8
+ //#region src/ActionsOidc.d.ts
9
+ declare const ActionsOidcClaims_base: Schema.Class<ActionsOidcClaims, Schema.Struct<{
10
+ /** The issuer: always {@link ActionsOidc.issuer} once verified. */
11
+ readonly iss: Schema.String;
12
+ /** The subject, e.g. `repo:octo-org/octo-repo:ref:refs/heads/main`. */
13
+ readonly sub: Schema.String;
14
+ /** The audience or audiences the token was minted for. */
15
+ readonly aud: Schema.Union<readonly [Schema.String, Schema.$Array<Schema.String>]>;
16
+ /** Expiry, in seconds since the epoch. */
17
+ readonly exp: Schema.Number;
18
+ /** Issued at, in seconds since the epoch. */
19
+ readonly iat: Schema.Number;
20
+ /** Not before, in seconds since the epoch. */
21
+ readonly nbf: Schema.optionalKey<Schema.Number>;
22
+ /** The token's unique id. */
23
+ readonly jti: Schema.optionalKey<Schema.String>;
24
+ /** The repository, `owner/name`. */
25
+ readonly repository: Schema.String;
26
+ /** The repository's numeric id (wire key `repository_id`). */
27
+ readonly repositoryId: Schema.decodeTo<Schema.Int, Schema.String, never, never>;
28
+ /** The repository owner's login (wire key `repository_owner`). */
29
+ readonly repositoryOwner: Schema.String;
30
+ /** The repository owner's numeric id (wire key `repository_owner_id`). */
31
+ readonly repositoryOwnerId: Schema.decodeTo<Schema.Int, Schema.String, never, never>;
32
+ /** The workflow run's id (wire key `run_id`). */
33
+ readonly runId: Schema.decodeTo<Schema.Int, Schema.String, never, never>;
34
+ /** The run attempt, from 1 (wire key `run_attempt`). */
35
+ readonly runAttempt: Schema.decodeTo<Schema.Int, Schema.String, never, never>;
36
+ /** The calling workflow's ref, e.g. `octo-org/octo-repo/.github/workflows/ci.yml@refs/heads/main` (wire key `workflow_ref`). */
37
+ readonly workflowRef: Schema.String;
38
+ /** The ref of the workflow file the job runs, which differs from `workflowRef` for a reusable workflow (wire key `job_workflow_ref`). */
39
+ readonly jobWorkflowRef: Schema.String;
40
+ /** The git ref the run is for. */
41
+ readonly ref: Schema.String;
42
+ /** The commit SHA the run is for. */
43
+ readonly sha: Schema.String;
44
+ /** The event that triggered the run, e.g. `push` (wire key `event_name`). */
45
+ readonly eventName: Schema.String;
46
+ /** The login of the user who triggered the run. */
47
+ readonly actor: Schema.String;
48
+ /** That user's numeric id (wire key `actor_id`). */
49
+ readonly actorId: Schema.decodeTo<Schema.Int, Schema.String, never, never>;
50
+ /** The deployment environment, when the job names one. */
51
+ readonly environment: Schema.optionalKey<Schema.String>;
52
+ /** `github-hosted` or `self-hosted` (wire key `runner_environment`). */
53
+ readonly runnerEnvironment: Schema.String;
54
+ }>, {}>;
55
+ /**
56
+ * The claims of a verified GitHub Actions OIDC token.
57
+ *
58
+ * @remarks
59
+ * Field names are camelCase; the token carries them in snake_case
60
+ * (`repository_id`, `job_workflow_ref`, ...), and {@link ActionsOidc.verify}
61
+ * maps between the two. GitHub sends the numeric ids (`repository_id`,
62
+ * `repository_owner_id`, `run_id`, `run_attempt`, `actor_id`) as strings;
63
+ * they decode to numbers here, and a non-numeric one fails verification as
64
+ * `claims`.
65
+ *
66
+ * Claims GitHub adds that are not modelled here are dropped on decode.
67
+ *
68
+ * @public
69
+ */
70
+ export declare class ActionsOidcClaims extends ActionsOidcClaims_base {}
71
+ /**
72
+ * Options for {@link ActionsOidc.verify}.
73
+ *
74
+ * @public
75
+ */
76
+ interface ActionsOidcVerifyOptions {
77
+ /**
78
+ * The audience the token must have been minted for: what the workflow
79
+ * passed to `core.getIDToken(audience)`. Required and non-empty, because
80
+ * GitHub's one JWKS signs tokens for every relying party; without an
81
+ * audience check, any workflow's token for any service would verify.
82
+ */
83
+ readonly audience: string;
84
+ /** Allowed clock skew for `exp`, `nbf` and `iat`; 60 seconds by default. */
85
+ readonly clockTolerance?: Duration.Input | undefined;
86
+ }
87
+ /**
88
+ * Verify GitHub Actions OIDC tokens: a workflow proving to your service which
89
+ * repository, workflow and run it is.
90
+ *
91
+ * @remarks
92
+ * {@link ActionsOidc.verify} checks the signature against GitHub's published
93
+ * JWKS (through `@effected/jwt`'s `JwksResolver`, which caches it), then `iss`,
94
+ * `aud`, `exp`, `nbf` and `iat`, then decodes the payload into
95
+ * {@link ActionsOidcClaims}. Every failure is a `JwtError`; route on its
96
+ * `reason` (`wrongAudience`, `expired`, `claims`, ...), never on its message.
97
+ *
98
+ * Authorizing the caller is still yours: a verified token proves who is
99
+ * calling, not that they may. Pin the **immutable ids**, `repositoryId` or
100
+ * `repositoryOwnerId`, together with `jobWorkflowRef` (or `workflowRef`),
101
+ * never the `repository` or `repositoryOwner` name alone: a deleted
102
+ * repository's or account's name can be registered again by someone else,
103
+ * whose workflows then mint tokens carrying the old name.
104
+ *
105
+ * Only the github.com issuer is accepted. A GitHub Enterprise Cloud
106
+ * enterprise's unique issuer (`https://token.actions.githubusercontent.com/<enterprise>`)
107
+ * and a GitHub Enterprise Server issuer (`https://<host>/_services/token`)
108
+ * are refused as `wrongIssuer`.
109
+ *
110
+ * This module does not reach octokit: a service that only verifies tokens
111
+ * links `@effected/jwt` and nothing else from this package.
112
+ *
113
+ * @example
114
+ * ```ts
115
+ * import { JwksResolver } from "@effected/jwt";
116
+ * import { ActionsOidc } from "@effected/github";
117
+ * import { Data, Effect } from "effect";
118
+ *
119
+ * class Forbidden extends Data.TaggedError("Forbidden")<{ readonly repositoryId: number }> {}
120
+ *
121
+ * // Pinned by id, which survives a rename and cannot be re-registered.
122
+ * const ALLOWED_REPOSITORY_ID = 123456789;
123
+ * const ALLOWED_WORKFLOW = "octo-org/octo-repo/.github/workflows/deploy.yml@refs/heads/main";
124
+ *
125
+ * const authorize = (bearer: string) =>
126
+ * Effect.gen(function* () {
127
+ * const claims = yield* ActionsOidc.verify(bearer, { audience: "https://my-service.example" });
128
+ * if (claims.repositoryId !== ALLOWED_REPOSITORY_ID || claims.jobWorkflowRef !== ALLOWED_WORKFLOW) {
129
+ * return yield* new Forbidden({ repositoryId: claims.repositoryId });
130
+ * }
131
+ * return claims;
132
+ * });
133
+ *
134
+ * // JwksResolver.layer needs an HttpClient and a JwksStore, provided once at the edge.
135
+ * export const program = authorize("eyJ...").pipe(Effect.provide(JwksResolver.layer));
136
+ * ```
137
+ *
138
+ * @public
139
+ */
140
+ export declare const ActionsOidc: {
141
+ /** The Actions OIDC issuer, `https://token.actions.githubusercontent.com`. */
142
+ readonly issuer: "https://token.actions.githubusercontent.com";
143
+ /** Verify a runner-issued OIDC token: signature against GitHub's JWKS, iss, aud, exp/nbf, then typed claims. */
144
+ readonly verify: (token: string, options: ActionsOidcVerifyOptions) => Effect.Effect<ActionsOidcClaims, JwtError, JwksResolver>;
145
+ /**
146
+ * A valid claims payload for tests, overridable field by field.
147
+ *
148
+ * @remarks
149
+ * Wire-shaped (snake_case keys, string ids) and complete except for the
150
+ * time claims `iat` and `exp`, which depend on the clock: a signer stamps
151
+ * them, as `TestIssuer.sign` from `@effected/jwt/testing` does. `iss` is
152
+ * {@link ActionsOidc.issuer}; `aud` is a placeholder to override with the
153
+ * audience under test.
154
+ */
155
+ readonly testClaims: (overrides?: Readonly<Record<string, unknown>>) => Record<string, unknown>;
156
+ };
157
+ //#endregion
7
158
  //#region src/GitHubError.d.ts
8
159
  /**
9
160
  * Why a GitHub call failed, as a value you can branch on.
@@ -133,6 +284,27 @@ export declare class GitHubError extends GitHubError_base {
133
284
  * under test.
134
285
  */
135
286
  static fromOctokit(operation: string, error: unknown, nowMillis: number): GitHubError;
287
+ /**
288
+ * Classify a raw HTTP response: a status, its headers and its JSON body.
289
+ *
290
+ * @remarks
291
+ * For a response that never went through octokit, such as one a test
292
+ * records with `GitHubFixtures.failure` or one a consumer fetched itself. It
293
+ * reads the same facts {@link GitHubError.fromOctokit} reads (`reason` from
294
+ * `body.message`, else `HTTP <status>`, sanitized the same way; `validation`
295
+ * from `body.errors`) and runs the same classifier, so a raw response and
296
+ * the octokit error for it classify identically.
297
+ *
298
+ * Header names are matched in any case. `nowMillis` is required, as in
299
+ * {@link GitHubError.fromOctokit}: it turns an absolute rate-limit reset
300
+ * (an epoch second) into a delay, so it must be the real current time,
301
+ * usually `Clock.currentTimeMillis`. A zero would make the delay decades.
302
+ */
303
+ static fromResponse(operation: string, response: {
304
+ readonly status: number;
305
+ readonly headers?: Readonly<Record<string, string>> | undefined;
306
+ readonly body?: unknown;
307
+ }, nowMillis: number): GitHubError;
136
308
  /**
137
309
  * A predicate over one or more kinds, for `Effect.catchIf`.
138
310
  *
@@ -650,7 +822,7 @@ interface GitHubClientOptions {
650
822
  }
651
823
  /**
652
824
  * One call served by {@link GitHubClient.layerFixture}, as recorded in
653
- * {@link GitHubFixtures.requested}.
825
+ * {@link (GitHubFixtures:interface).requested}.
654
826
  *
655
827
  * @public
656
828
  */
@@ -664,12 +836,80 @@ interface RecordedCall {
664
836
  /** The page size asked for; paginated reads only. */
665
837
  readonly perPage?: number;
666
838
  }
839
+ /**
840
+ * A raw HTTP failure recorded for {@link GitHubClient.layerFixture}, made by
841
+ * {@link (GitHubFixtures:variable).failure}.
842
+ *
843
+ * @remarks
844
+ * Branded, so a fixture's data payload that happens to have a `status` and a
845
+ * `body` is never mistaken for a failure: only a value built by
846
+ * `GitHubFixtures.failure` fails the call. The brand is a registered symbol
847
+ * rather than the class identity, so a failure built by another resolved copy
848
+ * of this package is still recognized.
849
+ *
850
+ * @public
851
+ */
852
+ export declare class RawFailure {
853
+ /** The HTTP status. */
854
+ readonly status: number;
855
+ /** The response headers, in any case. */
856
+ readonly headers: Readonly<Record<string, string>> | undefined;
857
+ /** The JSON body GitHub would send. */
858
+ readonly body: unknown;
859
+ /** @internal */
860
+ constructor(response: {
861
+ readonly status: number;
862
+ readonly headers?: Readonly<Record<string, string>> | undefined;
863
+ readonly body?: unknown;
864
+ });
865
+ }
866
+ /**
867
+ * Builders for {@link (GitHubFixtures:interface)} entries.
868
+ *
869
+ * @public
870
+ */
871
+ export declare const GitHubFixtures: {
872
+ /**
873
+ * A raw failure: what GitHub answered, classified at call time by the same
874
+ * classifier the live client uses.
875
+ *
876
+ * @remarks
877
+ * Record it wherever a fixture accepts a `GitHubError` (`request`, and so
878
+ * `requestDecoded`, and `paginate`). Each call fails with
879
+ * `GitHubError.fromResponse(route, response, now)`, `now` read from `Clock`,
880
+ * so a rate-limit reset header is measured against the `TestClock`. Prefer it
881
+ * to a hand-built `GitHubError` when the test is about GitHub's actual
882
+ * response: the classification is then the package's, not the test's.
883
+ *
884
+ * @example
885
+ * ```ts
886
+ * import { GitHubClient, GitHubFixtures } from "@effected/github";
887
+ *
888
+ * const layer = GitHubClient.layerFixture({
889
+ * request: {
890
+ * "POST /repos/{owner}/{repo}/releases": GitHubFixtures.failure({
891
+ * status: 422,
892
+ * body: {
893
+ * message: "Validation Failed",
894
+ * errors: [{ resource: "Release", code: "already_exists", field: "tag_name" }],
895
+ * },
896
+ * }),
897
+ * },
898
+ * });
899
+ * ```
900
+ */
901
+ readonly failure: (response: {
902
+ readonly status: number;
903
+ readonly headers?: Readonly<Record<string, string>> | undefined;
904
+ readonly body?: unknown;
905
+ }) => RawFailure;
906
+ };
667
907
  /**
668
908
  * A recorded response table for {@link GitHubClient.layerFixture}.
669
909
  *
670
910
  * @public
671
911
  */
672
- interface GitHubFixtures {
912
+ export interface GitHubFixtures {
673
913
  /**
674
914
  * Keyed by route; the value is the `data` payload a request answers with.
675
915
  *
@@ -677,14 +917,19 @@ interface GitHubFixtures {
677
917
  * A recorded **`GitHubError` is the response**: the call fails with it. That
678
918
  * is how a suite stubs a 404 (or a rate-limit, or a 422) deliberately,
679
919
  * rather than relying on a route's absence to produce one — absence is a
680
- * wiring mistake and {@link GitHubFixtures.unstubbed} treats it as such.
920
+ * wiring mistake and {@link (GitHubFixtures:interface).unstubbed} treats it as
921
+ * such. A {@link RawFailure} from {@link (GitHubFixtures:variable).failure}
922
+ * fails it too, classified from GitHub's raw response.
923
+ *
924
+ * A value of `undefined` counts as unstubbed, so stub a route that answers
925
+ * with no body (a 204) with `null`.
681
926
  */
682
927
  readonly request?: Readonly<Record<string, unknown>> | undefined;
683
928
  /**
684
929
  * Keyed by route; the value is the whole collection, paged on demand — or a
685
- * `GitHubError` the paginated read fails with.
930
+ * `GitHubError` or {@link RawFailure} the paginated read fails with.
686
931
  */
687
- readonly paginate?: Readonly<Record<string, ReadonlyArray<unknown> | GitHubError>> | undefined;
932
+ readonly paginate?: Readonly<Record<string, ReadonlyArray<unknown> | GitHubError | RawFailure>> | undefined;
688
933
  /** Keyed by document name; the value is the raw payload to decode. */
689
934
  readonly graphql?: Readonly<Record<string, unknown>> | undefined;
690
935
  /**
@@ -813,7 +1058,14 @@ export declare class GitHubClient extends GitHubClient_base {
813
1058
  * `perPage` and `maxPages` cannot behave differently here than in production.
814
1059
  *
815
1060
  * `fixtures.requested` is appended to as the test runs, so a suite can assert
816
- * which routes were walked and at what page size.
1061
+ * which routes were walked, with what params and at what page size.
1062
+ *
1063
+ * **It records every call it serves, including one that fails.** A call
1064
+ * answered by a recorded `GitHubError` or a
1065
+ * {@link (GitHubFixtures:variable).failure} is recorded before it fails, so
1066
+ * one fixture both records the calls and classifies a raw failure the way
1067
+ * the live client would; there is no need to classify by hand inside a
1068
+ * {@link GitHubClient.layerTest} handler.
817
1069
  */
818
1070
  static readonly layerFixture: (fixtures: GitHubFixtures) => Layer.Layer<GitHubClient>;
819
1071
  }
@@ -840,6 +1092,15 @@ declare const RepoRef_base: Schema.Class<RepoRef, Schema.Struct<{
840
1092
  /**
841
1093
  * Which repository an operation acts on.
842
1094
  *
1095
+ * @remarks
1096
+ * **`RepoRef.make` validates at construction and throws** on an empty `owner`
1097
+ * or `repo`, so inside an `Effect` a bad value dies rather than failing. That
1098
+ * is right for a coordinate the program itself spells out, and wrong for one
1099
+ * built from data: for a slug read from a database row, a config file or a
1100
+ * webhook, use {@link RepoRef.parse}, which fails with a typed
1101
+ * {@link InvalidRepoRefError} instead (or {@link RepoRef.parseResult}
1102
+ * outside an `Effect`).
1103
+ *
843
1104
  * @public
844
1105
  */
845
1106
  export declare class RepoRef extends RepoRef_base {
@@ -852,7 +1113,13 @@ export declare class RepoRef extends RepoRef_base {
852
1113
  * constructor, which is why string parsing is named rather than overloaded.
853
1114
  */
854
1115
  static parseResult(slug: string): Result.Result<RepoRef, InvalidRepoRefError>;
855
- /** Parse `"owner/repo"`. */
1116
+ /**
1117
+ * Parse `"owner/repo"`, failing with a typed {@link InvalidRepoRefError}.
1118
+ *
1119
+ * @remarks
1120
+ * The typed path for building a `Repo` from stored data, such as a stored
1121
+ * `full_name`: a malformed value fails here, where `make` would die.
1122
+ */
856
1123
  static readonly parse: (slug: string) => Effect.Effect<RepoRef, InvalidRepoRefError, never>;
857
1124
  /** `"owner/repo"`. */
858
1125
  get slug(): string;
@@ -1102,6 +1369,12 @@ declare const CheckRunRef_base: Schema.Class<CheckRunRef, Schema.Struct<{
1102
1369
  /** The web URL. */
1103
1370
  readonly url: Schema.String;
1104
1371
  readonly status: Schema.String;
1372
+ /**
1373
+ * The integrator's own id for the run (wire `external_id`), when it has
1374
+ * one. GitHub reports a run created without one as `null` or `""`; both
1375
+ * leave this absent.
1376
+ */
1377
+ readonly externalId: Schema.optionalKey<Schema.String>;
1105
1378
  }>, {}>;
1106
1379
  /**
1107
1380
  * A check run as GitHub reports it.
@@ -1109,6 +1382,56 @@ declare const CheckRunRef_base: Schema.Class<CheckRunRef, Schema.Struct<{
1109
1382
  * @public
1110
1383
  */
1111
1384
  export declare class CheckRunRef extends CheckRunRef_base {}
1385
+ /**
1386
+ * Options for {@link CheckRunShape.create}.
1387
+ *
1388
+ * @public
1389
+ */
1390
+ interface CreateCheckRunOptions {
1391
+ /**
1392
+ * The run's initial state. Defaults to `"in_progress"`, which also stamps
1393
+ * `started_at`; a `"queued"` run has not started, so it carries none.
1394
+ * Completing a run goes through {@link CheckRunShape.complete}.
1395
+ */
1396
+ readonly status?: "queued" | "in_progress" | undefined;
1397
+ /**
1398
+ * Your own id for the run (wire `external_id`), for
1399
+ * {@link CheckRunShape.findByExternalId}. An empty string is treated as no
1400
+ * id and not sent, since that lookup never matches `""`.
1401
+ */
1402
+ readonly externalId?: string | undefined;
1403
+ /** Where the integrator's full details live (wire `details_url`). */
1404
+ readonly detailsUrl?: string | undefined;
1405
+ }
1406
+ /**
1407
+ * Options for {@link CheckRunShape.update}.
1408
+ *
1409
+ * @public
1410
+ */
1411
+ interface UpdateCheckRunOptions {
1412
+ /**
1413
+ * Move the run to `"queued"` or `"in_progress"`; `"in_progress"` also
1414
+ * stamps `started_at`, as {@link CheckRunShape.create} does. Completing it
1415
+ * goes through {@link CheckRunShape.complete}, which also records the
1416
+ * conclusion.
1417
+ */
1418
+ readonly status?: "queued" | "in_progress" | undefined;
1419
+ /** Where the integrator's full details live (wire `details_url`). */
1420
+ readonly detailsUrl?: string | undefined;
1421
+ }
1422
+ /**
1423
+ * Options for {@link CheckRunShape.complete}.
1424
+ *
1425
+ * @public
1426
+ */
1427
+ interface CompleteCheckRunOptions {
1428
+ /**
1429
+ * Where the integrator's full details live (wire `details_url`) — for
1430
+ * instance the workflow run that produced the verdict. Omitted, the run
1431
+ * keeps whatever details URL it already had.
1432
+ */
1433
+ readonly detailsUrl?: string | undefined;
1434
+ }
1112
1435
  /**
1113
1436
  * Conclude the surrounding {@link CheckRunShape.withCheckRun} explicitly.
1114
1437
  *
@@ -1139,13 +1462,48 @@ type ConcludeCheckRun = (conclusion: (typeof CheckConclusion.literals)[number],
1139
1462
  * @public
1140
1463
  */
1141
1464
  interface CheckRunShape {
1142
- /** Start an in-progress check run against a commit. */
1143
- readonly create: (name: string, headSha: string) => Effect.Effect<CheckRunRef, GitHubError, Repo>;
1465
+ /** Start a check run against a commit: in progress, unless `options.status` queues it. */
1466
+ readonly create: (name: string, headSha: string, options?: CreateCheckRunOptions) => Effect.Effect<CheckRunRef, GitHubError, Repo>;
1144
1467
  readonly get: (id: number) => Effect.Effect<CheckRunRef, GitHubError, Repo>;
1145
- /** Update an in-flight run's output. */
1146
- readonly update: (id: number, output: CheckRunOutput) => Effect.Effect<void, GitHubError, Repo>;
1147
- /** Finish a run. */
1148
- readonly complete: (id: number, conclusion: (typeof CheckConclusion.literals)[number], output?: CheckRunOutput) => Effect.Effect<void, GitHubError, Repo>;
1468
+ /**
1469
+ * Update an in-flight run: its output, its status, its details URL, or any
1470
+ * combination.
1471
+ *
1472
+ * @remarks
1473
+ * Omit `output` (pass `undefined`) to change only the status or details
1474
+ * URL: no `output` key is sent, so the run keeps the output it has. That is
1475
+ * how a queued run moves to `"in_progress"` without rewriting its output.
1476
+ *
1477
+ * `update(id)` with neither still sends **one** PATCH, carrying nothing but
1478
+ * the run's coordinates. GitHub accepts it and changes nothing; it is not
1479
+ * skipped, so every call is exactly one request and a caller's error
1480
+ * handling sees a missing run or a revoked token the same way either way.
1481
+ */
1482
+ readonly update: (id: number, output?: CheckRunOutput, options?: UpdateCheckRunOptions) => Effect.Effect<void, GitHubError, Repo>;
1483
+ /**
1484
+ * The newest run on `headSha` named `name` whose external id is
1485
+ * `externalId`; none when there is no such run.
1486
+ *
1487
+ * @remarks
1488
+ * Lists every run of the commit filtered by name on GitHub's side
1489
+ * (`filter: "all"`, not GitHub's default of only the latest run per name),
1490
+ * paging through all of them, then matches `external_id` here; "newest" is
1491
+ * the highest id.
1492
+ * An empty `externalId` is none without a request: GitHub reports a run
1493
+ * created without an external id as `""`, so matching on it would find
1494
+ * every such run.
1495
+ */
1496
+ readonly findByExternalId: (headSha: string, name: string, externalId: string) => Effect.Effect<Option.Option<CheckRunRef>, GitHubError, Repo>;
1497
+ /**
1498
+ * Finish a run, stamping `completed_at` from `Clock`.
1499
+ *
1500
+ * @remarks
1501
+ * Omit `output` (pass `undefined`) to conclude without touching the run's
1502
+ * rendered output; a given output is cut to GitHub's byte limits first.
1503
+ * `options.detailsUrl` points the finished run somewhere, such as the
1504
+ * workflow run that produced it.
1505
+ */
1506
+ readonly complete: (id: number, conclusion: (typeof CheckConclusion.literals)[number], output?: CheckRunOutput, options?: CompleteCheckRunOptions) => Effect.Effect<void, GitHubError, Repo>;
1149
1507
  /**
1150
1508
  * Run `use` inside a check run, concluding it however `use` exits.
1151
1509
  *
@@ -1636,6 +1994,52 @@ export declare class GitCommit extends GitCommit_base {
1636
1994
  static readonly layerTest: (overrides?: Partial<GitCommitShape>) => Layer.Layer<GitCommit>;
1637
1995
  }
1638
1996
  //#endregion
1997
+ //#region src/InstallationTokenStore.d.ts
1998
+ /**
1999
+ * The operations of an {@link InstallationTokenStore}.
2000
+ *
2001
+ * @public
2002
+ */
2003
+ interface InstallationTokenStoreShape {
2004
+ /**
2005
+ * The encoded installation token stored for `installationId`, if one is held
2006
+ * and unexpired.
2007
+ */
2008
+ readonly get: (installationId: number) => Effect.Effect<Option.Option<string>>;
2009
+ /** Hold `encoded` for `installationId` for `ttl`. */
2010
+ readonly set: (installationId: number, encoded: string, ttl: Duration.Duration) => Effect.Effect<void>;
2011
+ }
2012
+ declare const InstallationTokenStore_base: Context.ServiceClass<InstallationTokenStore, "@effected/github/InstallationTokenStore", InstallationTokenStoreShape>;
2013
+ /**
2014
+ * Where `GitHubApp.cachedToken` keeps installation tokens between request
2015
+ * scopes.
2016
+ *
2017
+ * @remarks
2018
+ * Entries are keyed by installation id, never by token text. The value is an
2019
+ * `InstallationToken` encoded as JSON, and it **contains the raw token**, so
2020
+ * encrypting it at rest is the store's job, not the caller's.
2021
+ *
2022
+ * {@link InstallationTokenStore.layerMemory} keeps tokens in process. A
2023
+ * Cloudflare Worker, whose isolates do not share memory, backs the store with
2024
+ * KV, a Durable Object or D1 instead. A store's own failures should be
2025
+ * swallowed by the store (a miss on `get`, a no-op on `set`): a cache must
2026
+ * never fail the call it serves. `cachedToken` also guards against a store
2027
+ * that fails or dies anyway; only interruption propagates.
2028
+ *
2029
+ * This module imports nothing but `effect`, so a store implementation never
2030
+ * links the JWT signer.
2031
+ *
2032
+ * @public
2033
+ */
2034
+ export declare class InstallationTokenStore extends InstallationTokenStore_base {
2035
+ /** An in-process store over a `Ref`, expiring entries against `Clock`. */
2036
+ static readonly layerMemory: Layer.Layer<InstallationTokenStore>;
2037
+ /** An in-memory double; unstubbed members die naming themselves. */
2038
+ static readonly makeTest: (overrides?: Partial<InstallationTokenStoreShape>) => InstallationTokenStoreShape;
2039
+ /** {@link InstallationTokenStore.makeTest} behind a `Layer`. */
2040
+ static readonly layerTest: (overrides?: Partial<InstallationTokenStoreShape>) => Layer.Layer<InstallationTokenStore>;
2041
+ }
2042
+ //#endregion
1639
2043
  //#region src/GitHubApp.d.ts
1640
2044
  declare const GitHubAppError_base: Schema.Class<GitHubAppError, Schema.TaggedStruct<"GitHubAppError", {
1641
2045
  /** Which step failed. */
@@ -1644,7 +2048,7 @@ declare const GitHubAppError_base: Schema.Class<GitHubAppError, Schema.TaggedStr
1644
2048
  readonly reason: Schema.String;
1645
2049
  /** The underlying failure, when there is one. */
1646
2050
  readonly cause: Schema.optionalKey<Schema.Defect>;
1647
- }>, import("effect/Cause").YieldableError>;
2051
+ }>, Cause.YieldableError>;
1648
2052
  /**
1649
2053
  * A GitHub App call failed.
1650
2054
  *
@@ -1677,12 +2081,13 @@ interface AppCredentials {
1677
2081
  * The app's private key, in PEM.
1678
2082
  *
1679
2083
  * @remarks
1680
- * PKCS#1 (`-----BEGIN RSA PRIVATE KEY-----`, which is what github.com hands
1681
- * you) is converted to PKCS#8 automatically **on Node**. On a runtime without
1682
- * `node:crypto` a PKCS#1 key fails with an explicit `kind: "jwt"` error, and
1683
- * the fix is to convert the key once with
1684
- * `openssl pkcs8 -topk8 -inform PEM -outform PEM -nocrypt`. This constraint is
1685
- * inherited from the JWT signer and is identical to `@octokit/auth-app`'s.
2084
+ * Both PKCS#1 (`-----BEGIN RSA PRIVATE KEY-----`, which is what github.com
2085
+ * hands you) and PKCS#8 (`-----BEGIN PRIVATE KEY-----`) are accepted on every
2086
+ * runtime with WebCrypto, Node and workerd alike: a PKCS#1 key is wrapped to
2087
+ * PKCS#8 in-process, so no conversion step is needed. Newlines may also
2088
+ * arrive escaped as the two characters backslash and `n`, the one-line form
2089
+ * an environment variable carries. The key must be RSA of at least 2048
2090
+ * bits; anything else fails with a `kind: "jwt"` error.
1686
2091
  */
1687
2092
  readonly privateKey: Redacted.Redacted<string>;
1688
2093
  }
@@ -1705,6 +2110,41 @@ interface TokenRequest extends AppCredentials {
1705
2110
  */
1706
2111
  readonly owner?: string | undefined;
1707
2112
  }
2113
+ /**
2114
+ * What to fetch a cached installation token for.
2115
+ *
2116
+ * @public
2117
+ */
2118
+ interface CachedTokenRequest extends TokenRequest {
2119
+ /**
2120
+ * The installation. Required here: discovering it on every request would
2121
+ * cost a JWT mint and a paginated walk, which defeats the cache.
2122
+ */
2123
+ readonly installationId: number;
2124
+ /**
2125
+ * How long before its expiry a stored token stops being served. Defaults to
2126
+ * five minutes; must be finite and not negative.
2127
+ *
2128
+ * @remarks
2129
+ * A served token has at least `margin` left to live unless GitHub issued it
2130
+ * with less than that, in which case it is returned once and not stored.
2131
+ * GitHub issues installation tokens for an hour, so a margin of an hour or
2132
+ * more guarantees that case: every call mints and nothing is cached.
2133
+ */
2134
+ readonly margin?: Duration.Input | undefined;
2135
+ }
2136
+ /**
2137
+ * An installation token from {@link GitHubApp.cachedToken}, and where it came
2138
+ * from.
2139
+ *
2140
+ * @public
2141
+ */
2142
+ interface CachedToken {
2143
+ /** The token. */
2144
+ readonly token: InstallationToken;
2145
+ /** `"cached"` when it was read from the store, `"minted"` when GitHub issued it just now. */
2146
+ readonly source: "cached" | "minted";
2147
+ }
1708
2148
  declare const InstallationToken_base: Schema.Class<InstallationToken, Schema.Struct<{
1709
2149
  /** The token. Decodes to `Redacted`, encodes back to the raw string. */
1710
2150
  readonly token: Schema.RedactedFromValue<Schema.String>;
@@ -1806,12 +2246,36 @@ export declare class AppIdentity extends AppIdentity_base {
1806
2246
  declare const Installation_base: Schema.Class<Installation, Schema.Struct<{
1807
2247
  /** The installation id, which is what a token is minted against. */
1808
2248
  readonly id: Schema.Int;
1809
- /** The account the app is installed on, when GitHub reported one. */
2249
+ /** The account the app is installed on (its login), when GitHub reported one. */
1810
2250
  readonly account: Schema.optionalKey<Schema.String>;
2251
+ /** The account kind, e.g. "Organization" or "User"; open-ended, because GitHub adds kinds. */
2252
+ readonly accountType: Schema.optionalKey<Schema.String>;
2253
+ /**
2254
+ * The account's numeric id.
2255
+ *
2256
+ * @remarks
2257
+ * Not paired with `account`: an enterprise installation carries
2258
+ * `accountId` (and may carry `accountType`) but has no login, so `account`
2259
+ * is absent.
2260
+ */
2261
+ readonly accountId: Schema.optionalKey<Schema.Int>;
2262
+ /** When the installation was suspended; `Option.none()` when it is active. */
2263
+ readonly suspendedAt: Schema.optionalKey<Schema.OptionFromNullOr<Schema.DateTimeUtcFromString>>;
2264
+ /** When GitHub last changed the installation. */
2265
+ readonly updatedAt: Schema.optionalKey<Schema.DateTimeUtcFromString>;
1811
2266
  }>, {}>;
1812
2267
  /**
1813
2268
  * One installation of the app.
1814
2269
  *
2270
+ * @remarks
2271
+ * Every field but `id` is filled only when GitHub's response carries it and
2272
+ * it reads cleanly; an unreadable field is omitted rather than failing the
2273
+ * listing. A test double built with `Installation.make({ id })` stays valid.
2274
+ * An enterprise account carries `accountId` and `accountType` without a
2275
+ * login `account`. Encodable:
2276
+ * the dates encode to ISO strings and `suspendedAt` to an ISO string or
2277
+ * `null`, the same shape GitHub sends.
2278
+ *
1815
2279
  * @public
1816
2280
  */
1817
2281
  export declare class Installation extends Installation_base {}
@@ -1847,8 +2311,8 @@ declare const GitHubApp_base: Context.ServiceClass<GitHubApp, "@effected/github/
1847
2311
  * already — `@effected/workspaces` ships `localExecLayer`, which builds
1848
2312
  * `@effected/commands`' service, for the same reason.
1849
2313
  *
1850
- * The JWT signer is `universal-github-app-jwt` — zero dependencies, and
1851
- * `@octokit/auth-app`'s own JWT dependency.
2314
+ * The JWT signer is `@effected/jwt` — WebCrypto RS256 with no runtime
2315
+ * dependencies, so App auth runs on any runtime that has `crypto.subtle`.
1852
2316
  *
1853
2317
  * @example
1854
2318
  * ```ts
@@ -1904,6 +2368,128 @@ export declare class GitHubApp extends GitHubApp_base {
1904
2368
  * would tax every caller for a case only this layer can produce.
1905
2369
  */
1906
2370
  static readonly clientLayer: (request: TokenRequest, options?: GitHubAppOptions) => Layer.Layer<GitHubClient, GitHubAppError>;
2371
+ /**
2372
+ * A {@link GitHubClient} authenticated as the app itself, with an App JWT.
2373
+ *
2374
+ * @remarks
2375
+ * An App JWT authenticates only the app-level routes: `/app` and everything
2376
+ * under `/app/*` (the app's installations and their token mint, its webhook
2377
+ * configuration and deliveries), plus the three installation lookups that
2378
+ * require a JWT: `GET /repos/{owner}/{repo}/installation`,
2379
+ * `GET /orgs/{org}/installation` and `GET /users/{username}/installation`.
2380
+ * Installation-scoped routes (a repository's contents, issues, pulls and the
2381
+ * rest) answer 401 to it; reach those through {@link GitHubApp.clientLayer}
2382
+ * with an installation token instead.
2383
+ *
2384
+ * The motivating use is a webhook redelivery sweep, which lists recent
2385
+ * deliveries with `GET /app/hook/deliveries` and redelivers a failed one
2386
+ * with `POST /app/hook/deliveries/{delivery_id}/attempts`.
2387
+ *
2388
+ * The JWT is signed locally, never fetched, so building the layer makes no
2389
+ * request; a key that will not sign fails construction with
2390
+ * `GitHubAppError { kind: "jwt" }`. A JWT lives nine minutes and is
2391
+ * re-signed a minute before it expires, so a long-running sweep keeps
2392
+ * authenticating; calls in between reuse it. It cannot be revoked, so
2393
+ * release does nothing. A later signing failure surfaces to the caller as
2394
+ * `GitHubError { kind: "unauthorized" }` carrying the `GitHubAppError` as
2395
+ * its cause, as with {@link GitHubApp.clientLayer}.
2396
+ *
2397
+ * @example
2398
+ * ```ts
2399
+ * import { GitHubApp, GitHubClient } from "@effected/github";
2400
+ * import { Effect, Redacted } from "effect";
2401
+ *
2402
+ * const sweep = Effect.gen(function* () {
2403
+ * const client = yield* GitHubClient;
2404
+ * const deliveries = yield* client.request("GET /app/hook/deliveries", { per_page: 100 });
2405
+ * for (const delivery of deliveries) {
2406
+ * if (delivery.status_code >= 400) {
2407
+ * yield* client.request("POST /app/hook/deliveries/{delivery_id}/attempts", {
2408
+ * // octokit types a delivery id as number | bigint; the route takes a number.
2409
+ * delivery_id: Number(delivery.id),
2410
+ * });
2411
+ * }
2412
+ * }
2413
+ * });
2414
+ *
2415
+ * const layer = GitHubApp.appClientLayer({
2416
+ * appId: "12345",
2417
+ * privateKey: Redacted.make("-----BEGIN RSA PRIVATE KEY-----\n..."),
2418
+ * });
2419
+ *
2420
+ * Effect.runPromise(Effect.provide(sweep, layer));
2421
+ * ```
2422
+ */
2423
+ static readonly appClientLayer: (credentials: AppCredentials, options?: GitHubAppOptions) => Layer.Layer<GitHubClient, GitHubAppError>;
2424
+ /**
2425
+ * An installation token, reused from an {@link InstallationTokenStore}
2426
+ * while it has `margin` (five minutes by default) left to live.
2427
+ *
2428
+ * @remarks
2429
+ * For a program that authenticates per request scope (a Worker handling a
2430
+ * webhook, say) and would otherwise mint a fresh token every time. The store
2431
+ * is keyed by installation id. A stored value that will not decode, or a
2432
+ * token within `margin` of expiry, is a miss: the token is minted with
2433
+ * {@link GitHubAppShape.token} and written back with a TTL of its expiry
2434
+ * minus `margin`. A token minted with less than `margin` to live is returned
2435
+ * but not stored.
2436
+ *
2437
+ * The store never fails this call: a `get` that fails or dies is a miss and
2438
+ * a `set` that fails or dies is ignored. Only interruption propagates.
2439
+ *
2440
+ * **A cached token is never revoked**: another scope or isolate may be using
2441
+ * it. It expires on its own, within the hour.
2442
+ *
2443
+ * Two concurrent calls that both miss will both mint, and the later write
2444
+ * wins. That is expected: the store may span isolates, so the kit cannot
2445
+ * lock it, and an extra token costs one call and expires on its own.
2446
+ *
2447
+ * The encoded value written to the store contains the raw token, so
2448
+ * encrypting it at rest is the store's job.
2449
+ */
2450
+ static readonly cachedToken: (request: CachedTokenRequest) => Effect.Effect<CachedToken, GitHubAppError, GitHubApp | InstallationTokenStore>;
2451
+ /**
2452
+ * A {@link GitHubClient} authenticated with {@link GitHubApp.cachedToken}.
2453
+ *
2454
+ * @remarks
2455
+ * Built per request scope: provide it where the request is handled, over a
2456
+ * `GitHubApp` and an `InstallationTokenStore` provided once at the edge.
2457
+ * Unlike {@link GitHubApp.clientLayer} it **never revokes** the token, not
2458
+ * even on release, because the token is shared through the store, and it
2459
+ * does not rotate, so set `margin` longer than the scope's work can take;
2460
+ * the token is good for at least `margin` unless GitHub issued it with less
2461
+ * (see {@link CachedTokenRequest.margin}).
2462
+ *
2463
+ * `options` configures this client's transport only, not the mint: the
2464
+ * token is minted by whichever `GitHubApp` the edge provides. On GitHub
2465
+ * Enterprise, build that with the same API root, for example
2466
+ * `GitHubApp.layerWith({ baseUrl })`, or the mint goes to github.com.
2467
+ *
2468
+ * @example
2469
+ * ```ts
2470
+ * import { GitHubApp, GitHubClient, InstallationTokenStore } from "@effected/github";
2471
+ * import { Effect, Layer, Redacted } from "effect";
2472
+ *
2473
+ * const handle = (installationId: number) =>
2474
+ * Effect.flatMap(GitHubClient, (client) =>
2475
+ * client.request("GET /repos/{owner}/{repo}", { owner: "acme", repo: "widgets" }),
2476
+ * ).pipe(
2477
+ * Effect.provide(
2478
+ * GitHubApp.cachedClientLayer({
2479
+ * appId: "12345",
2480
+ * privateKey: Redacted.make("-----BEGIN RSA PRIVATE KEY-----\n..."),
2481
+ * installationId,
2482
+ * }),
2483
+ * ),
2484
+ * );
2485
+ *
2486
+ * // Once, at the edge. On GitHub Enterprise, use GitHubApp.layerWith({ baseUrl }).
2487
+ * const Live = Layer.mergeAll(GitHubApp.layer, InstallationTokenStore.layerMemory);
2488
+ *
2489
+ * Effect.runPromise(Effect.provide(handle(42), Live));
2490
+ * ```
2491
+ */
2492
+ static readonly cachedClientLayer: (request: CachedTokenRequest, options?: GitHubAppOptions) => Layer.Layer<GitHubClient, GitHubAppError, GitHubApp | InstallationTokenStore>;
1907
2493
  /** An in-memory double; unstubbed members die naming themselves. */
1908
2494
  static readonly makeTest: (overrides?: Partial<GitHubAppShape>) => GitHubAppShape;
1909
2495
  /** {@link GitHubApp.makeTest} behind a `Layer`. */
@@ -2105,6 +2691,12 @@ interface GitHubContentShape {
2105
2691
  * Fails `notFound` when the path does not exist, and `rejected` when it is a
2106
2692
  * directory, is not a regular file, or is too large for the contents API
2107
2693
  * (which answers an empty body above roughly a megabyte).
2694
+ *
2695
+ * Fails `decode` when the payload is not valid base64. Only **standard,
2696
+ * padded** base64 is accepted, which is what the contents API sends; the
2697
+ * line breaks it wraps the payload with are ignored. Unpadded or URL-safe
2698
+ * (`-`/`_`) input is rejected rather than guessed at. The bytes decode as
2699
+ * UTF-8, keeping a leading BOM as content.
2108
2700
  */
2109
2701
  readonly getFile: (path: string, options?: {
2110
2702
  readonly ref?: string | undefined;
@@ -3705,6 +4297,17 @@ interface WorkflowDispatchShape {
3705
4297
  readonly dispatch: (workflow: string, ref: string, inputs?: Record<string, string>) => Effect.Effect<void, GitHubError, Repo>;
3706
4298
  /** Read one workflow run's status. */
3707
4299
  readonly runStatus: (runId: number) => Effect.Effect<WorkflowRunStatus, GitHubError, Repo>;
4300
+ /**
4301
+ * Cancel a workflow run.
4302
+ *
4303
+ * @remarks
4304
+ * `"cancelled"` when GitHub accepted the cancellation (202; the run stops
4305
+ * shortly after, not necessarily before this returns). GitHub answers 409
4306
+ * when the run has already finished, which is `"alreadyCompleted"` rather
4307
+ * than a failure: the run is not running either way. Any other failure,
4308
+ * such as a 404 for an unknown run, is a `GitHubError`.
4309
+ */
4310
+ readonly cancelRun: (runId: number) => Effect.Effect<"cancelled" | "alreadyCompleted", GitHubError, Repo>;
3708
4311
  /**
3709
4312
  * Every workflow defined in the repository.
3710
4313
  *
@@ -3771,5 +4374,5 @@ export declare class WorkflowDispatch extends WorkflowDispatch_base {
3771
4374
  static readonly layerTest: (overrides?: Partial<WorkflowDispatchShape>) => Layer.Layer<WorkflowDispatch>;
3772
4375
  }
3773
4376
  //#endregion
3774
- export { type AppCredentials, type AppliedSettings, type ArtifactMetadataShape, type AttestationShape, type BareLineReference, type BranchOutcome, CLOSING_KEYWORDS, type CheckRunShape, type ClosingKeyword, type CodeScanningSetup, type CodeScanningShape, type ConcludeCheckRun, type DeploymentEnvironmentInfo, type DeploymentEnvironmentShape, type GitBranchShape, type GitCommitShape, type GitHubAppOptions, type GitHubAppShape, type GitHubClientOptions, type GitHubClientShape, type GitHubCommitShape, type GitHubContentShape, type GitHubFixtures, type GitHubIssueShape, type GitHubReleaseShape, type GitHubRepositoryShape, type GitTagShape, type IssueReference, type LatestSemverOptions, type OwnerType, type PollOptions, type PullRequestCommentShape, type PullRequestShape, type RecordedCall, type RepositoryPatch, type RepositoryPatchDraft, type RepositorySecretShape, type RepositorySecurityShape, type RepositorySettings, type RepositoryVariableShape, type Data as RestData, type RequestExtras as RestExtras, type Item as RestItem, type PaginatingRoute as RestPaginatingRoute, type Params as RestParams, type Response as RestResponse, type Route as RestRoute, type RetryableFailure, type RulesetInfo, type RulesetPayload, type RulesetShape, type SecretInfo, type SecretScope, type TokenRequest, type UpsertedPullRequest, type VariableInfo, type VersionFromTag, type WorkflowDispatchShape, type WorkflowInfo, harvestIssueReferences, parseBareLineReference };
4377
+ export { type ActionsOidcVerifyOptions, type AppCredentials, type AppliedSettings, type ArtifactMetadataShape, type AttestationShape, type BareLineReference, type BranchOutcome, CLOSING_KEYWORDS, type CachedToken, type CachedTokenRequest, type CheckRunShape, type ClosingKeyword, type CodeScanningSetup, type CodeScanningShape, type CompleteCheckRunOptions, type ConcludeCheckRun, type CreateCheckRunOptions, type DeploymentEnvironmentInfo, type DeploymentEnvironmentShape, type GitBranchShape, type GitCommitShape, type GitHubAppOptions, type GitHubAppShape, type GitHubClientOptions, type GitHubClientShape, type GitHubCommitShape, type GitHubContentShape, type GitHubIssueShape, type GitHubReleaseShape, type GitHubRepositoryShape, type GitTagShape, type InstallationTokenStoreShape, type IssueReference, type LatestSemverOptions, type OwnerType, type PollOptions, type PullRequestCommentShape, type PullRequestShape, type RecordedCall, type RepositoryPatch, type RepositoryPatchDraft, type RepositorySecretShape, type RepositorySecurityShape, type RepositorySettings, type RepositoryVariableShape, type Data as RestData, type RequestExtras as RestExtras, type Item as RestItem, type PaginatingRoute as RestPaginatingRoute, type Params as RestParams, type Response as RestResponse, type Route as RestRoute, type RetryableFailure, type RulesetInfo, type RulesetPayload, type RulesetShape, type SecretInfo, type SecretScope, type TokenRequest, type UpdateCheckRunOptions, type UpsertedPullRequest, type VariableInfo, type VersionFromTag, type WorkflowDispatchShape, type WorkflowInfo, harvestIssueReferences, parseBareLineReference };
3775
4378
  //# sourceMappingURL=index.d.ts.map