@effected/github-actions 0.24.0 → 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,3 +1,4 @@
1
+ import { decodeJson } from "./internal/json.js";
1
2
  import { Context, Effect, FileSystem, Layer, Option, Schema } from "effect";
2
3
 
3
4
  // src/ActionEnvironment.ts
@@ -176,14 +177,11 @@ const make = (base) => Effect.gen(function* () {
176
177
  name: "GITHUB_EVENT_PATH",
177
178
  detail: `could not read "${path}": ${String(cause)}`
178
179
  })));
179
- return yield* Effect.try({
180
- try: () => JSON.parse(raw),
181
- catch: (cause) => new ActionEnvironmentError({
182
- reason: "malformed",
183
- name: "GITHUB_EVENT_PATH",
184
- detail: `not valid JSON: ${String(cause)}`
185
- })
186
- });
180
+ return yield* decodeJson(raw).pipe(Effect.mapError((cause) => new ActionEnvironmentError({
181
+ reason: "malformed",
182
+ name: "GITHUB_EVENT_PATH",
183
+ detail: `not valid JSON: ${cause.message}`
184
+ })));
187
185
  }),
188
186
  withEnv: (overrides, effect) => Effect.flatMap(EnvOverrides, (current) => Effect.provideService(effect, EnvOverrides, {
189
187
  ...current,
package/ActionInput.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { decodeJson } from "./internal/json.js";
1
2
  import { Config, ConfigProvider, Context, Effect, Schema, SchemaIssue } from "effect";
2
3
 
3
4
  // src/ActionInput.ts
@@ -29,7 +30,17 @@ const inputVariable = (name) => `INPUT_${name.replaceAll(" ", "_").toUpperCase()
29
30
  * mutations the author meant to rehearse. `actual` carries the offending value
30
31
  * so the rendered error names it.
31
32
  */
32
- const configError = (message, actual) => new Config.ConfigError(new Schema.SchemaError(new SchemaIssue.InvalidValue({ message }, actual)));
33
+ /**
34
+ * A `ConfigError` carrying `message` as its first issue and, when a decode
35
+ * failed, that decode's own issue beside it, so the cause is structured
36
+ * rather than flattened into the message.
37
+ */
38
+ const configError = (message, actual, cause) => {
39
+ const stated = new SchemaIssue.InvalidValue({ message }, actual);
40
+ return new Config.ConfigError(new Schema.SchemaError(cause === void 0 ? stated : new SchemaIssue.Composite(Schema.Unknown.ast, [stated, cause.issue], actual)));
41
+ };
42
+ /** The JSON-array form of {@link ActionInput.list}. */
43
+ const decodeStringArray = Schema.decodeUnknownEffect(Schema.Array(Schema.String));
33
44
  /** YAML 1.2 core-schema booleans, which is what the runner documents. */
34
45
  const TRUE = /* @__PURE__ */ new Set([
35
46
  "true",
@@ -267,13 +278,7 @@ var ActionInput = class ActionInput {
267
278
  return Config.String(inputVariable(name)).pipe(Config.mapEffect((raw) => {
268
279
  const trimmed = raw.trim();
269
280
  if (trimmed === "") return Effect.succeed([]);
270
- if (trimmed.startsWith("[")) try {
271
- const parsed = JSON.parse(trimmed);
272
- if (Array.isArray(parsed) && parsed.every((item) => typeof item === "string")) return Effect.succeed(parsed);
273
- return Effect.fail(configError(`Input "${name}" is a JSON array but not an array of strings`, raw));
274
- } catch {
275
- return Effect.fail(configError(`Input "${name}" looks like JSON but could not be parsed`, raw));
276
- }
281
+ if (trimmed.startsWith("[")) return decodeJson(trimmed).pipe(Effect.mapError((cause) => configError(`Input "${name}" looks like JSON but could not be parsed`, raw, cause)), Effect.flatMap((parsed) => decodeStringArray(parsed).pipe(Effect.mapError((cause) => configError(`Input "${name}" is a JSON array but not an array of strings`, raw, cause)))));
277
282
  const entries = trimmed.split(/[\n,]/).map((item) => item.trim()).filter((item) => !item.startsWith("#")).map((item) => item.startsWith("- ") || item.startsWith("* ") ? item.slice(2).trim() : item).filter((item) => item !== "");
278
283
  return Effect.succeed(entries);
279
284
  }));
@@ -332,15 +337,7 @@ var ActionInput = class ActionInput {
332
337
  * which is exactly the missing-versus-malformed split above.
333
338
  */
334
339
  static schema(name, schema) {
335
- return Config.String(inputVariable(name)).pipe(Config.mapEffect((raw) => {
336
- let parsed;
337
- try {
338
- parsed = JSON.parse(raw);
339
- } catch {
340
- return Effect.fail(configError(`Input "${name}" is not valid JSON`, raw));
341
- }
342
- return Schema.decodeUnknownEffect(schema)(parsed).pipe(Effect.mapError(() => configError(`Input "${name}" did not satisfy its schema`, parsed)));
343
- }));
340
+ return Config.String(inputVariable(name)).pipe(Config.mapEffect((raw) => decodeJson(raw).pipe(Effect.mapError((cause) => configError(`Input "${name}" is not valid JSON`, raw, cause)), Effect.flatMap((parsed) => Schema.decodeUnknownEffect(schema)(parsed).pipe(Effect.mapError((cause) => configError(`Input "${name}" did not satisfy its schema`, parsed, cause)))))));
344
341
  }
345
342
  /**
346
343
  * A `ConfigProvider` over a record of runner variables **or input names**.
@@ -1,16 +1,16 @@
1
1
  import { OidcTokenIssuer } from "./OidcTokenIssuer.js";
2
2
  import { Effect, Layer } from "effect";
3
- import { IdentityToken, IdentityTokenError } from "@effected/sbom";
3
+ import { IdentityToken, IdentityTokenError } from "@effected/oidc";
4
4
 
5
5
  // src/ActionsIdentityToken.ts
6
6
  /**
7
- * Serves `@effected/sbom`'s `IdentityToken` contract from the runner's own OIDC
7
+ * Serves `@effected/oidc`'s `IdentityToken` contract from the runner's own OIDC
8
8
  * token service, so signing an SBOM inside a workflow needs no further wiring.
9
9
  *
10
10
  * @remarks
11
- * `@effected/sbom` declares `IdentityToken` — one method, one audience,
12
- * one redacted token — precisely so that signing does not drag the Actions
13
- * runtime into every consumer that only wants to emit an SBOM. This is the
11
+ * `@effected/oidc` declares `IdentityToken` — one method, one audience,
12
+ * one redacted token — precisely so that a consumer of a token, such as
13
+ * `@effected/sbom`'s signer, does not drag the Actions runtime in. This is the
14
14
  * layer that closes the inversion from the other side: it serves that contract
15
15
  * from the runner's own OIDC token service ({@link OidcTokenIssuer}), the same
16
16
  * arrangement as `@effected/workspaces` implementing `@effected/commands`'
@@ -20,12 +20,14 @@ const DEFAULT_SERVER_URL = "https://github.com";
20
20
  *
21
21
  * @remarks
22
22
  * `SlsaProvenance.forGitHubWorkflow` is total and does the real work, but it
23
- * takes a camelCase input record — and the only input anyone inside a workflow
24
- * actually holds is the runner's own OIDC claims, spelled in GitHub's
25
- * snake_case claim names. The rename between the two is eleven all-string
26
- * fields, so transposing `repository_id` and `repository_owner_id` compiles,
27
- * passes every type check, and produces a validly signed attestation asserting
28
- * the **wrong** provenance. This module owns that rename once, on the same
23
+ * takes an all-string input record — and the only input anyone inside a
24
+ * workflow actually holds is the runner's own OIDC claims, decoded as
25
+ * `@effected/github`'s `ActionsOidcClaims` with integer ids. The projection
26
+ * between the two is eleven close-cousin fields, so transposing `repositoryId`
27
+ * and `repositoryOwnerId` compiles, passes every type check, and produces a
28
+ * validly signed attestation asserting the **wrong** provenance. This module
29
+ * owns that projection once, rendering the ids back to the decimal strings the
30
+ * token carried, on the same
29
31
  * `github-actions` ↔ `sbom` seam as {@link ActionsIdentityToken}: `sbom` must
30
32
  * not depend on the Actions runtime, so the adapter lives here, beside the
31
33
  * services it reads.
@@ -55,7 +57,7 @@ var ActionsProvenance = class {
55
57
  *
56
58
  * @remarks
57
59
  * Every field `GitHubWorkflowProvenance` declares is populated from
58
- * {@link OidcClaims}, except `serverUrl`, which is read from
60
+ * the runner's `ActionsOidcClaims`, except `serverUrl`, which is read from
59
61
  * `GITHUB_SERVER_URL` through {@link ActionEnvironment}'s `getOptional`
60
62
  * with `https://github.com` as the default — **not** through the
61
63
  * `GitHubContext` projection. The projection fails typed when a `GITHUB_*`
@@ -88,14 +90,14 @@ var ActionsProvenance = class {
88
90
  repository: claims.repository,
89
91
  ref: claims.ref,
90
92
  sha: claims.sha,
91
- eventName: claims.event_name,
92
- workflowRef: claims.workflow_ref,
93
- jobWorkflowRef: claims.job_workflow_ref,
94
- repositoryId: claims.repository_id,
95
- repositoryOwnerId: claims.repository_owner_id,
96
- runnerEnvironment: claims.runner_environment,
97
- runId: claims.run_id,
98
- runAttempt: claims.run_attempt
93
+ eventName: claims.eventName,
94
+ workflowRef: claims.workflowRef,
95
+ jobWorkflowRef: claims.jobWorkflowRef,
96
+ repositoryId: String(claims.repositoryId),
97
+ repositoryOwnerId: String(claims.repositoryOwnerId),
98
+ runnerEnvironment: claims.runnerEnvironment,
99
+ runId: String(claims.runId),
100
+ runAttempt: String(claims.runAttempt)
99
101
  });
100
102
  });
