@effected/github-actions 0.23.1 → 0.25.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.
@@ -1,11 +1,13 @@
1
1
  import { ActionEnvironment } from "./ActionEnvironment.js";
2
2
  import { unstubbed } from "./internal/unstubbed.js";
3
- import { payloadOf, unsignedJwt } from "./internal/jwt.js";
4
3
  import { Context, Effect, Layer, Redacted, Schema } from "effect";
5
4
  import { HttpClient, HttpClientRequest } from "effect/http";
5
+ import { Oidc } from "@effected/oidc";
6
+ import { ActionsOidcClaimsFromWire } from "@effected/github";
7
+ import * as Base64Url from "effect/encoding/Base64Url";
6
8
  import * as HttpClientResponse from "effect/http/HttpClientResponse";
7
9
 
8
- //#region src/OidcTokenIssuer.ts
10
+ // src/OidcTokenIssuer.ts
9
11
  /**
10
12
  * Raised when an OIDC token cannot be issued or read.
11
13
  *
@@ -45,44 +47,6 @@ var OidcTokenError = class extends Schema.TaggedError()("OidcTokenError", {
45
47
  }
46
48
  }
47
49
  };
48
- /**
49
- * The claims a GitHub Actions OIDC token carries about the workflow that ran.
50
- *
51
- * @remarks
52
- * The field names are GitHub's own JWT claim names, kept verbatim rather than
53
- * translated to the kit's casing. They are an **external vocabulary** — the
54
- * same strings that appear in a SLSA provenance predicate and in every
55
- * published example of an Actions OIDC policy — and renaming them would put a
56
- * translation layer between a consumer and the specification they are reading.
57
- *
58
- * @public
59
- */
60
- var OidcClaims = class extends Schema.Class("OidcClaims")({
61
- /** The issuer, e.g. `https://token.actions.githubusercontent.com`. */
62
- iss: Schema.String,
63
- /** The full ref the workflow ran on. */
64
- ref: Schema.String,
65
- /** The commit. */
66
- sha: Schema.String,
67
- /** `owner/repo`. */
68
- repository: Schema.String,
69
- /** The event that triggered the run. */
70
- event_name: Schema.String,
71
- /** The reusable-workflow ref, which is what a verifier pins against. */
72
- job_workflow_ref: Schema.String,
73
- /** The calling workflow's ref. */
74
- workflow_ref: Schema.String,
75
- /** The numeric repository id, as a string. */
76
- repository_id: Schema.String,
77
- /** The numeric owner id, as a string. */
78
- repository_owner_id: Schema.String,
79
- /** `github-hosted` or `self-hosted`. */
80
- runner_environment: Schema.String,
81
- /** The run id, as a string. */
82
- run_id: Schema.String,
83
- /** The run attempt, as a string. */
84
- run_attempt: Schema.String
85
- }) {};
86
50
  /** The token envelope the runner's token service answers with. */
