@orkestrel/scaffold 0.0.25 → 0.0.26

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,7 +1,7 @@
1
1
  import { andOf, arrayOf, attempt, boundsOf, holds, isArray, isBoolean, isError, isFunction, isInteger, isRecord, isString, parseJSON, parseJSONAs, parseStringField, recordOf, stringOf } from "@orkestrel/contract";
2
- import { CATALOG_AGENT_PATH, CONTROL_CHARACTER_PATTERN, HOST_PATHS, MAX_ARTIFACT_BYTES, MAX_COLLECTION_ITEMS, MAX_MANIFEST_BYTES, MAX_PATH_LENGTH, MAX_RANGE_LENGTH, MAX_TOTAL_ARTIFACT_BYTES, ScaffoldError, bytesToHex, cloneValue, computeBytes, contentToHex, inferGroup, isAudit, isCatalogEntry, isCollection, isDependency, isDependencyName, isMirror, isPath, isPlan, isSnapshot, nameToGuide, planToFindings } from "../core/index.js";
2
+ import { CATALOG_AGENT_PATH, CONTROL_CHARACTER_PATTERN, EXECUTABLE_PATHS, HOST_PATHS, MAX_ARTIFACT_BYTES, MAX_COLLECTION_ITEMS, MAX_MANIFEST_BYTES, MAX_PATH_LENGTH, MAX_RANGE_LENGTH, MAX_TOTAL_ARTIFACT_BYTES, ScaffoldError, bytesToHex, cloneValue, computeBytes, contentToHex, inferGroup, isAudit, isCatalogEntry, isCollection, isDependency, isDependencyName, isMirror, isPath, isPlan, isSnapshot, matchesDriftReachability, nameToGuide, planToFindings } from "../core/index.js";
3
3
  import { createHash, randomUUID } from "node:crypto";
4
- import { chmodSync, closeSync, constants, copyFileSync, fstatSync, linkSync, lstatSync, mkdirSync, openSync, opendirSync, readSync, readdirSync, realpathSync, renameSync, rmSync, rmdirSync, writeFileSync } from "node:fs";
4
+ import { chmodSync, closeSync, constants, copyFileSync, fstatSync, linkSync, lstatSync, mkdirSync, openSync, opendirSync, readSync, readdirSync, readlinkSync, realpathSync, renameSync, rmSync, rmdirSync, writeFileSync } from "node:fs";
5
5
  import { basename, dirname, join, parse, relative, resolve, sep } from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import { Emitter } from "@orkestrel/emitter";
@@ -130,12 +130,23 @@ var MANIFEST_NAME = "manifest.json";
130
130
  * the byte ceiling. The character ceiling is read first so an oversized string is
131
131
  * refused before it is split.
132
132
  *
133
+ * The two spellings of an empty segment are answered differently. A trailing
134
+ * separator terminates a directory rather than opening a segment, and every
135
+ * supported filesystem and every Node path API reads `project/` and `project` as
136
+ * one location, so it is admitted. A doubled separator is a genuine empty
137
+ * segment, so `project//src` is refused. Nothing normalizes the argument first —
138
+ * every server entry point guards the caller's text and resolves it afterwards —
139
+ * so a directory taken from a shell completion arrives carrying the separator the
140
+ * shell appended and names the directory it appears to name.
141
+ *
133
142
  * @example
134
143
  * ```ts
135
144
  * import { isFilesystemPath } from '@orkestrel/scaffold/server'
136
145
  *
137
146
  * isFilesystemPath('C:/Users/sample/project') // true
138
147
  * isFilesystemPath('../sibling') // true
148
+ * isFilesystemPath('project/') // true
149
+ * isFilesystemPath('project//src') // false
139
150
  * isFilesystemPath('project/nul') // false
140
151
  * ```
141
152
  */