101
103
  };
package/Artifact.js CHANGED
@@ -7,7 +7,7 @@ import { digestFileHex } from "./internal/digest.js";
7
7
  import { isWindowsRunner } from "./internal/runner.js";
8
8
  import { spawnOnce } from "./internal/spawn.js";
9
9
  import { unzipCommand, zipCommand, zipManifest } from "./internal/archiveCommands.js";
10
- import { Context, Effect, FileSystem, Layer, Option, Path, Result, Schema } from "effect";
10
+ import { Context, DateTime, Duration, Effect, FileSystem, Layer, Option, Path, Result, Schema } from "effect";
11
11
  import { HttpClient } from "effect/http";
12
12
  import { BlobClient, BlockBlobClient } from "@azure/storage-blob";
13
13
  import { ChildProcessSpawner } from "effect/process";
@@ -263,11 +263,12 @@ const make = (transfer) => Effect.gen(function* () {
263
263
  cause
264
264
  })));
265
265
  const digest = yield* digestOf(packed, name);
266
+ const expiresAt = options?.retentionDays === void 0 ? void 0 : DateTime.formatIso(DateTime.addDuration(yield* DateTime.now, Duration.days(options.retentionDays)));
266
267
  const finalized = yield* call("FinalizeArtifact", () => ({
267
268
  name,
268
269
  size: String(size.size),
269
270
  hash: `sha256:${digest}`,
270
- ...options?.retentionDays === void 0 ? {} : { expiresAt: new Date(Date.now() + options.retentionDays * 864e5).toISOString() }
271
+ ...expiresAt === void 0 ? {} : { expiresAt }
271
272
  }), name);
