@vibe-agent-toolkit/utils 0.2.0-rc.6 → 0.2.0-rc.8
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.
- package/README.md +43 -9
- package/dist/asset-reference.js +3 -11
- package/dist/asset-reference.js.map +1 -1
- package/dist/crawl-timing.d.ts +3 -3
- package/dist/crawl-timing.js +3 -3
- package/dist/dirent-kind.d.ts +83 -0
- package/dist/dirent-kind.d.ts.map +1 -0
- package/dist/dirent-kind.js +129 -0
- package/dist/dirent-kind.js.map +1 -0
- package/dist/entrypoint.d.ts +54 -0
- package/dist/entrypoint.d.ts.map +1 -0
- package/dist/entrypoint.js +71 -0
- package/dist/entrypoint.js.map +1 -0
- package/dist/env-flag.d.ts +46 -0
- package/dist/env-flag.d.ts.map +1 -0
- package/dist/env-flag.js +57 -0
- package/dist/env-flag.js.map +1 -0
- package/dist/errors/errno.d.ts +51 -0
- package/dist/errors/errno.d.ts.map +1 -0
- package/dist/errors/errno.js +112 -0
- package/dist/errors/errno.js.map +1 -0
- package/dist/errors/vat-error.d.ts +60 -0
- package/dist/errors/vat-error.d.ts.map +1 -0
- package/dist/errors/vat-error.js +86 -0
- package/dist/errors/vat-error.js.map +1 -0
- package/dist/file-crawler.d.ts +22 -0
- package/dist/file-crawler.d.ts.map +1 -1
- package/dist/file-crawler.js +83 -22
- package/dist/file-crawler.js.map +1 -1
- package/dist/fs/file-hash.d.ts.map +1 -1
- package/dist/fs/file-hash.js +0 -1
- package/dist/fs/file-hash.js.map +1 -1
- package/dist/fs-utils.d.ts +422 -217
- package/dist/fs-utils.d.ts.map +1 -1
- package/dist/fs-utils.js +535 -283
- package/dist/fs-utils.js.map +1 -1
- package/dist/fs.d.ts +3 -2
- package/dist/fs.d.ts.map +1 -1
- package/dist/fs.js +5 -7
- package/dist/fs.js.map +1 -1
- package/dist/git-run.d.ts +1 -1
- package/dist/git-run.js +1 -1
- package/dist/git-tracker.d.ts +41 -2
- package/dist/git-tracker.d.ts.map +1 -1
- package/dist/git-tracker.js +84 -19
- package/dist/git-tracker.js.map +1 -1
- package/dist/git-utils.d.ts +47 -2
- package/dist/git-utils.d.ts.map +1 -1
- package/dist/git-utils.js +123 -20
- package/dist/git-utils.js.map +1 -1
- package/dist/git.d.ts +1 -0
- package/dist/git.d.ts.map +1 -1
- package/dist/git.js.map +1 -1
- package/dist/gitignore-checker.d.ts.map +1 -1
- package/dist/gitignore-checker.js +9 -3
- package/dist/gitignore-checker.js.map +1 -1
- package/dist/index.d.ts +10 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +48 -22
- package/dist/index.js.map +1 -1
- package/dist/listing-refusal.d.ts +112 -0
- package/dist/listing-refusal.d.ts.map +1 -0
- package/dist/listing-refusal.js +105 -0
- package/dist/listing-refusal.js.map +1 -0
- package/dist/path-containment.d.ts +64 -0
- package/dist/path-containment.d.ts.map +1 -0
- package/dist/path-containment.js +111 -0
- package/dist/path-containment.js.map +1 -0
- package/dist/path-core.d.ts +77 -3
- package/dist/path-core.d.ts.map +1 -1
- package/dist/path-core.js +96 -6
- package/dist/path-core.js.map +1 -1
- package/dist/path-utils.d.ts.map +1 -1
- package/dist/path-utils.js +42 -47
- package/dist/path-utils.js.map +1 -1
- package/dist/path.d.ts +1 -1
- package/dist/path.d.ts.map +1 -1
- package/dist/path.js +1 -1
- package/dist/path.js.map +1 -1
- package/dist/process.d.ts +7 -0
- package/dist/process.d.ts.map +1 -1
- package/dist/process.js +7 -0
- package/dist/process.js.map +1 -1
- package/dist/project-utils.d.ts.map +1 -1
- package/dist/project-utils.js +9 -6
- package/dist/project-utils.js.map +1 -1
- package/dist/safe-exec.d.ts +2 -1
- package/dist/safe-exec.d.ts.map +1 -1
- package/dist/safe-exec.js +26 -8
- package/dist/safe-exec.js.map +1 -1
- package/dist/skill-targets.d.ts +2 -2
- package/dist/skill-targets.js +2 -2
- package/dist/skill-test/auth-resolver.d.ts +10 -2
- package/dist/skill-test/auth-resolver.d.ts.map +1 -1
- package/dist/skill-test/auth-resolver.js +26 -11
- package/dist/skill-test/auth-resolver.js.map +1 -1
- package/dist/skill-test/spawn-claude.d.ts +2 -1
- package/dist/skill-test/spawn-claude.d.ts.map +1 -1
- package/dist/skill-test/spawn-claude.js +20 -3
- package/dist/skill-test/spawn-claude.js.map +1 -1
- package/dist/skill-test/transcript.d.ts.map +1 -1
- package/dist/skill-test/transcript.js +18 -17
- package/dist/skill-test/transcript.js.map +1 -1
- package/dist/stdio-blocking.js +13 -14
- package/dist/stdio-blocking.js.map +1 -1
- package/dist/test-helpers.d.ts +56 -193
- package/dist/test-helpers.d.ts.map +1 -1
- package/dist/test-helpers.js +115 -284
- package/dist/test-helpers.js.map +1 -1
- package/dist/testing/executables.d.ts +32 -0
- package/dist/testing/executables.d.ts.map +1 -0
- package/dist/testing/executables.js +70 -0
- package/dist/testing/executables.js.map +1 -0
- package/dist/testing/hostile-tree.d.ts +79 -0
- package/dist/testing/hostile-tree.d.ts.map +1 -0
- package/dist/testing/hostile-tree.js +164 -0
- package/dist/testing/hostile-tree.js.map +1 -0
- package/dist/testing/platform-gates.d.ts +31 -0
- package/dist/testing/platform-gates.d.ts.map +1 -0
- package/dist/testing/platform-gates.js +31 -0
- package/dist/testing/platform-gates.js.map +1 -0
- package/dist/testing/temp-dir.d.ts +255 -0
- package/dist/testing/temp-dir.d.ts.map +1 -0
- package/dist/testing/temp-dir.js +363 -0
- package/dist/testing/temp-dir.js.map +1 -0
- package/dist/testing.d.ts +21 -0
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +27 -1
- package/dist/testing.js.map +1 -1
- package/dist/text-content.d.ts.map +1 -1
- package/dist/text-content.js +7 -1
- package/dist/text-content.js.map +1 -1
- package/dist/text-file.d.ts.map +1 -1
- package/dist/text-file.js +0 -2
- package/dist/text-file.js.map +1 -1
- package/dist/timing-dump.d.ts +1 -1
- package/dist/timing-dump.d.ts.map +1 -1
- package/dist/timing-dump.js +1 -2
- package/dist/timing-dump.js.map +1 -1
- package/dist/windows-shell.d.ts +0 -1
- package/dist/windows-shell.d.ts.map +1 -1
- package/dist/windows-shell.js +0 -1
- package/dist/windows-shell.js.map +1 -1
- package/dist/zod-introspection.js +2 -2
- package/dist/zod-introspection.js.map +1 -1
- package/eslint/README.md +132 -60
- package/eslint/index.cjs +111 -147
- package/eslint/index.d.cts +14 -14
- package/eslint/rules/callee-name.cjs +23 -0
- package/eslint/rules/commands-import-boundary.cjs +148 -0
- package/eslint/rules/dead-import.cjs +1 -1
- package/eslint/rules/dirent-type-needs-symlink-check.cjs +282 -0
- package/eslint/rules/eslint-rule-factory.cjs +5 -2
- package/eslint/rules/exempt-path-matcher.cjs +1 -1
- package/eslint/rules/explicit-zod-strictness.cjs +120 -0
- package/eslint/rules/no-bare-dynamic-import-path.cjs +5 -1
- package/eslint/rules/no-bare-executable-spawn.cjs +70 -0
- package/eslint/rules/no-bare-symlink-in-tests.cjs +16 -3
- package/eslint/rules/no-blind-catch.cjs +171 -0
- package/eslint/rules/no-child-process-execSync.cjs +8 -0
- package/eslint/rules/no-decaying-referent.cjs +216 -0
- package/eslint/rules/no-dotdot-containment.cjs +160 -0
- package/eslint/rules/no-file-url-string-concat.cjs +4 -1
- package/eslint/rules/no-fragile-entrypoint-guard.cjs +228 -0
- package/eslint/rules/no-fs-mkdirSync.cjs +8 -0
- package/eslint/rules/no-fs-promises-cp.cjs +7 -0
- package/eslint/rules/no-fs-realpathSync.cjs +8 -0
- package/eslint/rules/no-hardcoded-path-split.cjs +7 -2
- package/eslint/rules/no-io-in-unit-tier.cjs +128 -0
- package/eslint/rules/no-literal-process-exit.cjs +129 -0
- package/eslint/rules/no-manual-path-normalize.cjs +7 -2
- package/eslint/rules/no-os-tmpdir.cjs +8 -0
- package/eslint/rules/no-path-operations-in-comparisons.cjs +7 -2
- package/eslint/rules/no-path-sep-in-strings.cjs +7 -2
- package/eslint/rules/no-path-startswith.cjs +7 -2
- package/eslint/rules/no-process-exit-in-phase.cjs +12 -2
- package/eslint/rules/no-raw-node-path.cjs +563 -0
- package/eslint/rules/no-raw-text-decode.cjs +10 -1
- package/eslint/rules/no-registry-count-pin.cjs +255 -0
- package/eslint/rules/no-self-package-import.cjs +13 -2
- package/eslint/rules/no-test-scoped-functions.cjs +10 -3
- package/eslint/rules/no-unix-shell-commands.cjs +6 -2
- package/eslint/rules/no-unsafe-root-join.cjs +23 -2
- package/eslint/rules/no-url-pathname-for-fs.cjs +5 -1
- package/eslint/rules/no-version-literal.cjs +151 -0
- package/eslint/rules/prefer-startswith-over-regex.cjs +9 -0
- package/eslint/rules/require-justified-skip.cjs +8 -2
- package/eslint/rules/safe-import.cjs +1 -1
- package/package.json +5 -10
- package/eslint/rules/no-path-join.cjs +0 -14
- package/eslint/rules/no-path-relative.cjs +0 -14
- package/eslint/rules/no-path-resolve.cjs +0 -14
- package/eslint/rules/path-function-rule-factory.cjs +0 -496
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a listing does with a directory it was refused — the vocabulary shared
|
|
3
|
+
* by the filesystem walk (`file-crawler.ts`) and the git listings
|
|
4
|
+
* (`git-utils.ts`), so a caller decides ONCE for both routes.
|
|
5
|
+
*
|
|
6
|
+
* 🚨 A refused listing is a gap in the POPULATION: every file beneath that
|
|
7
|
+
* directory the listing would have found was never seen and is absent from
|
|
8
|
+
* every count downstream. There are exactly two honest answers — stop, with a
|
|
9
|
+
* sentence the adopter can act on, or keep going and REPORT the gap — and
|
|
10
|
+
* which one is right is the caller's knowledge, not the crawler's. So the
|
|
11
|
+
* policy is {@link UnreadablePolicy}, it is REQUIRED wherever a listing is
|
|
12
|
+
* asked for, and it has no default.
|
|
13
|
+
*
|
|
14
|
+
* 🪤 It used to be `onUnreadable?`, an optional callback. Every caller that
|
|
15
|
+
* ignored it compiled; the two routes then defaulted differently (the walk
|
|
16
|
+
* threw a programmer-facing sentence, the git route dropped the refusal off
|
|
17
|
+
* its stderr without a word); and the miss surfaced one review round later as
|
|
18
|
+
* a HIGH. A required field is what makes `tsc` enumerate the callers.
|
|
19
|
+
*/
|
|
20
|
+
import { VatError } from './errors/vat-error.js';
|
|
21
|
+
import { transientRefusalClause } from './fs-utils.js';
|
|
22
|
+
import { safePath, toForwardSlash } from './path-utils.js';
|
|
23
|
+
/**
|
|
24
|
+
* Thrown by a listing under `{ refuse }` when a directory refused to be
|
|
25
|
+
* listed. The message is the adopter's sentence — see {@link refusedListingMessage}.
|
|
26
|
+
*/
|
|
27
|
+
export class DirectoryListingRefusedError extends VatError {
|
|
28
|
+
refusal;
|
|
29
|
+
constructor(refusal, context) {
|
|
30
|
+
super('DIRECTORY_LISTING_REFUSED', refusedListingMessage(refusal, context));
|
|
31
|
+
this.refusal = refusal;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* What a refused listing costs, said so it is true on BOTH routes.
|
|
36
|
+
*
|
|
37
|
+
* 🪤 It used to read "so nothing beneath it was enumerated: every file there
|
|
38
|
+
* is in the declared scan". Both halves were the readdir walk's truth and the
|
|
39
|
+
* git route's lie: `git ls-files --others` reports the refusal while the index
|
|
40
|
+
* has already named every TRACKED file beneath the directory — the same run
|
|
41
|
+
* that printed the sentence had scanned one of them — and the projection's
|
|
42
|
+
* population is the whole non-ignored tree, which no `include`/`exclude`
|
|
43
|
+
* "declared". What holds on every route is the hazard itself: whatever the
|
|
44
|
+
* listing would have found and nothing else named is missing, and the shorter
|
|
45
|
+
* result is indistinguishable from a complete one.
|
|
46
|
+
*/
|
|
47
|
+
const REFUSAL_GAP_CLAUSE = 'any file beneath it that no other listing named is absent from every count, ' +
|
|
48
|
+
'and the result cannot be told from a complete one';
|
|
49
|
+
/**
|
|
50
|
+
* The adopter-facing sentence: root-relative directory, errno, the gap, and
|
|
51
|
+
* the caller's remedy — **the one owner of that sentence.**
|
|
52
|
+
*
|
|
53
|
+
* Exported so every lane that refuses says the same thing. Three lanes used to
|
|
54
|
+
* compose this by hand and had drifted by a clause each ("enumerated" /
|
|
55
|
+
* "scanned" / "the population could not be enumerated"); the transient clause
|
|
56
|
+
* in particular was written once in `fs-utils.ts` precisely so nobody would.
|
|
57
|
+
* See {@link REFUSAL_GAP_CLAUSE} for why it asserts nothing about what WAS
|
|
58
|
+
* enumerated beneath the directory.
|
|
59
|
+
*
|
|
60
|
+
* @param refusal - What was refused
|
|
61
|
+
* @param context - The root to express it against, and what the adopter can do
|
|
62
|
+
* @returns One sentence, root-relative, never containing the absolute path
|
|
63
|
+
*/
|
|
64
|
+
export function refusedListingMessage(refusal, context) {
|
|
65
|
+
const relative = toForwardSlash(safePath.relative(context.root, refusal.directory));
|
|
66
|
+
const where = relative === '' ? 'the scan root itself' : `the directory '${relative}'`;
|
|
67
|
+
const remedy = refusal.transient
|
|
68
|
+
? `${transientRefusalClause(refusal.code)}, so nothing is wrong with the tree — re-run before investigating anything.`
|
|
69
|
+
: context.remedy;
|
|
70
|
+
return `Listing ${where} was refused (${refusal.code}): ${REFUSAL_GAP_CLAUSE}. ${remedy}`;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Apply the caller's policy to one refusal: throw under `{ refuse }`, hand it
|
|
74
|
+
* over under `{ degrade }`. The single place either route settles a refusal,
|
|
75
|
+
* so the two cannot drift.
|
|
76
|
+
*
|
|
77
|
+
* @param policy - The caller's decision
|
|
78
|
+
* @param refusal - The directory that could not be listed
|
|
79
|
+
*/
|
|
80
|
+
export function settleRefusal(policy, refusal) {
|
|
81
|
+
if ('refuse' in policy)
|
|
82
|
+
throw new DirectoryListingRefusedError(refusal, policy.refuse);
|
|
83
|
+
policy.degrade(refusal);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Refuse an omitted policy up front, by name, before any directory is listed.
|
|
87
|
+
*
|
|
88
|
+
* The type already makes `unreadable` required, and that is what enumerates
|
|
89
|
+
* the TypeScript callers. This is the runtime half, for the callers the type
|
|
90
|
+
* cannot reach — test files are not typechecked by the build, and a JavaScript
|
|
91
|
+
* adopter has no compiler at all. Without it an omitted policy would surface
|
|
92
|
+
* only on the first refusal, as a `TypeError` from the `in` operator, and a
|
|
93
|
+
* tree with nothing unreadable would keep returning complete lists — the
|
|
94
|
+
* optional seam back again, one layer down.
|
|
95
|
+
*
|
|
96
|
+
* @param policy - What the caller passed as `unreadable`
|
|
97
|
+
* @param api - The entry point, for the sentence
|
|
98
|
+
*/
|
|
99
|
+
export function requireUnreadablePolicy(policy, api) {
|
|
100
|
+
if (policy === undefined) {
|
|
101
|
+
throw new TypeError(`${api}: \`unreadable\` is required — pass { refuse: { root, remedy } } to stop on a directory the listing cannot open, ` +
|
|
102
|
+
'or { degrade: (refusal) => … } to keep going and report the gap yourself.');
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
//# sourceMappingURL=listing-refusal.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"listing-refusal.js","sourceRoot":"","sources":["../src/listing-refusal.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AACjD,OAAO,EAAyB,sBAAsB,EAAE,MAAM,eAAe,CAAC;AAC9E,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AA4C3D;;;GAGG;AACH,MAAM,OAAO,4BAA6B,SAAQ,QAAQ;IAC/C,OAAO,CAAmB;IAEnC,YAAY,OAAyB,EAAE,OAA6B;QAClE,KAAK,CAAC,2BAA2B,EAAE,qBAAqB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC;QAC5E,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IACzB,CAAC;CACF;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,kBAAkB,GACtB,8EAA8E;IAC9E,mDAAmD,CAAC;AAEtD;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,qBAAqB,CAAC,OAAyB,EAAE,OAA6B;IAC5F,MAAM,QAAQ,GAAG,cAAc,CAAC,QAAQ,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC;IACpF,MAAM,KAAK,GAAG,QAAQ,KAAK,EAAE,CAAC,CAAC,CAAC,sBAAsB,CAAC,CAAC,CAAC,kBAAkB,QAAQ,GAAG,CAAC;IACvF,MAAM,MAAM,GAAG,OAAO,CAAC,SAAS;QAC9B,CAAC,CAAC,GAAG,sBAAsB,CAAC,OAAO,CAAC,IAAI,CAAC,6EAA6E;QACtH,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC;IACnB,OAAO,WAAW,KAAK,iBAAiB,OAAO,CAAC,IAAI,MAAM,kBAAkB,KAAK,MAAM,EAAE,CAAC;AAC5F,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,aAAa,CAAC,MAAwB,EAAE,OAAyB;IAC/E,IAAI,QAAQ,IAAI,MAAM;QAAE,MAAM,IAAI,4BAA4B,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC;IACvF,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;AAC1B,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,uBAAuB,CAAC,MAAoC,EAAE,GAAW;IACvF,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QACzB,MAAM,IAAI,SAAS,CACjB,GAAG,GAAG,mHAAmH;YACvH,2EAA2E,CAC9E,CAAC;IACJ,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,64 @@
|
|
|
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
|
+
/**
|
|
25
|
+
* Where a candidate stands relative to a root, as the filesystem sees it.
|
|
26
|
+
*
|
|
27
|
+
* - `inside` — the candidate exists and its realpath is a STRICT descendant of
|
|
28
|
+
* the root's realpath. A delete or copy may proceed.
|
|
29
|
+
* - `outside` — its realpath (or, when it does not exist, the realpath of its
|
|
30
|
+
* deepest existing ancestor with the missing remainder re-appended) is not
|
|
31
|
+
* under the root. The root itself is `outside` too: nothing is under itself,
|
|
32
|
+
* and a sink that could delete its own root has no business here.
|
|
33
|
+
* - `absent` — nothing exists at the candidate, and creating it would land
|
|
34
|
+
* inside the root. A create sink proceeds; a delete sink has nothing to do.
|
|
35
|
+
*/
|
|
36
|
+
export type Containment = 'inside' | 'outside' | 'absent';
|
|
37
|
+
/**
|
|
38
|
+
* Is `candidate` strictly under `root`, as the filesystem judges it?
|
|
39
|
+
*
|
|
40
|
+
* Both paths may be relative (resolved from cwd) or absolute, in either
|
|
41
|
+
* separator; neither has to exist. See {@link Containment} for the three
|
|
42
|
+
* answers and which sinks accept which.
|
|
43
|
+
*
|
|
44
|
+
* ⚠️ Costs one `lstat` per missing component plus two `realpath`s. That is
|
|
45
|
+
* nothing for a sink — a delete or a copy dwarfs it — and too much for a loop
|
|
46
|
+
* over every link in a corpus, which is why the projection and link lanes
|
|
47
|
+
* classify lexically with `relativeEscapesRoot` and never call this.
|
|
48
|
+
*
|
|
49
|
+
* @param root - The directory the action must stay inside
|
|
50
|
+
* @param candidate - The path the action is about to touch
|
|
51
|
+
* @returns `'inside'`, `'outside'`, or `'absent'`
|
|
52
|
+
* @throws When the OS refuses to examine either path for any reason but
|
|
53
|
+
* absence — a refusal is reported, never read as "not there"
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* // A delete sink: only an existing, contained target may go.
|
|
57
|
+
* if (isUnderRoot(skillsDir, target) !== 'inside') throw new Error(`refusing to remove ${target}: not inside ${skillsDir}`);
|
|
58
|
+
* await rm(target, { recursive: true, force: true });
|
|
59
|
+
*
|
|
60
|
+
* // A create sink: absent-but-contained is exactly the happy path.
|
|
61
|
+
* if (isUnderRoot(skillsDir, dest) === 'outside') throw new Error(`refusing to install to ${dest}: not inside ${skillsDir}`);
|
|
62
|
+
*/
|
|
63
|
+
export declare function isUnderRoot(root: string, candidate: string): Containment;
|
|
64
|
+
//# sourceMappingURL=path-containment.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"path-containment.d.ts","sourceRoot":"","sources":["../src/path-containment.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAQH;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,WAAW,GAAG,QAAQ,GAAG,SAAS,GAAG,QAAQ,CAAC;AAkD1D;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,WAAW,CAQxE"}
|
|
@@ -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"}
|
package/dist/path-core.d.ts
CHANGED
|
@@ -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
|
*
|
|
@@ -56,6 +76,59 @@ export declare function isAbsoluteAnyPlatform(p: string): boolean;
|
|
|
56
76
|
* hasParentTraversalSegment('a..b/c') // false (".." must be a whole segment)
|
|
57
77
|
*/
|
|
58
78
|
export declare function hasParentTraversalSegment(p: string): boolean;
|
|
79
|
+
/**
|
|
80
|
+
* True when a root-relative path — as `safePath.relative(root, p)` spells it —
|
|
81
|
+
* names something the root does not contain.
|
|
82
|
+
*
|
|
83
|
+
* The three shapes `path.relative` can return for an outsider: the parent
|
|
84
|
+
* itself (`..`), a climb through it (`../x`), and, on Windows only, an
|
|
85
|
+
* absolute path (a target on another drive has no relative spelling). A name
|
|
86
|
+
* that merely BEGINS with two dots (`..notes.md`) is a member and reads as one;
|
|
87
|
+
* the bare `startsWith('..')` this replaces refused it, dropped the file from
|
|
88
|
+
* the package and unlinked the reference, at exit 0.
|
|
89
|
+
*
|
|
90
|
+
* ⚠️ **Lexical, on purpose.** This classifies a relative path that was already
|
|
91
|
+
* computed; it does not ask the filesystem, so a symlink inside the root that
|
|
92
|
+
* points outside reads as inside here. That is the right answer for the
|
|
93
|
+
* callers that own no root to ask about — a projection identity, a permission
|
|
94
|
+
* pattern, a report relativizer — and the WRONG one for a delete or copy sink,
|
|
95
|
+
* which must ask {@link isUnderRoot} from `@vibe-agent-toolkit/utils` instead.
|
|
96
|
+
* The empty relative (the root itself) is not an escape; whether equality is
|
|
97
|
+
* acceptable is the caller's question and is asked beside this one.
|
|
98
|
+
*
|
|
99
|
+
* @param normalizedRelative - A forward-slashed root-relative path
|
|
100
|
+
* @returns True when the root does not contain it
|
|
101
|
+
*
|
|
102
|
+
* @example
|
|
103
|
+
* relativeEscapesRoot(safePath.relative(root, p)) // the whole idiom
|
|
104
|
+
* relativeEscapesRoot('../x') // true
|
|
105
|
+
* relativeEscapesRoot('..notes.md') // false — a member whose name starts with dots
|
|
106
|
+
*/
|
|
107
|
+
export declare function relativeEscapesRoot(normalizedRelative: string): boolean;
|
|
108
|
+
/**
|
|
109
|
+
* True when `name` can only ever be ONE directory entry under whatever it is
|
|
110
|
+
* joined to: non-empty, not `.` or `..`, no separator of either platform, no
|
|
111
|
+
* NUL, no drive-letter prefix.
|
|
112
|
+
*
|
|
113
|
+
* The check for a caller-controlled NAME — a skill name from a manifest, a
|
|
114
|
+
* session id, a positional the user typed — that is about to become
|
|
115
|
+
* `join(root, name)`. A name is not a path: `..cache` and `a..b` are legitimate
|
|
116
|
+
* entries, and `includes('..')` refused them while `startsWith` let `x/../..`
|
|
117
|
+
* through. The question is whether the join can land anywhere but directly
|
|
118
|
+
* under `root`, and that is answered by the segment's shape alone, with no
|
|
119
|
+
* filesystem — which is also why this belongs beside the path helpers rather
|
|
120
|
+
* than beside {@link isUnderRoot}, which is the check for a PATH.
|
|
121
|
+
*
|
|
122
|
+
* @param name - The proposed entry name
|
|
123
|
+
* @returns True when `join(root, name)` is a direct child of `root`
|
|
124
|
+
*
|
|
125
|
+
* @example
|
|
126
|
+
* isSingleFsSegment('my-skill') // true
|
|
127
|
+
* isSingleFsSegment('..cache') // true — dots inside a name are just dots
|
|
128
|
+
* isSingleFsSegment('../victim') // false
|
|
129
|
+
* isSingleFsSegment('..') // false
|
|
130
|
+
*/
|
|
131
|
+
export declare function isSingleFsSegment(name: string): boolean;
|
|
59
132
|
/**
|
|
60
133
|
* Compute a `ValidationIssue.location`: an absolute source file path made
|
|
61
134
|
* relative to the scan/project root, forward-slashed.
|
|
@@ -171,7 +244,8 @@ export declare function toNfc(value: string): string;
|
|
|
171
244
|
* strings, or matched with glob patterns.
|
|
172
245
|
*
|
|
173
246
|
* **Use these instead of importing from `node:path` directly.**
|
|
174
|
-
* ESLint
|
|
247
|
+
* An ESLint rule enforces this — see `no-raw-node-path` (its `functions` option
|
|
248
|
+
* table maps each of `join`/`resolve`/`relative` to its `safePath.*` replacement).
|
|
175
249
|
*
|
|
176
250
|
* @example
|
|
177
251
|
* ```typescript
|
|
@@ -182,7 +256,7 @@ export declare function toNfc(value: string): string;
|
|
|
182
256
|
* safePath.resolve('/project', './docs') // → '/project/docs'
|
|
183
257
|
* safePath.relative('/project/docs', '/project') // → '..'
|
|
184
258
|
* safePath.joinUnderRoot('/harness', 'skill-abc') // → '/harness/skill-abc'
|
|
185
|
-
* safePath.joinUnderRoot('/harness', '../escape') // throws
|
|
259
|
+
* safePath.joinUnderRoot('/harness', '../escape') // throws PathEscapesRootError
|
|
186
260
|
* ```
|
|
187
261
|
*/
|
|
188
262
|
export declare const safePath: {
|
|
@@ -210,7 +284,7 @@ export declare const safePath: {
|
|
|
210
284
|
* skill-test staging code was vulnerable to on Windows.
|
|
211
285
|
*
|
|
212
286
|
* @returns Forward-slash absolute path guaranteed to be inside `root`.
|
|
213
|
-
* @throws {
|
|
287
|
+
* @throws {PathEscapesRootError} If the resolved path would escape `root`.
|
|
214
288
|
*
|
|
215
289
|
* @example
|
|
216
290
|
* ```typescript
|
package/dist/path-core.d.ts.map
CHANGED
|
@@ -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,
|
|
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;;;;;;;;;;;;;GAaG;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;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;;;;;;;;;;;;;;;;;;;;;;;GAuBG;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
|
*
|
|
@@ -61,8 +83,75 @@ export function isAbsoluteAnyPlatform(p) {
|
|
|
61
83
|
* hasParentTraversalSegment('a..b/c') // false (".." must be a whole segment)
|
|
62
84
|
*/
|
|
63
85
|
export function hasParentTraversalSegment(p) {
|
|
86
|
+
// 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().
|
|
64
87
|
return toForwardSlash(p).split('/').includes('..');
|
|
65
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* True when a root-relative path — as `safePath.relative(root, p)` spells it —
|
|
91
|
+
* names something the root does not contain.
|
|
92
|
+
*
|
|
93
|
+
* The three shapes `path.relative` can return for an outsider: the parent
|
|
94
|
+
* itself (`..`), a climb through it (`../x`), and, on Windows only, an
|
|
95
|
+
* absolute path (a target on another drive has no relative spelling). A name
|
|
96
|
+
* that merely BEGINS with two dots (`..notes.md`) is a member and reads as one;
|
|
97
|
+
* the bare `startsWith('..')` this replaces refused it, dropped the file from
|
|
98
|
+
* the package and unlinked the reference, at exit 0.
|
|
99
|
+
*
|
|
100
|
+
* ⚠️ **Lexical, on purpose.** This classifies a relative path that was already
|
|
101
|
+
* computed; it does not ask the filesystem, so a symlink inside the root that
|
|
102
|
+
* points outside reads as inside here. That is the right answer for the
|
|
103
|
+
* callers that own no root to ask about — a projection identity, a permission
|
|
104
|
+
* pattern, a report relativizer — and the WRONG one for a delete or copy sink,
|
|
105
|
+
* which must ask {@link isUnderRoot} from `@vibe-agent-toolkit/utils` instead.
|
|
106
|
+
* The empty relative (the root itself) is not an escape; whether equality is
|
|
107
|
+
* acceptable is the caller's question and is asked beside this one.
|
|
108
|
+
*
|
|
109
|
+
* @param normalizedRelative - A forward-slashed root-relative path
|
|
110
|
+
* @returns True when the root does not contain it
|
|
111
|
+
*
|
|
112
|
+
* @example
|
|
113
|
+
* relativeEscapesRoot(safePath.relative(root, p)) // the whole idiom
|
|
114
|
+
* relativeEscapesRoot('../x') // true
|
|
115
|
+
* relativeEscapesRoot('..notes.md') // false — a member whose name starts with dots
|
|
116
|
+
*/
|
|
117
|
+
export function relativeEscapesRoot(normalizedRelative) {
|
|
118
|
+
// 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().
|
|
119
|
+
return normalizedRelative === '..' || normalizedRelative.startsWith('../') || isAbsoluteAnyPlatform(normalizedRelative);
|
|
120
|
+
}
|
|
121
|
+
/** A Windows drive-relative spelling (`C:`) — `path.resolve` sends it to that drive's cwd. */
|
|
122
|
+
const DRIVE_RELATIVE = /^[A-Za-z]:/u;
|
|
123
|
+
/**
|
|
124
|
+
* True when `name` can only ever be ONE directory entry under whatever it is
|
|
125
|
+
* joined to: non-empty, not `.` or `..`, no separator of either platform, no
|
|
126
|
+
* NUL, no drive-letter prefix.
|
|
127
|
+
*
|
|
128
|
+
* The check for a caller-controlled NAME — a skill name from a manifest, a
|
|
129
|
+
* session id, a positional the user typed — that is about to become
|
|
130
|
+
* `join(root, name)`. A name is not a path: `..cache` and `a..b` are legitimate
|
|
131
|
+
* entries, and `includes('..')` refused them while `startsWith` let `x/../..`
|
|
132
|
+
* through. The question is whether the join can land anywhere but directly
|
|
133
|
+
* under `root`, and that is answered by the segment's shape alone, with no
|
|
134
|
+
* filesystem — which is also why this belongs beside the path helpers rather
|
|
135
|
+
* than beside {@link isUnderRoot}, which is the check for a PATH.
|
|
136
|
+
*
|
|
137
|
+
* @param name - The proposed entry name
|
|
138
|
+
* @returns True when `join(root, name)` is a direct child of `root`
|
|
139
|
+
*
|
|
140
|
+
* @example
|
|
141
|
+
* isSingleFsSegment('my-skill') // true
|
|
142
|
+
* isSingleFsSegment('..cache') // true — dots inside a name are just dots
|
|
143
|
+
* isSingleFsSegment('../victim') // false
|
|
144
|
+
* isSingleFsSegment('..') // false
|
|
145
|
+
*/
|
|
146
|
+
export function isSingleFsSegment(name) {
|
|
147
|
+
return (name !== '' &&
|
|
148
|
+
name !== '.' &&
|
|
149
|
+
name !== '..' &&
|
|
150
|
+
!name.includes('/') &&
|
|
151
|
+
!name.includes('\\') &&
|
|
152
|
+
!name.includes('\0') &&
|
|
153
|
+
!DRIVE_RELATIVE.test(name));
|
|
154
|
+
}
|
|
66
155
|
/**
|
|
67
156
|
* Compute a `ValidationIssue.location`: an absolute source file path made
|
|
68
157
|
* relative to the scan/project root, forward-slashed.
|
|
@@ -194,7 +283,8 @@ export function toNfc(value) {
|
|
|
194
283
|
* strings, or matched with glob patterns.
|
|
195
284
|
*
|
|
196
285
|
* **Use these instead of importing from `node:path` directly.**
|
|
197
|
-
* ESLint
|
|
286
|
+
* An ESLint rule enforces this — see `no-raw-node-path` (its `functions` option
|
|
287
|
+
* table maps each of `join`/`resolve`/`relative` to its `safePath.*` replacement).
|
|
198
288
|
*
|
|
199
289
|
* @example
|
|
200
290
|
* ```typescript
|
|
@@ -205,7 +295,7 @@ export function toNfc(value) {
|
|
|
205
295
|
* safePath.resolve('/project', './docs') // → '/project/docs'
|
|
206
296
|
* safePath.relative('/project/docs', '/project') // → '..'
|
|
207
297
|
* safePath.joinUnderRoot('/harness', 'skill-abc') // → '/harness/skill-abc'
|
|
208
|
-
* safePath.joinUnderRoot('/harness', '../escape') // throws
|
|
298
|
+
* safePath.joinUnderRoot('/harness', '../escape') // throws PathEscapesRootError
|
|
209
299
|
* ```
|
|
210
300
|
*/
|
|
211
301
|
export const safePath = {
|
|
@@ -239,7 +329,7 @@ export const safePath = {
|
|
|
239
329
|
* skill-test staging code was vulnerable to on Windows.
|
|
240
330
|
*
|
|
241
331
|
* @returns Forward-slash absolute path guaranteed to be inside `root`.
|
|
242
|
-
* @throws {
|
|
332
|
+
* @throws {PathEscapesRootError} If the resolved path would escape `root`.
|
|
243
333
|
*
|
|
244
334
|
* @example
|
|
245
335
|
* ```typescript
|
|
@@ -255,12 +345,12 @@ export const safePath = {
|
|
|
255
345
|
// BEFORE resolving, so the error message can name the offending segment.
|
|
256
346
|
for (const seg of segments) {
|
|
257
347
|
if (path.isAbsolute(seg)) {
|
|
258
|
-
throw new
|
|
348
|
+
throw new PathEscapesRootError(root, `segment "${seg}" is absolute and`);
|
|
259
349
|
}
|
|
260
350
|
// Windows drive-letter check for POSIX hosts (path.isAbsolute won't catch
|
|
261
351
|
// 'C:\...' on POSIX, but node's path.win32.isAbsolute does).
|
|
262
352
|
if (path.win32.isAbsolute(seg)) {
|
|
263
|
-
throw new
|
|
353
|
+
throw new PathEscapesRootError(root, `segment "${seg}" contains a Windows drive letter and`);
|
|
264
354
|
}
|
|
265
355
|
}
|
|
266
356
|
const resolvedRoot = path.resolve(root);
|
|
@@ -275,7 +365,7 @@ export const safePath = {
|
|
|
275
365
|
// which would match '/rootEvil' when root is '/root').
|
|
276
366
|
const rootPrefix = fwdRoot.endsWith('/') ? fwdRoot : `${fwdRoot}/`;
|
|
277
367
|
if (fwdResult !== fwdRoot && !fwdResult.startsWith(rootPrefix)) {
|
|
278
|
-
throw new
|
|
368
|
+
throw new PathEscapesRootError(fwdRoot, `result "${fwdResult}"`);
|
|
279
369
|
}
|
|
280
370
|
return fwdResult;
|
|
281
371
|
},
|
package/dist/path-core.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"path-core.js","sourceRoot":"","sources":["../src/path-core.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,cAAc,CAAC,CAAS;IACtC,OAAO,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;AAC5B,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,qBAAqB,CAAC,CAAS;IAC7C,OAAO,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;AAC9D,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,yBAAyB,CAAC,CAAS;IACjD,OAAO,cAAc,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;AACrD,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,UAAU,aAAa,CAAC,cAAsB,EAAE,WAAmB;IACvE,OAAO,cAAc,CAAC,IAAI,CAAC,QAAQ,CAAC,WAAW,EAAE,cAAc,CAAC,CAAC,CAAC;AACpE,CAAC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,cAAc,CAAC,CAAS,EAAE,OAAe;IACvD,IAAI,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,CAAC;QACvB,OAAO,cAAc,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC;IAC3C,CAAC;IACD,OAAO,cAAc,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC;AAClD,CAAC;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,eAAe,CAAC,IAAY,EAAE,EAAU;IACtD,qDAAqD;IACrD,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IAEnC,+DAA+D;IAC/D,OAAO,cAAc,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC;AACpD,CAAC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,cAAc,CAAC,CAAS;IACtC,OAAO,CAAC,CAAC,UAAU,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;AACjC,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,MAAM,UAAU,KAAK,CAAC,KAAa;IACjC,OAAO,KAAK,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;AAChC,CAAC;AAED
|
|
1
|
+
{"version":3,"file":"path-core.js","sourceRoot":"","sources":["../src/path-core.ts"],"names":[],"mappings":"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"}
|
package/dist/path-utils.d.ts.map
CHANGED
|
@@ -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;
|
|
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"}
|