@effected/sbom 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,223 @@
1
+ import { Schema } from "effect";
2
+ import { License, isValidExpression } from "@effected/spdx";
3
+
4
+ //#region src/SbomDocument.ts
5
+ /** The BOM format discriminator. CycloneDX requires this exact string. */
6
+ const BOM_FORMAT = "CycloneDX";
7
+ /** The only specification version this package emits. */
8
+ const SPEC_VERSION = "1.6";
9
+ /**
10
+ * The CycloneDX component types this package emits.
11
+ *
12
+ * @remarks
13
+ * A deliberate subset of the specification's fourteen: an npm SBOM describes
14
+ * libraries and applications. The full enum is available in the schema; adding
15
+ * a member here is a one-line change when something needs one.
16
+ *
17
+ * @public
18
+ */
19
+ const ComponentType = Schema.Literals([
20
+ "library",
21
+ "application",
22
+ "framework"
23
+ ]);
24
+ /**
25
+ * An external reference's kind.
26
+ *
27
+ * @remarks
28
+ * The four the manifest mapping produces, out of the specification's 43. Each
29
+ * corresponds to a `package.json` field: `vcs` ← `repository`,
30
+ * `issue-tracker` ← `bugs`, `website` and `documentation` ← `homepage`.
31
+ *
32
+ * @public
33
+ */
34
+ const ExternalReferenceType = Schema.Literals([
35
+ "vcs",
36
+ "issue-tracker",
37
+ "website",
38
+ "documentation"
39
+ ]);
40
+ /**
41
+ * A link from a component to something outside the BOM.
42
+ *
43
+ * @public
44
+ */
45
+ var ExternalReference = class extends Schema.Class("ExternalReference")({
46
+ /** The reference kind. */
47
+ type: ExternalReferenceType,
48
+ /** The URL it points at, passed through exactly as supplied. */
49
+ url: Schema.String
50
+ }) {};
51
+ /**
52
+ * A point of contact — a person at a supplier, or an author of the BOM.
53
+ *
54
+ * @public
55
+ */
56
+ var Contact = class extends Schema.Class("Contact")({
57
+ /** The contact's name. */
58
+ name: Schema.optionalKey(Schema.String),
59
+ /** Their email address. */
60
+ email: Schema.optionalKey(Schema.String),
61
+ /** Their telephone number. */
62
+ phone: Schema.optionalKey(Schema.String)
63
+ }) {};
64
+ /**
65
+ * The organization that supplied a component.
66
+ *
67
+ * @remarks
68
+ * `name` is required because `metadata.supplier.name` is **NTIA minimum
69
+ * element 1**; a supplier without one satisfies nothing.
70
+ *
71
+ * @public
72
+ */
73
+ var Supplier = class extends Schema.Class("Supplier")({
74
+ /** The supplier organization's name. */
75
+ name: Schema.String,
76
+ /** Its URLs. */
77
+ url: Schema.optionalKey(Schema.Array(Schema.String)),
78
+ /** Its points of contact. */
79
+ contact: Schema.optionalKey(Schema.Array(Contact))
80
+ }) {};
81
+ /**
82
+ * One component in the BOM — the root, or a dependency.
83
+ *
84
+ * @remarks
85
+ * `bomRef` is spelled **`bom-ref`** in the emitted JSON; the rename happens in
86
+ * `Sbom.toJson`. Emitting `bomRef` produces a document that looks
87
+ * correct and validates wrong, which is why a test pins the key name.
88
+ *
89
+ * @public
90
+ */
91
+ var Component = class extends Schema.Class("Component")({
92
+ /** What kind of component this is. */
93
+ type: ComponentType,
94
+ /** The component's name — NTIA minimum element 2. */
95
+ name: Schema.String,
96
+ /** Its version — NTIA minimum element 3. */
97
+ version: Schema.optionalKey(Schema.String),
98
+ /** The package URL uniquely identifying it — NTIA minimum element 4. */
99
+ purl: Schema.optionalKey(Schema.String),
100
+ /** The identifier other parts of the document reference it by. */
101
+ bomRef: Schema.optionalKey(Schema.String),
102
+ /** A short description. */
103
+ description: Schema.optionalKey(Schema.String),
104
+ /** SPDX license identifiers or expressions. */
105
+ licenses: Schema.optionalKey(Schema.Array(Schema.String)),
106
+ /** Links out of the BOM. */
107
+ externalReferences: Schema.optionalKey(Schema.Array(ExternalReference)),
108
+ /** Discovery keywords — CycloneDX 1.6's `tags`, from the manifest's `keywords`. */
109
+ tags: Schema.optionalKey(Schema.Array(Schema.String)),
110
+ /** The component's authors. */
111
+ authors: Schema.optionalKey(Schema.Array(Contact)),
112
+ /** The entity that published it. */
113
+ publisher: Schema.optionalKey(Schema.String),
114
+ /** A copyright statement. */
115
+ copyright: Schema.optionalKey(Schema.String)
116
+ }) {};
117
+ /**
118
+ * Document-level metadata: who made the BOM, when, and about what.
119
+ *
120
+ * @public
121
+ */
122
+ var SbomMetadata = class extends Schema.Class("SbomMetadata")({
123
+ /** When the BOM was assembled — NTIA minimum element 7. */
124
+ timestamp: Schema.optionalKey(Schema.String),
125
+ /** Who created the BOM — NTIA minimum element 6. */
126
+ authors: Schema.optionalKey(Schema.Array(Contact)),
127
+ /** The component the BOM describes. */
128
+ component: Schema.optionalKey(Component),
129
+ /** Who supplied that component — NTIA minimum element 1. */
130
+ supplier: Schema.optionalKey(Supplier)
131
+ }) {};
132
+ /**
133
+ * A CycloneDX 1.6 bill of materials.
134
+ *
135
+ * @remarks
136
+ * Constructed by `Sbom.generate` and serialized by `Sbom.toJson`; both are
137
+ * total functions, because an owned model over validated values has nothing to
138
+ * fail at.
139
+ *
140
+ * @public
141
+ */
142
+ var SbomDocument = class extends Schema.Class("SbomDocument")({
143
+ /** Always `"CycloneDX"`. */
144
+ bomFormat: Schema.Literal(BOM_FORMAT),
145
+ /** Always `"1.6"`. */
146
+ specVersion: Schema.Literal("1.6"),
147
+ /** The document revision, `1` for a freshly assembled BOM. */
148
+ version: Schema.Number,
149
+ /** Document metadata. */
150
+ metadata: Schema.optionalKey(SbomMetadata),
151
+ /** The components the BOM describes, sorted by name. */
152
+ components: Schema.Array(Component)
153
+ }) {};
154
+ /** Drop absent keys so the emitted JSON omits them rather than carrying nulls. */
155
+ const compact = (value) => {
156
+ const out = {};
157
+ for (const [key, entry] of Object.entries(value)) if (entry !== void 0) out[key] = entry;
158
+ return out;
159
+ };
160
+ /**
161
+ * The `licenses` array in one of the three shapes CycloneDX permits.
162
+ *
163
+ * A manifest's `license` field is an SPDX **expression** field: `MIT`,
164
+ * `MIT OR Apache-2.0` and `UNLICENSED` are all legal values of it, and the
165
+ * specification renders them three different ways — `license.id` is constrained
166
+ * to the SPDX identifier enumeration, an expression goes in a one-element
167
+ * `{ expression }` tuple, and anything else is a named license. Emitting every
168
+ * value as an id produces a document that looks right and validates wrong.
169
+ *
170
+ * The identifier-versus-expression question is `@effected/spdx`'s to answer:
171
+ * the kit's one SPDX engine, never a local regex.
172
+ */
173
+ const licensesJson = (licenses) => {
174
+ const [only] = licenses;
175
+ if (licenses.length === 1 && only !== void 0 && !License.isKnownId(only) && isValidExpression(only)) return [{ expression: only }];
176
+ return licenses.map((license) => License.isKnownId(license) ? { license: { id: license } } : { license: { name: license } });
177
+ };
178
+ const contactJson = (contact) => compact({
179
+ name: contact.name,
180
+ email: contact.email,
181
+ phone: contact.phone
182
+ });
183
+ const componentJson = (component) => compact({
184
+ type: component.type,
185
+ "bom-ref": component.bomRef,
186
+ name: component.name,
187
+ version: component.version,
188
+ description: component.description,
189
+ publisher: component.publisher,
190
+ copyright: component.copyright,
191
+ authors: component.authors?.map(contactJson),
192
+ licenses: component.licenses === void 0 ? void 0 : licensesJson(component.licenses),
193
+ purl: component.purl,
194
+ externalReferences: component.externalReferences?.map((reference) => ({
195
+ url: reference.url,
196
+ type: reference.type
197
+ })),
198
+ tags: component.tags
199
+ });
200
+ /**
201
+ * The document as a plain JSON value, in CycloneDX's key shapes.
202
+ *
203
+ * @internal
204
+ */
205
+ const documentJson = (document) => compact({
206
+ bomFormat: document.bomFormat,
207
+ specVersion: document.specVersion,
208
+ version: document.version,
209
+ metadata: document.metadata === void 0 ? void 0 : compact({
210
+ timestamp: document.metadata.timestamp,
211
+ authors: document.metadata.authors?.map(contactJson),
212
+ component: document.metadata.component === void 0 ? void 0 : componentJson(document.metadata.component),
213
+ supplier: document.metadata.supplier === void 0 ? void 0 : compact({
214
+ name: document.metadata.supplier.name,
215
+ url: document.metadata.supplier.url,
216
+ contact: document.metadata.supplier.contact?.map(contactJson)
217
+ })
218
+ }),
219
+ components: document.components.map(componentJson)
220
+ });
221
+
222
+ //#endregion
223
+ export { BOM_FORMAT, Component, ComponentType, Contact, ExternalReference, ExternalReferenceType, SbomDocument, SbomMetadata, Supplier, documentJson };
@@ -0,0 +1,202 @@
1
+ import { Component, Contact, ExternalReference, SbomMetadata, Supplier } from "./SbomDocument.js";
2
+ import { Option } from "effect";
3
+
4
+ //#region src/SbomMetadataSource.ts
5
+ /** The purl namespace and name segments for an npm package name. */
6
+ const purlSegments = (name) => {
7
+ const separator = name.lastIndexOf("/");
8
+ if (!name.startsWith("@") || separator <= 0) return encodeURIComponent(name);
9
+ return `${encodeURIComponent(name.slice(0, separator))}/${encodeURIComponent(name.slice(separator + 1))}`;
10
+ };
11
+ const npmPurl = (name, version) => {
12
+ const path = purlSegments(name);
13
+ return version === void 0 ? `pkg:npm/${path}` : `pkg:npm/${path}@${version}`;
14
+ };
15
+ const contactOf = (person) => Contact.make({
16
+ name: person.name,
17
+ ...person.email !== void 0 && { email: person.email }
18
+ });
19
+ /** The manifest's authors: its maintainers, or its lone author when it lists none. */
20
+ const authorsOf = (pkg) => {
21
+ if (pkg.maintainers !== void 0 && pkg.maintainers.length > 0) return pkg.maintainers.map(contactOf);
22
+ return pkg.author === void 0 ? void 0 : [contactOf(pkg.author)];
23
+ };
24
+ /** The supplier's first URL, which becomes a `website` reference when it says something new. */
25
+ const supplierUrl = (options) => options?.supplier?.url?.[0];
26
+ const documentationUrl = (pkg, options) => options?.documentationUrl ?? pkg.homepage;
27
+ const externalReferences = (pkg, options) => {
28
+ const references = [];
29
+ const vcs = pkg.repository === void 0 ? Option.none() : pkg.repository.browseUrl;
30
+ if (Option.isSome(vcs)) references.push(ExternalReference.make({
31
+ type: "vcs",
32
+ url: vcs.value
33
+ }));
34
+ if (pkg.bugs?.url !== void 0) references.push(ExternalReference.make({
35
+ type: "issue-tracker",
36
+ url: pkg.bugs.url
37
+ }));
38
+ const documentation = documentationUrl(pkg, options);
39
+ if (documentation !== void 0) references.push(ExternalReference.make({
40
+ type: "documentation",
41
+ url: documentation
42
+ }));
43
+ const website = supplierUrl(options);
44
+ if (website !== void 0 && website !== documentation) references.push(ExternalReference.make({
45
+ type: "website",
46
+ url: website
47
+ }));
48
+ return references;
49
+ };
50
+ const componentFor = (input) => Component.make({
51
+ type: input.type ?? "library",
52
+ name: input.name,
53
+ ...input.version !== void 0 && { version: input.version },
54
+ purl: npmPurl(input.name, input.version),
55
+ bomRef: input.version === void 0 ? input.name : `${input.name}@${input.version}`,
56
+ ...input.description !== void 0 && { description: input.description },
57
+ ...input.license !== void 0 && { licenses: [input.license] }
58
+ });
59
+ const rootComponent = (pkg, options) => {
60
+ const version = pkg.version.toString();
61
+ const references = externalReferences(pkg, options);
62
+ const authors = authorsOf(pkg);
63
+ const publisher = options?.publisher ?? options?.supplier?.name ?? pkg.author?.name;
64
+ return Component.make({
65
+ type: options?.type ?? "library",
66
+ name: pkg.name,
67
+ version,
68
+ purl: npmPurl(pkg.name, version),
69
+ bomRef: `${pkg.name}@${version}`,
70
+ ...pkg.description !== void 0 && { description: pkg.description },
71
+ ...pkg.license !== void 0 && { licenses: [pkg.license] },
72
+ ...references.length > 0 && { externalReferences: references },
73
+ ...pkg.keywords !== void 0 && pkg.keywords.length > 0 && { tags: pkg.keywords },
74
+ ...authors !== void 0 && { authors },
75
+ ...publisher !== void 0 && { publisher },
76
+ ...options?.copyright !== void 0 && { copyright: options.copyright }
77
+ });
78
+ };
79
+ const fromPackage = (pkg, options) => {
80
+ const supplier = options?.supplier;
81
+ const contacts = supplier?.contact ?? authorsOf(pkg);
82
+ const resolved = supplier === void 0 ? void 0 : Supplier.make({
83
+ name: supplier.name,
84
+ ...supplier.url !== void 0 && { url: supplier.url },
85
+ ...contacts !== void 0 && { contact: contacts }
86
+ });
87
+ return SbomMetadata.make({
88
+ ...options?.timestamp !== void 0 && { timestamp: options.timestamp },
89
+ ...options?.authors !== void 0 && { authors: options.authors },
90
+ ...resolved !== void 0 && { supplier: resolved }
91
+ });
92
+ };
93
+ const formatCopyright = (holder, years) => years.startYear === void 0 || years.startYear === years.year ? `Copyright ${years.year} ${holder}` : `Copyright ${years.startYear}-${years.year} ${holder}`;
94
+ const merge = (base, override) => {
95
+ const timestamp = override.timestamp ?? base.timestamp;
96
+ const authors = override.authors ?? base.authors;
97
+ const component = override.component ?? base.component;
98
+ const supplier = override.supplier ?? base.supplier;
99
+ return SbomMetadata.make({
100
+ ...timestamp !== void 0 && { timestamp },
101
+ ...authors !== void 0 && { authors },
102
+ ...component !== void 0 && { component },
103
+ ...supplier !== void 0 && { supplier }
104
+ });
105
+ };
106
+ /**
107
+ * Derivation of CycloneDX metadata from a `package.json` manifest.
108
+ *
109
+ * @example
110
+ * ```ts
111
+ * import { Sbom, SbomMetadataSource } from "@effected/sbom";
112
+ *
113
+ * const root = SbomMetadataSource.rootComponent(pkg, { supplier });
114
+ * const metadata = SbomMetadataSource.fromPackage(pkg, { supplier, timestamp });
115
+ * const document = Sbom.generate({ root, components, metadata });
116
+ * ```
117
+ *
118
+ * @public
119
+ */
120
+ var SbomMetadataSource = class {
121
+ constructor() {}
122
+ /**
123
+ * The canonical npm package URL for a name and optional version.
124
+ *
125
+ * @remarks
126
+ * The NTIA's "unique identifier" element, and the identifier an in-toto
127
+ * subject names. Exposed because a caller assembling its own components —
128
+ * or a statement subject — needs the same encoding this module applies.
129
+ */
130
+ static npmPurl = npmPurl;
131
+ /**
132
+ * A component entry for one resolved dependency.
133
+ *
134
+ * @remarks
135
+ * The caller assembles the component list — the kit has no second merge
136
+ * rule for sibling packages released in the same wave, because which
137
+ * versions are in flight is release planning and `@effected/workspaces`
138
+ * already knows it. This is the mapping that would otherwise be
139
+ * re-derived at every call site.
140
+ */
141
+ static componentFor = componentFor;
142
+ /**
143
+ * The root component the BOM is about, derived from its own manifest.
144
+ *
145
+ * @remarks
146
+ * `publisher` resolves explicit → supplier name → the manifest's author,
147
+ * which is what lets NTIA element 6 be satisfied from a manifest alone.
148
+ */
149
+ static rootComponent = rootComponent;
150
+ /**
151
+ * The manifest's outward links, as CycloneDX external references.
152
+ *
153
+ * @remarks
154
+ * Four of the specification's 43 types, one per manifest field: `vcs` ←
155
+ * `repository`, `issue-tracker` ← `bugs`, `documentation` ← `homepage`,
156
+ * `website` ← the supplier's first URL.
157
+ *
158
+ * A `repository` value the package-json model cannot interpret produces
159
+ * **no** reference rather than a passed-through string: CycloneDX's
160
+ * `externalReference.url` is a URL, and emitting `owner/name` there is a
161
+ * document that validates and misleads.
162
+ */
163
+ static externalReferences = externalReferences;
164
+ /**
165
+ * Document-level metadata for a manifest.
166
+ *
167
+ * @remarks
168
+ * The root component is **not** on the returned value: `Sbom.generate`
169
+ * threads its `root` argument onto the metadata itself, so setting it
170
+ * here would only be overwritten. Build the root with
171
+ * {@link SbomMetadataSource.rootComponent} and pass both.
172
+ *
173
+ * When the caller supplies a supplier with no contacts, the manifest's
174
+ * maintainers fill them — the one derivation that crosses from manifest
175
+ * vocabulary into supplier vocabulary, and only where the caller left a
176
+ * hole.
177
+ */
178
+ static fromPackage = fromPackage;
179
+ /**
180
+ * A copyright statement for a holder and a year, or a span of years.
181
+ *
182
+ * @remarks
183
+ * The year is an **argument**. The predecessor defaulted it to
184
+ * `new Date().getFullYear()`, which made its output untestable and its
185
+ * purity a claim rather than a property; the ambient read belongs at the
186
+ * caller's edge.
187
+ */
188
+ static formatCopyright = formatCopyright;
189
+ /**
190
+ * Field-wise metadata merge: every field the override carries wins.
191
+ *
192
+ * @remarks
193
+ * A helper, not a policy. Which side is the override — a config file over
194
+ * inferred values, or the reverse — is the consumer's precedence rule,
195
+ * and a library that decided it would be encoding one repository's
196
+ * release policy.
197
+ */
198
+ static merge = merge;
199
+ };
200
+
201
+ //#endregion
202
+ export { SbomMetadataSource };
@@ -0,0 +1,48 @@
1
+ import { Schema } from "effect";
2
+
3
+ //#region src/SigstoreBundle.ts
4
+ /**
5
+ * The Sigstore bundle media type this package produces.
6
+ *
7
+ * @remarks
8
+ * v0.3 with a single certificate — what `DSSEBundleBuilder` emits by default,
9
+ * and what GitHub's `POST /repos/{owner}/{repo}/attestations` accepts.
10
+ *
11
+ * @public
12
+ */
13
+ const SIGSTORE_BUNDLE_V0_3_MEDIA_TYPE = "application/vnd.dev.sigstore.bundle.v0.3+json";
14
+ /**
15
+ * The DSSE payload type for an in-toto statement, per the GitHub attestations
16
+ * specification.
17
+ *
18
+ * @public
19
+ */
20
+ const IN_TOTO_PAYLOAD_TYPE = "application/vnd.in-toto+json";
21
+ /**
22
+ * A signed Sigstore bundle: the wire form of an attestation.
23
+ *
24
+ * @remarks
25
+ * `verificationMaterial` and `dsseEnvelope` are `unknown` because their shapes
26
+ * belong to the Sigstore protobuf specifications, and re-declaring them here
27
+ * would be a second, drifting copy of a wire format we do not own. The bundle
28
+ * is opaque to everything that merely stores or forwards it.
29
+ *
30
+ * `mediaType` is carried through from what the builder produced rather than
31
+ * asserted — the version is the producer's statement about the bundle, and a
32
+ * literal here would quietly lie the day a builder emits a different one.
33
+ *
34
+ * @see {@link https://github.com/sigstore/protobuf-specs/blob/main/protos/sigstore_bundle.proto | sigstore_bundle.proto}
35
+ *
36
+ * @public
37
+ */
38
+ var SigstoreBundle = class extends Schema.Class("SigstoreBundle")({
39
+ /** The bundle's media type, usually {@link SIGSTORE_BUNDLE_V0_3_MEDIA_TYPE}. */
40
+ mediaType: Schema.String,
41
+ /** The certificate and transparency-log entries a verifier checks. */
42
+ verificationMaterial: Schema.Unknown,
43
+ /** The signed DSSE envelope carrying the statement. */
44
+ dsseEnvelope: Schema.Unknown
45
+ }) {};
46
+
47
+ //#endregion
48
+ export { IN_TOTO_PAYLOAD_TYPE, SIGSTORE_BUNDLE_V0_3_MEDIA_TYPE, SigstoreBundle };
@@ -0,0 +1,150 @@
1
+ import { IdentityToken } from "./IdentityToken.js";
2
+ import { IN_TOTO_PAYLOAD_TYPE, SigstoreBundle } from "./SigstoreBundle.js";
3
+ import { Context, Effect, Layer, Redacted, Schema } from "effect";
4
+ import { bundleToJSON } from "@sigstore/bundle";
5
+ import { DSSEBundleBuilder, FulcioSigner, RekorWitness } from "@sigstore/sign";
6
+
7
+ //#region src/SigstoreSigner.ts
8
+ /**
9
+ * The OIDC audience Sigstore's certificate authority requires.
10
+ *
11
+ * @remarks
12
+ * It lives here, not at the call site, because it is the **signing protocol's**
13
+ * requirement rather than the caller's knowledge — which is why
14
+ * {@link SigstoreSignerShape.sign} takes only a statement and asks the identity
15
+ * contract for a token. Considered and rejected: `sign(statement, { token })`,
16
+ * which reads simpler and forces every caller to learn a constant that is none
17
+ * of its business.
18
+ *
19
+ * @public
20
+ */
21
+ const SIGSTORE_OIDC_AUDIENCE = "sigstore";
22
+ /**
23
+ * Which step of signing failed.
24
+ *
25
+ * @public
26
+ */
27
+ const SigningErrorKind = Schema.Literals([
28
+ "identity",
29
+ "certificate",
30
+ "transparencyLog",
31
+ "bundle"
32
+ ]);
33
+ /**
34
+ * Raised when a statement cannot be signed.
35
+ *
36
+ * @remarks
37
+ * Sized to what a caller can act on: an `identity` failure is a workflow
38
+ * permissions problem, `certificate` is Fulcio, `transparencyLog` is Rekor, and
39
+ * `bundle` is everything else about assembling the result. The original failure
40
+ * is preserved structurally on `cause` rather than flattened into a message.
41
+ *
42
+ * @public
43
+ */
44
+ var SigningError = class extends Schema.TaggedErrorClass()("SigningError", {
45
+ /** Which step failed. */
46
+ kind: SigningErrorKind,
47
+ /** The underlying failure, preserved structurally. */
48
+ cause: Schema.Defect()
49
+ }) {
50
+ get message() {
51
+ return `Failed to sign the statement (${this.kind})`;
52
+ }
53
+ };
54
+ /**
55
+ * Attribute a `@sigstore/sign` failure to a step.
56
+ *
57
+ * `InternalError` carries a `code`, which is a far better signal than the
58
+ * message text the predecessor scraped. An unrecognized failure is `bundle` —
59
+ * literally "the bundle did not get built" — rather than being guessed into a
60
+ * step it may not belong to.
61
+ */
62
+ const kindOf = (cause) => {
63
+ const code = cause?.code;
64
+ if (typeof code !== "string") return "bundle";
65
+ if (code.startsWith("IDENTITY_TOKEN_")) return "identity";
66
+ if (code.startsWith("CA_")) return "certificate";
67
+ if (code.startsWith("TLOG_") || code.startsWith("TSA_")) return "transparencyLog";
68
+ return "bundle";
69
+ };
70
+ const make = (identity, options) => ({ sign: Effect.fn("SigstoreSigner.sign")(function* (statement) {
71
+ const token = yield* identity.token(SIGSTORE_OIDC_AUDIENCE).pipe(Effect.mapError((cause) => new SigningError({
72
+ kind: "identity",
73
+ cause
74
+ })));
75
+ const builder = new DSSEBundleBuilder({
76
+ signer: options.signer ?? new FulcioSigner({
77
+ identityProvider: { getToken: () => Promise.resolve(Redacted.value(token)) },
78
+ ...options.fulcioBaseUrl !== void 0 && { fulcioBaseURL: options.fulcioBaseUrl }
79
+ }),
80
+ witnesses: [...options.witnesses ?? [new RekorWitness({
81
+ entryType: "dsse",
82
+ ...options.rekorBaseUrl !== void 0 && { rekorBaseURL: options.rekorBaseUrl }
83
+ })]]
84
+ });
85
+ const bundle = yield* Effect.tryPromise({
86
+ try: () => builder.create({
87
+ data: Buffer.from(statement.toJson(), "utf8"),
88
+ type: IN_TOTO_PAYLOAD_TYPE
89
+ }),
90
+ catch: (cause) => new SigningError({
91
+ kind: kindOf(cause),
92
+ cause
93
+ })
94
+ });
95
+ return yield* Effect.try({
96
+ try: () => {
97
+ const serialized = bundleToJSON(bundle);
98
+ return SigstoreBundle.make({
99
+ mediaType: serialized.mediaType,
100
+ verificationMaterial: serialized.verificationMaterial,
101
+ dsseEnvelope: serialized.dsseEnvelope
102
+ });
103
+ },
104
+ catch: (cause) => new SigningError({
105
+ kind: "bundle",
106
+ cause
107
+ })
108
+ });
109
+ }) });
110
+ const unstubbed = () => {
111
+ throw new Error("SigstoreSigner.makeTest: sign() was called but not stubbed — a fabricated bundle would be a signature-shaped lie. Pass a `sign` override, or drive the real builder through SigstoreSigner.layerWith({ signer, witnesses }).");
112
+ };
113
+ /**
114
+ * Sigstore signing.
115
+ *
116
+ * @example
117
+ * ```ts
118
+ * import { IdentityToken, SigstoreSigner } from "@effected/sbom";
119
+ * import { Effect, Layer } from "effect";
120
+ *
121
+ * const program = Effect.gen(function* () {
122
+ * const signer = yield* SigstoreSigner;
123
+ * return yield* signer.sign(statement);
124
+ * });
125
+ * ```
126
+ *
127
+ * @public
128
+ */
129
+ var SigstoreSigner = class SigstoreSigner extends Context.Service()("@effected/sbom/SigstoreSigner") {
130
+ /** Signing against the public-good Fulcio and Rekor instances. */
131
+ static layer = Layer.effect(this, Effect.map(IdentityToken, (identity) => make(identity, {})));
132
+ /** {@link (SigstoreSigner:class).layer} with the signing endpoints, or the signer and witnesses, replaced. */
133
+ static layerWith = (options) => Layer.effect(SigstoreSigner, Effect.map(IdentityToken, (identity) => make(identity, options)));
134
+ /**
135
+ * An in-memory double whose `sign` **dies** unless stubbed.
136
+ *
137
+ * @remarks
138
+ * The strongest case in the kit for the die-loudly default: no honest
139
+ * fabricated answer exists, because a bundle that looks signed and is not is
140
+ * exactly the failure an attestation exists to prevent. A test that wants a
141
+ * real bundle without a network drives the real builder through
142
+ * {@link (SigstoreSigner:class).layerWith}.
143
+ */
144
+ static makeTest = (overrides = {}) => ({ sign: overrides.sign ?? (() => unstubbed()) });
145
+ /** {@link (SigstoreSigner:class).makeTest} behind a `Layer`. */
146
+ static layerTest = (overrides = {}) => Layer.succeed(SigstoreSigner, SigstoreSigner.makeTest(overrides));
147
+ };
148
+
149
+ //#endregion
150
+ export { SIGSTORE_OIDC_AUDIENCE, SigningError, SigningErrorKind, SigstoreSigner };