@vibe-agent-toolkit/utils 0.2.0-rc.7 → 0.2.0-rc.9

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.
Files changed (189) hide show
  1. package/README.md +24 -4
  2. package/dist/asset-reference.js +3 -11
  3. package/dist/asset-reference.js.map +1 -1
  4. package/dist/crawl-timing.d.ts +3 -3
  5. package/dist/crawl-timing.js +3 -3
  6. package/dist/dirent-kind.d.ts +83 -0
  7. package/dist/dirent-kind.d.ts.map +1 -0
  8. package/dist/dirent-kind.js +129 -0
  9. package/dist/dirent-kind.js.map +1 -0
  10. package/dist/env-flag.d.ts +6 -1
  11. package/dist/env-flag.d.ts.map +1 -1
  12. package/dist/env-flag.js +6 -1
  13. package/dist/env-flag.js.map +1 -1
  14. package/dist/errors/errno.d.ts +51 -0
  15. package/dist/errors/errno.d.ts.map +1 -0
  16. package/dist/errors/errno.js +112 -0
  17. package/dist/errors/errno.js.map +1 -0
  18. package/dist/errors/vat-error.d.ts +60 -0
  19. package/dist/errors/vat-error.d.ts.map +1 -0
  20. package/dist/errors/vat-error.js +86 -0
  21. package/dist/errors/vat-error.js.map +1 -0
  22. package/dist/file-crawler.js +9 -9
  23. package/dist/file-crawler.js.map +1 -1
  24. package/dist/fs/file-hash.d.ts.map +1 -1
  25. package/dist/fs/file-hash.js +0 -1
  26. package/dist/fs/file-hash.js.map +1 -1
  27. package/dist/fs-utils.d.ts +16 -18
  28. package/dist/fs-utils.d.ts.map +1 -1
  29. package/dist/fs-utils.js +56 -80
  30. package/dist/fs-utils.js.map +1 -1
  31. package/dist/fs.d.ts +2 -1
  32. package/dist/fs.d.ts.map +1 -1
  33. package/dist/fs.js +2 -1
  34. package/dist/fs.js.map +1 -1
  35. package/dist/git-run.d.ts +1 -1
  36. package/dist/git-run.js +1 -1
  37. package/dist/git-snapshot.d.ts +18 -0
  38. package/dist/git-snapshot.d.ts.map +1 -1
  39. package/dist/git-snapshot.js +18 -0
  40. package/dist/git-snapshot.js.map +1 -1
  41. package/dist/git-tracker.d.ts +1 -1
  42. package/dist/git-tracker.d.ts.map +1 -1
  43. package/dist/git-tracker.js +1 -2
  44. package/dist/git-tracker.js.map +1 -1
  45. package/dist/git-utils.d.ts.map +1 -1
  46. package/dist/git-utils.js +0 -1
  47. package/dist/git-utils.js.map +1 -1
  48. package/dist/git.d.ts +1 -1
  49. package/dist/git.d.ts.map +1 -1
  50. package/dist/git.js +1 -1
  51. package/dist/git.js.map +1 -1
  52. package/dist/gitignore-checker.d.ts.map +1 -1
  53. package/dist/gitignore-checker.js +9 -3
  54. package/dist/gitignore-checker.js.map +1 -1
  55. package/dist/glob/glob-pattern.js +5 -5
  56. package/dist/glob/glob-pattern.js.map +1 -1
  57. package/dist/index.d.ts +7 -3
  58. package/dist/index.d.ts.map +1 -1
  59. package/dist/index.js +19 -4
  60. package/dist/index.js.map +1 -1
  61. package/dist/listing-refusal.d.ts +2 -1
  62. package/dist/listing-refusal.d.ts.map +1 -1
  63. package/dist/listing-refusal.js +3 -3
  64. package/dist/listing-refusal.js.map +1 -1
  65. package/dist/path-containment.d.ts +64 -0
  66. package/dist/path-containment.d.ts.map +1 -0
  67. package/dist/path-containment.js +111 -0
  68. package/dist/path-containment.js.map +1 -0
  69. package/dist/path-core.d.ts +112 -17
  70. package/dist/path-core.d.ts.map +1 -1
  71. package/dist/path-core.js +138 -22
  72. package/dist/path-core.js.map +1 -1
  73. package/dist/path-utils.d.ts.map +1 -1
  74. package/dist/path-utils.js +42 -47
  75. package/dist/path-utils.js.map +1 -1
  76. package/dist/path.d.ts +1 -1
  77. package/dist/path.d.ts.map +1 -1
  78. package/dist/path.js +1 -1
  79. package/dist/path.js.map +1 -1
  80. package/dist/project-utils.d.ts.map +1 -1
  81. package/dist/project-utils.js +9 -6
  82. package/dist/project-utils.js.map +1 -1
  83. package/dist/safe-exec.d.ts +2 -1
  84. package/dist/safe-exec.d.ts.map +1 -1
  85. package/dist/safe-exec.js +25 -7
  86. package/dist/safe-exec.js.map +1 -1
  87. package/dist/skill-targets.d.ts +2 -2
  88. package/dist/skill-targets.js +2 -2
  89. package/dist/skill-test/auth-resolver.d.ts +10 -2
  90. package/dist/skill-test/auth-resolver.d.ts.map +1 -1
  91. package/dist/skill-test/auth-resolver.js +26 -11
  92. package/dist/skill-test/auth-resolver.js.map +1 -1
  93. package/dist/skill-test/spawn-claude.d.ts +2 -1
  94. package/dist/skill-test/spawn-claude.d.ts.map +1 -1
  95. package/dist/skill-test/spawn-claude.js +20 -3
  96. package/dist/skill-test/spawn-claude.js.map +1 -1
  97. package/dist/skill-test/transcript.d.ts.map +1 -1
  98. package/dist/skill-test/transcript.js +18 -17
  99. package/dist/skill-test/transcript.js.map +1 -1
  100. package/dist/stdio-blocking.js +13 -14
  101. package/dist/stdio-blocking.js.map +1 -1
  102. package/dist/test-helpers.d.ts +47 -204
  103. package/dist/test-helpers.d.ts.map +1 -1
  104. package/dist/test-helpers.js +105 -310
  105. package/dist/test-helpers.js.map +1 -1
  106. package/dist/testing/executables.d.ts +32 -0
  107. package/dist/testing/executables.d.ts.map +1 -0
  108. package/dist/testing/executables.js +70 -0
  109. package/dist/testing/executables.js.map +1 -0
  110. package/dist/testing/hostile-tree.d.ts +79 -0
  111. package/dist/testing/hostile-tree.d.ts.map +1 -0
  112. package/dist/testing/hostile-tree.js +164 -0
  113. package/dist/testing/hostile-tree.js.map +1 -0
  114. package/dist/testing/platform-gates.d.ts +31 -0
  115. package/dist/testing/platform-gates.d.ts.map +1 -0
  116. package/dist/testing/platform-gates.js +31 -0
  117. package/dist/testing/platform-gates.js.map +1 -0
  118. package/dist/testing/temp-dir.d.ts +255 -0
  119. package/dist/testing/temp-dir.d.ts.map +1 -0
  120. package/dist/testing/temp-dir.js +363 -0
  121. package/dist/testing/temp-dir.js.map +1 -0
  122. package/dist/testing.d.ts +5 -0
  123. package/dist/testing.d.ts.map +1 -1
  124. package/dist/testing.js +10 -1
  125. package/dist/testing.js.map +1 -1
  126. package/dist/text-content.d.ts.map +1 -1
  127. package/dist/text-content.js +7 -1
  128. package/dist/text-content.js.map +1 -1
  129. package/dist/text-file.d.ts.map +1 -1
  130. package/dist/text-file.js +0 -2
  131. package/dist/text-file.js.map +1 -1
  132. package/dist/timing-dump.d.ts +1 -1
  133. package/dist/timing-dump.d.ts.map +1 -1
  134. package/dist/timing-dump.js +1 -2
  135. package/dist/timing-dump.js.map +1 -1
  136. package/dist/windows-shell.d.ts +0 -1
  137. package/dist/windows-shell.d.ts.map +1 -1
  138. package/dist/windows-shell.js +0 -1
  139. package/dist/windows-shell.js.map +1 -1
  140. package/dist/zod-introspection.js +2 -2
  141. package/dist/zod-introspection.js.map +1 -1
  142. package/eslint/README.md +126 -68
  143. package/eslint/index.cjs +111 -162
  144. package/eslint/index.d.cts +14 -17
  145. package/eslint/rules/callee-name.cjs +23 -0
  146. package/eslint/rules/commands-import-boundary.cjs +148 -0
  147. package/eslint/rules/dead-import.cjs +1 -1
  148. package/eslint/rules/dirent-type-needs-symlink-check.cjs +282 -0
  149. package/eslint/rules/eslint-rule-factory.cjs +5 -2
  150. package/eslint/rules/exempt-path-matcher.cjs +2 -1
  151. package/eslint/rules/explicit-zod-strictness.cjs +120 -0
  152. package/eslint/rules/no-bare-dynamic-import-path.cjs +5 -1
  153. package/eslint/rules/no-bare-executable-spawn.cjs +70 -0
  154. package/eslint/rules/no-bare-symlink-in-tests.cjs +16 -3
  155. package/eslint/rules/no-blind-catch.cjs +171 -0
  156. package/eslint/rules/no-child-process-execSync.cjs +8 -0
  157. package/eslint/rules/no-decaying-referent.cjs +217 -0
  158. package/eslint/rules/no-dotdot-containment.cjs +160 -0
  159. package/eslint/rules/no-file-url-string-concat.cjs +4 -1
  160. package/eslint/rules/no-fragile-entrypoint-guard.cjs +14 -1
  161. package/eslint/rules/no-fs-mkdirSync.cjs +8 -0
  162. package/eslint/rules/no-fs-promises-cp.cjs +7 -0
  163. package/eslint/rules/no-fs-realpathSync.cjs +8 -0
  164. package/eslint/rules/no-hardcoded-path-split.cjs +29 -18
  165. package/eslint/rules/no-io-in-unit-tier.cjs +129 -0
  166. package/eslint/rules/no-literal-process-exit.cjs +129 -0
  167. package/eslint/rules/no-manual-path-normalize.cjs +136 -107
  168. package/eslint/rules/no-os-tmpdir.cjs +8 -0
  169. package/eslint/rules/no-path-operations-in-comparisons.cjs +7 -2
  170. package/eslint/rules/no-path-sep-in-strings.cjs +7 -2
  171. package/eslint/rules/no-path-startswith.cjs +7 -2
  172. package/eslint/rules/no-process-exit-in-phase.cjs +12 -2
  173. package/eslint/rules/no-raw-node-path.cjs +563 -0
  174. package/eslint/rules/no-raw-text-decode.cjs +10 -1
  175. package/eslint/rules/no-registry-count-pin.cjs +255 -0
  176. package/eslint/rules/no-self-package-import.cjs +13 -2
  177. package/eslint/rules/no-test-scoped-functions.cjs +10 -3
  178. package/eslint/rules/no-unix-shell-commands.cjs +6 -2
  179. package/eslint/rules/no-unsafe-root-join.cjs +23 -2
  180. package/eslint/rules/no-url-pathname-for-fs.cjs +5 -1
  181. package/eslint/rules/no-version-literal.cjs +151 -0
  182. package/eslint/rules/prefer-startswith-over-regex.cjs +9 -0
  183. package/eslint/rules/require-justified-skip.cjs +8 -2
  184. package/eslint/rules/safe-import.cjs +1 -1
  185. package/package.json +4 -9
  186. package/eslint/rules/no-path-join.cjs +0 -14
  187. package/eslint/rules/no-path-relative.cjs +0 -14
  188. package/eslint/rules/no-path-resolve.cjs +0 -14
  189. package/eslint/rules/path-function-rule-factory.cjs +0 -496