87
51
  const TokenEnvelope = Schema.Struct({
88
52
  value: Schema.String,
@@ -97,16 +61,23 @@ const REQUEST_URL = "ACTIONS_ID_TOKEN_REQUEST_URL";
97
61
  * The absence of verification is deliberate and is documented on
98
62
  * {@link OidcTokenIssuerShape.claims}; do not "fix" it here.
99
63
  */
100
- const readClaims = (token) => Effect.gen(function* () {
101
- const decoded = yield* Effect.fromResult(payloadOf(token)).pipe(Effect.mapError((failure) => new OidcTokenError({
102
- reason: "malformedToken",
103
- ...failure
104
- })));
105
- return yield* Schema.decodeUnknownEffect(OidcClaims)(decoded).pipe(Effect.mapError((cause) => new OidcTokenError({
106
- reason: "missingClaims",
107
- cause
108
- })));
109
- });
64
+ const readClaims = (token) => Oidc.decodeUnverified(token, ActionsOidcClaimsFromWire).pipe(Effect.mapError((error) => error.reason === "claims" ? new OidcTokenError({
65
+ reason: "missingClaims",
66
+ cause: error.cause ?? error
67
+ }) : new OidcTokenError({
68
+ reason: "malformedToken",
69
+ detail: error.detail,
70
+ cause: error
71
+ })));
72
+ /**
73
+ * An **unsigned** JWT: `header.payload.unsigned`, each JSON segment
74
+ * base64url-encoded. For test doubles only; the signature segment is a
75
+ * placeholder, so the token fails any verifier.
76
+ */
77
+ const unsignedJwt = (header, payload) => {
78
+ const segment = (value) => Base64Url.encode(JSON.stringify(value));
79
+ return `${segment(header)}.${segment(payload)}.unsigned`;
80
+ };
110
81
  const make = Effect.gen(function* () {
111
82
  const env = yield* ActionEnvironment;
112
83
  const http = yield* HttpClient.HttpClient;
@@ -166,7 +137,7 @@ const dies = unstubbed("OidcTokenIssuer.makeTest");
166
137
  * const program = Effect.gen(function* () {
167
138
  * const issuer = yield* OidcTokenIssuer;
168
139
  * const claims = yield* issuer.claims("sigstore");
169
- * return claims.job_workflow_ref;
140
+ * return claims.jobWorkflowRef;
170
141
  * });
171
142
  * ```
172
143
  *
@@ -193,7 +164,7 @@ var OidcTokenIssuer = class OidcTokenIssuer extends Context.Service()("@effected
193
164
  static unsignedTokenFor = (claims) => Redacted.make(unsignedJwt({
194
165
  alg: "RS256",
195
166
  typ: "JWT"
196
- }, Schema.encodeUnknownSync(OidcClaims)(claims)));
167
+ }, Schema.encodeSync(ActionsOidcClaimsFromWire)(claims)));
197
168
  /** A test double. Unstubbed members die rather than answering with a non-token. */
198
169
  static makeTest = (overrides = {}) => ({
199
170
  token: () => dies("token"),
@@ -218,5 +189,4 @@ var OidcTokenIssuer = class OidcTokenIssuer extends Context.Service()("@effected
218
189
  });
219
190
  };
220
191
 
221
- //#endregion
222
- export { OidcClaims, OidcTokenError, OidcTokenIssuer };
192
+ export { OidcTokenError, OidcTokenIssuer };
@@ -1,7 +1,7 @@
1
1
  import { ActionEnvironment } from "./ActionEnvironment.js";
2
2
  import { unstubbed } from "./internal/unstubbed.js";
3
3
  import { digestFileHex } from "./internal/digest.js";
4
- import { isWindowsRunner } from "./internal/runner.js";
4
+ import { isWindowsRunner, runnerArch } from "./internal/runner.js";
5
5
  import { typeAt } from "./internal/fsProbe.js";
6
6
  import { PNPM_EXE_PREFIX, detectMusl, isNodeScript, pnpmExeTarget, strongestSri } from "./internal/pnpmExe.js";
7
7
  import { cmdShim, gitBashShim, posixShim } from "./internal/shim.js";
@@ -10,7 +10,18 @@ import { Context, Effect, FileSystem, Layer, Option, Path, Schema } from "effect
10
10
  import { ChildProcess, ChildProcessSpawner } from "effect/process";
11
11
  import { CorepackIntegrityHash, DEFAULT_REGISTRY, PackageManagerPin, PackageManagerPinName } from "@effected/npm";
12
12
 
13
- //#region src/PackageManagerInstaller.ts
13
+ // src/PackageManagerInstaller.ts
14
+ /**
15
+ * The two `package.json` members the installer reads, as JSON text decoded to
16
+ * an object. Both stay `unknown`: they are attacker-supplied, and their own
17
+ * normalizers decide what survives.
18
+ */
19
+ const decodeManifest = Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Struct({
20
+ bin: Schema.optionalKey(Schema.Unknown),
21
+ optionalDependencies: Schema.optionalKey(Schema.Unknown)
22
+ })));
23
+ /** A packument's `dist.integrity`, left `unknown` for `expectedFromSri` to judge. */
24
+ const decodePackument = Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Struct({ dist: Schema.optionalKey(Schema.Struct({ integrity: Schema.optionalKey(Schema.Unknown) })) })));
14
25
  /**
15
26
  * Raised when a package manager cannot be provisioned on the runner.
16
27
  *
@@ -202,11 +213,6 @@ const normalizeBins = (bin, fallbackName) => {
202
213
  }
203
214
  return Object.keys(entries).length === 0 ? Option.none() : Option.some(entries);
204
215
  };
205
- /** Map a `RUNNER_ARCH` value (`X64`, `ARM64`) onto the Node arch spelling. */
206
- const archFromRunner = (runnerArch) => {
207
- const lowered = runnerArch.toLowerCase();
208
- return lowered === "x64" || lowered === "arm64" ? lowered : runnerArch;
209
- };
210
216
  const make = Effect.gen(function* () {
211
217
  const env = yield* ActionEnvironment;
212
218
  const fs = yield* FileSystem.FileSystem;
@@ -214,10 +220,7 @@ const make = Effect.gen(function* () {
214
220
  const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
215
221
  const installer = yield* ToolInstaller;
216
222
  const runnerOs = yield* Effect.map(env.getOptional("RUNNER_OS"), (found) => Option.getOrElse(found, () => ""));
217
- const arch = yield* Effect.map(env.getOptional("RUNNER_ARCH"), (found) => Option.match(found, {
218
- onNone: () => process.arch,
219
- onSome: archFromRunner
220
- }));
223
+ const arch = yield* runnerArch(env);
221
224
  const windows = yield* isWindowsRunner(env);
222
225
  const musl = detectMusl();
223
226
  const bunBinaryName = windows ? "bun.exe" : "bun";
@@ -305,14 +308,11 @@ const make = Effect.gen(function* () {
305
308
  subject: "no package.json",
306
309
  cause
307
310
  })));
308
- const manifest = yield* Effect.try({
309
- try: () => JSON.parse(raw),
310
- catch: (cause) => errorFor(pin)({
311
- reason: "layoutUnexpected",
312
- subject: "unparseable package.json",
313
- cause
314
- })
315
- });
311
+ const manifest = yield* decodeManifest(raw).pipe(Effect.mapError((cause) => errorFor(pin)({
312
+ reason: "layoutUnexpected",
313
+ subject: "unparseable package.json",
314
+ cause
315
+ })));
316
316
  const bins = Option.getOrUndefined(normalizeBins(manifest.bin, pin.name));
317
317
  if (bins === void 0) return yield* Effect.fail(errorFor(pin)({
318
318
  reason: "layoutUnexpected",
@@ -458,10 +458,7 @@ const make = Effect.gen(function* () {
458
458
  subject: packumentUrl,
459
459
  cause
460
460
  });
461
- const packument = yield* fs.readFileString(packumentFile).pipe(Effect.mapError(unverifiable), Effect.flatMap((raw) => Effect.try({
462
- try: () => JSON.parse(raw),
463
- catch: unverifiable
464
- })));
461
+ const packument = yield* fs.readFileString(packumentFile).pipe(Effect.mapError(unverifiable), Effect.flatMap((raw) => decodePackument(raw).pipe(Effect.mapError(unverifiable))));
465
462
  return yield* expectedFromSri(pin, packument.dist?.integrity, packumentUrl);
466
463
  });
467
464
  /**
@@ -734,5 +731,4 @@ var PackageManagerInstaller = class PackageManagerInstaller extends Context.Serv
734
731
  static layerTest = (overrides = {}) => Layer.succeed(PackageManagerInstaller, PackageManagerInstaller.makeTest(overrides));
735
732
  };
736
733
 
737
- //#endregion
738
734
  export { AmbientPackageManager, CachedPackageManager, InstalledPackageManager, PackageManagerInstaller, PackageManagerInstallerError };
package/README.md CHANGED
@@ -155,7 +155,7 @@ Two decisions make that safe for any segment value, with no prefix reasoning at
155
155
 
156
156
  `CacheKey.hashFiles` is byte-compatible with `@actions/glob`'s `hashFiles`, and `OidcTokenIssuer.claims` decodes the runner's OIDC token for a SLSA provenance predicate without verifying its signature — the token arrived over TLS from the runner's own token service, so the transport is the trust boundary and nothing here branches on the claims for authorization.
157
157
 
158
- `ActionsProvenance.capture` turns those claims into an `@effected/sbom` `SlsaProvenance` predicate, and `ActionsIdentityToken.layer` serves that package's narrow `IdentityToken` contract from the same issuer — the inversion that keeps the Actions runtime out of every consumer that only wants to emit an SBOM:
158
+ `ActionsProvenance.capture` turns those claims into an `@effected/sbom` `SlsaProvenance` predicate, and `ActionsIdentityToken.layer` serves `@effected/oidc`'s narrow `IdentityToken` contract, which that package's signer consumes, from the same issuer — the inversion that keeps the Actions runtime out of every consumer that only wants to emit an SBOM:
159
159
 
160
160
  ```ts
161
161
  import { ActionsIdentityToken, ActionsProvenance, OidcTokenIssuer } from "@effected/github-actions";
@@ -303,13 +303,13 @@ const TestOutputs = ActionOutputs.layerTest({
303
303
  - `ActionCache` / `Artifact` — the runner's cache and artifact protocols over Twirp v2, excluded from the default runtime so a consumer that skips them never links Azure.
304
304
  - `CacheKey` — cache keys and their restore-key ladder, `withNamespace(segment)` for a bust token or a shared-runner prefix, plus `hashFiles`/`matchingFiles`/`hashMatching` byte-compatible with `@actions/glob`.
305
305
  - `OidcTokenIssuer` — the runner's OIDC token service, with unverified claim decoding for provenance predicates.
306
- - `ActionsIdentityToken` / `ActionsProvenance` — the `@effected/sbom` seam: `layer` serves that package's `IdentityToken` contract from the runner's issuer, and `capture` builds a `SlsaProvenance` predicate out of the runner's OIDC claims.
306
+ - `ActionsIdentityToken` / `ActionsProvenance` — the `@effected/sbom` seam: `layer` serves `@effected/oidc`'s `IdentityToken` contract from the runner's issuer, and `capture` builds a `SlsaProvenance` predicate out of the runner's OIDC claims.
307
307
  - `GitHubMarkdown` — the escaping-safe writer for GitHub surfaces: `table`, `tableFor(schema)`, `heading`, `link`, `code`, `codeBlock`, `list`, `details` and `raw`, and the package's only importer of `@effected/markdown`.
308
308
  - `ManagedDocument` — marker-delimited named regions in text a human also edits, for a sticky comment or a managed pull request description; content outside the regions survives byte-for-byte, and each region can carry marker metadata that round-trips through `entry(key)` without ever addressing the region.
309
309
  - `CheckState` / `projectCheckState` — the kit's check vocabulary, wider than GitHub's conclusions, and its projection onto the check-run wire form.
310
310
  - `CheckDocument` — the debounced check-to-document reconciler: report states as they change, and one write lands carrying the final one. `stamp` plus a readable sink lets two runs share one document, dropping a strictly older run's pass instead of clobbering the newer one; `flush` reports `written`, `unchanged` or `stale`.
311
311
  - `ToolInstaller` — download, extract and cache a toolchain in the runner's tool cache, with no `@actions/tool-cache` dependency; `ToolInstallerError` always names its `subject`.
312
- - `BlobStore` — durable `get`/`put`/`has` over `StoredBlob<A>` values (bytes plus caller-schema metadata; the type was named `Blob<A>` before, which collided with the global), backed by `layerS3` (SigV4-signed, no `@aws-sdk/*` dependency) or `GitHubCacheBlobStore.layer` (the runner's own cache); `layerMemory` runs the real envelope framing for tests.
312
+ - `BlobStore` — durable `get`/`put`/`has` over `StoredBlob<A>` values (bytes plus caller-schema metadata; the type was named `Blob<A>` before, which collided with the global), backed by `layerS3` (SigV4-signed through `@effected/crypto`, so it needs core `Crypto` in `R`; no `@aws-sdk/*` dependency) or `GitHubCacheBlobStore.layer` (the runner's own cache); `layerMemory` runs the real envelope framing for tests.
313
313
 
314
314
  ## License
315
315
 
package/Secret.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { ActionOutputs } from "./ActionOutputs.js";
2
2
  import { Config, Effect, Redacted } from "effect";
3
3
 
4
- //#region src/Secret.ts
4
+ // src/Secret.ts
5
5
  /**
6
6
  * The declassification seam: the **only** place in this package where a
7
7
  * `Redacted` value becomes a plain string.
@@ -94,9 +94,13 @@ var Secret = class Secret {
94
94
  * Declassify one secret for an in-process use that needs the raw bytes.
95
95
  *
96
96
  * @remarks
97
- * Request signing is the case this exists for: an HMAC cannot be computed
98
- * over a `Redacted`, so a signing key has to become a string somewhere, and
99
- * the point of this module is that "somewhere" is here and nowhere else.
97
+ * For a consumer that must hand the raw key to code taking a plain string,
98
+ * such as a third-party signing library. A signer that takes a `Redacted`
99
+ * needs no declassification at all: `@effected/crypto`'s `SigV4` and `Hmac`
100
+ * unwrap the key only where they compute, so a caller of those (this
101
+ * package's own S3 `BlobStore` is one) registers the value with
102
+ * {@link Secret.mask} and passes the `Redacted` on. No module in this
103
+ * package calls this member any more.
100
104
  *
101
105
  * Signing named the member; it does not gate it. **Any in-process use that
102
106
  * needs the raw value without writing it to a runner file is this
@@ -121,9 +125,9 @@ var Secret = class Secret {
121
125
  * `::add-mask::` command only masks when the runner parses this process's
122
126
  * stdout; a detached worker's stdout is a log file no runner parses, so
123
127
  * there the command masks nothing *and* spells the plaintext into the log.
124
- * A worker that signs (an S3-style `BlobStore` is the canonical case) must compose
125
- * `ActionOutputs.layerDetached`, under which this member still returns the
126
- * raw key for the HMAC but the mask is a documented no-op; the masking
128
+ * A worker that signs must compose `ActionOutputs.layerDetached`, under
129
+ * which this member still returns the raw key but the mask is a
130
+ * documented no-op; the masking
127
131
  * itself is the **parent's** job, done before the spawn via
128
132
  * {@link Secret.forChildEnv} under the real layer.
129
133
  */
@@ -198,5 +202,4 @@ var Secret = class Secret {
198
202
  static adopt = (name) => Config.Redacted(name);
199
203
  };
200
204
 
201
- //#endregion
202
205
  export { Secret };
package/ToolInstaller.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { ActionEnvironment } from "./ActionEnvironment.js";
2
2
  import { unstubbed } from "./internal/unstubbed.js";
3
- import { isWindowsRunner, toolCacheRoot } from "./internal/runner.js";
3
+ import { isWindowsRunner, runnerArch, toolCacheRoot } from "./internal/runner.js";
4
4
  import { spawnOnce } from "./internal/spawn.js";
5
5
  import { tarExtractCommand, unzipCommand } from "./internal/archiveCommands.js";
6
6
  import { isErrno, typeAt } from "./internal/fsProbe.js";
@@ -8,7 +8,7 @@ import { Context, Effect, FileSystem, Layer, Option, Path, Schedule, Schema, Str
8
8
  import { HttpClient } from "effect/http";
9
9
  import { ChildProcessSpawner } from "effect/process";
10
10
 
11
- //#region src/ToolInstaller.ts
11
+ // src/ToolInstaller.ts
12
12
  /**
13
13
  * Raised when a tool cannot be downloaded, extracted or cached.
14
14
  *
@@ -68,11 +68,12 @@ const make = Effect.gen(function* () {
68
68
  const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
69
69
  const root = yield* toolCacheRoot(env, path);
70
70
  const windows = yield* isWindowsRunner(env);
71
+ const arch = yield* runnerArch(env);
71
72
  const cachePath = (tool, version) => ToolInstaller.cachePath({
72
73
  root,
73
74
  tool,
74
75
  version,
75
- arch: process.arch
76
+ arch
76
77
  });
77
78
  const destinationFor = (options) => {
78
79
  const requested = options?.destination;
@@ -376,5 +377,4 @@ var ToolInstaller = class ToolInstaller extends Context.Service()("@effected/git
376
377
  static layerTest = (overrides = {}) => Layer.succeed(ToolInstaller, ToolInstaller.makeTest(overrides));
377
378
  };
378
379
 
379
- //#endregion
380
380
  export { ToolInstaller, ToolInstallerError };