@@ -144,7 +155,8 @@ function isFilesystemPath(value) {
144
155
  if (!isString(value) || value.length === 0 || value.length > MAX_PATH_LENGTH) return false;
145
156
  if (CONTROL_CHARACTER_PATTERN.test(value)) return false;
146
157
  const normalized = value.replaceAll("\\", "/");
147
- const segments = (normalized.startsWith("//") ? normalized.slice(2) : normalized.startsWith("/") ? normalized.slice(1) : normalized).split("/");
158
+ const rooted = normalized.startsWith("//") ? normalized.slice(2) : normalized.startsWith("/") ? normalized.slice(1) : normalized;
159
+ const segments = (rooted.endsWith("/") ? rooted.slice(0, -1) : rooted).split("/");
148
160
  if (segments.length > 64) return false;
149
161
  for (const [index, segment] of segments.entries()) {
150
162
  if (segment === "." || segment === "..") continue;
@@ -509,6 +521,32 @@ function matchesSensitivePath(path) {
509
521
  return /(?:^|\/)(?:(?:\.ssh|\.aws|\.azure|\.docker|\.kube|\.gnupg|\.env(?:\.[^/]*)?)(?:\/|$)|(?:\.npmrc|\.pypirc|\.netrc|\.git-credentials|settings\.local\.json|auth\.json|credentials(?:\.json)?|application_default_credentials\.json|id_rsa|id_ed25519|kubeconfig)$|\.config\/(?:gh|gcloud)(?:\/|$)|\.local\/share\/keyrings(?:\/|$)|[^/]*service-account[^/]*\.json$|[^/]*\.(?:jks|key|p12|pem|pfx|pkcs12)$)/i.test(normalized);
510
522
  }
511
523
  /**
524
+ * Test whether a vendored path is one a target receives executable.
525
+ *
526
+ * @param path - The target-relative path to classify; either separator is read.
527
+ * @returns `true` when the path is declared in {@link EXECUTABLE_PATHS}.
528
+ *
529
+ * @remarks
530
+ * The declaration is the whole answer, and deliberately so. Reading the staging
531
+ * host's mode instead makes the manifest depend on where the package was built:
532
+ * Windows carries no executable bit, so a host staged there declares every entry
533
+ * non-executable and every target it later fills receives hooks at `0644`. One
534
+ * checkout stages one manifest on every host because this predicate never
535
+ * consults the filesystem.
536
+ *
537
+ * @example
538
+ * ```ts
539
+ * import { matchesExecutablePath } from '@orkestrel/scaffold/server'
540
+ *
541
+ * matchesExecutablePath('scripts/codex.sh') // true
542
+ * matchesExecutablePath('scripts\\deps.sh') // true
543
+ * matchesExecutablePath('AGENTS.md') // false
544
+ * ```
545
+ */
546
+ function matchesExecutablePath(path) {
547
+ return EXECUTABLE_PATHS.includes(path.replaceAll("\\", "/"));
548
+ }
549
+ /**
512
550
  * Project a target-relative path to the storage name a vendored host holds it under.
513
551
  *
514
552
  * @param path - The target-relative path the file is written to.
@@ -727,9 +765,10 @@ function computeFileDigest(path) {
727
765
  * Resolve a path through the real filesystem, keeping the part that does not exist yet.
728
766
  *
729
767
  * @param path - The absolute or relative host path to resolve.
730
- * @returns The path with its existing prefix resolved through every link, or
731
- * `undefined` when the text is not a host path, no bounded existing ancestor
732
- * resolves, or an ancestor cannot be read.
768
+ * @returns The lexical resolution of `path`, with its existing prefix then
769
+ * resolved through every link, or `undefined` when the text is not a host path,
770
+ * no bounded existing ancestor resolves, a link target cannot be read, a link
771
+ * target carries a `..` segment, or an ancestor cannot be read.
733
772
  *
734
773
  * @remarks
735
774
  * A containment decision has to be made about a destination that does not exist
@@ -739,6 +778,30 @@ function computeFileDigest(path) {
739
778
  * it. The climb is bounded by the path-depth ceiling, so an adversarial path
740
779
  * cannot make it walk indefinitely.
741
780
  *
781
+ * The caller's own text is collapsed first, which is what `resolve` does with a
782
+ * `..` the caller wrote: it cancels the segment before it as text, before any
783
+ * link in that segment is read. So `<root>/hop/..` answers `<root>` even where
784
+ * `hop` links elsewhere, rather than the directory holding what `hop` points at.
785
+ * The collapse only ever shortens the caller's path, so nothing reaches outside
786
+ * it by this; the answer is that lexical location resolved through links, not
787
+ * the physical location the links lead to. {@link resolveContainedPath} passes
788
+ * its `root` through here, so a root written with a parent segment is contained
789
+ * against its collapsed spelling.
790
+ *
791
+ * `realpath` answers `ENOENT` both for a name that is not there and for a link
792
+ * whose target is not there. The name is therefore inspected without following
793
+ * it: a dangling link redirects the walk to its target, while a genuinely absent
794
+ * name is retained as one segment of the unresolved suffix. A dangling link
795
+ * target containing a `..` segment is refused. Resolving that target as one
796
+ * lexical string could discard a preceding link before the filesystem gives
797
+ * `..` its physical meaning.
798
+ *
799
+ * That target is split on both separators on every host, which is the reading
800
+ * `isPath` already gives a planned path. A POSIX filename legally containing a
801
+ * backslash is therefore refused with it: `weird\..\name` is one name to the
802
+ * host and three segments here. The package keeps one separator law rather than
803
+ * a host-dependent second one, and this is the conservative side of it.
804
+ *
742
805
  * @example
743
806
  * ```ts
744
807
  * import { resolveRealPath } from '@orkestrel/scaffold/server'
@@ -758,6 +821,16 @@ function resolveRealPath(path) {
758
821
  return physical;
759
822
  }
760
823
  if (!matchesMissingPath(real.error)) return void 0;
824
+ const status = attempt(() => lstatSync(current));
825
+ if (status.success) {
826
+ if (!status.value.isSymbolicLink()) return void 0;
827
+ const target = attempt(() => readlinkSync(current));
828
+ if (!target.success) return void 0;
829
+ if (target.value.split(/[\\/]/u).includes("..")) return void 0;
830
+ current = resolve(dirname(current), target.value);
831
+ continue;
832
+ }
833
+ if (!matchesMissingPath(status.error)) return void 0;
761
834
  const parent = dirname(current);
762
835
  if (parent === current) return void 0;
763
836
  pending.unshift(relative(parent, current));
@@ -774,20 +847,31 @@ function resolveRealPath(path) {
774
847
  *
775
848
  * @remarks
776
849
  * The containment law, and the one door every read in this module goes through.
777
- * Both sides are resolved through the real filesystem before they are compared,
778
- * so a link planted inside the root cannot smuggle a destination out of it; the
779
- * answer is then the lexical join, so the caller operates on the path it named
780
- * rather than on a resolved form the target may not recognize.
850
+ * Both sides are resolved through the real filesystem before they are compared.
851
+ * A dangling link is followed only when its raw target contains no parent
852
+ * traversal. The answer is then the lexical join of `root` and `path` an
853
+ * absolute path under `root`, not a root-relative one so the caller operates
854
+ * on the path it named rather than on a resolved form the target may not
855
+ * recognize. A `root` written with a parent segment is collapsed by that
856
+ * resolution before anything is read, so containment is measured against the
857
+ * directory the caller's text names.
781
858
  *
782
859
  * Comparison is exact text, which fails closed on a case-insensitive
783
860
  * filesystem: a root and a path spelled with different case resolve to
784
861
  * different strings there and are refused, never wrongly admitted.
785
862
  *
863
+ * The answer describes the namespace this call read. The contract excludes a
864
+ * concurrent rename or link swap during the call or before the caller finishes
865
+ * using the returned path. This helper returns a string, not a filesystem
866
+ * handle, so it cannot bind its containment check to a later operation. A caller
867
+ * that admits hostile concurrent namespace mutation needs a handle-bound
868
+ * operation instead.
869
+ *
786
870
  * @example
787
871
  * ```ts
788
872
  * import { resolveContainedPath } from '@orkestrel/scaffold/server'
789
873
  *
790
- * resolveContainedPath('/tmp/project', 'guides/router.md') // '/tmp/project/guides/router.md'
874
+ * resolveContainedPath('/tmp/project', 'guides/router.md')?.endsWith('router.md') // true
791
875
  * resolveContainedPath('/tmp/project', '../secrets') // undefined
792
876
  * ```
793
877
  */
@@ -864,7 +948,7 @@ function isVacant(target) {
864
948
  * ```ts
865
949
  * import { listFiles } from '@orkestrel/scaffold/server'
866
950
  *
867
- * listFiles('./dist/host') // ['AGENTS.md', 'claude/rules/names.md', …]
951
+ * listFiles('./dist/host') // ['AGENTS.md', 'CLAUDE.md', 'LICENSE', …]
868
952
  * ```
869
953
  */
870
954
  function listFiles(root) {
@@ -1238,14 +1322,14 @@ function readHostManifest(host) {
1238
1322
  *
1239
1323
  * @remarks
1240
1324
  * The one place the three declared fields are decided together, because they are
1241
- * three readings of one file: {@link pathToStorage} decides where it is stored,
1242
- * the destination is the path it answers for, and the executable bit is read
1243
- * from the source's own mode.
1325
+ * three readings of one path: {@link pathToStorage} decides where it is stored,
1326
+ * the destination is the path it answers for, and {@link matchesExecutablePath}
1327
+ * decides whether a target receives it executable.
1244
1328
  *
1245
- * That mode is the honest limit of this reading. A Windows host reports no
1246
- * executable bit at all, so a host staged there declares every entry
1247
- * non-executable and a consumer receives scripts without it. Staging on a POSIX
1248
- * host is what carries the bit through.
1329
+ * The bit is read from that declaration rather than from the source's mode, so
1330
+ * the entry does not depend on where the package was staged. A Windows host
1331
+ * reports no executable bit at all, and reading the mode there declared every
1332
+ * entry non-executable and shipped consumers hooks they could not run.
1249
1333
  *
1250
1334
  * @example
1251
1335
  * ```ts
@@ -1262,7 +1346,7 @@ function readManifestEntry(destination, source) {
1262
1346
  return {
1263
1347
  storage: pathToStorage(destination),
1264
1348
  destination,
1265
- executable: (status.value.mode & 73) !== 0
1349
+ executable: matchesExecutablePath(destination)
1266
1350
  };
1267
1351
  }
1268
1352
  /**
@@ -1432,8 +1516,8 @@ function stageHost(checkout, host) {
1432
1516
  * @remarks
1433
1517
  * Device and inode rather than the path, because the path is the thing that can
1434
1518
  * be swapped underneath a write. An anchor captured before a mutation and
1435
- * checked again after it is what proves the directory written into is the
1436
- * directory that was inspected.
1519
+ * checked again after it proves the directory written into sits where the
1520
+ * inspected one sat, not that it is the one that was inspected.
1437
1521
  *
1438
1522
  * @example
1439
1523
  * ```ts
@@ -1459,9 +1543,13 @@ function readAnchor(path) {
1459
1543
  * device and inode.
1460
1544
  *
1461
1545
  * @remarks
1462
- * The check a write repeats between steps. A directory replaced by another
1463
- * directory of the same name answers `false` here, which is the case a path
1464
- * comparison alone cannot see.
1546
+ * This binds location rather than history. `true` means the path still resolves
1547
+ * to the same physical directory on the same device, so the next write lands
1548
+ * where the last one did. A path now holding nothing, a file, or a symlink
1549
+ * answers `false`; a directory swapped in by `rename` also answers `false`
1550
+ * because the replacement carries its own inode. A directory deleted and made
1551
+ * again under the same name can receive the old inode back and answers `true`,
1552
+ * which nothing here detects.
1465
1553
  *
1466
1554
  * @example
1467
1555
  * ```ts
@@ -1607,6 +1695,12 @@ function matchesPrecondition(precondition) {
1607
1695
  * - **No partly written destination.** Every file is written whole into the
1608
1696
  * private root and digested there before commit, so a destination never
1609
1697
  * receives bytes that were still being produced.
1698
+ * - **Containment, not continuity, of the directories it creates.** Every
1699
+ * ancestor is re-read between `mkdir` calls and again before the first
1700
+ * promotion, so an ancestor that became a file, a symlink, a directory
1701
+ * elsewhere, or nothing is refused. An ancestor deleted and recreated under
1702
+ * the same name can receive its old inode back and is indistinguishable here
1703
+ * from one that never moved.
1610
1704
  * - **No crash atomicity across destinations.** A process killed between two
1611
1705
  * promotions leaves the target holding some new files and some old ones, and
1612
1706
  * leaves the private root behind. Nothing here is a journal, and the private
@@ -1701,7 +1795,7 @@ var WriteTransaction = class {
1701
1795
  this.#stage = join(this.#root, "stage");
1702
1796
  this.#backup = join(this.#root, "backup");
1703
1797
  const opened = attempt(() => {
1704
- this.#created.push(...this.#establish(parent).created);
1798
+ this.#establish(parent);
1705
1799
  for (const directory of [
1706
1800
  this.#root,
1707
1801
  this.#stage,
@@ -1793,7 +1887,7 @@ var WriteTransaction = class {
1793
1887
  const staged = this.#stagePath(path);
1794
1888
  const copied = attempt(() => {
1795
1889
  copyFileSync(source, staged, constants.COPYFILE_EXCL);
1796
- if (executable) chmodSync(staged, 493);
1890
+ chmodSync(staged, executable ? 493 : 420);
1797
1891
  });
1798
1892
  if (!copied.success) throw new ScaffoldError("WRITE", `The staged copy at ${path} could not be made.`, {
1799
1893
  path,
@@ -1825,8 +1919,12 @@ var WriteTransaction = class {
1825
1919
  directory(path) {
1826
1920
  this.#assertOpen();
1827
1921
  if (this.#expectation(path).shape === "file") throw new ScaffoldError("TARGET", `The destination at ${path} holds a file.`, { path });
1828
- const result = this.#establish(this.#resolve(this.#target, path));
1829
- this.#created.push(...result.created);
1922
+ const established = attempt(() => this.#establish(this.#resolve(this.#target, path)));
1923
+ if (!established.success) throw new ScaffoldError("WRITE", `The directory at ${path} could not be established.`, {
1924
+ path,
1925
+ error: established.error
1926
+ });
1927
+ const result = established.value;
1830
1928
  if (result.created.length > 0 && !this.#established.includes(path)) this.#established.push(path);
1831
1929
  return result;
1832
1930
  }
@@ -1870,14 +1968,8 @@ var WriteTransaction = class {
1870
1968
  const taken = [];
1871
1969
  const applied = attempt(() => {
1872
1970
  this.#preflight();
1873
- for (const path of this.#staged) {
1874
- this.#promote(path);
1875
- promoted.push(path);
1876
- }
1877
- for (const path of this.#taken) {
1878
- this.#take(path);
1879
- taken.push(path);
1880
- }
1971
+ for (const path of this.#staged) this.#promote(path, promoted);
1972
+ for (const path of this.#taken) this.#take(path, taken);
1881
1973
  });
1882
1974
  this.#open = false;
1883
1975
  if (!applied.success) {
@@ -1977,9 +2069,11 @@ var WriteTransaction = class {
1977
2069
  ancestor: anchor.path
1978
2070
  });
1979
2071
  mkdirSync(segment);
2072
+ this.#created.push(segment);
1980
2073
  const established = readAnchor(segment);
1981
2074
  if (established === void 0) throw new ScaffoldError("WRITE", `The directory at ${segment} changed while writing.`, { path: destination });
1982
2075
  anchor = established;
2076
+ this.#created[this.#created.length - 1] = established;
1983
2077
  created.push(established);
1984
2078
  }
1985
2079
  return {
@@ -1988,10 +2082,16 @@ var WriteTransaction = class {
1988
2082
  };
1989
2083
  }
1990
2084
  #preflight() {
1991
- for (const anchor of this.#created) if (!matchesAnchor(anchor)) throw new ScaffoldError("WRITE", `The directory at ${anchor.path} changed while writing.`, {
1992
- target: this.#target,
1993
- path: anchor.path
1994
- });
2085
+ for (const anchor of this.#created) {
2086
+ if (typeof anchor === "string") throw new ScaffoldError("WRITE", `The directory at ${anchor} changed while writing.`, {
2087
+ target: this.#target,
2088
+ path: anchor
2089
+ });
2090
+ if (!matchesAnchor(anchor)) throw new ScaffoldError("WRITE", `The directory at ${anchor.path} changed while writing.`, {
2091
+ target: this.#target,
2092
+ path: anchor.path
2093
+ });
2094
+ }
1995
2095
  for (const path of [...this.#staged, ...this.#taken]) if (!matchesExpectation(this.#expectation(path))) throw new ScaffoldError("WRITE", `The destination at ${path} moved while writing.`, {
1996
2096
  target: this.#target,
1997
2097
  path
@@ -2001,24 +2101,26 @@ var WriteTransaction = class {
2001
2101
  path
2002
2102
  });
2003
2103
  }
2004
- #promote(path) {
2104
+ #promote(path, promoted) {
2005
2105
  const expectation = this.#expectation(path);
2006
2106
  if (!matchesExpectation(expectation)) throw new ScaffoldError("WRITE", `The destination at ${path} moved while writing.`, { path });
2007
2107
  const staged = this.#resolve(this.#stage, path);
2008
- if (expectation.shape === "absent") this.#created.push(...this.#establish(dirname(expectation.path)).created);
2108
+ if (expectation.shape === "absent") this.#establish(dirname(expectation.path));
2009
2109
  else {
2010
2110
  const backup = this.#resolve(this.#backup, path);
2011
2111
  mkdirSync(dirname(backup), { recursive: true });
2012
2112
  linkSync(expectation.path, backup);
2013
2113
  }
2014
2114
  renameSync(staged, expectation.path);
2115
+ promoted.push(path);
2015
2116
  }
2016
- #take(path) {
2117
+ #take(path, taken) {
2017
2118
  const expectation = this.#expectation(path);
2018
2119
  if (!matchesExpectation(expectation)) throw new ScaffoldError("WRITE", `The destination at ${path} moved while writing.`, { path });
2019
2120
  const backup = this.#resolve(this.#backup, path);
2020
2121
  mkdirSync(dirname(backup), { recursive: true });
2021
2122
  renameSync(expectation.path, backup);
2123
+ taken.push(path);
2022
2124
  }
2023
2125
  #recover(promoted, taken) {
2024
2126
  const residue = [];
@@ -2044,8 +2146,9 @@ var WriteTransaction = class {
2044
2146
  }));
2045
2147
  if (!cleared.success) residue.push(cleared.error);
2046
2148
  }
2047
- for (const anchor of [...this.#created].reverse()) {
2048
- const removed = attempt(() => rmdirSync(anchor.path));
2149
+ for (const created of [...this.#created].reverse()) {
2150
+ const path = typeof created === "string" ? created : created.path;
2151
+ const removed = attempt(() => rmdirSync(path));
2049
2152
  if (!removed.success && !matchesMissingPath(removed.error)) residue.push(removed.error);
2050
2153
  }
2051
2154
  return residue;
@@ -2222,10 +2325,13 @@ var Materializer = class Materializer {
2222
2325
  * The audit is a preview, not an instruction. The plan is hydrated and
2223
2326
  * compared against the target again here, and the verdicts that produces must
2224
2327
  * match the ones the audit carried for every path the plan owns; anything else
2225
- * means the target moved, and the whole call is refused. A missing destination
2226
- * is restored whatever its ownership; a stale one is replaced only where the
2227
- * artifact claims its bytes, which is what leaves a presence-owned file a
2228
- * consumer has edited exactly as it is.
2328
+ * means the target moved, and the whole call is refused. The audit is checked
2329
+ * for agreement rather than for plausibility, so a verdict the comparison could
2330
+ * not have produced a birth-owned path reported stale, which the `Finding`
2331
+ * shape admits disagrees with the derived one and is refused. A missing
2332
+ * destination is restored whatever its ownership; a stale one is replaced only
2333
+ * where the artifact claims its bytes, which is what leaves a presence-owned
2334
+ * file a consumer has edited exactly as it is.
2229
2335
  */
2230
2336
  repair(plan, audit, target) {
2231
2337
  this.#assertAlive();
@@ -2598,6 +2704,12 @@ var Materializer = class Materializer {
2598
2704
  path: finding.path
2599
2705
  });
2600
2706
  if (other.drift === finding.drift && other.observed === finding.observed) continue;
2707
+ if (!matchesDriftReachability(finding.ownership, other)) throw this.#error("TARGET", `The path ${finding.path} carries an audit verdict this plan could not produce.`, {
2708
+ target,
2709
+ path: finding.path,
2710
+ ownership: finding.ownership,
2711
+ drift: other.drift
2712
+ });
2601
2713
  throw this.#error("TARGET", `The path ${finding.path} moved since its audit.`, {
2602
2714
  target,
2603
2715
  path: finding.path
@@ -2846,6 +2958,7 @@ var Upstream = class Upstream {
2846
2958
  static #defaultBudget = 16777216;
2847
2959
  static #scope = "orkestrel";
2848
2960
  static #unreadable = "the answer carries no readable latest version";
2961
+ static #packument = "application/vnd.npm.install-v1+json";
2849
2962
  #emitter;
2850
2963
  #guideBase;
2851
2964
  #guideBranch;
@@ -3038,7 +3151,7 @@ var Upstream = class Upstream {
3038
3151
  return parsed.href.replace(/\/+$/u, "");
3039
3152
  }
3040
3153
  async #release(dependency, allowance) {
3041
- const outcome = await this.#read(this.#registryURL(dependency.name), this.#registryTimeout, allowance);
3154
+ const outcome = await this.#read(this.#registryURL(dependency.name), this.#registryTimeout, allowance, Upstream.#packument);
3042
3155
  const latest = outcome.lookup === "found" ? this.#latest(outcome.content) : void 0;
3043
3156
  const release = latest === void 0 ? {
3044
3157
  name: dependency.name,
@@ -3075,7 +3188,7 @@ var Upstream = class Upstream {
3075
3188
  return mirror;
3076
3189
  }
3077
3190
  async #entry(name, allowance) {
3078
- const outcome = await this.#read(this.#registryURL(name), this.#registryTimeout, allowance);
3191
+ const outcome = await this.#read(this.#registryURL(name), this.#registryTimeout, allowance, Upstream.#packument);
3079
3192
  const version = outcome.lookup === "found" ? this.#latest(outcome.content) : void 0;
3080
3193
  if (version !== void 0) return {
3081
3194
  name,
@@ -3119,11 +3232,11 @@ var Upstream = class Upstream {
3119
3232
  const repository = encodeURIComponent(name.slice(name.lastIndexOf("/") + 1));
3120
3233
  return `${this.#guideBase}/${Upstream.#scope}/${repository}/refs/heads/${branch}/${nameToGuide(name)}`;
3121
3234
  }
3122
- async #read(url, timeout, allowance) {
3235
+ async #read(url, timeout, allowance, accept) {
3123
3236
  let note = "";
3124
3237
  for (let attempt = 0; attempt <= this.#retries; attempt += 1) {
3125
3238
  this.#assertAlive();
3126
- const outcome = await this.#request(url, timeout, allowance);
3239
+ const outcome = await this.#request(url, timeout, allowance, accept);
3127
3240
  if (outcome.lookup !== "failed") return outcome;
3128
3241
  note = outcome.note;
3129
3242
  }
@@ -3137,7 +3250,7 @@ var Upstream = class Upstream {
3137
3250
  note
3138
3251
  };
3139
3252
  }
3140
- async #request(url, timeout, allowance) {
3253
+ async #request(url, timeout, allowance, accept) {
3141
3254
  if (allowance.remaining <= 0) return {
3142
3255
  lookup: "failed",
3143
3256
  content: "",
@@ -3146,7 +3259,8 @@ var Upstream = class Upstream {
3146
3259
  try {
3147
3260
  const response = await fetch(url, {
3148
3261
  signal: AbortSignal.any([this.#controller.signal, AbortSignal.timeout(timeout)]),
3149
- redirect: "manual"
3262
+ redirect: "manual",
3263
+ ...accept === void 0 ? {} : { headers: { accept } }
3150
3264
  });
3151
3265
  if (response.status === 404) {
3152
3266
  await response.body?.cancel();
@@ -3183,21 +3297,11 @@ var Upstream = class Upstream {
3183
3297
  }
3184
3298
  }
3185
3299
  async #body(response, allowance) {
3186
- const declared = response.headers.get("content-length");
3187
- const stated = declared === null ? void 0 : Number(declared);
3188
- if (stated !== void 0 && Number.isFinite(stated) && (stated > this.#limit || stated > allowance.remaining)) {
3189
- await response.body?.cancel();
3190
- return {
3191
- lookup: "failed",
3192
- content: "",
3193
- note: `the response declares ${String(stated)} bytes, past the ${String(Math.min(this.#limit, allowance.remaining))}-byte allowance`
3194
- };
3195
- }
3196
3300
  const body = response.body;
3197
3301
  if (body === null) return {
3198
- lookup: "found",
3302
+ lookup: "failed",
3199
3303
  content: "",
3200
- note: ""
3304
+ note: `HTTP ${String(response.status)}, and the answer carries no body`
3201
3305
  };
3202
3306
  const reader = body.getReader();
3203
3307
  const decoder = new TextDecoder("utf-8", { fatal: true });
@@ -3332,6 +3436,6 @@ function createUpstream(options) {
3332
3436
  return new Upstream(options);
3333
3437
  }
3334
3438
  //#endregion
3335
- export { BRANCH_PATTERN, DIGEST_PATTERN, DRIVE_PATTERN, INVALID_SEGMENT_CHARACTER_PATTERN, MANIFEST_NAME, MAX_BRANCH_LENGTH, MAX_ENDPOINT_LENGTH, MAX_INVENTORY_PATHS, MAX_PATH_DEPTH, MAX_PATH_SEGMENT_BYTES, MAX_UPSTREAM_CONCURRENCY, MAX_UPSTREAM_RETRIES, MAX_UPSTREAM_TIMEOUT, Materializer, RESERVED_SEGMENT_PATTERN, Upstream, WriteTransaction, computeDigest, computeFileDigest, computeManifestDigest, createMaterializer, createUpstream, isBranch, isCatalogEntries, isDependencies, isDependencyNames, isDigest, isEndpoint, isExactCaseFile, isFilesystemPath, isHostManifest, isInventory, isManifestEntry, isMaterializerHooks, isMaterializerOptions, isMirrors, isPhysicalDirectory, isPhysicalFile, isRepository, isTimeout, isUpstreamHooks, isUpstreamOptions, isVacant, listDirectories, listFiles, matchesAnchor, matchesExpectation, matchesGitPath, matchesMissingPath, matchesPrecondition, matchesProtectedPath, matchesSensitivePath, pathToStorage, readAnchor, readExpectation, readFileHex, readFileText, readHostManifest, readManifestEntry, readSnapshot, resolveContainedPath, resolveRealPath, stageHost };
3439
+ export { BRANCH_PATTERN, DIGEST_PATTERN, DRIVE_PATTERN, INVALID_SEGMENT_CHARACTER_PATTERN, MANIFEST_NAME, MAX_BRANCH_LENGTH, MAX_ENDPOINT_LENGTH, MAX_INVENTORY_PATHS, MAX_PATH_DEPTH, MAX_PATH_SEGMENT_BYTES, MAX_UPSTREAM_CONCURRENCY, MAX_UPSTREAM_RETRIES, MAX_UPSTREAM_TIMEOUT, Materializer, RESERVED_SEGMENT_PATTERN, Upstream, WriteTransaction, computeDigest, computeFileDigest, computeManifestDigest, createMaterializer, createUpstream, isBranch, isCatalogEntries, isDependencies, isDependencyNames, isDigest, isEndpoint, isExactCaseFile, isFilesystemPath, isHostManifest, isInventory, isManifestEntry, isMaterializerHooks, isMaterializerOptions, isMirrors, isPhysicalDirectory, isPhysicalFile, isRepository, isTimeout, isUpstreamHooks, isUpstreamOptions, isVacant, listDirectories, listFiles, matchesAnchor, matchesExecutablePath, matchesExpectation, matchesGitPath, matchesMissingPath, matchesPrecondition, matchesProtectedPath, matchesSensitivePath, pathToStorage, readAnchor, readExpectation, readFileHex, readFileText, readHostManifest, readManifestEntry, readSnapshot, resolveContainedPath, resolveRealPath, stageHost };
3336
3440
 
3337
3441
  //# sourceMappingURL=index.js.map