@openmbee/opensysml 0.9.1

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,101 @@
1
+ // Node entry point: everything the core does, plus the private-child lifecycle.
2
+ import { createConnectTransport, createGrpcTransport } from "@connectrpc/connect-node";
3
+ import { Connection } from "../core/connection.js";
4
+ import { OpenSysMLError } from "../core/errors.js";
5
+ import { Model } from "../core/model.js";
6
+ import { baseUrl, encodingOf, interceptors, timeoutOf } from "../core/transport.js";
7
+ import { acquirePrivateService } from "./service.js";
8
+ export * from "../core/index.js";
9
+ export { ALLOW_UNPINNED_ENV, API_BASE_URL, BINARY_ENV, BinaryNotFoundError, DEFAULT_GITHUB_REPO, NETWORK_TIMEOUT_MS, PINNED_SHA256, RELEASES_BASE_URL, REPO_ENV, VERSION_ENV, binaryName, cachedBinaryPath, cachedRelease, defaultGithubRepo, downloadBinary, expectedDigest, metadataPath, pinnedDigest, platformPackage, releaseAssetName, releaseDownloadUrl, resolveBinary, resolveLatestVersion, signedManifestDigest, staleCacheReason, unpinnedDownloadsAllowed, verifyChecksum, writeMetadata, } from "./binary.js";
10
+ export { BUNDLE_ASSET, MANIFEST_ASSET, ReleaseSigner, SIGNED_MANIFEST_SIGNERS, manifestDigest, signerFor, verifiedManifestDigest, verifyManifest, } from "./signing.js";
11
+ export { PrivateService, currentPrivateService } from "./service.js";
12
+ /** Names a service to connect to instead of starting one. */
13
+ export const SERVICE_ENV = "OPENSYSML_SERVICE";
14
+ /**
15
+ * Connects to a sysml-grpc service.
16
+ *
17
+ * With no address, this starts a private child of this process — one per thread,
18
+ * shared by every connection, stopped when the last one closes. With an address,
19
+ * or with `$OPENSYSML_SERVICE` set, it connects to a service someone else runs and
20
+ * closing the connection leaves that service running.
21
+ */
22
+ export async function connect(options = {}) {
23
+ const address = options.address ?? process.env[SERVICE_ENV];
24
+ if (address !== undefined && address !== "") {
25
+ return connectExternal(address, options);
26
+ }
27
+ return connectPrivate(options);
28
+ }
29
+ /** Parses a file over a connection of its own, which the model closes. */
30
+ export async function load(path, options = {}) {
31
+ const connection = await connect(options);
32
+ try {
33
+ return await Model.parse(connection, { source: { case: "filePath", value: path } }, options, true);
34
+ }
35
+ catch (error) {
36
+ await connection.close();
37
+ throw error;
38
+ }
39
+ }
40
+ /** Parses inline source over a connection of its own, which the model closes. */
41
+ export async function loads(source, options = {}) {
42
+ const connection = await connect(options);
43
+ try {
44
+ return await Model.parse(connection, { source: { case: "content", value: source } }, options, true);
45
+ }
46
+ catch (error) {
47
+ await connection.close();
48
+ throw error;
49
+ }
50
+ }
51
+ async function connectPrivate(options) {
52
+ // Checked before a child is started, so a bad option costs no process.
53
+ const encoding = encodingOf(options);
54
+ const timeoutMs = timeoutOf(options);
55
+ const service = await acquirePrivateService();
56
+ const backend = {
57
+ origin: `${service.binary.path}, started by this client`,
58
+ release: () => service.release(),
59
+ };
60
+ try {
61
+ return await Connection.open({
62
+ transport: transportFor(baseUrl(service.address), options),
63
+ encoding,
64
+ backend,
65
+ timeoutMs,
66
+ });
67
+ }
68
+ catch (error) {
69
+ await service.release();
70
+ throw error;
71
+ }
72
+ }
73
+ async function connectExternal(address, options) {
74
+ const url = baseUrl(address);
75
+ const timeoutMs = timeoutOf(options);
76
+ return Connection.open({
77
+ transport: transportFor(url, options),
78
+ encoding: encodingOf(options),
79
+ backend: {
80
+ origin: `${url}, which this client did not start`,
81
+ // A service this client did not start is never stopped by it.
82
+ release: () => Promise.resolve(),
83
+ },
84
+ timeoutMs,
85
+ });
86
+ }
87
+ function transportFor(url, options) {
88
+ const binary = encodingOf(options) === "protobuf";
89
+ if (options.protocol === "grpc") {
90
+ if (!binary) {
91
+ throw new OpenSysMLError("the gRPC protocol carries protobuf bodies; JSON needs the Connect protocol");
92
+ }
93
+ return createGrpcTransport({ baseUrl: url, interceptors: interceptors(options) });
94
+ }
95
+ return createConnectTransport({
96
+ baseUrl: url,
97
+ httpVersion: options.httpVersion ?? "2",
98
+ useBinaryFormat: binary,
99
+ interceptors: interceptors(options),
100
+ });
101
+ }
@@ -0,0 +1,31 @@
1
+ import { type Binary } from "./binary.js";
2
+ /** A sysml-grpc child of this process, shared by every connection that needs one. */
3
+ export declare class PrivateService {
4
+ readonly address: string;
5
+ readonly binary: Binary;
6
+ /** Connections holding it; the last one released stops it. */
7
+ refs: number;
8
+ private readonly child;
9
+ private readonly stderr;
10
+ private ended;
11
+ private constructor();
12
+ /** The process id of the child, which the orphan-safety test reads. */
13
+ get pid(): number | undefined;
14
+ /** Whether the child is still there to be used. */
15
+ get alive(): boolean;
16
+ /** The last lines the child wrote to stderr, for an error message. */
17
+ get log(): readonly string[];
18
+ /** Drops one hold, stopping the child when it was the last. */
19
+ release(): Promise<void>;
20
+ /**
21
+ * Stops the child. Closing the pipe would end it on its own; it is signalled as
22
+ * well so a connection closed in a long-lived process does not wait on the read.
23
+ */
24
+ stop(): Promise<void>;
25
+ /** Starts a child and waits for the address it bound. */
26
+ static start(): Promise<PrivateService>;
27
+ }
28
+ /** Takes a hold on this thread's private child, starting one when there is none. */
29
+ export declare function acquirePrivateService(): Promise<PrivateService>;
30
+ /** The private child this thread holds, for tests and diagnostics. */
31
+ export declare function currentPrivateService(): PrivateService | undefined;
@@ -0,0 +1,238 @@
1
+ // The private child: a sysml-grpc this process started and only it can reach.
2
+ // It reports its kernel-assigned port on stdout, and exits at end of file on the
3
+ // stdin pipe this process holds open — which SIGKILL closes and an exit hook does not.
4
+ import { spawn } from "node:child_process";
5
+ import { ServiceStartError } from "../core/errors.js";
6
+ import { resolveBinary } from "./binary.js";
7
+ const START_TIMEOUT_MS = 10_000;
8
+ const STOP_TIMEOUT_MS = 5_000;
9
+ const STDERR_LINES_KEPT = 20;
10
+ /** The one private child of this thread, and the connections holding it. */
11
+ let shared;
12
+ /** A start in flight, so concurrent connect() calls share one child. */
13
+ let starting;
14
+ /** A sysml-grpc child of this process, shared by every connection that needs one. */
15
+ export class PrivateService {
16
+ address;
17
+ binary;
18
+ /** Connections holding it; the last one released stops it. */
19
+ refs = 0;
20
+ child;
21
+ stderr;
22
+ ended;
23
+ constructor(init) {
24
+ this.child = init.child;
25
+ this.binary = init.binary;
26
+ this.address = init.address;
27
+ this.stderr = init.stderr;
28
+ this.ended = false;
29
+ this.child.once("exit", () => {
30
+ this.ended = true;
31
+ });
32
+ }
33
+ /** The process id of the child, which the orphan-safety test reads. */
34
+ get pid() {
35
+ return this.child.pid;
36
+ }
37
+ /** Whether the child is still there to be used. */
38
+ get alive() {
39
+ return !this.ended && this.child.exitCode === null && this.child.signalCode === null;
40
+ }
41
+ /** The last lines the child wrote to stderr, for an error message. */
42
+ get log() {
43
+ return this.stderr;
44
+ }
45
+ /** Drops one hold, stopping the child when it was the last. */
46
+ async release() {
47
+ this.refs -= 1;
48
+ if (this.refs > 0) {
49
+ return;
50
+ }
51
+ if (shared === this) {
52
+ shared = undefined;
53
+ }
54
+ await this.stop();
55
+ }
56
+ /**
57
+ * Stops the child. Closing the pipe would end it on its own; it is signalled as
58
+ * well so a connection closed in a long-lived process does not wait on the read.
59
+ */
60
+ async stop() {
61
+ if (!this.alive) {
62
+ this.child.stdin.destroy();
63
+ return;
64
+ }
65
+ // Waiting for the child is this program's work again, so its handle is
66
+ // referenced for as long as the wait lasts.
67
+ this.child.ref();
68
+ const exited = new Promise((resolve) => {
69
+ this.child.once("exit", () => {
70
+ resolve();
71
+ });
72
+ });
73
+ this.child.stdin.end();
74
+ this.child.kill("SIGTERM");
75
+ const killed = await withTimeout(exited, STOP_TIMEOUT_MS);
76
+ if (!killed) {
77
+ this.child.kill("SIGKILL");
78
+ await exited;
79
+ }
80
+ this.child.stdout.destroy();
81
+ this.child.stderr.destroy();
82
+ this.child.stdin.destroy();
83
+ }
84
+ /** Starts a child and waits for the address it bound. */
85
+ static async start() {
86
+ const binary = await resolveBinary();
87
+ const child = spawn(binary.path, ["-port", "0", "-health-port", "0", "-report-address", "-exit-with-parent"], {
88
+ stdio: ["pipe", "pipe", "pipe"],
89
+ // A new session (a new process group on Windows), so a Ctrl-C meant for
90
+ // this process does not reach the child mid-call.
91
+ detached: true,
92
+ windowsHide: true,
93
+ });
94
+ const stderr = tail(child, STDERR_LINES_KEPT);
95
+ const address = firstLine(child);
96
+ let reported;
97
+ try {
98
+ // The handles stay referenced until the address arrives: this read is the
99
+ // program's work, and unreferencing it first would let Node exit mid-start.
100
+ reported = await address;
101
+ }
102
+ catch (cause) {
103
+ child.stdin.end();
104
+ child.kill("SIGKILL");
105
+ throw new ServiceStartError(startFailure(binary, stderr, cause), { cause });
106
+ }
107
+ // From here the child is not this program's work: a referenced handle would
108
+ // stop a script that forgot close() from ever exiting.
109
+ child.unref();
110
+ unrefHandle(child.stdin);
111
+ unrefHandle(child.stdout);
112
+ unrefHandle(child.stderr);
113
+ return new PrivateService({
114
+ child,
115
+ binary,
116
+ address: reported,
117
+ stderr,
118
+ });
119
+ }
120
+ }
121
+ /** Takes a hold on this thread's private child, starting one when there is none. */
122
+ export async function acquirePrivateService() {
123
+ for (;;) {
124
+ const existing = shared;
125
+ if (existing?.alive) {
126
+ existing.refs += 1;
127
+ return existing;
128
+ }
129
+ starting ??= PrivateService.start()
130
+ .then((service) => {
131
+ shared = service;
132
+ return service;
133
+ })
134
+ .finally(() => {
135
+ starting = undefined;
136
+ });
137
+ await starting;
138
+ }
139
+ }
140
+ /** The private child this thread holds, for tests and diagnostics. */
141
+ export function currentPrivateService() {
142
+ return shared;
143
+ }
144
+ function startFailure(binary, stderr, cause) {
145
+ const why = cause instanceof Error ? cause.message : String(cause);
146
+ const log = stderr.length === 0 ? "" : `\n it wrote: ${stderr.join("\n ")}`;
147
+ return `${binary.path} did not report an address it is serving: ${why}${log}`;
148
+ }
149
+ /** Resolves with the child's first stdout line, then drains the rest. */
150
+ function firstLine(child) {
151
+ return new Promise((resolve, reject) => {
152
+ let buffered = "";
153
+ let settled = false;
154
+ const timer = setTimeout(() => {
155
+ finish(() => {
156
+ reject(new Error(`no address within ${START_TIMEOUT_MS} ms`));
157
+ });
158
+ }, START_TIMEOUT_MS);
159
+ timer.unref();
160
+ const finish = (settle) => {
161
+ if (settled) {
162
+ return;
163
+ }
164
+ settled = true;
165
+ clearTimeout(timer);
166
+ child.stdout.off("data", onData);
167
+ // The child must never block writing to a full pipe, so keep reading.
168
+ child.stdout.resume();
169
+ settle();
170
+ };
171
+ const onData = (chunk) => {
172
+ buffered += chunk.toString();
173
+ const newline = buffered.indexOf("\n");
174
+ if (newline === -1) {
175
+ return;
176
+ }
177
+ const line = buffered.slice(0, newline).trim();
178
+ finish(() => {
179
+ if (line === "") {
180
+ reject(new Error("its first line of output was empty"));
181
+ }
182
+ else {
183
+ resolve(line);
184
+ }
185
+ });
186
+ };
187
+ child.stdout.on("data", onData);
188
+ child.once("error", (error) => {
189
+ finish(() => {
190
+ reject(error);
191
+ });
192
+ });
193
+ child.once("exit", (code, signal) => {
194
+ finish(() => {
195
+ reject(new Error(`it exited (code ${code ?? "none"}, signal ${signal ?? "none"}) first`));
196
+ });
197
+ });
198
+ });
199
+ }
200
+ /** Stops a stdio pipe from holding the event loop open; they are sockets, which unref. */
201
+ function unrefHandle(stream) {
202
+ const handle = stream;
203
+ handle.unref?.();
204
+ }
205
+ /** Keeps the last `lines` lines the child wrote to stderr. */
206
+ function tail(child, lines) {
207
+ const kept = [];
208
+ let buffered = "";
209
+ child.stderr.on("data", (chunk) => {
210
+ buffered += chunk.toString();
211
+ const parts = buffered.split("\n");
212
+ buffered = parts.pop() ?? "";
213
+ for (const part of parts) {
214
+ kept.push(part);
215
+ if (kept.length > lines) {
216
+ kept.shift();
217
+ }
218
+ }
219
+ });
220
+ return kept;
221
+ }
222
+ async function withTimeout(promise, ms) {
223
+ let timer;
224
+ const timeout = new Promise((resolve) => {
225
+ timer = setTimeout(() => {
226
+ resolve(false);
227
+ }, ms);
228
+ timer.unref();
229
+ });
230
+ try {
231
+ return await Promise.race([promise.then(() => true), timeout]);
232
+ }
233
+ finally {
234
+ if (timer !== undefined) {
235
+ clearTimeout(timer);
236
+ }
237
+ }
238
+ }
@@ -0,0 +1,55 @@
1
+ /** Manifest of every published artifact's digest, and its sigstore bundle. */
2
+ export declare const MANIFEST_ASSET = "SHA256SUMS.txt";
3
+ export declare const BUNDLE_ASSET = "SHA256SUMS.txt.bundle";
4
+ /** The sigstore packages this module verifies with, imported on demand. */
5
+ interface Sigstore {
6
+ bundle: typeof import("@sigstore/bundle");
7
+ specs: typeof import("@sigstore/protobuf-specs");
8
+ tuf: typeof import("@sigstore/tuf");
9
+ verify: typeof import("@sigstore/verify");
10
+ }
11
+ /** The identity a release manifest's signature must carry. */
12
+ export declare class ReleaseSigner {
13
+ /** OIDC issuer the certificate must have been issued for. */
14
+ readonly issuer: string;
15
+ /** CircleCI project API URL the certificate subject must belong to. */
16
+ readonly project: string;
17
+ /** Pipeline definition the subject must name exactly, or any of the project. */
18
+ readonly definition: string | undefined;
19
+ /** Sigstore trusted root to verify against, or Sigstore's production instance. */
20
+ readonly trustedRootPath: string | undefined;
21
+ constructor(init: {
22
+ issuer: string;
23
+ project: string;
24
+ definition?: string;
25
+ trustedRootPath?: string;
26
+ });
27
+ /** Certificate subject required, or undefined when any definition is accepted. */
28
+ get subject(): string | undefined;
29
+ /** The identity, for an error message. */
30
+ describe(): string;
31
+ /**
32
+ * The verification policy for this identity. The subject pattern is anchored,
33
+ * since sigstore matches it as an unanchored regular expression.
34
+ */
35
+ policy(): import("@sigstore/verify").VerificationPolicy;
36
+ /** A verifier holding the root of trust this signer's certificates chain to. */
37
+ verifier(sigstore: Sigstore): Promise<import("@sigstore/verify").Verifier>;
38
+ }
39
+ /**
40
+ * Identity each repository's release manifest must be signed with: the CircleCI OIDC
41
+ * issuer of its organization and its project. A repository absent here signs nothing.
42
+ */
43
+ export declare const SIGNED_MANIFEST_SIGNERS: Readonly<Record<string, ReleaseSigner>>;
44
+ /** The signer whose signature a repository's release manifest must carry. */
45
+ export declare function signerFor(githubRepo: string): ReleaseSigner | undefined;
46
+ /** The digest a checksum manifest lists for an asset, as sha256sum writes them. */
47
+ export declare function manifestDigest(manifest: Buffer, asset: string): string | undefined;
48
+ /**
49
+ * Verify a manifest's sigstore bundle against the signer expected: nothing checked at
50
+ * all is an UnsignedReleaseError, a signature that fails a ManifestSignatureError.
51
+ */
52
+ export declare function verifyManifest(manifest: Buffer, bundle: Buffer, signer: ReleaseSigner): Promise<void>;
53
+ /** The digest a signed manifest lists for an asset, once its signature verified. */
54
+ export declare function verifiedManifestDigest(manifest: Buffer, bundle: Buffer, asset: string, signer: ReleaseSigner): Promise<string>;
55
+ export {};
@@ -0,0 +1,167 @@
1
+ // Verification of the signed checksum manifest a release publishes. A core release
2
+ // signs its SHA256SUMS.txt in CI with cosign keyless, using the release pipeline's
3
+ // CircleCI OIDC identity, and publishes the sigstore bundle beside it. A bundle
4
+ // that verifies against the identity pinned here says the manifest came from that
5
+ // pipeline, so a release published after this client can still be installed. A
6
+ // manifest that does not verify is refused exactly as an unpinned release is.
7
+ import { readFile } from "node:fs/promises";
8
+ import { ManifestSignatureError, UnsignedReleaseError } from "../core/errors.js";
9
+ /** Manifest of every published artifact's digest, and its sigstore bundle. */
10
+ export const MANIFEST_ASSET = "SHA256SUMS.txt";
11
+ export const BUNDLE_ASSET = `${MANIFEST_ASSET}.bundle`;
12
+ /** How long the root of trust may take to load, matching the download timeout. */
13
+ const TRUST_ROOT_TIMEOUT_MS = 15_000;
14
+ // A CircleCI signing certificate's subject names the pipeline definition that
15
+ // produced it, under the project it belongs to.
16
+ const PIPELINE_DEFINITIONS = "/pipeline-definitions/";
17
+ const DEFINITION_ID = "[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}";
18
+ const SHA256 = /^[0-9a-f]{64}$/;
19
+ /** The identity a release manifest's signature must carry. */
20
+ export class ReleaseSigner {
21
+ /** OIDC issuer the certificate must have been issued for. */
22
+ issuer;
23
+ /** CircleCI project API URL the certificate subject must belong to. */
24
+ project;
25
+ /** Pipeline definition the subject must name exactly, or any of the project. */
26
+ definition;
27
+ /** Sigstore trusted root to verify against, or Sigstore's production instance. */
28
+ trustedRootPath;
29
+ constructor(init) {
30
+ this.issuer = init.issuer;
31
+ this.project = init.project;
32
+ this.definition = init.definition;
33
+ this.trustedRootPath = init.trustedRootPath;
34
+ }
35
+ /** Certificate subject required, or undefined when any definition is accepted. */
36
+ get subject() {
37
+ if (this.definition === undefined) {
38
+ return undefined;
39
+ }
40
+ return this.project + PIPELINE_DEFINITIONS + this.definition;
41
+ }
42
+ /** The identity, for an error message. */
43
+ describe() {
44
+ const subject = this.subject ?? `${this.project}${PIPELINE_DEFINITIONS}*`;
45
+ return `issuer ${this.issuer}, subject ${subject}`;
46
+ }
47
+ /**
48
+ * The verification policy for this identity. The subject pattern is anchored,
49
+ * since sigstore matches it as an unanchored regular expression.
50
+ */
51
+ policy() {
52
+ const subject = this.subject;
53
+ const pattern = subject === undefined
54
+ ? // No definition is pinned, so any definition of the project is accepted:
55
+ // no unauthenticated API publishes the identifier to pin.
56
+ escapeForRegExp(this.project + PIPELINE_DEFINITIONS) + DEFINITION_ID
57
+ : escapeForRegExp(subject);
58
+ return {
59
+ extensions: { issuer: this.issuer },
60
+ subjectAlternativeName: `^${pattern}$`,
61
+ };
62
+ }
63
+ /** A verifier holding the root of trust this signer's certificates chain to. */
64
+ async verifier(sigstore) {
65
+ const root = this.trustedRootPath === undefined
66
+ ? await sigstore.tuf.getTrustedRoot({ timeout: TRUST_ROOT_TIMEOUT_MS })
67
+ : sigstore.specs.TrustedRoot.fromJSON(JSON.parse(await readFile(this.trustedRootPath, "utf8")));
68
+ return new sigstore.verify.Verifier(sigstore.verify.toTrustMaterial(root));
69
+ }
70
+ }
71
+ /**
72
+ * Identity each repository's release manifest must be signed with: the CircleCI OIDC
73
+ * issuer of its organization and its project. A repository absent here signs nothing.
74
+ */
75
+ export const SIGNED_MANIFEST_SIGNERS = {
76
+ "Open-MBEE/OpenSysML": new ReleaseSigner({
77
+ issuer: "https://oidc.circleci.com/org/1169df8b-0b59-400f-82d2-c9d8e98bdb62",
78
+ project: "https://circleci.com/api/v2/projects/eeb0dddd-237f-4f02-9e51-8e24caef589d",
79
+ }),
80
+ };
81
+ /** The signer whose signature a repository's release manifest must carry. */
82
+ export function signerFor(githubRepo) {
83
+ return Object.hasOwn(SIGNED_MANIFEST_SIGNERS, githubRepo) ? SIGNED_MANIFEST_SIGNERS[githubRepo] : undefined;
84
+ }
85
+ /** The digest a checksum manifest lists for an asset, as sha256sum writes them. */
86
+ export function manifestDigest(manifest, asset) {
87
+ for (const line of manifest.toString("utf8").split("\n")) {
88
+ const fields = line.split(/\s+/).filter((field) => field !== "");
89
+ if (fields.length !== 2) {
90
+ continue;
91
+ }
92
+ const [digest, name] = fields;
93
+ // sha256sum marks a file it read in binary mode with a leading '*'.
94
+ if (name.replace(/^\*+/, "") !== asset) {
95
+ continue;
96
+ }
97
+ const lowered = digest.toLowerCase();
98
+ return SHA256.test(lowered) ? lowered : undefined;
99
+ }
100
+ return undefined;
101
+ }
102
+ /**
103
+ * Verify a manifest's sigstore bundle against the signer expected: nothing checked at
104
+ * all is an UnsignedReleaseError, a signature that fails a ManifestSignatureError.
105
+ */
106
+ export async function verifyManifest(manifest, bundle, signer) {
107
+ const sigstore = await loadSigstore();
108
+ let read;
109
+ try {
110
+ read = sigstore.bundle.bundleFromJSON(JSON.parse(bundle.toString("utf8")));
111
+ }
112
+ catch (cause) {
113
+ // A truncated download looks like this too, so it is an absent signature
114
+ // rather than evidence of a tampered one.
115
+ throw new UnsignedReleaseError(`the sigstore bundle published for ${MANIFEST_ASSET} could not be read ` +
116
+ `(${describe(cause)}), so the manifest was not verified.`, { cause });
117
+ }
118
+ let verifier;
119
+ try {
120
+ verifier = await signer.verifier(sigstore);
121
+ }
122
+ catch (cause) {
123
+ throw new UnsignedReleaseError(`this client could not load the sigstore root of trust needed to verify ` +
124
+ `${MANIFEST_ASSET} (${describe(cause)}), so the manifest was not verified.`, { cause });
125
+ }
126
+ try {
127
+ verifier.verify(sigstore.verify.toSignedEntity(read, manifest), signer.policy());
128
+ }
129
+ catch (cause) {
130
+ throw new ManifestSignatureError(`the signature on ${MANIFEST_ASSET} does not verify against the release ` +
131
+ `pipeline of this repository (${signer.describe()}): ${describe(cause)}. The ` +
132
+ `manifest, the signature, or both were replaced; nothing was installed.`, { cause });
133
+ }
134
+ }
135
+ /** The digest a signed manifest lists for an asset, once its signature verified. */
136
+ export async function verifiedManifestDigest(manifest, bundle, asset, signer) {
137
+ await verifyManifest(manifest, bundle, signer);
138
+ const digest = manifestDigest(manifest, asset);
139
+ if (digest === undefined) {
140
+ throw new UnsignedReleaseError(`the signed ${MANIFEST_ASSET} of this release lists no SHA-256 digest for ` +
141
+ `${asset}, so that asset is not covered by the signature.`);
142
+ }
143
+ return digest;
144
+ }
145
+ /** The verifier packages, or a refusal explaining that they are not installed. */
146
+ async function loadSigstore() {
147
+ try {
148
+ const [bundle, specs, tuf, verify] = await Promise.all([
149
+ import("@sigstore/bundle"),
150
+ import("@sigstore/protobuf-specs"),
151
+ import("@sigstore/tuf"),
152
+ import("@sigstore/verify"),
153
+ ]);
154
+ return { bundle, specs, tuf, verify };
155
+ }
156
+ catch (cause) {
157
+ throw new UnsignedReleaseError(`this client cannot verify the signature on ${MANIFEST_ASSET}: ` +
158
+ `${describe(cause)}. Install its optional sigstore dependencies, or ask for ` +
159
+ `a release it pins a digest for with version.`, { cause });
160
+ }
161
+ }
162
+ function describe(cause) {
163
+ return cause instanceof Error ? `${cause.name}: ${cause.message}` : String(cause);
164
+ }
165
+ function escapeForRegExp(literal) {
166
+ return literal.replace(/[.*+?^${}()|[\]\\/]/g, String.raw `\$&`);
167
+ }
package/package.json ADDED
@@ -0,0 +1,77 @@
1
+ {
2
+ "name": "@openmbee/opensysml",
3
+ "version": "0.9.1",
4
+ "description": "OpenSysML client: parse, evaluate and instantiate SysML v2 models over the sysml-grpc Connect API",
5
+ "license": "Apache-2.0",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/Open-MBEE/OpenSysML.git",
9
+ "directory": "client/node"
10
+ },
11
+ "homepage": "https://github.com/Open-MBEE/OpenSysML/tree/main/client/node#readme",
12
+ "keywords": [
13
+ "sysml",
14
+ "sysml-v2",
15
+ "mbse",
16
+ "connect",
17
+ "opensysml"
18
+ ],
19
+ "type": "module",
20
+ "engines": {
21
+ "node": ">=20.11"
22
+ },
23
+ "exports": {
24
+ ".": {
25
+ "types": "./dist/node/index.d.ts",
26
+ "browser": "./dist/browser/index.js",
27
+ "default": "./dist/node/index.js"
28
+ },
29
+ "./browser": {
30
+ "types": "./dist/browser/index.d.ts",
31
+ "default": "./dist/browser/index.js"
32
+ }
33
+ },
34
+ "files": [
35
+ "dist",
36
+ "README.md",
37
+ "release-digests.json"
38
+ ],
39
+ "scripts": {
40
+ "build": "tsc -p tsconfig.build.json",
41
+ "build:tools": "tsc -p tsconfig.json",
42
+ "typecheck": "tsc --noEmit -p tsconfig.json",
43
+ "lint": "eslint .",
44
+ "test": "npm run build:tools && node --test \"build/test/**/*.test.js\"",
45
+ "test:coverage": "npm run build:tools && c8 --reporter=text --reporter=lcovonly node --test \"build/test/**/*.test.js\"",
46
+ "example": "npm run build:tools && node scripts/run-example.mjs",
47
+ "examples": "npm run build:tools && node scripts/run-example.mjs --all",
48
+ "conformance": "npm run build:tools && node build/conformance/main.js --dir ../../conformance --repo ../..",
49
+ "platform-packages": "node scripts/build-platform-packages.mjs"
50
+ },
51
+ "dependencies": {
52
+ "@bufbuild/protobuf": "2.14.0",
53
+ "@connectrpc/connect": "2.1.2",
54
+ "@connectrpc/connect-node": "2.1.2",
55
+ "@connectrpc/connect-web": "2.1.2"
56
+ },
57
+ "optionalDependencies": {
58
+ "@openmbee/opensysml-sysml-grpc-darwin-arm64": "0.9.1",
59
+ "@openmbee/opensysml-sysml-grpc-darwin-x64": "0.9.1",
60
+ "@openmbee/opensysml-sysml-grpc-linux-arm64": "0.9.1",
61
+ "@openmbee/opensysml-sysml-grpc-linux-x64": "0.9.1",
62
+ "@openmbee/opensysml-sysml-grpc-win32-x64": "0.9.1",
63
+ "@sigstore/bundle": "4.0.0",
64
+ "@sigstore/protobuf-specs": "0.5.1",
65
+ "@sigstore/tuf": "4.0.2",
66
+ "@sigstore/verify": "3.1.1"
67
+ },
68
+ "devDependencies": {
69
+ "@bufbuild/protoc-gen-es": "2.14.0",
70
+ "@types/make-fetch-happen": "^10.0.4",
71
+ "@types/node": "22.10.5",
72
+ "c8": "11.0.0",
73
+ "eslint": "9.39.5",
74
+ "typescript": "5.7.3",
75
+ "typescript-eslint": "8.67.0"
76
+ }
77
+ }