@@ -0,0 +1,111 @@
1
+ /**
2
+ * `isUnderRoot` — the containment question a SINK asks.
3
+ *
4
+ * A delete, a copy, an uninstall: each takes a path that somebody else spelled
5
+ * (a manifest's `name:`, a `flatSkills` entry, a positional the user typed) and
6
+ * is about to act on `join(root, that)`. The question is whether the action
7
+ * lands inside the root, and the only honest answer comes from the
8
+ * filesystem: `startsWith('..')` on a lexical `relative()` misses a symlink
9
+ * inside the root that points out, refuses a member named `..cache`, and is
10
+ * blind to a drive letter — the sweep behind this module watched all three
11
+ * happen at exit 0.
12
+ *
13
+ * Both sides are canonicalized the same way, through the deepest existing
14
+ * ancestor, so a root reached through a symlink (macOS `/tmp`, a linked
15
+ * `~/.claude`) still contains its members, and a candidate that does not exist
16
+ * yet is judged by where creating it would land. Absence is reported as its
17
+ * own answer rather than folded into either side, because a delete sink and a
18
+ * create sink want opposite things from it.
19
+ *
20
+ * Lexical classification of an already-relative path — a projection identity,
21
+ * a report relativizer — is a different question with a different helper:
22
+ * `relativeEscapesRoot` in `path-core.ts`, which touches no filesystem.
23
+ */
24
+ import { lstatSync } from 'node:fs';
25
+ import path from 'node:path';
26
+ import { isPathAbsentError } from './errors/errno.js';
27
+ import { normalizePath, safePath, toForwardSlash } from './path-utils.js';
28
+ /**
29
+ * The canonical spelling of `target`, from its deepest existing ancestor.
30
+ *
31
+ * Walks up until `lstat` answers, canonicalizes THAT with the same realpath
32
+ * every path helper uses, and re-appends the missing tail. A refusal is not
33
+ * an absence: `EACCES` on an ancestor, `ELOOP` on a cycle, an invalid name —
34
+ * the OS is saying it cannot examine the path, and a containment verdict
35
+ * built on the spelling it refused is the bug this module exists to remove.
36
+ * Those stay loud.
37
+ *
38
+ * `lstat` rather than `stat`, so a dangling symlink counts as existing: it is
39
+ * an entry the sink can act on (a delete removes the link), and its realpath
40
+ * failing is answered by {@link normalizePath} with the lexical spelling, which
41
+ * is where the entry is.
42
+ */
43
+ function canonicalFromAncestor(target) {
44
+ const absolute = safePath.resolve(target);
45
+ const missing = [];
46
+ let candidate = absolute;
47
+ for (;;) {
48
+ if (entryExists(candidate)) {
49
+ return {
50
+ canonical: safePath.join(toForwardSlash(normalizePath(candidate)), ...missing),
51
+ exists: missing.length === 0,
52
+ };
53
+ }
54
+ const parent = toForwardSlash(path.dirname(candidate));
55
+ // Fixpoint at a filesystem root: `dirname` returns its own input, nothing
56
+ // on the path resolved, and the lexical form is the only answer there is.
57
+ if (parent === candidate) {
58
+ return { canonical: absolute, exists: false };
59
+ }
60
+ missing.unshift(path.basename(candidate));
61
+ candidate = parent;
62
+ }
63
+ }
64
+ /** Whether an entry (a file, a directory, or a link — dangling or not) is at `p`. */
65
+ function entryExists(p) {
66
+ try {
67
+ lstatSync(p);
68
+ return true;
69
+ }
70
+ catch (error) {
71
+ if (isPathAbsentError(error))
72
+ return false;
73
+ throw error;
74
+ }
75
+ }
76
+ /**
77
+ * Is `candidate` strictly under `root`, as the filesystem judges it?
78
+ *
79
+ * Both paths may be relative (resolved from cwd) or absolute, in either
80
+ * separator; neither has to exist. See {@link Containment} for the three
81
+ * answers and which sinks accept which.
82
+ *
83
+ * ⚠️ Costs one `lstat` per missing component plus two `realpath`s. That is
84
+ * nothing for a sink — a delete or a copy dwarfs it — and too much for a loop
85
+ * over every link in a corpus, which is why the projection and link lanes
86
+ * classify lexically with `relativeEscapesRoot` and never call this.
87
+ *
88
+ * @param root - The directory the action must stay inside
89
+ * @param candidate - The path the action is about to touch
90
+ * @returns `'inside'`, `'outside'`, or `'absent'`
91
+ * @throws When the OS refuses to examine either path for any reason but
92
+ * absence — a refusal is reported, never read as "not there"
93
+ *
94
+ * @example
95
+ * // A delete sink: only an existing, contained target may go.
96
+ * if (isUnderRoot(skillsDir, target) !== 'inside') throw new Error(`refusing to remove ${target}: not inside ${skillsDir}`);
97
+ * await rm(target, { recursive: true, force: true });
98
+ *
99
+ * // A create sink: absent-but-contained is exactly the happy path.
100
+ * if (isUnderRoot(skillsDir, dest) === 'outside') throw new Error(`refusing to install to ${dest}: not inside ${skillsDir}`);
101
+ */
102
+ export function isUnderRoot(root, candidate) {
103
+ const rootReal = canonicalFromAncestor(root).canonical;
104
+ const target = canonicalFromAncestor(candidate);
105
+ const prefix = rootReal.endsWith('/') ? rootReal : `${rootReal}/`;
106
+ if (!target.canonical.startsWith(prefix)) {
107
+ return 'outside';
108
+ }
109
+ return target.exists ? 'inside' : 'absent';
110
+ }
111
+ //# sourceMappingURL=path-containment.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"path-containment.js","sourceRoot":"","sources":["../src/path-containment.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AACtD,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAgB1E;;;;;;;;;;;;;;GAcG;AACH,SAAS,qBAAqB,CAAC,MAAc;IAC3C,MAAM,QAAQ,GAAG,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IAC1C,MAAM,OAAO,GAAa,EAAE,CAAC;IAC7B,IAAI,SAAS,GAAG,QAAQ,CAAC;IACzB,SAAS,CAAC;QACR,IAAI,WAAW,CAAC,SAAS,CAAC,EAAE,CAAC;YAC3B,OAAO;gBACL,SAAS,EAAE,QAAQ,CAAC,IAAI,CAAC,cAAc,CAAC,aAAa,CAAC,SAAS,CAAC,CAAC,EAAE,GAAG,OAAO,CAAC;gBAC9E,MAAM,EAAE,OAAO,CAAC,MAAM,KAAK,CAAC;aAC7B,CAAC;QACJ,CAAC;QACD,MAAM,MAAM,GAAG,cAAc,CAAC,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC;QACvD,0EAA0E;QAC1E,0EAA0E;QAC1E,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;YACzB,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;QAChD,CAAC;QACD,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC;QAC1C,SAAS,GAAG,MAAM,CAAC;IACrB,CAAC;AACH,CAAC;AAED,qFAAqF;AACrF,SAAS,WAAW,CAAC,CAAS;IAC5B,IAAI,CAAC;QACH,SAAS,CAAC,CAAC,CAAC,CAAC;QACb,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,iBAAiB,CAAC,KAAK,CAAC;YAAE,OAAO,KAAK,CAAC;QAC3C,MAAM,KAAK,CAAC;IACd,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,UAAU,WAAW,CAAC,IAAY,EAAE,SAAiB;IACzD,MAAM,QAAQ,GAAG,qBAAqB,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC;IACvD,MAAM,MAAM,GAAG,qBAAqB,CAAC,SAAS,CAAC,CAAC;IAChD,MAAM,MAAM,GAAG,QAAQ,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,GAAG,QAAQ,GAAG,CAAC;IAClE,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC;QACzC,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,OAAO,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC;AAC7C,CAAC"}
@@ -10,6 +10,26 @@
10
10
  * `mkdirSyncReal`, `resolveFromImportMeta`, `dynamicImportPath`) live in
