@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,564 @@
1
+ // Finding the sysml-grpc binary, and fetching one when a release is asked for:
2
+ // $OPENSYSML_BINARY, then the optional per-platform npm package, then a release
3
+ // download shared with the Python client's cache, then $PATH. A download is only
4
+ // ever installed once its bytes match a digest this client shipped, or one from a
5
+ // checksum manifest the release pipeline signed.
6
+ import { createHash } from "node:crypto";
7
+ import { accessSync, constants, existsSync, readFileSync } from "node:fs";
8
+ import { chmod, mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
9
+ import { createRequire } from "node:module";
10
+ import { homedir } from "node:os";
11
+ import { delimiter, dirname, join } from "node:path";
12
+ import { fileURLToPath } from "node:url";
13
+ import { ChecksumMismatchError, DownloadError, OpenSysMLError, UnpinnedReleaseError, UnsignedReleaseError, } from "../core/errors.js";
14
+ import { PLATFORM_PACKAGE_PREFIX } from "../core/package.js";
15
+ import { BUNDLE_ASSET, MANIFEST_ASSET, ReleaseSigner, signerFor, verifiedManifestDigest, } from "./signing.js";
16
+ /** Names a build of sysml-grpc to use, in place of any package, download or cache. */
17
+ export const BINARY_ENV = "OPENSYSML_BINARY";
18
+ /** Names the release to download, when the caller asks for none. */
19
+ export const VERSION_ENV = "OPENSYSML_GRPC_VERSION";
20
+ /** Overrides the repository releases are downloaded from. */
21
+ export const REPO_ENV = "OPENSYSML_GITHUB_REPO";
22
+ /** Repository whose unpinned downloads may be accepted (`1` for any): same-origin trust. */
23
+ export const ALLOW_UNPINNED_ENV = "OPENSYSML_ALLOW_UNPINNED_DOWNLOAD";
24
+ /** Repository releases are downloaded from unless $OPENSYSML_GITHUB_REPO names another. */
25
+ export const DEFAULT_GITHUB_REPO = "Open-MBEE/OpenSysML";
26
+ /** Origin a release publishes its assets at, and the API the latest tag is read from. */
27
+ export const RELEASES_BASE_URL = "https://github.com";
28
+ export const API_BASE_URL = "https://api.github.com";
29
+ /** Per-request timeout: this runs while a service is starting, so it must not hang. */
30
+ export const NETWORK_TIMEOUT_MS = 15_000;
31
+ const SHA256 = /^[0-9a-f]{64}$/;
32
+ /** No usable binary was found, and none could be fetched. */
33
+ export class BinaryNotFoundError extends OpenSysMLError {
34
+ }
35
+ /**
36
+ * The digests this package ships, synced from client/release-digests.json: a pin
37
+ * resolved from outside the published artifact would not be a pin.
38
+ */
39
+ export const PINNED_SHA256 = readPinnedDigests(join(packageRoot(), "release-digests.json"));
40
+ /** The npm package that carries the service binary for this platform. */
41
+ export function platformPackage(platform = process.platform, arch = process.arch) {
42
+ return `${PLATFORM_PACKAGE_PREFIX}${platform}-${arch}`;
43
+ }
44
+ /** The binary's file name on this platform. */
45
+ export function binaryName(platform = process.platform) {
46
+ return platform === "win32" ? "sysml-grpc.exe" : "sysml-grpc";
47
+ }
48
+ /** Repository releases are downloaded from. */
49
+ export function defaultGithubRepo() {
50
+ const named = process.env[REPO_ENV];
51
+ return named !== undefined && named !== "" ? named : DEFAULT_GITHUB_REPO;
52
+ }
53
+ /** Name a release publishes the service binary for a platform under. */
54
+ export function releaseAssetName(platform = process.platform, arch = process.arch) {
55
+ const goos = own(GOOS, platform);
56
+ const goarch = own(GOARCH, arch);
57
+ if (goos === undefined || goarch === undefined || !SUPPORTED_PAIRS.includes(`${goos}-${goarch}`)) {
58
+ throw new BinaryNotFoundError(`no sysml-grpc release is published for ${platform}/${arch}; releases carry ` +
59
+ `${SUPPORTED_PAIRS.join(", ")}. Build the service yourself (make build-grpc) ` +
60
+ `and set $${BINARY_ENV} to it.`);
61
+ }
62
+ const name = `sysml-grpc-${goos}-${goarch}`;
63
+ return goos === "windows" ? `${name}.exe` : name;
64
+ }
65
+ /** The digest this release of the client pins for a release asset. */
66
+ export function pinnedDigest(version, asset, githubRepo, table = PINNED_SHA256) {
67
+ const repo = githubRepo ?? defaultGithubRepo();
68
+ return own(own(own(table, repo), version), asset);
69
+ }
70
+ /** Whether an unpinned download from a repository may fall back to same-origin trust. */
71
+ export function unpinnedDownloadsAllowed(githubRepo) {
72
+ const allowed = (process.env[ALLOW_UNPINNED_ENV] ?? "").trim();
73
+ const lowered = allowed.toLowerCase();
74
+ if (["", "0", "false", "no"].includes(lowered)) {
75
+ return false;
76
+ }
77
+ if (["1", "true", "yes"].includes(lowered)) {
78
+ return true;
79
+ }
80
+ // Naming repositories keeps the trust with the fork it is granted for.
81
+ const repo = githubRepo ?? defaultGithubRepo();
82
+ return allowed.split(",").some((named) => named.trim() === repo);
83
+ }
84
+ /**
85
+ * The digest a download must have: the pin wherever there is one, else the signed
86
+ * manifest's, else nothing, unless same-origin trust was allowed explicitly.
87
+ */
88
+ export function expectedDigest(request) {
89
+ const repo = request.githubRepo ?? defaultGithubRepo();
90
+ const { version, asset, servedDigest, verifiedDigest } = request;
91
+ const pinned = pinnedDigest(version, asset, repo, request.pinnedDigests ?? PINNED_SHA256);
92
+ if (pinned === undefined) {
93
+ if (verifiedDigest !== undefined) {
94
+ // Signed by the pipeline that built the release, so it is not the origin
95
+ // vouching for itself: as good as a pin.
96
+ return verifiedDigest;
97
+ }
98
+ if (!unpinnedDownloadsAllowed(repo)) {
99
+ const unverified = request.unverifiedReason === undefined ? "" : ` and ${request.unverifiedReason}`;
100
+ throw new UnpinnedReleaseError(`this client pins no SHA-256 digest for ${asset} of ${version} of ${repo}` +
101
+ `${unverified}, so the only checksum available is the one served beside the ` +
102
+ `binary, which a compromised release would serve too. Upgrade the client to a ` +
103
+ `release that pins ${version}, ask for a pinned release with version, or ` +
104
+ `accept same-origin trust for this repository by setting ` +
105
+ `$${ALLOW_UNPINNED_ENV}=${repo} (or =1 for any repository).`);
106
+ }
107
+ if (!SHA256.test(servedDigest)) {
108
+ throw new ChecksumMismatchError(`the checksum ${repo} serves beside ${asset} of ${version} is not a SHA-256 ` +
109
+ `digest (${JSON.stringify(servedDigest)}); nothing was installed.`);
110
+ }
111
+ warnWith(request.warn)(`this client pins no digest for ${asset} of ${version} of ${repo}; verifying it ` +
112
+ `against the checksum served beside it, which detects corruption but not a ` +
113
+ `compromised release ($${ALLOW_UNPINNED_ENV} is set).`);
114
+ return servedDigest;
115
+ }
116
+ if (verifiedDigest !== undefined && verifiedDigest !== pinned) {
117
+ throw new ChecksumMismatchError(`Checksum mismatch for ${asset} of ${version}: the signed ${MANIFEST_ASSET} of ` +
118
+ `${repo} lists ${verifiedDigest}, but this client pins ${pinned}. The release ` +
119
+ `was rebuilt after this client pinned it; it was not installed.`);
120
+ }
121
+ if (servedDigest !== pinned) {
122
+ throw new ChecksumMismatchError(`Checksum mismatch for ${asset} of ${version}: ${repo} serves ${servedDigest}, ` +
123
+ `but this client pins ${pinned}. The release was republished with another ` +
124
+ `binary, or the download is being tampered with; it was not installed.`);
125
+ }
126
+ return pinned;
127
+ }
128
+ /** URL a release publishes an asset at. */
129
+ export function releaseDownloadUrl(version, asset, options = {}) {
130
+ const repo = options.githubRepo ?? defaultGithubRepo();
131
+ const base = options.releasesBaseUrl ?? RELEASES_BASE_URL;
132
+ return `${base}/${repo}/releases/download/${version}/${asset}`;
133
+ }
134
+ /** The digest for an asset from the release's signed checksum manifest. */
135
+ export async function signedManifestDigest(version, asset, options = {}) {
136
+ const repo = options.githubRepo ?? defaultGithubRepo();
137
+ const signer = options.signer ?? signerFor(repo);
138
+ if (signer === undefined) {
139
+ throw new UnsignedReleaseError(`this client knows no release pipeline identity for ${repo}, so a signed ` +
140
+ `${MANIFEST_ASSET} of it would not be verifiable`);
141
+ }
142
+ const published = async (name) => {
143
+ const url = releaseDownloadUrl(version, name, { ...options, githubRepo: repo });
144
+ try {
145
+ return await fetchBytes(url);
146
+ }
147
+ catch (cause) {
148
+ throw new UnsignedReleaseError(`${version} of ${repo} publishes no readable ${name} (${url}: ` +
149
+ `${describe(cause)}), so its checksums carry no signature to verify`, { cause });
150
+ }
151
+ };
152
+ const manifest = await published(MANIFEST_ASSET);
153
+ const bundle = await published(BUNDLE_ASSET);
154
+ return verifiedManifestDigest(manifest, bundle, asset, signer);
155
+ }
156
+ /** Resolve the tag of the repository's latest published release. */
157
+ export async function resolveLatestVersion(options = {}) {
158
+ const repo = options.githubRepo ?? defaultGithubRepo();
159
+ const url = `${options.apiBaseUrl ?? API_BASE_URL}/repos/${repo}/releases/latest`;
160
+ let release;
161
+ try {
162
+ release = JSON.parse((await fetchBytes(url)).toString("utf8"));
163
+ }
164
+ catch (cause) {
165
+ throw new DownloadError(`Failed to resolve latest release from ${url}: ${describe(cause)}`, {
166
+ cause,
167
+ });
168
+ }
169
+ const tag = typeof release === "object" && release !== null
170
+ ? release.tag_name
171
+ : undefined;
172
+ if (typeof tag !== "string" || tag === "") {
173
+ throw new DownloadError(`Latest release of ${repo} has no tag name`);
174
+ }
175
+ return tag;
176
+ }
177
+ /** Path the downloaded binary is cached at, shared with the Python client. */
178
+ export function cachedBinaryPath(options = {}) {
179
+ return join(options.cacheDir ?? join(homedir(), ".opensysml", "bin"), binaryName());
180
+ }
181
+ /** Path of the record of which release the cached binary was downloaded from. */
182
+ export function metadataPath(options = {}) {
183
+ return `${cachedBinaryPath(options)}.json`;
184
+ }
185
+ /** Read the record beside the cached binary, or nothing when it says nothing. */
186
+ export async function readMetadata(options = {}) {
187
+ let parsed;
188
+ try {
189
+ parsed = JSON.parse(await readFile(metadataPath(options), "utf8"));
190
+ }
191
+ catch {
192
+ return undefined;
193
+ }
194
+ if (typeof parsed !== "object" || parsed === null) {
195
+ return undefined;
196
+ }
197
+ const record = parsed;
198
+ const { version, sha256, repo } = record;
199
+ if (typeof version !== "string" || typeof sha256 !== "string" || typeof repo !== "string") {
200
+ return undefined;
201
+ }
202
+ return { version, sha256, repo };
203
+ }
204
+ /** Record which release of which repository the cached binary is, and its digest. */
205
+ export async function writeMetadata(version, sha256, options = {}) {
206
+ const path = metadataPath(options);
207
+ await mkdir(dirname(path), { recursive: true });
208
+ const metadata = {
209
+ version,
210
+ sha256,
211
+ repo: options.githubRepo ?? defaultGithubRepo(),
212
+ };
213
+ await writeFile(path, JSON.stringify(metadata));
214
+ }
215
+ /**
216
+ * Release tag the cached binary was downloaded from, if from this repository: the
217
+ * digest is re-checked, so a binary swapped in by hand does not answer for it.
218
+ */
219
+ export async function cachedRelease(options = {}) {
220
+ const recorded = await readMetadata(options);
221
+ if (recorded === undefined) {
222
+ return undefined;
223
+ }
224
+ if (recorded.repo !== (options.githubRepo ?? defaultGithubRepo())) {
225
+ return undefined;
226
+ }
227
+ return (await verifyChecksum(cachedBinaryPath(options), recorded.sha256))
228
+ ? recorded.version
229
+ : undefined;
230
+ }
231
+ /**
232
+ * Why the cached binary is not the release asked for, or nothing when it is: an
233
+ * older cache would otherwise fail on whatever that build cannot do.
234
+ */
235
+ export async function staleCacheReason(version, options = {}) {
236
+ if (version === undefined) {
237
+ return undefined;
238
+ }
239
+ const repo = options.githubRepo ?? defaultGithubRepo();
240
+ let wanted = version;
241
+ if (wanted === "latest") {
242
+ try {
243
+ wanted = await resolveLatestVersion({ ...options, githubRepo: repo });
244
+ }
245
+ catch (error) {
246
+ if (error instanceof DownloadError) {
247
+ // Unreachable releases are no reason to discard a working cache.
248
+ return undefined;
249
+ }
250
+ throw error;
251
+ }
252
+ }
253
+ const have = await cachedRelease({ ...options, githubRepo: repo });
254
+ if (have === wanted) {
255
+ return undefined;
256
+ }
257
+ const path = cachedBinaryPath(options);
258
+ if (have === undefined) {
259
+ const recorded = await readMetadata(options);
260
+ if (recorded !== undefined && recorded.repo !== repo) {
261
+ return (`the binary cached at ${path} was downloaded from ${recorded.repo}, but ` +
262
+ `${wanted} of ${repo} was asked for`);
263
+ }
264
+ return (`the binary cached at ${path} was not downloaded by this client, so which ` +
265
+ `release it is cannot be told, and ${wanted} was asked for`);
266
+ }
267
+ return `the binary cached at ${path} is ${have}, but ${wanted} was asked for`;
268
+ }
269
+ /**
270
+ * Download a release's service binary and install it in the cache, replacing what is
271
+ * there only once the bytes match the digest expected of them.
272
+ */
273
+ export async function downloadBinary(options = {}) {
274
+ const repo = options.githubRepo ?? defaultGithubRepo();
275
+ const asked = options.version ?? "latest";
276
+ const version = asked === "latest" ? await resolveLatestVersion({ ...options, githubRepo: repo }) : asked;
277
+ const settled = { ...options, githubRepo: repo, version };
278
+ const asset = releaseAssetName(options.platform ?? process.platform, options.arch ?? process.arch);
279
+ const binaryUrl = releaseDownloadUrl(version, asset, settled);
280
+ const checksumUrl = releaseDownloadUrl(version, `${asset}.sha256`, settled);
281
+ const binaryPath = cachedBinaryPath(settled);
282
+ const temporaryPath = `${binaryPath}.tmp`;
283
+ await mkdir(dirname(binaryPath), { recursive: true });
284
+ let served;
285
+ try {
286
+ // The sidecar's format is "hexdigest filename".
287
+ served = (await fetchBytes(checksumUrl)).toString("utf8").trim().split(/\s+/)[0] ?? "";
288
+ }
289
+ catch (cause) {
290
+ throw new DownloadError(`Failed to download ${checksumUrl}: ${describe(cause)}`, { cause });
291
+ }
292
+ // A release nothing is pinned for is vouched for by the signature on its checksum
293
+ // manifest instead, which the origin cannot forge.
294
+ let verifiedDigest;
295
+ let unverifiedReason;
296
+ if (pinnedDigest(version, asset, repo, settled.pinnedDigests ?? PINNED_SHA256) === undefined) {
297
+ try {
298
+ verifiedDigest = await signedManifestDigest(version, asset, settled);
299
+ }
300
+ catch (error) {
301
+ if (!(error instanceof UnsignedReleaseError)) {
302
+ throw error;
303
+ }
304
+ unverifiedReason = error.message;
305
+ }
306
+ }
307
+ const expected = expectedDigest({
308
+ version,
309
+ asset,
310
+ servedDigest: served.toLowerCase(),
311
+ githubRepo: repo,
312
+ ...(verifiedDigest === undefined ? {} : { verifiedDigest }),
313
+ ...(unverifiedReason === undefined ? {} : { unverifiedReason }),
314
+ ...(settled.pinnedDigests === undefined ? {} : { pinnedDigests: settled.pinnedDigests }),
315
+ ...(settled.warn === undefined ? {} : { warn: settled.warn }),
316
+ });
317
+ let downloaded;
318
+ try {
319
+ downloaded = await fetchBytes(binaryUrl);
320
+ }
321
+ catch (cause) {
322
+ throw new DownloadError(`Failed to download binary from ${binaryUrl}: ${describe(cause)}`, {
323
+ cause,
324
+ });
325
+ }
326
+ await writeFile(temporaryPath, downloaded);
327
+ if (!(await verifyChecksum(temporaryPath, expected))) {
328
+ await remove(temporaryPath);
329
+ throw new ChecksumMismatchError(`Checksum mismatch for ${asset}. Expected ${expected}, but the download does ` +
330
+ `not match. It may be corrupted or tampered with; it was not installed.`);
331
+ }
332
+ if (process.platform !== "win32") {
333
+ // Before it is in place, so the cache is never a file no one may run.
334
+ // The cache is this user's, so no one else needs it.
335
+ await chmod(temporaryPath, 0o700);
336
+ }
337
+ try {
338
+ // rename overwrites the cache being replaced, and is atomic within a directory.
339
+ await rename(temporaryPath, binaryPath);
340
+ }
341
+ catch (cause) {
342
+ await remove(temporaryPath);
343
+ throw new DownloadError(`Downloaded ${version} but could not install it at ${binaryPath}: ` +
344
+ `${describe(cause)}. A running service holding that file is the usual cause.`, { cause });
345
+ }
346
+ await writeMetadata(version, expected, settled);
347
+ return binaryPath;
348
+ }
349
+ /** Whether a file's SHA-256 digest is the one expected of it. */
350
+ export async function verifyChecksum(path, expectedSha256) {
351
+ let content;
352
+ try {
353
+ content = await readFile(path);
354
+ }
355
+ catch {
356
+ return false;
357
+ }
358
+ return createHash("sha256").update(content).digest("hex") === expectedSha256;
359
+ }
360
+ /**
361
+ * Finds a sysml-grpc binary: $OPENSYSML_BINARY, then the platform package, then a
362
+ * release download into the cache the Python client shares, then $PATH. A release is
363
+ * downloaded only when one is asked for, by `version` or $OPENSYSML_GRPC_VERSION.
364
+ */
365
+ export async function resolveBinary(options = {}) {
366
+ const looked = [];
367
+ const named = process.env[BINARY_ENV];
368
+ if (named !== undefined && named !== "") {
369
+ if (!isExecutable(named)) {
370
+ throw new BinaryNotFoundError(`$${BINARY_ENV} names ${named}, which is not an executable file`);
371
+ }
372
+ return { path: named, source: `${named} (from $${BINARY_ENV})` };
373
+ }
374
+ looked.push(`$${BINARY_ENV}`);
375
+ const packaged = fromPlatformPackage();
376
+ if (packaged !== undefined) {
377
+ return packaged;
378
+ }
379
+ looked.push(`the ${platformPackage()} package`);
380
+ const installed = await fromRelease(options);
381
+ if (installed !== undefined) {
382
+ return installed;
383
+ }
384
+ looked.push(cachedBinaryPath(options));
385
+ const onPath = fromPath();
386
+ if (onPath !== undefined) {
387
+ return onPath;
388
+ }
389
+ looked.push("$PATH");
390
+ throw new BinaryNotFoundError(`no sysml-grpc binary: looked at ${looked.join(", ")}.\n` +
391
+ ` fix: install the service for this platform (npm install ${platformPackage()}), or\n` +
392
+ ` ask for a release to download by setting $${VERSION_ENV} (e.g. latest), or\n` +
393
+ ` build it (make build-grpc) and set $${BINARY_ENV} to the result, or\n` +
394
+ ` start a service yourself and pass its address to connect().`);
395
+ }
396
+ /** The cached binary, or a release downloaded into the cache when one is asked for. */
397
+ async function fromRelease(options) {
398
+ const asked = options.version ?? process.env[VERSION_ENV];
399
+ const version = asked === undefined || asked === "" ? undefined : asked;
400
+ const request = { ...options, ...(version === undefined ? {} : { version }) };
401
+ const path = cachedBinaryPath(request);
402
+ const warn = warnWith(options.warn);
403
+ let cached;
404
+ if (isExecutable(path)) {
405
+ const stale = await staleCacheReason(version, request);
406
+ if (stale === undefined) {
407
+ return { path, source: `${path} (cached)` };
408
+ }
409
+ cached = { path, source: `${path} (cached, kept)` };
410
+ warn(`Replacing the cached sysml-grpc: ${stale}. Downloading ${version ?? "latest"}.`);
411
+ }
412
+ if (version === undefined) {
413
+ // Nothing cached and no release asked for: auto-download is off.
414
+ return undefined;
415
+ }
416
+ try {
417
+ const installed = await downloadBinary(request);
418
+ return { path: installed, source: `${installed} (downloaded ${version} from ${request.githubRepo ?? defaultGithubRepo()})` };
419
+ }
420
+ catch (error) {
421
+ if (error instanceof UnpinnedReleaseError) {
422
+ // A release this client pins nothing for contradicts nothing, so a working
423
+ // cache stands.
424
+ if (cached === undefined) {
425
+ throw error;
426
+ }
427
+ warn(`Keeping the cached sysml-grpc at ${cached.path}: ${version} was not downloaded ` +
428
+ `(${error.message}). It may be an older release than asked for.`);
429
+ return cached;
430
+ }
431
+ if (error instanceof ChecksumMismatchError) {
432
+ // A download that may have been tampered with is never answered from the cache.
433
+ throw error;
434
+ }
435
+ if (error instanceof DownloadError && cached !== undefined) {
436
+ // A release with no binary to fetch is no reason to lose a working one.
437
+ warn(`Keeping the cached sysml-grpc at ${cached.path}: ${version} could not be ` +
438
+ `downloaded (${error.message}). It may be an older release than asked for.`);
439
+ return cached;
440
+ }
441
+ throw error;
442
+ }
443
+ }
444
+ function fromPlatformPackage() {
445
+ const name = platformPackage();
446
+ const require_ = createRequire(import.meta.url);
447
+ let manifest;
448
+ try {
449
+ manifest = require_.resolve(`${name}/package.json`);
450
+ }
451
+ catch {
452
+ // An optional dependency npm skipped for this platform, or none published for it.
453
+ return undefined;
454
+ }
455
+ const path = join(manifest, "..", "bin", binaryName());
456
+ if (!isExecutable(path)) {
457
+ throw new BinaryNotFoundError(`the ${name} package is installed but holds no executable at ${path}; reinstall it`);
458
+ }
459
+ return { path, source: `${path} (from ${name})` };
460
+ }
461
+ function fromPath() {
462
+ const directories = (process.env["PATH"] ?? "").split(delimiter).filter((entry) => entry !== "");
463
+ for (const directory of directories) {
464
+ const candidate = join(directory, binaryName());
465
+ if (isExecutable(candidate)) {
466
+ return { path: candidate, source: `${candidate} (on $PATH)` };
467
+ }
468
+ }
469
+ return undefined;
470
+ }
471
+ function isExecutable(path) {
472
+ try {
473
+ accessSync(path, constants.X_OK);
474
+ return true;
475
+ }
476
+ catch {
477
+ return false;
478
+ }
479
+ }
480
+ /** Release assets are published for these GOOS/GOARCH pairs, and no others. */
481
+ const SUPPORTED_PAIRS = ["linux-amd64", "linux-arm64", "darwin-amd64", "darwin-arm64", "windows-amd64"];
482
+ const GOOS = {
483
+ linux: "linux",
484
+ darwin: "darwin",
485
+ win32: "windows",
486
+ };
487
+ const GOARCH = {
488
+ x64: "amd64",
489
+ arm64: "arm64",
490
+ };
491
+ async function fetchBytes(url) {
492
+ const response = await fetch(url, {
493
+ redirect: "follow",
494
+ signal: AbortSignal.timeout(NETWORK_TIMEOUT_MS),
495
+ });
496
+ if (!response.ok) {
497
+ throw new Error(`HTTP ${response.status} ${response.statusText}`);
498
+ }
499
+ return Buffer.from(await response.arrayBuffer());
500
+ }
501
+ async function remove(path) {
502
+ try {
503
+ await unlink(path);
504
+ }
505
+ catch {
506
+ // Already gone, which is all this was for.
507
+ }
508
+ }
509
+ function warnWith(warn) {
510
+ return (warn ??
511
+ ((message) => {
512
+ process.emitWarning(message);
513
+ }));
514
+ }
515
+ function describe(cause) {
516
+ return cause instanceof Error ? `${cause.name}: ${cause.message}` : String(cause);
517
+ }
518
+ /** The directory this package's own files sit in, which is what npm publishes. */
519
+ function packageRoot() {
520
+ let directory = dirname(fileURLToPath(import.meta.url));
521
+ for (;;) {
522
+ if (existsSync(join(directory, "package.json"))) {
523
+ return directory;
524
+ }
525
+ const parent = dirname(directory);
526
+ if (parent === directory) {
527
+ throw new OpenSysMLError("this client's package directory could not be found, so the pinned release " +
528
+ "digests it ships cannot be read");
529
+ }
530
+ directory = parent;
531
+ }
532
+ }
533
+ function readPinnedDigests(path) {
534
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
535
+ const table = {};
536
+ for (const [repo, releases] of Object.entries(asObject(parsed, path))) {
537
+ const byVersion = {};
538
+ for (const [version, assets] of Object.entries(asObject(releases, path))) {
539
+ const byAsset = {};
540
+ for (const [asset, digest] of Object.entries(asObject(assets, path))) {
541
+ if (typeof digest !== "string" || !SHA256.test(digest)) {
542
+ throw new OpenSysMLError(`${path} pins ${JSON.stringify(digest)} for ${asset} of ${version} of ` +
543
+ `${repo}, which is not a SHA-256 digest`);
544
+ }
545
+ byAsset[asset] = digest;
546
+ }
547
+ byVersion[version] = byAsset;
548
+ }
549
+ table[repo] = byVersion;
550
+ }
551
+ return table;
552
+ }
553
+ function asObject(value, path) {
554
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
555
+ throw new OpenSysMLError(`${path} is not the pinned digest table this client expects`);
556
+ }
557
+ return value;
558
+ }
559
+ function own(record, key) {
560
+ if (record === undefined || !Object.hasOwn(record, key)) {
561
+ return undefined;
562
+ }
563
+ return record[key];
564
+ }
@@ -0,0 +1,31 @@
1
+ import { Connection } from "../core/connection.js";
2
+ import type { TransportOptions } from "../core/connection.js";
3
+ import { Model } from "../core/model.js";
4
+ import type { ParseOptions } from "../core/model.js";
5
+ export * from "../core/index.js";
6
+ 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";
7
+ export type { Binary, CacheMetadata, DownloadOptions, PinnedDigests } from "./binary.js";
8
+ export { BUNDLE_ASSET, MANIFEST_ASSET, ReleaseSigner, SIGNED_MANIFEST_SIGNERS, manifestDigest, signerFor, verifiedManifestDigest, verifyManifest, } from "./signing.js";
9
+ export { PrivateService, currentPrivateService } from "./service.js";
10
+ /** Names a service to connect to instead of starting one. */
11
+ export declare const SERVICE_ENV = "OPENSYSML_SERVICE";
12
+ /** How this process connects. Without an address it starts a private child. */
13
+ export interface ConnectOptions extends TransportOptions {
14
+ /** HTTP version used to reach the service. HTTP/2 (h2c over plain http) by default. */
15
+ httpVersion?: "1.1" | "2";
16
+ /** Wire protocol. Connect by default; gRPC needs HTTP/2 and a service that serves it. */
17
+ protocol?: "connect" | "grpc";
18
+ }
19
+ /**
20
+ * Connects to a sysml-grpc service.
21
+ *
22
+ * With no address, this starts a private child of this process — one per thread,
23
+ * shared by every connection, stopped when the last one closes. With an address,
24
+ * or with `$OPENSYSML_SERVICE` set, it connects to a service someone else runs and
25
+ * closing the connection leaves that service running.
26
+ */
27
+ export declare function connect(options?: ConnectOptions): Promise<Connection>;
28
+ /** Parses a file over a connection of its own, which the model closes. */
29
+ export declare function load(path: string, options?: ConnectOptions & ParseOptions): Promise<Model>;
30
+ /** Parses inline source over a connection of its own, which the model closes. */
31
+ export declare function loads(source: string, options?: ConnectOptions & ParseOptions): Promise<Model>;