272
273
  if (finalized === CONFLICT || !isOk(finalized)) return yield* Effect.fail(new ArtifactError({
273
274
  reason: "refused",
package/BlobStore.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import { unstubbed } from "./internal/unstubbed.js";
2
2
  import { BlobEnvelope } from "./BlobEnvelope.js";
3
- import { sign } from "./internal/sigv4.js";
4
3
  import { Secret } from "./Secret.js";
5
- import { Context, Effect, Layer, Option, Schema } from "effect";
4
+ import { Context, Crypto, DateTime, Effect, Layer, Option, Schema } from "effect";
6
5
  import { HttpClient, HttpClientRequest } from "effect/http";
6
+ import { SigV4 } from "@effected/crypto";
7
7
 
8
8
  // src/BlobStore.ts
9
9
  /**
@@ -15,12 +15,17 @@ var BlobStoreError = class extends Schema.TaggedError()("BlobStoreError", {
15
15
  /**
16
16
  * `unreachable` — the store could not be contacted. `refused` — it answered,
17
17
  * unhappily; `status` says how. `misconfigured` — the layer was built with
18
- * settings the store cannot use.
18
+ * settings the store cannot use, or a request could not be signed with them
19
+ * (no WebCrypto in the runtime, or key material it refuses). `invalidKey` —
20
+ * the caller's key cannot be stored at all, whatever the settings: on S3, a
21
+ * key with a `.` or `..` path segment, which no URL carries as signed.
22
+ * Nothing is sent.
19
23
  */
20
24
  reason: Schema.Literals([
21
25
  "unreachable",
22
26
  "refused",
23
- "misconfigured"
27
+ "misconfigured",
28
+ "invalidKey"
24
29
  ]),
25
30
  /** The key involved. A stable identifier, never a value. */
26
31
  key: Schema.optionalKey(Schema.String),
@@ -35,6 +40,7 @@ var BlobStoreError = class extends Schema.TaggedError()("BlobStoreError", {
35
40
  switch (this.reason) {
36
41
  case "unreachable": return `The blob store could not be reached${this.key === void 0 ? "" : ` for "${this.key}"`}`;
37
42
  case "refused": return `The blob store refused "${this.key}"${this.status === void 0 ? "" : ` with status ${this.status}`}${this.detail === void 0 ? "" : `: ${this.detail}`}`;
43
+ case "invalidKey": return `The blob store cannot hold the key "${this.key}"${this.detail === void 0 ? "" : `: ${this.detail}`}`;
38
44
  default: return `The blob store is misconfigured${this.detail === void 0 ? "" : `: ${this.detail}`}`;
39
45
  }
40
46
  }
@@ -75,10 +81,17 @@ var BlobStore = class BlobStore extends Context.Service()("@effected/github-acti
75
81
  * An S3-compatible backend, signed with SigV4.
76
82
  *
77
83
  * @remarks
78
- * No `@aws-sdk/*` dependency: signing is a specified algorithm over strings
79
- * and HMACs, and this package already has `node:crypto`. The SDK's weight is
80
- * in credential management, retries and a service catalogue none of which
81
- * this needs.
84
+ * No `@aws-sdk/*` dependency: signing is `@effected/crypto`'s `SigV4`, a
85
+ * specified algorithm over strings and HMACs. The SDK's weight is in
86
+ * credential management, retries and a service catalogue none of which this
87
+ * needs.
88
+ *
89
+ * Requires core `Crypto` for the SHA-256 digests signing takes, resolved
90
+ * once when the layer is built; `NodeServices.layer` supplies it. A request
91
+ * that cannot be signed fails as {@link BlobStoreError} `misconfigured`. A
92
+ * key with a `.` or `..` path segment fails as `invalidKey` before anything
93
+ * is signed or sent: every URL parser collapses such a segment, so the store
94
+ * would receive another path than the one signed.
82
95
  *
83
96
  * A parameterized layer factory mints a fresh layer per call and layers
84
97
  * memoize by reference — bind it to a `const` rather than calling it at each
@@ -119,43 +132,60 @@ var BlobStore = class BlobStore extends Context.Service()("@effected/github-acti
119
132
  };
120
133
  const dies = unstubbed("BlobStore.makeTest");
121
134
  const makeS3 = (config) => Effect.gen(function* () {
122
- const http = yield* HttpClient.HttpClient;
135
+ const http = HttpClient.transform(yield* HttpClient.HttpClient, (effect) => Effect.provideService(effect, HttpClient.TracerPropagationEnabled, false));
136
+ const crypto = yield* Crypto.Crypto;
123
137
  let endpoint = config.endpoint ?? `https://s3.${config.region}.amazonaws.com`;
124
138
  while (endpoint.endsWith("/")) endpoint = endpoint.slice(0, -1);
125
139
  const host = new URL(endpoint).host;
126
- const secretAccessKey = yield* Secret.forSigning(config.secretAccessKey);
127
- const sessionToken = config.sessionToken === void 0 ? void 0 : yield* Secret.forSigning(config.sessionToken);
140
+ yield* Secret.mask(config.secretAccessKey);
141
+ if (config.sessionToken !== void 0) yield* Secret.mask(config.sessionToken);
142
+ const credentials = {
143
+ accessKeyId: config.accessKeyId,
144
+ secretAccessKey: config.secretAccessKey,
145
+ ...config.sessionToken === void 0 ? {} : { sessionToken: config.sessionToken },
146
+ region: config.region,
147
+ service: "s3"
148
+ };
128
149
  const objectPath = (key) => [
129
150
  config.bucket,
130
151
  ...config.prefix === void 0 ? [] : [config.prefix],
131
152
  key
132
153
  ].join("/").replaceAll(/\/{2,}/g, "/");
133
- const request = (method, key, body) => {
154
+ const unsignable = (key, cause) => new BlobStoreError({
155
+ reason: "misconfigured",
156
+ key,
157
+ detail: "the request could not be signed",
158
+ cause
159
+ });
160
+ const request = (method, key, body) => Effect.gen(function* () {
134
161
  const path = objectPath(key);
135
- const headers = sign({
162
+ const now = yield* DateTime.now;
163
+ const signed = yield* SigV4.sign({
136
164
  method,
137
165
  path,
138
166
  host,
139
- headers: {},
140
167
  body,
141
- now: /* @__PURE__ */ new Date()
142
- }, {
143
- accessKeyId: config.accessKeyId,
144
- secretAccessKey,
145
- ...sessionToken === void 0 ? {} : { sessionToken },
146
- region: config.region,
147
- service: "s3"
148
- });
168
+ now
169
+ }, credentials).pipe(Effect.provideService(Crypto.Crypto, crypto), Effect.catchTags({
170
+ SigV4PathError: (cause) => Effect.fail(new BlobStoreError({
171
+ reason: "invalidKey",
172
+ key,
173
+ detail: `the path has a "${cause.segment}" segment, which a URL collapses, so it cannot be sent as signed`,
174
+ cause
175
+ })),
176
+ CryptoError: (cause) => Effect.fail(unsignable(key, cause)),
177
+ PlatformError: (cause) => Effect.fail(unsignable(key, cause))
178
+ }));
149
179
  return {
150
- url: `${endpoint}/${path}`,
151
- headers
180
+ url: `${endpoint}${signed.path}${signed.query === "" ? "" : `?${signed.query}`}`,
181
+ headers: signed.headers
152
182
  };
153
- };
154
- const send = (method, key, body) => Effect.suspend(() => {
155
- const { url, headers } = request(method, key, body);
183
+ });
184
+ const send = (method, key, body) => Effect.gen(function* () {
185
+ const { url, headers } = yield* request(method, key, body);
156
186
  const base = HttpClientRequest.make(method)(url, { headers });
157
187
  const built = body.length === 0 ? base : HttpClientRequest.bodyUint8Array(base, body);
158
- return http.execute(built).pipe(Effect.mapError((cause) => new BlobStoreError({
188
+ return yield* http.execute(built).pipe(Effect.mapError((cause) => new BlobStoreError({
159
189
  reason: "unreachable",
160
190
  key,
161
191
  cause
@@ -1,8 +1,10 @@
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
10
  // src/OidcTokenIssuer.ts
@@ -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,4 +189,4 @@ var OidcTokenIssuer = class OidcTokenIssuer extends Context.Service()("@effected
218
189
  });
219
190
  };
220
191
 
221
- 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";
@@ -12,6 +12,17 @@ import { CorepackIntegrityHash, DEFAULT_REGISTRY, PackageManagerPin, PackageMana
12
12
 
13
13
  // src/PackageManagerInstaller.ts
14
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) })) })));
25
+ /**
15
26
  * Raised when a package manager cannot be provisioned on the runner.
16
27
  *
17
28
  * @public
@@ -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
  /**
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
@@ -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
  */
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";
@@ -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;
package/index.d.ts CHANGED
@@ -1,10 +1,11 @@
1
1
  import { AnnotationProperties, AnnotationProperties as AnnotationProperties$1, WorkflowCommand } from "@effected/github-commands";
2
2
  import { NodeServices } from "@effect/platform-node";
3
- import { Cause, Config, ConfigProvider, Context, Duration, Effect, FileSystem, Layer, Logger, Option, Path, Redacted, Result, Schema } from "effect";
3
+ import { Cause, Config, ConfigProvider, Context, Crypto, Duration, Effect, FileSystem, Layer, Logger, Option, Path, Redacted, Result, Schema } from "effect";
4
4
  import { HttpClient } from "effect/http";
5
5
  import { ChildProcessSpawner } from "effect/process";
6
- import { IdentityToken, SlsaProvenance } from "@effected/sbom";
7
- import { BotIdentity, GitHubApp, GitHubClient, InstallationToken, PermissionLevel, RetryPolicy } from "@effected/github";
6
+ import { IdentityToken } from "@effected/oidc";
7
+ import { ActionsOidcClaims, BotIdentity, GitHubApp, GitHubClient, InstallationToken, PermissionLevel, RetryPolicy } from "@effected/github";
8
+ import { SlsaProvenance } from "@effected/sbom";
8
9
  import { IntegrityHashBrand, PackageManagerPin } from "@effected/npm";
9
10
  declare const ActionEnvironmentError_base: Schema.Class<ActionEnvironmentError, Schema.TaggedStruct<"ActionEnvironmentError", {
10
11
  /** `missing` — absent or empty; `malformed` — present but unusable. */
@@ -1953,45 +1954,6 @@ declare const OidcTokenError_base: Schema.Class<OidcTokenError, Schema.TaggedStr
1953
1954
  export declare class OidcTokenError extends OidcTokenError_base {
1954
1955
  get message(): string;
1955
1956
  }
1956
- declare const OidcClaims_base: Schema.Class<OidcClaims, Schema.Struct<{
1957
- /** The issuer, e.g. `https://token.actions.githubusercontent.com`. */
1958
- readonly iss: Schema.String;
1959
- /** The full ref the workflow ran on. */
1960
- readonly ref: Schema.String;
1961
- /** The commit. */
1962
- readonly sha: Schema.String;
1963
- /** `owner/repo`. */
1964
- readonly repository: Schema.String;
1965
- /** The event that triggered the run. */
1966
- readonly event_name: Schema.String;
1967
- /** The reusable-workflow ref, which is what a verifier pins against. */
1968
- readonly job_workflow_ref: Schema.String;
1969
- /** The calling workflow's ref. */
1970
- readonly workflow_ref: Schema.String;
1971
- /** The numeric repository id, as a string. */
1972
- readonly repository_id: Schema.String;
1973
- /** The numeric owner id, as a string. */
1974
- readonly repository_owner_id: Schema.String;
1975
- /** `github-hosted` or `self-hosted`. */
1976
- readonly runner_environment: Schema.String;
1977
- /** The run id, as a string. */
1978
- readonly run_id: Schema.String;
1979
- /** The run attempt, as a string. */
1980
- readonly run_attempt: Schema.String;
1981
- }>, {}>;
1982
- /**
1983
- * The claims a GitHub Actions OIDC token carries about the workflow that ran.
1984
- *
1985
- * @remarks
1986
- * The field names are GitHub's own JWT claim names, kept verbatim rather than
1987
- * translated to the kit's casing. They are an **external vocabulary** — the
1988
- * same strings that appear in a SLSA provenance predicate and in every
1989
- * published example of an Actions OIDC policy — and renaming them would put a
1990
- * translation layer between a consumer and the specification they are reading.
1991
- *
1992
- * @public
1993
- */
1994
- export declare class OidcClaims extends OidcClaims_base {}
1995
1957
  /**
1996
1958
  * The members of the {@link OidcTokenIssuer} service: request an ID `token`
1997
1959
  * and read its decoded `claims`, both failing with {@link OidcTokenError}.
@@ -2009,7 +1971,8 @@ interface OidcTokenIssuerShape {
2009
1971
  */
2010
1972
  readonly token: (audience?: string) => Effect.Effect<Redacted.Redacted<string>, OidcTokenError>;
2011
1973
  /**
2012
- * The token's claims, decoded.
1974
+ * The token's claims, decoded into `@effected/github`'s `ActionsOidcClaims`,
1975
+ * the same class `ActionsOidc.verify` returns.
2013
1976
  *
2014
1977
  * @remarks
2015
1978
  * **The signature is deliberately not verified**, for three reasons that are
@@ -2034,7 +1997,7 @@ interface OidcTokenIssuerShape {
2034
1997
  * a test cannot silently skip the path under test because the double
2035
1998
  * returned a token that is not a JWT.
2036
1999
  */
2037
- readonly claims: (audience?: string) => Effect.Effect<OidcClaims, OidcTokenError>;
2000
+ readonly claims: (audience?: string) => Effect.Effect<ActionsOidcClaims, OidcTokenError>;
2038
2001
  }
2039
2002
  declare const OidcTokenIssuer_base: Context.ServiceClass<OidcTokenIssuer, "@effected/github-actions/OidcTokenIssuer", OidcTokenIssuerShape>;
2040
2003
  /**
@@ -2054,7 +2017,7 @@ declare const OidcTokenIssuer_base: Context.ServiceClass<OidcTokenIssuer, "@effe
2054
2017
  * const program = Effect.gen(function* () {
2055
2018
  * const issuer = yield* OidcTokenIssuer;
2056
2019
  * const claims = yield* issuer.claims("sigstore");
2057
- * return claims.job_workflow_ref;
2020
+ * return claims.jobWorkflowRef;
2058
2021
  * });
2059
2022
  * ```
2060
2023
  *
@@ -2078,7 +2041,7 @@ export declare class OidcTokenIssuer extends OidcTokenIssuer_base {
2078
2041
  * double returning a synthetic non-JWT would make the provenance path
2079
2042
  * structurally unreachable in a test.
2080
2043
  */
2081
- static readonly unsignedTokenFor: (claims: OidcClaims) => Redacted.Redacted<string>;
2044
+ static readonly unsignedTokenFor: (claims: ActionsOidcClaims) => Redacted.Redacted<string>;
2082
2045
  /** A test double. Unstubbed members die rather than answering with a non-token. */
2083
2046
  static readonly makeTest: (overrides?: Partial<OidcTokenIssuerShape>) => OidcTokenIssuerShape;
2084
2047
  /** {@link OidcTokenIssuer.makeTest} behind `Layer.succeed`. */
@@ -2093,16 +2056,16 @@ export declare class OidcTokenIssuer extends OidcTokenIssuer_base {
2093
2056
  * members can disagree is how a test proves a path works while production
2094
2057
  * takes the other one.
2095
2058
  */
2096
- static readonly layerFor: (claims: OidcClaims) => Layer.Layer<OidcTokenIssuer>;
2059
+ static readonly layerFor: (claims: ActionsOidcClaims) => Layer.Layer<OidcTokenIssuer>;
2097
2060
  }
2098
2061
  /**
2099
- * Serves `@effected/sbom`'s `IdentityToken` contract from the runner's own OIDC
2062
+ * Serves `@effected/oidc`'s `IdentityToken` contract from the runner's own OIDC
2100
2063
  * token service, so signing an SBOM inside a workflow needs no further wiring.
2101
2064
  *
2102
2065
  * @remarks
2103
- * `@effected/sbom` declares `IdentityToken` — one method, one audience,
2104
- * one redacted token — precisely so that signing does not drag the Actions
2105
- * runtime into every consumer that only wants to emit an SBOM. This is the
2066
+ * `@effected/oidc` declares `IdentityToken` — one method, one audience,
2067
+ * one redacted token — precisely so that a consumer of a token, such as
2068
+ * `@effected/sbom`'s signer, does not drag the Actions runtime in. This is the
2106
2069
  * layer that closes the inversion from the other side: it serves that contract
2107
2070
  * from the runner's own OIDC token service ({@link OidcTokenIssuer}), the same
2108
2071
  * arrangement as `@effected/workspaces` implementing `@effected/commands`'
@@ -2152,12 +2115,14 @@ export declare class ActionsIdentityToken {
2152
2115
  *
2153
2116
  * @remarks
2154
2117
  * `SlsaProvenance.forGitHubWorkflow` is total and does the real work, but it
2155
- * takes a camelCase input record — and the only input anyone inside a workflow
2156
- * actually holds is the runner's own OIDC claims, spelled in GitHub's
2157
- * snake_case claim names. The rename between the two is eleven all-string
2158
- * fields, so transposing `repository_id` and `repository_owner_id` compiles,
2159
- * passes every type check, and produces a validly signed attestation asserting
2160
- * the **wrong** provenance. This module owns that rename once, on the same
2118
+ * takes an all-string input record — and the only input anyone inside a
2119
+ * workflow actually holds is the runner's own OIDC claims, decoded as
2120
+ * `@effected/github`'s `ActionsOidcClaims` with integer ids. The projection
2121
+ * between the two is eleven close-cousin fields, so transposing `repositoryId`
2122
+ * and `repositoryOwnerId` compiles, passes every type check, and produces a
2123
+ * validly signed attestation asserting the **wrong** provenance. This module
2124
+ * owns that projection once, rendering the ids back to the decimal strings the
2125
+ * token carried, on the same
2161
2126
  * `github-actions` ↔ `sbom` seam as {@link ActionsIdentityToken}: `sbom` must
2162
2127
  * not depend on the Actions runtime, so the adapter lives here, beside the
2163
2128
  * services it reads.
@@ -2187,7 +2152,7 @@ export declare class ActionsProvenance {
2187
2152
  *
2188
2153
  * @remarks
2189
2154
  * Every field `GitHubWorkflowProvenance` declares is populated from
2190
- * {@link OidcClaims}, except `serverUrl`, which is read from
2155
+ * the runner's `ActionsOidcClaims`, except `serverUrl`, which is read from
2191
2156
  * `GITHUB_SERVER_URL` through {@link ActionEnvironment}'s `getOptional`
2192
2157
  * with `https://github.com` as the default — **not** through the
2193
2158
  * `GitHubContext` projection. The projection fails typed when a `GITHUB_*`
@@ -2548,9 +2513,13 @@ declare const BlobStoreError_base: Schema.Class<BlobStoreError, Schema.TaggedStr
2548
2513
  /**
2549
2514
  * `unreachable` — the store could not be contacted. `refused` — it answered,
2550
2515
  * unhappily; `status` says how. `misconfigured` — the layer was built with
2551
- * settings the store cannot use.
2516
+ * settings the store cannot use, or a request could not be signed with them
2517
+ * (no WebCrypto in the runtime, or key material it refuses). `invalidKey` —
2518
+ * the caller's key cannot be stored at all, whatever the settings: on S3, a
2519
+ * key with a `.` or `..` path segment, which no URL carries as signed.
2520
+ * Nothing is sent.
2552
2521
  */
2553
- readonly reason: Schema.Literals<readonly ["unreachable", "refused", "misconfigured"]>;
2522
+ readonly reason: Schema.Literals<readonly ["unreachable", "refused", "misconfigured", "invalidKey"]>;
2554
2523
  /** The key involved. A stable identifier, never a value. */
2555
2524
  readonly key: Schema.optionalKey<Schema.String>;
2556
2525
  /** The HTTP status, when the store answered. */
@@ -2665,16 +2634,23 @@ export declare class BlobStore extends BlobStore_base {
2665
2634
  * An S3-compatible backend, signed with SigV4.
2666
2635
  *
2667
2636
  * @remarks
2668
- * No `@aws-sdk/*` dependency: signing is a specified algorithm over strings
2669
- * and HMACs, and this package already has `node:crypto`. The SDK's weight is
2670
- * in credential management, retries and a service catalogue none of which
2671
- * this needs.
2637
+ * No `@aws-sdk/*` dependency: signing is `@effected/crypto`'s `SigV4`, a
2638
+ * specified algorithm over strings and HMACs. The SDK's weight is in
2639
+ * credential management, retries and a service catalogue none of which this
2640
+ * needs.
2641
+ *
2642
+ * Requires core `Crypto` for the SHA-256 digests signing takes, resolved
2643
+ * once when the layer is built; `NodeServices.layer` supplies it. A request
2644
+ * that cannot be signed fails as {@link BlobStoreError} `misconfigured`. A
2645
+ * key with a `.` or `..` path segment fails as `invalidKey` before anything
2646
+ * is signed or sent: every URL parser collapses such a segment, so the store
2647
+ * would receive another path than the one signed.
2672
2648
  *
2673
2649
  * A parameterized layer factory mints a fresh layer per call and layers
2674
2650
  * memoize by reference — bind it to a `const` rather than calling it at each
2675
2651
  * composition site.
2676
2652
  */
2677
- static readonly layerS3: (config: S3Config) => Layer.Layer<BlobStore, never, HttpClient.HttpClient | ActionOutputs>;
2653
+ static readonly layerS3: (config: S3Config) => Layer.Layer<BlobStore, never, HttpClient.HttpClient | ActionOutputs | Crypto.Crypto>;
2678
2654
  /** A test double. Unstubbed members die rather than reporting a miss. */
2679
2655
  static readonly makeTest: (overrides?: Partial<BlobStoreShape>) => BlobStoreShape;
2680
2656
  /** {@link BlobStore.makeTest} behind `Layer.succeed`. */
@@ -4988,9 +4964,13 @@ export declare class Secret {
4988
4964
  * Declassify one secret for an in-process use that needs the raw bytes.
4989
4965
  *
4990
4966
  * @remarks
4991
- * Request signing is the case this exists for: an HMAC cannot be computed
4992
- * over a `Redacted`, so a signing key has to become a string somewhere, and
4993
- * the point of this module is that "somewhere" is here and nowhere else.
4967
+ * For a consumer that must hand the raw key to code taking a plain string,
4968
+ * such as a third-party signing library. A signer that takes a `Redacted`
4969
+ * needs no declassification at all: `@effected/crypto`'s `SigV4` and `Hmac`
4970
+ * unwrap the key only where they compute, so a caller of those (this
4971
+ * package's own S3 `BlobStore` is one) registers the value with
4972
+ * {@link Secret.mask} and passes the `Redacted` on. No module in this
4973
+ * package calls this member any more.
4994
4974
  *
4995
4975
  * Signing named the member; it does not gate it. **Any in-process use that
4996
4976
  * needs the raw value without writing it to a runner file is this
@@ -5015,9 +4995,9 @@ export declare class Secret {
5015
4995
  * `::add-mask::` command only masks when the runner parses this process's
5016
4996
  * stdout; a detached worker's stdout is a log file no runner parses, so
5017
4997
  * there the command masks nothing *and* spells the plaintext into the log.
5018
- * A worker that signs (an S3-style `BlobStore` is the canonical case) must compose
5019
- * `ActionOutputs.layerDetached`, under which this member still returns the
5020
- * raw key for the HMAC but the mask is a documented no-op; the masking
4998
+ * A worker that signs must compose `ActionOutputs.layerDetached`, under
4999
+ * which this member still returns the raw key but the mask is a
5000
+ * documented no-op; the masking
5021
5001
  * itself is the **parent's** job, done before the spawn via
5022
5002
  * {@link Secret.forChildEnv} under the real layer.
5023
5003
  */
package/index.js CHANGED
@@ -6,7 +6,7 @@ import { ActionState, ActionStateError } from "./ActionState.js";
6
6
  import { Action, ActionRuntime, describeCause } from "./Action.js";
7
7
  import { BlobTransferError } from "./BlobTransfer.js";
8
8
  import { ActionCache, ActionCacheError } from "./ActionCache.js";
9
- import { OidcClaims, OidcTokenError, OidcTokenIssuer } from "./OidcTokenIssuer.js";
9
+ import { OidcTokenError, OidcTokenIssuer } from "./OidcTokenIssuer.js";
10
10
  import { ActionsIdentityToken } from "./ActionsIdentityToken.js";
11
11
  import { ActionsProvenance } from "./ActionsProvenance.js";
12
12
  import { Artifact, ArtifactError } from "./Artifact.js";
@@ -27,4 +27,4 @@ import { ToolInstaller, ToolInstallerError } from "./ToolInstaller.js";
27
27
  import { AmbientPackageManager, CachedPackageManager, InstalledPackageManager, PackageManagerInstaller, PackageManagerInstallerError } from "./PackageManagerInstaller.js";
28
28
  import { WorkflowCommand } from "@effected/github-commands";
29
29
 
30
- export { Action, ActionCache, ActionCacheError, ActionEnvironment, ActionEnvironmentError, ActionInput, ActionLogger, ActionOutputs, ActionRuntime, ActionState, ActionStateError, ActionsIdentityToken, ActionsProvenance, AmbientPackageManager, Artifact, ArtifactError, BlobEnvelope, BlobMetadataDecodeError, BlobMetadataEncodeError, BlobStore, BlobStoreError, BlobTransferError, CacheKey, CacheKeyBadPatternError, CacheKeyReadError, CachedPackageManager, CheckDocument, CheckDocumentError, CheckDocumentStamp, CheckReport, CheckState, ChildEnv, DetachedLogUnavailableError, DetachedNotReadyError, DetachedOutputError, DetachedProcess, DetachedSignalFailedError, DetachedSpawnFailedError, DryRun, GitHubCacheBlobStore, GitHubContext, GitHubMarkdown, GitHubToken, GitHubTokenError, InstalledPackageManager, InvalidOutputNameError, InvalidPidError, ManagedDocument, ManagedDocumentError, NotABlobEnvelopeError, OidcClaims, OidcTokenError, OidcTokenIssuer, OutputEncodeError, PackageManagerInstaller, PackageManagerInstallerError, ProcessId, RecordedOutput, RunnerContext, RunnerFileUnavailableError, RunnerFileWriteError, Secret, ToolInstaller, ToolInstallerError, TruncatedBlobEnvelopeError, UnsupportedBlobEnvelopeVersionError, WorkflowCommand, describeCause, projectCheckState };
30
+ export { Action, ActionCache, ActionCacheError, ActionEnvironment, ActionEnvironmentError, ActionInput, ActionLogger, ActionOutputs, ActionRuntime, ActionState, ActionStateError, ActionsIdentityToken, ActionsProvenance, AmbientPackageManager, Artifact, ArtifactError, BlobEnvelope, BlobMetadataDecodeError, BlobMetadataEncodeError, BlobStore, BlobStoreError, BlobTransferError, CacheKey, CacheKeyBadPatternError, CacheKeyReadError, CachedPackageManager, CheckDocument, CheckDocumentError, CheckDocumentStamp, CheckReport, CheckState, ChildEnv, DetachedLogUnavailableError, DetachedNotReadyError, DetachedOutputError, DetachedProcess, DetachedSignalFailedError, DetachedSpawnFailedError, DryRun, GitHubCacheBlobStore, GitHubContext, GitHubMarkdown, GitHubToken, GitHubTokenError, InstalledPackageManager, InvalidOutputNameError, InvalidPidError, ManagedDocument, ManagedDocumentError, NotABlobEnvelopeError, OidcTokenError, OidcTokenIssuer, OutputEncodeError, PackageManagerInstaller, PackageManagerInstallerError, ProcessId, RecordedOutput, RunnerContext, RunnerFileUnavailableError, RunnerFileWriteError, Secret, ToolInstaller, ToolInstallerError, TruncatedBlobEnvelopeError, UnsupportedBlobEnvelopeVersionError, WorkflowCommand, describeCause, projectCheckState };
@@ -1,5 +1,5 @@
1
- import { payloadOf } from "./jwt.js";
2
1
  import { Effect, Option, Redacted, Result } from "effect";
2
+ import { Jws } from "@effected/jwt";
3
3
 
4
4
  // src/internal/actionsResults.ts
5
5
  /** The variable naming the backend. Absent outside a `uses:` step. */
@@ -26,9 +26,10 @@ const misconfiguredDetail = (variable, service) => `${variable} is not set — t
26
26
  * @internal
27
27
  */
28
28
  const backendIdsFrom = (token) => {
29
- const payload = payloadOf(token);
30
- if (Result.isFailure(payload)) return Result.fail(`the runtime token is not a readable JWT: ${payload.failure.detail}`);
31
- const scope = payload.success?.scp;
29
+ const decoded = Jws.decodeUnverified(token);
30
+ if (Result.isFailure(decoded)) return Result.fail(`the runtime token is not a readable JWT: ${decoded.failure.detail}`);
31
+ const payload = decoded.success.payload;
32
+ const scope = typeof payload === "object" && payload !== null ? payload.scp : void 0;
32
33
  if (typeof scope !== "string") return Result.fail("the runtime token carries no `scp` claim");
33
34
  for (const entry of scope.split(" ")) {
34
35
  const parts = entry.split(":");
@@ -0,0 +1,11 @@
1
+ import { Schema } from "effect";
2
+
3
+ // src/internal/json.ts
4
+ /**
5
+ * Parse JSON text to an unknown value, failing with a `SchemaError` on text that is not JSON.
6
+ *
7
+ * @internal
8
+ */
9
+ const decodeJson = Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Unknown));
10
+
11
+ export { decodeJson };
@@ -26,5 +26,25 @@ const isWindowsRunner = (env) => Effect.map(env.getOptional("RUNNER_OS"), (found
26
26
  * @internal
27
27
  */
28
28
  const toolCacheRoot = (env, path) => Effect.map(env.getOptional("RUNNER_TOOL_CACHE"), (found) => Option.getOrElse(found, () => path.join("/tmp", "runner-tool-cache")));
29
+ /** `RUNNER_ARCH`'s spellings (`X86`, `X64`, `ARM`, `ARM64`), lower-cased, in Node's `process.arch` spelling. */
30
+ const NODE_ARCH = /* @__PURE__ */ new Map([
31
+ ["x86", "ia32"],
32
+ ["x64", "x64"],
33
+ ["arm", "arm"],
34
+ ["arm64", "arm64"]
35
+ ]);
36
+ /** A `RUNNER_ARCH` value in Node's spelling; anything unrecognised passes through. */
37
+ const archFromRunner = (runnerArch) => NODE_ARCH.get(runnerArch.toLowerCase()) ?? runnerArch;
38
+ /**
39
+ * The host architecture in Node's spelling: `RUNNER_ARCH` mapped through
40
+ * `archFromRunner`, or `process.arch` off a runner, where the variable is
41
+ * absent and the host's own answer is the honest fallback.
42
+ *
43
+ * @internal
44
+ */
45
+ const runnerArch = (env) => Effect.map(env.getOptional("RUNNER_ARCH"), (found) => Option.match(found, {
46
+ onNone: () => process.arch,
47
+ onSome: archFromRunner
48
+ }));
29
49
 
30
- export { isWindowsRunner, toolCacheRoot };
50
+ export { isWindowsRunner, runnerArch, toolCacheRoot };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effected/github-actions",
3
- "version": "0.24.0",
3
+ "version": "0.25.0",
4
4
  "private": false,
5
5
  "description": "The GitHub Actions runtime: env, inputs, outputs, state, logging, cache, artifacts, blob storage, OIDC and the token bridge.",
6
6
  "keywords": [
@@ -39,12 +39,15 @@
39
39
  },
40
40
  "dependencies": {
41
41
  "@azure/storage-blob": "^12.33.0",
42
- "@effected/github": "^0.22.0",
42
+ "@effected/crypto": "^0.1.0",
43
+ "@effected/github": "^0.23.0",
43
44
  "@effected/github-commands": "^0.6.0",
44
45
  "@effected/glob": "^0.14.0",
46
+ "@effected/jwt": "^0.5.0",
45
47
  "@effected/markdown": "^0.20.0",
46
- "@effected/npm": "^0.24.0",
47
- "@effected/sbom": "^0.14.0",
48
+ "@effected/npm": "^0.25.0",
49
+ "@effected/oidc": "^0.1.0",
50
+ "@effected/sbom": "^0.15.0",
48
51
  "@effected/semver": "^0.15.0",
49
52
  "@effected/templates": "^0.14.0",
50
53
  "@effected/walker": "^0.19.0"
package/internal/jwt.js DELETED
@@ -1,41 +0,0 @@
1
- import { Result } from "effect";
2
- import * as Base64Url from "effect/encoding/Base64Url";
3
-
4
- // src/internal/jwt.ts
5
- /**
6
- * The decoded payload of `token` — the middle segment, base64url JSON —
7
- * without any check on the signature.
8
- *
9
- * @internal
10
- */
11
- const payloadOf = (token) => {
12
- const segments = token.split(".");
13
- const payload = segments[1];
14
- if (segments.length !== 3 || payload === void 0 || payload === "") return Result.fail({ detail: `expected three segments, got ${segments.length}` });
15
- const json = Base64Url.decodeString(payload);
16
- if (Result.isFailure(json)) return Result.fail({
17
- detail: "the payload is not base64url JSON",
18
- cause: json.failure
19
- });
20
- try {
21
- return Result.succeed(JSON.parse(json.success));
22
- } catch (cause) {
23
- return Result.fail({
24
- detail: "the payload is not base64url JSON",
25
- cause
26
- });
27
- }
28
- };
29
- /**
30
- * An **unsigned** JWT: `header.payload.unsigned`, each JSON segment
31
- * base64url-encoded. For building test doubles and nothing else — the
32
- * signature segment is a placeholder, so this token fails any verifier.
33
- *
34
- * @internal
35
- */
36
- const unsignedJwt = (header, payload) => {
37
- const segment = (value) => Base64Url.encode(JSON.stringify(value));
38
- return `${segment(header)}.${segment(payload)}.unsigned`;
39
- };
40
-
41
- export { payloadOf, unsignedJwt };
package/internal/sigv4.js DELETED
@@ -1,114 +0,0 @@
1
- import { sha256Hex } from "./digest.js";
2
- import { createHmac } from "node:crypto";
3
-
4
- // src/internal/sigv4.ts
5
- /**
6
- * AWS Signature Version 4, for S3-compatible object stores.
7
- *
8
- * @remarks
9
- * **Pure**, and about a hundred lines, which is the whole argument for not
10
- * taking an `@aws-sdk/*` dependency to obtain it. Signing is a specified
11
- * algorithm over strings and HMACs; the SDK around it exists to manage
12
- * credentials, retries and a service catalogue this package does not want.
13
- *
14
- * `node:crypto` rather than core `Crypto` because core's `Crypto` offers only
15
- * one-shot SHA digests over bytes already in memory — no HMAC, no incremental
16
- * hasher (see `internal/digest.ts`, this package's other `node:crypto`
17
- * licence). This package is the one place in the kit where a `node:` import
18
- * is sanctioned, and this is one of the four reasons.
19
- *
20
- * @internal
21
- */
22
- const ALGORITHM = "AWS4-HMAC-SHA256";
23
- const hmac = (key, value) => new Uint8Array(createHmac("sha256", key).update(value).digest());
24
- /**
25
- * Percent-encode one path segment the way SigV4 requires.
26
- *
27
- * @remarks
28
- * `encodeURIComponent` leaves `!'()*` alone and AWS does not, so a key
29
- * containing any of them signs one string and requests another — which fails as
30
- * an opaque `SignatureDoesNotMatch` rather than as anything that names the
31
- * character. The unreserved set is exactly `A-Za-z0-9-_.~`.
32
- */
33
- const uriEncode = (value) => encodeURIComponent(value).replace(/[!'()*]/g, (character) => `%${character.charCodeAt(0).toString(16).toUpperCase()}`);
34
- /** `20130524T000000Z` and `20130524`, the two forms the algorithm uses. */
35
- const timestamps = (now) => {
36
- const amzDate = `${now.toISOString().replace(/[:-]|\.\d{3}/g, "").slice(0, 15)}Z`;
37
- return {
38
- amzDate,
39
- dateStamp: amzDate.slice(0, 8)
40
- };
41
- };
42
- /**
43
- * Assemble the headers that will be signed, and the canonical request over them.
44
- *
45
- * @remarks
46
- * Exported separately from {@link sign} so a test can pin it against AWS's
47
- * **own documented** canonical request rather than against this
48
- * implementation's output.
49
- */
50
- const canonicalize = (request, credentials) => {
51
- const { amzDate, dateStamp } = timestamps(request.now);
52
- const payloadHash = sha256Hex(request.body);
53
- const headers = {
54
- ...request.headers,
55
- host: request.host,
56
- "x-amz-date": amzDate,
57
- "x-amz-content-sha256": payloadHash,
58
- ...credentials.sessionToken === void 0 ? {} : { "x-amz-security-token": credentials.sessionToken }
59
- };
60
- const lowered = new Map(Object.entries(headers).map(([name, value]) => [name.toLowerCase(), value]));
61
- const canonicalNames = [...lowered.keys()].sort();
62
- const canonicalHeaders = canonicalNames.map((name) => `${name}:${(lowered.get(name) ?? "").trim().replace(/\s+/g, " ")}\n`).join("");
63
- const signedHeaders = canonicalNames.join(";");
64
- const canonicalPath = `/${request.path.split("/").filter((segment) => segment !== "").map(uriEncode).join("/")}`;
65
- return {
66
- headers,
67
- signedHeaders,
68
- amzDate,
69
- dateStamp,
70
- canonicalRequest: [
71
- request.method,
72
- canonicalPath,
73
- "",
74
- canonicalHeaders,
75
- signedHeaders,
76
- payloadHash
77
- ].join("\n")
78
- };
79
- };
80
- /**
81
- * Derive a signing key.
82
- *
83
- * @remarks
84
- * Four nested HMACs over date, region, service and the literal `aws4_request`.
85
- * Exported so a test can reproduce AWS's documented derivation example.
86
- */
87
- const signingKey = (secretAccessKey, dateStamp, region, service) => hmac(hmac(hmac(hmac(`AWS4${secretAccessKey}`, dateStamp), region), service), "aws4_request");
88
- /**
89
- * Sign a request, returning the headers to send.
90
- *
91
- * @remarks
92
- * The returned record is the caller's headers **plus** everything signing added
93
- * — `host`, `x-amz-date`, `x-amz-content-sha256`, the optional
94
- * `x-amz-security-token` and `authorization` — so a caller sends exactly what
95
- * was signed rather than assembling a second, subtly different set.
96
- */
97
- const sign = (request, credentials) => {
98
- const { headers, canonicalRequest, signedHeaders, amzDate, dateStamp } = canonicalize(request, credentials);
99
- const scope = `${dateStamp}/${credentials.region}/${credentials.service}/aws4_request`;
100
- const stringToSign = [
101
- ALGORITHM,
102
- amzDate,
103
- scope,
104
- sha256Hex(canonicalRequest)
105
- ].join("\n");
106
- const key = signingKey(credentials.secretAccessKey, dateStamp, credentials.region, credentials.service);
107
- const signature = Buffer.from(hmac(key, stringToSign)).toString("hex");
108
- return {
109
- ...headers,
110
- authorization: `${ALGORITHM} Credential=${credentials.accessKeyId}/${scope}, SignedHeaders=${signedHeaders}, Signature=${signature}`
111
- };
112
- };
113
-
114
- export { canonicalize, sha256Hex as digestHex, sign, signingKey, uriEncode };