11
11
  * `./path-utils.ts` and are exposed via the `./fs` entry.
12
12
  */
13
+ import { VatError } from './errors/vat-error.js';
14
+ /**
15
+ * Thrown by {@link safePath.joinUnderRoot} when the joined path would land
16
+ * outside its root.
17
+ *
18
+ * A class with a code rather than a prefixed sentence: three packages used to
19
+ * recognise this refusal with `error.message.startsWith('safePath.joinUnderRoot:')`,
20
+ * which is a contract on prose. Dispatch with
21
+ * `isVatError(error, PathEscapesRootError.code)` — it survives the `src`/`dist`
22
+ * boundary that `instanceof` does not.
23
+ */
24
+ export declare class PathEscapesRootError extends VatError {
25
+ /** The code every instance carries, for `isVatError(error, PathEscapesRootError.code)`. */
26
+ static readonly code = "PATH_ESCAPES_ROOT";
27
+ /**
28
+ * @param root - The root the path had to stay under
29
+ * @param detail - Which segment, or which result, escaped it
30
+ */
31
+ constructor(root: string, detail: string);
32
+ }
13
33
  /**
14
34
  * Check if a path is absolute
15
35
  *
@@ -44,8 +64,9 @@ export declare function isAbsoluteAnyPlatform(p: string): boolean;
44
64
  /**
45
65
  * True if `p` contains a `..` parent-directory traversal segment.
46
66
  *
47
- * Forward-slash-normalized, then inspects each `/`-delimited segment — so a
48
- * `..` is caught regardless of the original OS separator. A containment guard
67
+ * Backslashes are read as separators on every host (the input is author-written
68
+ * config), then each `/`-delimited segment is inspected — so a `..` is caught
69
+ * regardless of the separator the author typed. A containment guard
49
70
  * for config-supplied relative paths (skill `files:` dest values, glob magic
50
71
  * remainders) that must never climb above their anchor directory.
51
72
  *
@@ -56,6 +77,59 @@ export declare function isAbsoluteAnyPlatform(p: string): boolean;
56
77
  * hasParentTraversalSegment('a..b/c') // false (".." must be a whole segment)
57
78
  */
58
79
  export declare function hasParentTraversalSegment(p: string): boolean;
80
+ /**
81
+ * True when a root-relative path — as `safePath.relative(root, p)` spells it —
82
+ * names something the root does not contain.
83
+ *
84
+ * The three shapes `path.relative` can return for an outsider: the parent
85
+ * itself (`..`), a climb through it (`../x`), and, on Windows only, an
86
+ * absolute path (a target on another drive has no relative spelling). A name
87
+ * that merely BEGINS with two dots (`..notes.md`) is a member and reads as one;
88
+ * the bare `startsWith('..')` this replaces refused it, dropped the file from
89
+ * the package and unlinked the reference, at exit 0.
90
+ *
91
+ * ⚠️ **Lexical, on purpose.** This classifies a relative path that was already
92
+ * computed; it does not ask the filesystem, so a symlink inside the root that
93
+ * points outside reads as inside here. That is the right answer for the
94
+ * callers that own no root to ask about — a projection identity, a permission
95
+ * pattern, a report relativizer — and the WRONG one for a delete or copy sink,
96
+ * which must ask {@link isUnderRoot} from `@vibe-agent-toolkit/utils` instead.
97
+ * The empty relative (the root itself) is not an escape; whether equality is
98
+ * acceptable is the caller's question and is asked beside this one.
99
+ *
100
+ * @param normalizedRelative - A forward-slashed root-relative path
101
+ * @returns True when the root does not contain it
102
+ *
103
+ * @example
104
+ * relativeEscapesRoot(safePath.relative(root, p)) // the whole idiom
105
+ * relativeEscapesRoot('../x') // true
106
+ * relativeEscapesRoot('..notes.md') // false — a member whose name starts with dots
107
+ */
108
+ export declare function relativeEscapesRoot(normalizedRelative: string): boolean;
109
+ /**
110
+ * True when `name` can only ever be ONE directory entry under whatever it is
111
+ * joined to: non-empty, not `.` or `..`, no separator of either platform, no
112
+ * NUL, no drive-letter prefix.
113
+ *
114
+ * The check for a caller-controlled NAME — a skill name from a manifest, a
115
+ * session id, a positional the user typed — that is about to become
116
+ * `join(root, name)`. A name is not a path: `..cache` and `a..b` are legitimate
117
+ * entries, and `includes('..')` refused them while `startsWith` let `x/../..`
118
+ * through. The question is whether the join can land anywhere but directly
119
+ * under `root`, and that is answered by the segment's shape alone, with no
120
+ * filesystem — which is also why this belongs beside the path helpers rather
121
+ * than beside {@link isUnderRoot}, which is the check for a PATH.
122
+ *
123
+ * @param name - The proposed entry name
124
+ * @returns True when `join(root, name)` is a direct child of `root`
125
+ *
126
+ * @example
127
+ * isSingleFsSegment('my-skill') // true
128
+ * isSingleFsSegment('..cache') // true — dots inside a name are just dots
129
+ * isSingleFsSegment('../victim') // false
130
+ * isSingleFsSegment('..') // false
131
+ */
132
+ export declare function isSingleFsSegment(name: string): boolean;
59
133
  /**
60
134
  * Compute a `ValidationIssue.location`: an absolute source file path made
61
135
  * relative to the scan/project root, forward-slashed.
@@ -115,23 +189,42 @@ export declare function toAbsolutePath(p: string, baseDir: string): string;
115
189
  */
116
190
  export declare function getRelativePath(from: string, to: string): string;
117
191
  /**
118
- * Convert a path to forward slashes
192
+ * Convert a NATIVE path — one the filesystem, `path.*`, `readdir` or git
193
+ * handed you — to forward slashes.
119
194
  *
120
- * Windows accepts both forward slashes and backslashes as path separators.
121
- * This function normalizes all paths to use forward slashes for consistency.
122
- * Useful for glob pattern matching, cross-platform comparisons, and string operations.
195
+ * Converts only where a backslash is a separator (win32). On POSIX a backslash is a
196
+ * legal filename character, so `docs/x\y.md` is one file and is returned
197
+ * unchanged: converting it would invent a phantom `docs/x/` directory and, in
198
+ * {@link safePath.joinUnderRoot}, turn an `x\..\..` NAME into a climb.
123
199
  *
124
- * @param p - Path to convert
125
- * @returns Path with forward slashes
200
+ * For AUTHOR-WRITTEN text — an href, a glob, a config value, a CLI argument,
201
+ * an archive entry name — whose backslashes must read as separators on every
202
+ * host, use {@link toForwardSlashAnyPlatform}.
126
203
  *
127
- * @example
128
- * toForwardSlash('C:\\Users\\docs\\README.md')
129
- * // Returns: 'C:/Users/docs/README.md'
204
+ * @param p - A native path
205
+ * @returns The path with forward slashes (identity on POSIX)
130
206
  *
131
- * toForwardSlash('/project/docs/README.md')
132
- * // Returns: '/project/docs/README.md' (unchanged)
207
+ * @example
208
+ * toForwardSlash('C:\\Users\\docs\\README.md') // win32: 'C:/Users/docs/README.md'
209
+ * toForwardSlash('docs/x\\y.md') // POSIX: 'docs/x\\y.md' (unchanged)
133
210
  */
134
211
  export declare function toForwardSlash(p: string): string;
212
+ /**
213
+ * Convert every backslash to a forward slash, on every host.
214
+ *
215
+ * For AUTHOR-WRITTEN text that may carry Windows spellings regardless of where
216
+ * VAT runs — markdown hrefs, globs, config values, CLI arguments, zip entry
217
+ * names — and for containment guards that must refuse `..\x` everywhere.
218
+ * Never use it on a path read from the filesystem or git: on POSIX that
219
+ * backslash is part of a filename. Use {@link toForwardSlash} for those.
220
+ *
221
+ * @param text - Author-written path text
222
+ * @returns The text with every backslash replaced by `/`
223
+ *
224
+ * @example
225
+ * toForwardSlashAnyPlatform('..\\evil') // '../evil' on every host
226
+ */
227
+ export declare function toForwardSlashAnyPlatform(text: string): string;
135
228
  /**
136
229
  * Normalize text to Unicode NFC — the form in which two *visually identical*
137
230
  * filenames compare equal.
@@ -166,12 +259,14 @@ export declare function toNfc(value: string): string;
166
259
  * Cross-platform safe path operations.
167
260
  *
168
261
  * Wraps Node's `path.join()`, `path.resolve()`, and `path.relative()` to always
169
- * return forward-slash paths. On Windows, the native `path.*` functions return
262
+ * return forward-slash paths (converted through {@link toForwardSlash}, so a
263
+ * backslash inside a POSIX filename survives). On Windows, the native `path.*` functions return
170
264
  * backslashes, which causes bugs when paths are used as Map keys, compared as
171
265
  * strings, or matched with glob patterns.
172
266
  *
173
267
  * **Use these instead of importing from `node:path` directly.**
174
- * ESLint rules enforce this — see `no-path-join`, `no-path-resolve`, `no-path-relative`.
268
+ * An ESLint rule enforces this — see `no-raw-node-path` (its `functions` option
269
+ * table maps each of `join`/`resolve`/`relative` to its `safePath.*` replacement).
175
270
  *
176
271
  * @example
177
272
  * ```typescript
@@ -182,7 +277,7 @@ export declare function toNfc(value: string): string;
182
277
  * safePath.resolve('/project', './docs') // → '/project/docs'
183
278
  * safePath.relative('/project/docs', '/project') // → '..'
184
279
  * safePath.joinUnderRoot('/harness', 'skill-abc') // → '/harness/skill-abc'
185
- * safePath.joinUnderRoot('/harness', '../escape') // throws Error
280
+ * safePath.joinUnderRoot('/harness', '../escape') // throws PathEscapesRootError
186
281
  * ```
187
282
  */
188
283
  export declare const safePath: {
@@ -210,7 +305,7 @@ export declare const safePath: {
210
305
  * skill-test staging code was vulnerable to on Windows.
211
306
  *
212
307
  * @returns Forward-slash absolute path guaranteed to be inside `root`.
213
- * @throws {Error} If the resolved path would escape `root`.
308
+ * @throws {PathEscapesRootError} If the resolved path would escape `root`.
214
309
  *
215
310
  * @example
216
311
  * ```typescript
@@ -1 +1 @@
1
- {"version":3,"file":"path-core.d.ts","sourceRoot":"","sources":["../src/path-core.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAIH;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,cAAc,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAEjD;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,qBAAqB,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAExD;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,yBAAyB,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAE5D;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,aAAa,CAAC,cAAc,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,CAEjF;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,cAAc,CAAC,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAKjE;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAMhE;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,cAAc,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAEhD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE3C;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,eAAO,MAAM,QAAQ;IACnB,6DAA6D;8BAC9C,MAAM,EAAE,KAAG,MAAM;IAIhC,gEAAgE;iCAC9C,MAAM,EAAE,KAAG,MAAM;IAInC,iEAAiE;8BAClD,MAAM,MAAM,MAAM,KAAG,MAAM;IAI1C;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA4BG;mCACiB,MAAM,eAAe,MAAM,EAAE,KAAG,MAAM;CAuClD,CAAC"}
1
+ {"version":3,"file":"path-core.d.ts","sourceRoot":"","sources":["../src/path-core.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAIH,OAAO,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAEjD;;;;;;;;;GASG;AACH,qBAAa,oBAAqB,SAAQ,QAAQ;IAChD,2FAA2F;IAC3F,MAAM,CAAC,QAAQ,CAAC,IAAI,uBAAuB;IAE3C;;;OAGG;gBACS,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;CAGzC;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,cAAc,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAEjD;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,qBAAqB,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAExD;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,yBAAyB,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAG5D;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,mBAAmB,CAAC,kBAAkB,EAAE,MAAM,GAAG,OAAO,CAGvE;AAKD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAUvD;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,aAAa,CAAC,cAAc,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,CAEjF;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,cAAc,CAAC,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAKjE;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAMhE;AAKD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,cAAc,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAEhD;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAG9D;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE3C;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,eAAO,MAAM,QAAQ;IACnB,6DAA6D;8BAC9C,MAAM,EAAE,KAAG,MAAM;IAIhC,gEAAgE;iCAC9C,MAAM,EAAE,KAAG,MAAM;IAInC,iEAAiE;8BAClD,MAAM,MAAM,MAAM,KAAG,MAAM;IAI1C;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA4BG;mCACiB,MAAM,eAAe,MAAM,EAAE,KAAG,MAAM;CAiClD,CAAC"}
package/dist/path-core.js CHANGED
@@ -11,6 +11,28 @@
11
11
  * `./path-utils.ts` and are exposed via the `./fs` entry.
12
12
  */
13
13
  import path from 'node:path';
14
+ import { VatError } from './errors/vat-error.js';
15
+ /**
16
+ * Thrown by {@link safePath.joinUnderRoot} when the joined path would land
17
+ * outside its root.
18
+ *
19
+ * A class with a code rather than a prefixed sentence: three packages used to
20
+ * recognise this refusal with `error.message.startsWith('safePath.joinUnderRoot:')`,
21
+ * which is a contract on prose. Dispatch with
22
+ * `isVatError(error, PathEscapesRootError.code)` — it survives the `src`/`dist`
23
+ * boundary that `instanceof` does not.
24
+ */
25
+ export class PathEscapesRootError extends VatError {
26
+ /** The code every instance carries, for `isVatError(error, PathEscapesRootError.code)`. */
27
+ static code = 'PATH_ESCAPES_ROOT';
28
+ /**
29
+ * @param root - The root the path had to stay under
30
+ * @param detail - Which segment, or which result, escaped it
31
+ */
32
+ constructor(root, detail) {
33
+ super(PathEscapesRootError.code, `safePath.joinUnderRoot: ${detail} escapes root "${root}".`);
34
+ }
35
+ }
14
36
  /**
15
37
  * Check if a path is absolute
16
38
  *
@@ -49,8 +71,9 @@ export function isAbsoluteAnyPlatform(p) {
49
71
  /**
50
72
  * True if `p` contains a `..` parent-directory traversal segment.
51
73
  *
52
- * Forward-slash-normalized, then inspects each `/`-delimited segment — so a
53
- * `..` is caught regardless of the original OS separator. A containment guard
74
+ * Backslashes are read as separators on every host (the input is author-written
75
+ * config), then each `/`-delimited segment is inspected — so a `..` is caught
76
+ * regardless of the separator the author typed. A containment guard
54
77
  * for config-supplied relative paths (skill `files:` dest values, glob magic
55
78
  * remainders) that must never climb above their anchor directory.
56
79
  *
@@ -61,7 +84,74 @@ export function isAbsoluteAnyPlatform(p) {
61
84
  * hasParentTraversalSegment('a..b/c') // false (".." must be a whole segment)
62
85
  */
63
86
  export function hasParentTraversalSegment(p) {
64
- return toForwardSlash(p).split('/').includes('..');
87
+ // eslint-disable-next-line local/no-dotdot-containment -- this IS the one lexical `..`-segment test the rule points to; it classifies a config-supplied RELATIVE spelling before any root exists to ask the filesystem about. Sinks use isUnderRoot().
88
+ return toForwardSlashAnyPlatform(p).split('/').includes('..');
89
+ }
90
+ /**
91
+ * True when a root-relative path — as `safePath.relative(root, p)` spells it —
92
+ * names something the root does not contain.
93
+ *
94
+ * The three shapes `path.relative` can return for an outsider: the parent
95
+ * itself (`..`), a climb through it (`../x`), and, on Windows only, an
96
+ * absolute path (a target on another drive has no relative spelling). A name
97
+ * that merely BEGINS with two dots (`..notes.md`) is a member and reads as one;
98
+ * the bare `startsWith('..')` this replaces refused it, dropped the file from
99
+ * the package and unlinked the reference, at exit 0.
100
+ *
101
+ * ⚠️ **Lexical, on purpose.** This classifies a relative path that was already
102
+ * computed; it does not ask the filesystem, so a symlink inside the root that
103
+ * points outside reads as inside here. That is the right answer for the
104
+ * callers that own no root to ask about — a projection identity, a permission
105
+ * pattern, a report relativizer — and the WRONG one for a delete or copy sink,
106
+ * which must ask {@link isUnderRoot} from `@vibe-agent-toolkit/utils` instead.
107
+ * The empty relative (the root itself) is not an escape; whether equality is
108
+ * acceptable is the caller's question and is asked beside this one.
109
+ *
110
+ * @param normalizedRelative - A forward-slashed root-relative path
111
+ * @returns True when the root does not contain it
112
+ *
113
+ * @example
114
+ * relativeEscapesRoot(safePath.relative(root, p)) // the whole idiom
115
+ * relativeEscapesRoot('../x') // true
116
+ * relativeEscapesRoot('..notes.md') // false — a member whose name starts with dots
117
+ */
118
+ export function relativeEscapesRoot(normalizedRelative) {
119
+ // eslint-disable-next-line local/no-dotdot-containment -- this IS the one lexical relative-path classifier the rule points to; every former copy of this pair now calls here. Sinks use isUnderRoot().
120
+ return normalizedRelative === '..' || normalizedRelative.startsWith('../') || isAbsoluteAnyPlatform(normalizedRelative);
121
+ }
122
+ /** A Windows drive-relative spelling (`C:`) — `path.resolve` sends it to that drive's cwd. */
123
+ const DRIVE_RELATIVE = /^[A-Za-z]:/u;
124
+ /**
125
+ * True when `name` can only ever be ONE directory entry under whatever it is
126
+ * joined to: non-empty, not `.` or `..`, no separator of either platform, no
127
+ * NUL, no drive-letter prefix.
128
+ *
129
+ * The check for a caller-controlled NAME — a skill name from a manifest, a
130
+ * session id, a positional the user typed — that is about to become
131
+ * `join(root, name)`. A name is not a path: `..cache` and `a..b` are legitimate
132
+ * entries, and `includes('..')` refused them while `startsWith` let `x/../..`
133
+ * through. The question is whether the join can land anywhere but directly
134
+ * under `root`, and that is answered by the segment's shape alone, with no
135
+ * filesystem — which is also why this belongs beside the path helpers rather
136
+ * than beside {@link isUnderRoot}, which is the check for a PATH.
137
+ *
138
+ * @param name - The proposed entry name
139
+ * @returns True when `join(root, name)` is a direct child of `root`
140
+ *
141
+ * @example
142
+ * isSingleFsSegment('my-skill') // true
143
+ * isSingleFsSegment('..cache') // true — dots inside a name are just dots
144
+ * isSingleFsSegment('../victim') // false
145
+ * isSingleFsSegment('..') // false
146
+ */
147
+ export function isSingleFsSegment(name) {
148
+ return (name !== '' &&
149
+ name !== '.' &&
150
+ name !== '..' &&
151
+ !name.includes('/') &&
152
+ !name.includes('\\') &&
153
+ !name.includes('\0') &&
154
+ !DRIVE_RELATIVE.test(name));
65
155
  }
66
156
  /**
67
157
  * Compute a `ValidationIssue.location`: an absolute source file path made
@@ -133,25 +223,49 @@ export function getRelativePath(from, to) {
133
223
  // Calculate relative path from source directory to target file
134
224
  return toForwardSlash(path.relative(fromDir, to));
135
225
  }
226
+ /** True on hosts (win32) where a backslash is a path separator; everywhere else it is a filename character. */
227
+ const BACKSLASH_IS_NATIVE_SEPARATOR = path.sep === '\\';
136
228
  /**
137
- * Convert a path to forward slashes
229
+ * Convert a NATIVE path — one the filesystem, `path.*`, `readdir` or git
230
+ * handed you — to forward slashes.
138
231
  *
139
- * Windows accepts both forward slashes and backslashes as path separators.
140
- * This function normalizes all paths to use forward slashes for consistency.
141
- * Useful for glob pattern matching, cross-platform comparisons, and string operations.
232
+ * Converts only where a backslash is a separator (win32). On POSIX a backslash is a
233
+ * legal filename character, so `docs/x\y.md` is one file and is returned
234
+ * unchanged: converting it would invent a phantom `docs/x/` directory and, in
235
+ * {@link safePath.joinUnderRoot}, turn an `x\..\..` NAME into a climb.
142
236
  *
143
- * @param p - Path to convert
144
- * @returns Path with forward slashes
237
+ * For AUTHOR-WRITTEN text — an href, a glob, a config value, a CLI argument,
238
+ * an archive entry name — whose backslashes must read as separators on every
239
+ * host, use {@link toForwardSlashAnyPlatform}.
145
240
  *
146
- * @example
147
- * toForwardSlash('C:\\Users\\docs\\README.md')
148
- * // Returns: 'C:/Users/docs/README.md'
241
+ * @param p - A native path
242
+ * @returns The path with forward slashes (identity on POSIX)
149
243
  *
150
- * toForwardSlash('/project/docs/README.md')
151
- * // Returns: '/project/docs/README.md' (unchanged)
244
+ * @example
245
+ * toForwardSlash('C:\\Users\\docs\\README.md') // win32: 'C:/Users/docs/README.md'
246
+ * toForwardSlash('docs/x\\y.md') // POSIX: 'docs/x\\y.md' (unchanged)
152
247
  */
153
248
  export function toForwardSlash(p) {
154
- return p.replaceAll('\\', '/');
249
+ return BACKSLASH_IS_NATIVE_SEPARATOR ? toForwardSlashAnyPlatform(p) : p;
250
+ }
251
+ /**
252
+ * Convert every backslash to a forward slash, on every host.
253
+ *
254
+ * For AUTHOR-WRITTEN text that may carry Windows spellings regardless of where
255
+ * VAT runs — markdown hrefs, globs, config values, CLI arguments, zip entry
256
+ * names — and for containment guards that must refuse `..\x` everywhere.
257
+ * Never use it on a path read from the filesystem or git: on POSIX that
258
+ * backslash is part of a filename. Use {@link toForwardSlash} for those.
259
+ *
260
+ * @param text - Author-written path text
261
+ * @returns The text with every backslash replaced by `/`
262
+ *
263
+ * @example
264
+ * toForwardSlashAnyPlatform('..\\evil') // '../evil' on every host
265
+ */
266
+ export function toForwardSlashAnyPlatform(text) {
267
+ // eslint-disable-next-line local/no-manual-path-normalize -- this IS the converter the rule's autofix writes; it cannot call itself.
268
+ return text.replaceAll('\\', '/');
155
269
  }
156
270
  /**
157
271
  * Normalize text to Unicode NFC — the form in which two *visually identical*
@@ -189,12 +303,14 @@ export function toNfc(value) {
189
303
  * Cross-platform safe path operations.
190
304
  *
191
305
  * Wraps Node's `path.join()`, `path.resolve()`, and `path.relative()` to always
192
- * return forward-slash paths. On Windows, the native `path.*` functions return
306
+ * return forward-slash paths (converted through {@link toForwardSlash}, so a
307
+ * backslash inside a POSIX filename survives). On Windows, the native `path.*` functions return
193
308
  * backslashes, which causes bugs when paths are used as Map keys, compared as
194
309
  * strings, or matched with glob patterns.
195
310
  *
196
311
  * **Use these instead of importing from `node:path` directly.**
197
- * ESLint rules enforce this — see `no-path-join`, `no-path-resolve`, `no-path-relative`.
312
+ * An ESLint rule enforces this — see `no-raw-node-path` (its `functions` option
313
+ * table maps each of `join`/`resolve`/`relative` to its `safePath.*` replacement).
198
314
  *
199
315
  * @example
200
316
  * ```typescript
@@ -205,7 +321,7 @@ export function toNfc(value) {
205
321
  * safePath.resolve('/project', './docs') // → '/project/docs'
206
322
  * safePath.relative('/project/docs', '/project') // → '..'
207
323
  * safePath.joinUnderRoot('/harness', 'skill-abc') // → '/harness/skill-abc'
208
- * safePath.joinUnderRoot('/harness', '../escape') // throws Error
324
+ * safePath.joinUnderRoot('/harness', '../escape') // throws PathEscapesRootError
209
325
  * ```
210
326
  */
211
327
  export const safePath = {
@@ -239,7 +355,7 @@ export const safePath = {
239
355
  * skill-test staging code was vulnerable to on Windows.
240
356
  *
241
357
  * @returns Forward-slash absolute path guaranteed to be inside `root`.
242
- * @throws {Error} If the resolved path would escape `root`.
358
+ * @throws {PathEscapesRootError} If the resolved path would escape `root`.
243
359
  *
244
360
  * @example
245
361
  * ```typescript
@@ -255,12 +371,12 @@ export const safePath = {
255
371
  // BEFORE resolving, so the error message can name the offending segment.
256
372
  for (const seg of segments) {
257
373
  if (path.isAbsolute(seg)) {
258
- throw new Error(`safePath.joinUnderRoot: segment "${seg}" is absolute and escapes root "${root}".`);
374
+ throw new PathEscapesRootError(root, `segment "${seg}" is absolute and`);
259
375
  }
260
376
  // Windows drive-letter check for POSIX hosts (path.isAbsolute won't catch
261
377
  // 'C:\...' on POSIX, but node's path.win32.isAbsolute does).
262
378
  if (path.win32.isAbsolute(seg)) {
263
- throw new Error(`safePath.joinUnderRoot: segment "${seg}" contains a Windows drive letter and escapes root "${root}".`);
379
+ throw new PathEscapesRootError(root, `segment "${seg}" contains a Windows drive letter and`);
264
380
  }
265
381
  }
266
382
  const resolvedRoot = path.resolve(root);
@@ -275,7 +391,7 @@ export const safePath = {
275
391
  // which would match '/rootEvil' when root is '/root').
276
392
  const rootPrefix = fwdRoot.endsWith('/') ? fwdRoot : `${fwdRoot}/`;
277
393
  if (fwdResult !== fwdRoot && !fwdResult.startsWith(rootPrefix)) {
278
- throw new Error(`safePath.joinUnderRoot: result "${fwdResult}" escapes root "${fwdRoot}".`);
394
+ throw new PathEscapesRootError(fwdRoot, `result "${fwdResult}"`);
279
395
  }
280
396
  return fwdResult;
281
397
  },
@@ -1 +1 @@
1
- {"version":3,"file":"path-core.js","sourceRoot":"","sources":["../src/path-core.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"path-core.js","sourceRoot":"","sources":["../src/path-core.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"path-utils.d.ts","sourceRoot":"","sources":["../src/path-utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAgB,MAAM,SAAS,CAAC;AAOlD;;;;;;;;;;GAUG;AACH,cAAc,gBAAgB,CAAC;AAE/B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAgB,aAAa,CAAC,GAAG,KAAK,EAAE,MAAM,EAAE,GAAG,MAAM,CA+BxD;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,CAezC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC,CAAC,CAAC,GACxC,MAAM,CAiBR;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,qBAAqB,CAAC,aAAa,EAAE,MAAM,EAAE,GAAG,QAAQ,EAAE,MAAM,EAAE,GAAG,MAAM,CAQ1F;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,iBAAiB,CAAC,CAAC,GAAG,OAAO,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,CAGhF"}
1
+ {"version":3,"file":"path-utils.d.ts","sourceRoot":"","sources":["../src/path-utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAgB,MAAM,SAAS,CAAC;AAQlD;;;;;;;;;;GAUG;AACH,cAAc,gBAAgB,CAAC;AAmC/B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAgB,aAAa,CAAC,GAAG,KAAK,EAAE,MAAM,EAAE,GAAG,MAAM,CAqBxD;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,CAEzC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC,CAAC,CAAC,GACxC,MAAM,CAIR;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,qBAAqB,CAAC,aAAa,EAAE,MAAM,EAAE,GAAG,QAAQ,EAAE,MAAM,EAAE,GAAG,MAAM,CAQ1F;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,iBAAiB,CAAC,CAAC,GAAG,OAAO,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,CAGhF"}