abb-opencode-local-rag 0.1.0
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/LICENSE +21 -0
- package/README.de.md +416 -0
- package/README.es.md +416 -0
- package/README.fr.md +416 -0
- package/README.md +491 -0
- package/README.pt-BR.md +416 -0
- package/README.zh-CN.md +416 -0
- package/dist/bin/install-skills.d.ts +20 -0
- package/dist/bin/install-skills.d.ts.map +1 -0
- package/dist/bin/install-skills.js +195 -0
- package/dist/bin/install-skills.js.map +1 -0
- package/dist/chunker/index.d.ts +24 -0
- package/dist/chunker/index.d.ts.map +1 -0
- package/dist/chunker/index.js +2 -0
- package/dist/chunker/index.js.map +1 -0
- package/dist/chunker/semantic-chunker.d.ts +97 -0
- package/dist/chunker/semantic-chunker.d.ts.map +1 -0
- package/dist/chunker/semantic-chunker.js +294 -0
- package/dist/chunker/semantic-chunker.js.map +1 -0
- package/dist/chunker/sentence-splitter.d.ts +28 -0
- package/dist/chunker/sentence-splitter.d.ts.map +1 -0
- package/dist/chunker/sentence-splitter.js +219 -0
- package/dist/chunker/sentence-splitter.js.map +1 -0
- package/dist/cli/common.d.ts +65 -0
- package/dist/cli/common.d.ts.map +1 -0
- package/dist/cli/common.js +138 -0
- package/dist/cli/common.js.map +1 -0
- package/dist/cli/delete.d.ts +8 -0
- package/dist/cli/delete.d.ts.map +1 -0
- package/dist/cli/delete.js +173 -0
- package/dist/cli/delete.js.map +1 -0
- package/dist/cli/file-collection.d.ts +2 -0
- package/dist/cli/file-collection.d.ts.map +1 -0
- package/dist/cli/file-collection.js +53 -0
- package/dist/cli/file-collection.js.map +1 -0
- package/dist/cli/ingest.d.ts +100 -0
- package/dist/cli/ingest.d.ts.map +1 -0
- package/dist/cli/ingest.js +363 -0
- package/dist/cli/ingest.js.map +1 -0
- package/dist/cli/list.d.ts +35 -0
- package/dist/cli/list.d.ts.map +1 -0
- package/dist/cli/list.js +210 -0
- package/dist/cli/list.js.map +1 -0
- package/dist/cli/options.d.ts +100 -0
- package/dist/cli/options.d.ts.map +1 -0
- package/dist/cli/options.js +241 -0
- package/dist/cli/options.js.map +1 -0
- package/dist/cli/query.d.ts +24 -0
- package/dist/cli/query.d.ts.map +1 -0
- package/dist/cli/query.js +191 -0
- package/dist/cli/query.js.map +1 -0
- package/dist/cli/read-neighbors.d.ts +11 -0
- package/dist/cli/read-neighbors.d.ts.map +1 -0
- package/dist/cli/read-neighbors.js +224 -0
- package/dist/cli/read-neighbors.js.map +1 -0
- package/dist/cli/status.d.ts +8 -0
- package/dist/cli/status.d.ts.map +1 -0
- package/dist/cli/status.js +80 -0
- package/dist/cli/status.js.map +1 -0
- package/dist/cli/sync.d.ts +8 -0
- package/dist/cli/sync.d.ts.map +1 -0
- package/dist/cli/sync.js +244 -0
- package/dist/cli/sync.js.map +1 -0
- package/dist/cli-main.d.ts +12 -0
- package/dist/cli-main.d.ts.map +1 -0
- package/dist/cli-main.js +63 -0
- package/dist/cli-main.js.map +1 -0
- package/dist/embedder/index.d.ts +85 -0
- package/dist/embedder/index.d.ts.map +1 -0
- package/dist/embedder/index.js +284 -0
- package/dist/embedder/index.js.map +1 -0
- package/dist/features/list.d.ts +37 -0
- package/dist/features/list.d.ts.map +1 -0
- package/dist/features/list.js +40 -0
- package/dist/features/list.js.map +1 -0
- package/dist/features/sync.d.ts +207 -0
- package/dist/features/sync.d.ts.map +1 -0
- package/dist/features/sync.js +380 -0
- package/dist/features/sync.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +53 -0
- package/dist/index.js.map +1 -0
- package/dist/ingest/compute.d.ts +86 -0
- package/dist/ingest/compute.d.ts.map +1 -0
- package/dist/ingest/compute.js +177 -0
- package/dist/ingest/compute.js.map +1 -0
- package/dist/ingest/file.d.ts +27 -0
- package/dist/ingest/file.d.ts.map +1 -0
- package/dist/ingest/file.js +67 -0
- package/dist/ingest/file.js.map +1 -0
- package/dist/ingest/visual.d.ts +45 -0
- package/dist/ingest/visual.d.ts.map +1 -0
- package/dist/ingest/visual.js +234 -0
- package/dist/ingest/visual.js.map +1 -0
- package/dist/parser/docx-parser.d.ts +12 -0
- package/dist/parser/docx-parser.d.ts.map +1 -0
- package/dist/parser/docx-parser.js +328 -0
- package/dist/parser/docx-parser.js.map +1 -0
- package/dist/parser/html-parser.d.ts +18 -0
- package/dist/parser/html-parser.d.ts.map +1 -0
- package/dist/parser/html-parser.js +102 -0
- package/dist/parser/html-parser.js.map +1 -0
- package/dist/parser/index.d.ts +214 -0
- package/dist/parser/index.d.ts.map +1 -0
- package/dist/parser/index.js +454 -0
- package/dist/parser/index.js.map +1 -0
- package/dist/parser/pdf-extract.d.ts +81 -0
- package/dist/parser/pdf-extract.d.ts.map +1 -0
- package/dist/parser/pdf-extract.js +112 -0
- package/dist/parser/pdf-extract.js.map +1 -0
- package/dist/parser/pdf-filter.d.ts +117 -0
- package/dist/parser/pdf-filter.d.ts.map +1 -0
- package/dist/parser/pdf-filter.js +528 -0
- package/dist/parser/pdf-filter.js.map +1 -0
- package/dist/parser/title-extractor.d.ts +69 -0
- package/dist/parser/title-extractor.d.ts.map +1 -0
- package/dist/parser/title-extractor.js +145 -0
- package/dist/parser/title-extractor.js.map +1 -0
- package/dist/pdf-visual/captioner.d.ts +16 -0
- package/dist/pdf-visual/captioner.d.ts.map +1 -0
- package/dist/pdf-visual/captioner.js +63 -0
- package/dist/pdf-visual/captioner.js.map +1 -0
- package/dist/pdf-visual/captioners/fast.d.ts +7 -0
- package/dist/pdf-visual/captioners/fast.d.ts.map +1 -0
- package/dist/pdf-visual/captioners/fast.js +103 -0
- package/dist/pdf-visual/captioners/fast.js.map +1 -0
- package/dist/pdf-visual/captioners/quality.d.ts +7 -0
- package/dist/pdf-visual/captioners/quality.d.ts.map +1 -0
- package/dist/pdf-visual/captioners/quality.js +127 -0
- package/dist/pdf-visual/captioners/quality.js.map +1 -0
- package/dist/pdf-visual/captioners/shared.d.ts +44 -0
- package/dist/pdf-visual/captioners/shared.d.ts.map +1 -0
- package/dist/pdf-visual/captioners/shared.js +104 -0
- package/dist/pdf-visual/captioners/shared.js.map +1 -0
- package/dist/pdf-visual/detector.d.ts +9 -0
- package/dist/pdf-visual/detector.d.ts.map +1 -0
- package/dist/pdf-visual/detector.js +234 -0
- package/dist/pdf-visual/detector.js.map +1 -0
- package/dist/pdf-visual/index.d.ts +13 -0
- package/dist/pdf-visual/index.d.ts.map +1 -0
- package/dist/pdf-visual/index.js +45 -0
- package/dist/pdf-visual/index.js.map +1 -0
- package/dist/pdf-visual/renderer.d.ts +9 -0
- package/dist/pdf-visual/renderer.d.ts.map +1 -0
- package/dist/pdf-visual/renderer.js +177 -0
- package/dist/pdf-visual/renderer.js.map +1 -0
- package/dist/pdf-visual/types.d.ts +62 -0
- package/dist/pdf-visual/types.d.ts.map +1 -0
- package/dist/pdf-visual/types.js +32 -0
- package/dist/pdf-visual/types.js.map +1 -0
- package/dist/server/error-utils.d.ts +79 -0
- package/dist/server/error-utils.d.ts.map +1 -0
- package/dist/server/error-utils.js +148 -0
- package/dist/server/error-utils.js.map +1 -0
- package/dist/server/index.d.ts +258 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +1104 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/list-scanner.d.ts +52 -0
- package/dist/server/list-scanner.d.ts.map +1 -0
- package/dist/server/list-scanner.js +72 -0
- package/dist/server/list-scanner.js.map +1 -0
- package/dist/server/tool-definitions.d.ts +8 -0
- package/dist/server/tool-definitions.d.ts.map +1 -0
- package/dist/server/tool-definitions.js +181 -0
- package/dist/server/tool-definitions.js.map +1 -0
- package/dist/server/tool-input.d.ts +37 -0
- package/dist/server/tool-input.d.ts.map +1 -0
- package/dist/server/tool-input.js +216 -0
- package/dist/server/tool-input.js.map +1 -0
- package/dist/server/types.d.ts +331 -0
- package/dist/server/types.d.ts.map +1 -0
- package/dist/server/types.js +3 -0
- package/dist/server/types.js.map +1 -0
- package/dist/server-main.d.ts +46 -0
- package/dist/server-main.d.ts.map +1 -0
- package/dist/server-main.js +242 -0
- package/dist/server-main.js.map +1 -0
- package/dist/utils/base-dirs.d.ts +212 -0
- package/dist/utils/base-dirs.d.ts.map +1 -0
- package/dist/utils/base-dirs.js +422 -0
- package/dist/utils/base-dirs.js.map +1 -0
- package/dist/utils/errors.d.ts +24 -0
- package/dist/utils/errors.d.ts.map +1 -0
- package/dist/utils/errors.js +53 -0
- package/dist/utils/errors.js.map +1 -0
- package/dist/utils/limits.d.ts +26 -0
- package/dist/utils/limits.d.ts.map +1 -0
- package/dist/utils/limits.js +28 -0
- package/dist/utils/limits.js.map +1 -0
- package/dist/utils/list-sources.d.ts +47 -0
- package/dist/utils/list-sources.d.ts.map +1 -0
- package/dist/utils/list-sources.js +50 -0
- package/dist/utils/list-sources.js.map +1 -0
- package/dist/utils/raw-data-utils.d.ts +131 -0
- package/dist/utils/raw-data-utils.d.ts.map +1 -0
- package/dist/utils/raw-data-utils.js +255 -0
- package/dist/utils/raw-data-utils.js.map +1 -0
- package/dist/utils/scan.d.ts +126 -0
- package/dist/utils/scan.d.ts.map +1 -0
- package/dist/utils/scan.js +221 -0
- package/dist/utils/scan.js.map +1 -0
- package/dist/utils/scope-match.d.ts +43 -0
- package/dist/utils/scope-match.d.ts.map +1 -0
- package/dist/utils/scope-match.js +87 -0
- package/dist/utils/scope-match.js.map +1 -0
- package/dist/utils/sensitive-path.d.ts +23 -0
- package/dist/utils/sensitive-path.d.ts.map +1 -0
- package/dist/utils/sensitive-path.js +91 -0
- package/dist/utils/sensitive-path.js.map +1 -0
- package/dist/utils/sync-path-key.d.ts +20 -0
- package/dist/utils/sync-path-key.d.ts.map +1 -0
- package/dist/utils/sync-path-key.js +33 -0
- package/dist/utils/sync-path-key.js.map +1 -0
- package/dist/vectordb/index.d.ts +168 -0
- package/dist/vectordb/index.d.ts.map +1 -0
- package/dist/vectordb/index.js +619 -0
- package/dist/vectordb/index.js.map +1 -0
- package/dist/vectordb/search-filters.d.ts +39 -0
- package/dist/vectordb/search-filters.d.ts.map +1 -0
- package/dist/vectordb/search-filters.js +136 -0
- package/dist/vectordb/search-filters.js.map +1 -0
- package/dist/vectordb/types.d.ts +196 -0
- package/dist/vectordb/types.d.ts.map +1 -0
- package/dist/vectordb/types.js +224 -0
- package/dist/vectordb/types.js.map +1 -0
- package/package.json +105 -0
- package/skills/mcp-local-rag/SKILL.md +308 -0
- package/skills/mcp-local-rag/references/cli-reference.md +175 -0
- package/skills/mcp-local-rag/references/html-ingestion.md +78 -0
- package/skills/mcp-local-rag/references/query-optimization.md +57 -0
- package/skills/mcp-local-rag/references/result-refinement.md +56 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
// Shared bounded directory scan for supported document files.
|
|
2
|
+
//
|
|
3
|
+
// The single bounded directory walker behind the CLI `ingest` walker, the CLI
|
|
4
|
+
// `list` walker, and the MCP server's `list_files` scan: bounded depth, symlink
|
|
5
|
+
// skipping, exclude-path filtering, and supported-extension matching.
|
|
6
|
+
//
|
|
7
|
+
// The four collect predicates live in `classifyScanEntry` so a path a caller
|
|
8
|
+
// names explicitly (`classifyRequestedPath`) is judged by the same rules as a
|
|
9
|
+
// path the walk discovers — sync accepts both, and only one of them used to be
|
|
10
|
+
// filtered.
|
|
11
|
+
//
|
|
12
|
+
// Presentation (warning wording, when/where warnings are surfaced) and
|
|
13
|
+
// post-processing (sort/dedup) stay with each caller — this helper returns
|
|
14
|
+
// structured coverage facts (`unreadableDirs`, `depthLimitedDirs`,
|
|
15
|
+
// `skippedSymlinks`, and the derived `depthLimited`) so callers preserve their
|
|
16
|
+
// own, intentionally-different, user-facing messages. The path-granular facts
|
|
17
|
+
// let a caller tell an unobserved region apart from an observed one instead of
|
|
18
|
+
// treating any gap as a whole-scan failure.
|
|
19
|
+
import { lstat, readdir, realpath } from 'node:fs/promises';
|
|
20
|
+
import { basename, dirname, extname, join } from 'node:path';
|
|
21
|
+
import { SUPPORTED_EXTENSIONS } from '../parser/index.js';
|
|
22
|
+
import { MAX_SCAN_DEPTH } from './limits.js';
|
|
23
|
+
import { isInScope, isUnderOrEqual, shouldVisitDir } from './scope-match.js';
|
|
24
|
+
/**
|
|
25
|
+
* Canonical identity key for the `list`/`list_files` cross-reference: a file's
|
|
26
|
+
* realpath, falling back to the input path when realpath fails (orphaned or
|
|
27
|
+
* raw-data entries). Matching ingested DB entries against scanned files by this
|
|
28
|
+
* key recognizes the same physical file across symlinked spellings (prefix or
|
|
29
|
+
* alias). Storage, lookup, and display still use the normal resolve() path —
|
|
30
|
+
* realpath here is the file-identity comparison, not a user-facing value.
|
|
31
|
+
*/
|
|
32
|
+
export async function realpathForMatch(filePath) {
|
|
33
|
+
try {
|
|
34
|
+
return await realpath(filePath);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return filePath;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Canonical form of one explicitly requested path: its parent chain resolved
|
|
42
|
+
* through symbolic links, with the final component appended verbatim. `null` when
|
|
43
|
+
* the parent chain cannot be resolved at all — absent, or a directory this
|
|
44
|
+
* process may not traverse — which a caller must treat as "not contained",
|
|
45
|
+
* because telling those cases apart would report the state of paths outside its
|
|
46
|
+
* configured roots.
|
|
47
|
+
*
|
|
48
|
+
* Only the parent chain is resolved, because the requested entry itself is judged
|
|
49
|
+
* by {@link classifyRequestedPath}'s `lstat`: a symbolic link named directly
|
|
50
|
+
* inside a root is an in-root entry that is refused as a link, not a path to be
|
|
51
|
+
* reported by whatever it points at.
|
|
52
|
+
*
|
|
53
|
+
* `realpath` here is the containment (security) boundary, the same role it plays
|
|
54
|
+
* in `DocumentParser.validateFilePath` — never a spelling anything is stored,
|
|
55
|
+
* looked up, or displayed under. Those stay `resolve()`-only.
|
|
56
|
+
*/
|
|
57
|
+
export async function canonicalizeRequestedPath(path) {
|
|
58
|
+
try {
|
|
59
|
+
return join(await realpath(dirname(path)), basename(path));
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* True when `fullPath` sits under one of the configured excluded prefixes (the
|
|
67
|
+
* database or cache directory).
|
|
68
|
+
*
|
|
69
|
+
* Case-folded on Windows, whose filesystem is case-insensitive: the prefixes are
|
|
70
|
+
* built with `resolve()` only, which preserves whatever case `BASE_DIRS` and
|
|
71
|
+
* `DB_PATH` were spelled in, so a raw comparison let `C:\Docs\lancedb\raw.md`
|
|
72
|
+
* past `c:\docs\lancedb\`. That is worse than a plain miss, because sync's prune
|
|
73
|
+
* guard compares case-folded keys (`toSyncPathKey`): the internals were ingested
|
|
74
|
+
* and then could never be pruned. Both sides now agree.
|
|
75
|
+
*
|
|
76
|
+
* Exact-or-descendant via `isUnderOrEqual`: the prefixes carry a trailing
|
|
77
|
+
* separator, so `startsWith` matched the directory's contents but not the
|
|
78
|
+
* directory itself, and the walk descended into it once per run.
|
|
79
|
+
*
|
|
80
|
+
* Purely lexical — no `realpath`, `stat`, or any other syscall, because this runs
|
|
81
|
+
* once per directory entry on the walk shared with `list_files`, CLI `list`, and
|
|
82
|
+
* CLI `ingest`.
|
|
83
|
+
*/
|
|
84
|
+
function isUnderExcludedPrefix(fullPath, excludePaths, platform) {
|
|
85
|
+
const fold = (path) => (platform === 'win32' ? path.toLowerCase() : path);
|
|
86
|
+
const candidate = fold(fullPath);
|
|
87
|
+
return excludePaths.some((prefix) => isUnderOrEqual(candidate, fold(prefix)));
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* The collect predicates of {@link bfsCollectSupportedFiles} as one decision, so
|
|
91
|
+
* a discovered directory entry and an explicitly requested path
|
|
92
|
+
* ({@link classifyRequestedPath}) are judged by exactly the same rules instead of
|
|
93
|
+
* by two implementations that can drift.
|
|
94
|
+
*
|
|
95
|
+
* Evaluation order is part of the contract and matches the walk: a symbolic link
|
|
96
|
+
* is reported as a link even under an excluded prefix, and a directory is
|
|
97
|
+
* accepted without any extension test.
|
|
98
|
+
*
|
|
99
|
+
* `platform` is a parameter rather than a direct `process.platform` read — the
|
|
100
|
+
* same reason `toSyncPathKey` takes one: the Windows exclusion semantics
|
|
101
|
+
* ({@link isUnderExcludedPrefix}) must be provable on a macOS/Linux machine. The
|
|
102
|
+
* default leaves every call site unchanged.
|
|
103
|
+
*
|
|
104
|
+
* Both `Dirent` (from `readdir`) and `Stats` (from `lstat`) satisfy
|
|
105
|
+
* {@link EntryTypeFacts} structurally.
|
|
106
|
+
*/
|
|
107
|
+
export function classifyScanEntry(fullPath, entry, excludePaths, platform = process.platform) {
|
|
108
|
+
if (entry.isSymbolicLink())
|
|
109
|
+
return 'symlink';
|
|
110
|
+
if (isUnderExcludedPrefix(fullPath, excludePaths, platform))
|
|
111
|
+
return 'excluded';
|
|
112
|
+
if (entry.isDirectory())
|
|
113
|
+
return 'directory';
|
|
114
|
+
if (!entry.isFile())
|
|
115
|
+
return 'irregular';
|
|
116
|
+
return SUPPORTED_EXTENSIONS.has(extname(fullPath).toLowerCase()) ? 'file' : 'unsupported';
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Classify one explicitly requested path with {@link classifyScanEntry}, so a
|
|
120
|
+
* path a caller names is subject to the same predicates as a path the walker
|
|
121
|
+
* discovers.
|
|
122
|
+
*
|
|
123
|
+
* `lstat` rather than `stat`, so a symbolic link is reported as a link instead of
|
|
124
|
+
* as whatever it points at; and `lstat` rather than any read, so a caller can
|
|
125
|
+
* refuse the path before its bytes cost anything — reading a FIFO blocks forever,
|
|
126
|
+
* and reading through a link reaches outside the configured roots.
|
|
127
|
+
*
|
|
128
|
+
* Any stat failure is `'missing'`: an unreachable path and an absent one are the
|
|
129
|
+
* same non-answer to "what is here".
|
|
130
|
+
*/
|
|
131
|
+
export async function classifyRequestedPath(path, excludePaths, platform = process.platform) {
|
|
132
|
+
try {
|
|
133
|
+
return classifyScanEntry(path, await lstat(path), excludePaths, platform);
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return 'missing';
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Bounded BFS scan of a single root, collecting every supported file up to
|
|
141
|
+
* `maxDepth` levels deep, counted from `rootPath` itself. Symlinks are skipped
|
|
142
|
+
* (never followed) and recorded in `skippedSymlinks`; paths under any
|
|
143
|
+
* `excludePaths` prefix are filtered out. A per-directory `readdir` failure is
|
|
144
|
+
* captured into `unreadableDirs` and does not abort the scan (best-effort per
|
|
145
|
+
* directory); a branch pruned at `maxDepth` is captured into `depthLimitedDirs`.
|
|
146
|
+
*
|
|
147
|
+
* When `scope` is provided (non-empty), the predicate is pushed into the
|
|
148
|
+
* traversal: a directory is visited only if it is in-scope or an ancestor of
|
|
149
|
+
* some scope prefix, and a file is collected only if it is in-scope. A root that
|
|
150
|
+
* intersects no prefix is skipped without any `readdir`. An absent/empty `scope`
|
|
151
|
+
* leaves traversal and collection byte-for-byte unchanged.
|
|
152
|
+
*
|
|
153
|
+
* `platform` only selects how the exclusion comparison treats case (see
|
|
154
|
+
* {@link classifyScanEntry}); it defaults to the host, so every existing call is
|
|
155
|
+
* unchanged.
|
|
156
|
+
*
|
|
157
|
+
* Does not sort, dedupe, or emit warnings — callers handle those so their
|
|
158
|
+
* existing output contracts are preserved.
|
|
159
|
+
*/
|
|
160
|
+
export async function bfsCollectSupportedFiles(rootPath, excludePaths, maxDepth = MAX_SCAN_DEPTH, scope, platform = process.platform) {
|
|
161
|
+
const files = [];
|
|
162
|
+
const unreadableDirs = [];
|
|
163
|
+
const depthLimitedDirs = [];
|
|
164
|
+
const skippedSymlinks = [];
|
|
165
|
+
// Scope pushdown (shared with scanBaseDir via scope-match): visit a directory
|
|
166
|
+
// only if it is in-scope or an ancestor of the scoped subtree, and collect a
|
|
167
|
+
// file only if it is in-scope. A root intersecting no prefix is skipped
|
|
168
|
+
// without any `readdir`; absent scope leaves traversal/collection unchanged.
|
|
169
|
+
const queue = shouldVisitDir(rootPath, scope)
|
|
170
|
+
? [{ dirPath: rootPath, depth: 0 }]
|
|
171
|
+
: [];
|
|
172
|
+
while (queue.length > 0) {
|
|
173
|
+
const { dirPath, depth } = queue.shift();
|
|
174
|
+
if (depth >= maxDepth) {
|
|
175
|
+
// `dirPath` was reached but never read, so it is the first unvisited
|
|
176
|
+
// directory of this branch: it and all its descendants are unobserved.
|
|
177
|
+
depthLimitedDirs.push(dirPath);
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
// TypeScript's `readdir` has overloads keyed on the options shape; pin the
|
|
181
|
+
// encoding to `'utf8'` and cast so the loop operates on string-encoded
|
|
182
|
+
// Dirent entries (matches the rest of the codebase).
|
|
183
|
+
let entries;
|
|
184
|
+
try {
|
|
185
|
+
entries = (await readdir(dirPath, {
|
|
186
|
+
withFileTypes: true,
|
|
187
|
+
encoding: 'utf8',
|
|
188
|
+
}));
|
|
189
|
+
}
|
|
190
|
+
catch (error) {
|
|
191
|
+
const code = error && typeof error === 'object' && 'code' in error
|
|
192
|
+
? (error.code ?? 'UNKNOWN')
|
|
193
|
+
: 'UNKNOWN';
|
|
194
|
+
unreadableDirs.push({ dirPath, code });
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
for (const entry of entries) {
|
|
198
|
+
const fullPath = join(dirPath, entry.name);
|
|
199
|
+
const kind = classifyScanEntry(fullPath, entry, excludePaths, platform);
|
|
200
|
+
if (kind === 'symlink') {
|
|
201
|
+
skippedSymlinks.push(fullPath);
|
|
202
|
+
}
|
|
203
|
+
else if (kind === 'directory') {
|
|
204
|
+
if (shouldVisitDir(fullPath, scope)) {
|
|
205
|
+
queue.push({ dirPath: fullPath, depth: depth + 1 });
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
else if (kind === 'file' && isInScope(fullPath, scope)) {
|
|
209
|
+
files.push(fullPath);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return {
|
|
214
|
+
files,
|
|
215
|
+
unreadableDirs,
|
|
216
|
+
depthLimitedDirs,
|
|
217
|
+
skippedSymlinks,
|
|
218
|
+
depthLimited: depthLimitedDirs.length > 0,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
//# sourceMappingURL=scan.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scan.js","sourceRoot":"","sources":["../../src/utils/scan.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export interface NormalizedScopePrefix {
|
|
2
|
+
exact: string;
|
|
3
|
+
descendant: string;
|
|
4
|
+
}
|
|
5
|
+
/** Normalize one scope prefix for both in-memory and LanceDB matching. */
|
|
6
|
+
export declare function normalizeScopePrefix(prefix: string): NormalizedScopePrefix;
|
|
7
|
+
/**
|
|
8
|
+
* True when `path` equals `prefix` or is a descendant of it, using a separator
|
|
9
|
+
* boundary so `/foo/bar` does not match `/foo/barista`. The separator is derived
|
|
10
|
+
* from `prefix`; trailing separators on `prefix` are normalized so `/a/b`,
|
|
11
|
+
* `/a/b/`, and `/a/b//` are equivalent. `path` is compared verbatim, matching
|
|
12
|
+
* the SQL contract in `buildPrefixPredicate`.
|
|
13
|
+
*/
|
|
14
|
+
export declare function isUnderOrEqual(path: string, prefix: string): boolean;
|
|
15
|
+
/**
|
|
16
|
+
* True when `path` is under-or-equal any prefix in `prefixes` (union). Empty or
|
|
17
|
+
* undefined `prefixes` semantics are the caller's concern; an empty list yields
|
|
18
|
+
* false (membership against no prefixes).
|
|
19
|
+
*/
|
|
20
|
+
export declare function matchesAnyScope(path: string, prefixes: string[]): boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Return the prefixes in `scope` that are NOT absolute paths (server-OS path
|
|
23
|
+
* style, via `node:path` `isAbsolute` — the same path style scope matching
|
|
24
|
+
* uses). A non-absolute prefix matches nothing under the exact-or-descendant
|
|
25
|
+
* contract, so both list surfaces surface these as non-fatal warnings while
|
|
26
|
+
* preserving the "matches nothing" result semantics. Input order is preserved.
|
|
27
|
+
*/
|
|
28
|
+
export declare function nonAbsolutePrefixes(scope: string[]): string[];
|
|
29
|
+
/**
|
|
30
|
+
* Directory-visit predicate for the scoped BFS walk, shared by both walkers so
|
|
31
|
+
* the boundary semantics live in one place. Visit `dir` when there is no scope,
|
|
32
|
+
* when `dir` is in-scope (under-or-equal a prefix), or when `dir` is an ancestor
|
|
33
|
+
* of some prefix (must be descended to reach the scoped subtree). An absent or
|
|
34
|
+
* empty `scope` visits every directory (traversal unchanged).
|
|
35
|
+
*/
|
|
36
|
+
export declare function shouldVisitDir(dir: string, scope?: string[]): boolean;
|
|
37
|
+
/**
|
|
38
|
+
* File-collect predicate for the scoped BFS walk, shared by both walkers.
|
|
39
|
+
* Collect `path` when there is no scope, or when `path` is in-scope. An absent
|
|
40
|
+
* or empty `scope` collects every supported file (collection unchanged).
|
|
41
|
+
*/
|
|
42
|
+
export declare function isInScope(path: string, scope?: string[]): boolean;
|
|
43
|
+
//# sourceMappingURL=scope-match.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scope-match.d.ts","sourceRoot":"","sources":["../../src/utils/scope-match.ts"],"names":[],"mappings":"AAgCA,MAAM,WAAW,qBAAqB;IACpC,KAAK,EAAE,MAAM,CAAA;IACb,UAAU,EAAE,MAAM,CAAA;CACnB;AAED,0EAA0E;AAC1E,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,GAAG,qBAAqB,CAO1E;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAGpE;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,OAAO,CAEzE;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAE7D;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,EAAE,GAAG,OAAO,CAGrE;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,EAAE,GAAG,OAAO,CAEjE"}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// Boundary-safe, exact-or-descendant path prefix matcher shared by both BFS
|
|
2
|
+
// walkers and both list surfaces (MCP `list_files` and the `list` CLI).
|
|
3
|
+
//
|
|
4
|
+
// This is the JS counterpart of `vectordb`'s SQL `buildPrefixPredicate`
|
|
5
|
+
// (`src/vectordb/index.ts`): both implement the same exact-or-descendant
|
|
6
|
+
// contract (`path = prefix OR path startsWith prefix + separator`) with the
|
|
7
|
+
// same separator-boundary and trailing-separator normalization, so `/foo/bar`
|
|
8
|
+
// matches `/foo/bar` and `/foo/bar/x.md` but not `/foo/barista`. Both reference
|
|
9
|
+
// #146's boundary rules — a change to one must be mirrored in the other.
|
|
10
|
+
import { isAbsolute, sep as PATH_SEP } from 'node:path';
|
|
11
|
+
// A slash identifies slash-style paths even when a legal filename segment
|
|
12
|
+
// contains a backslash. Pure backslash-style paths retain Windows support.
|
|
13
|
+
function deriveSeparator(prefix) {
|
|
14
|
+
return prefix.includes('/') ? '/' : prefix.includes('\\') ? '\\' : PATH_SEP;
|
|
15
|
+
}
|
|
16
|
+
// Strip trailing separators so `/a/b`, `/a/b/`, `/a/b//` normalize alike. A
|
|
17
|
+
// prefix of only separators (e.g. a lone posix root `/`) is kept as a single
|
|
18
|
+
// separator so its descendant boundary is `/<sep>` rather than empty.
|
|
19
|
+
function stripTrailingSeparators(prefix, separator) {
|
|
20
|
+
let end = prefix.length;
|
|
21
|
+
while (end > 0 && prefix[end - 1] === separator) {
|
|
22
|
+
end--;
|
|
23
|
+
}
|
|
24
|
+
if (end === 0) {
|
|
25
|
+
return separator;
|
|
26
|
+
}
|
|
27
|
+
return prefix.slice(0, end);
|
|
28
|
+
}
|
|
29
|
+
/** Normalize one scope prefix for both in-memory and LanceDB matching. */
|
|
30
|
+
export function normalizeScopePrefix(prefix) {
|
|
31
|
+
const separator = deriveSeparator(prefix);
|
|
32
|
+
const exact = stripTrailingSeparators(prefix, separator);
|
|
33
|
+
return {
|
|
34
|
+
exact,
|
|
35
|
+
descendant: exact.endsWith(separator) ? exact : exact + separator,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* True when `path` equals `prefix` or is a descendant of it, using a separator
|
|
40
|
+
* boundary so `/foo/bar` does not match `/foo/barista`. The separator is derived
|
|
41
|
+
* from `prefix`; trailing separators on `prefix` are normalized so `/a/b`,
|
|
42
|
+
* `/a/b/`, and `/a/b//` are equivalent. `path` is compared verbatim, matching
|
|
43
|
+
* the SQL contract in `buildPrefixPredicate`.
|
|
44
|
+
*/
|
|
45
|
+
export function isUnderOrEqual(path, prefix) {
|
|
46
|
+
const { exact, descendant } = normalizeScopePrefix(prefix);
|
|
47
|
+
return path === exact || path.startsWith(descendant);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* True when `path` is under-or-equal any prefix in `prefixes` (union). Empty or
|
|
51
|
+
* undefined `prefixes` semantics are the caller's concern; an empty list yields
|
|
52
|
+
* false (membership against no prefixes).
|
|
53
|
+
*/
|
|
54
|
+
export function matchesAnyScope(path, prefixes) {
|
|
55
|
+
return prefixes.some((prefix) => isUnderOrEqual(path, prefix));
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Return the prefixes in `scope` that are NOT absolute paths (server-OS path
|
|
59
|
+
* style, via `node:path` `isAbsolute` — the same path style scope matching
|
|
60
|
+
* uses). A non-absolute prefix matches nothing under the exact-or-descendant
|
|
61
|
+
* contract, so both list surfaces surface these as non-fatal warnings while
|
|
62
|
+
* preserving the "matches nothing" result semantics. Input order is preserved.
|
|
63
|
+
*/
|
|
64
|
+
export function nonAbsolutePrefixes(scope) {
|
|
65
|
+
return scope.filter((prefix) => !isAbsolute(prefix));
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Directory-visit predicate for the scoped BFS walk, shared by both walkers so
|
|
69
|
+
* the boundary semantics live in one place. Visit `dir` when there is no scope,
|
|
70
|
+
* when `dir` is in-scope (under-or-equal a prefix), or when `dir` is an ancestor
|
|
71
|
+
* of some prefix (must be descended to reach the scoped subtree). An absent or
|
|
72
|
+
* empty `scope` visits every directory (traversal unchanged).
|
|
73
|
+
*/
|
|
74
|
+
export function shouldVisitDir(dir, scope) {
|
|
75
|
+
if (!scope || scope.length === 0)
|
|
76
|
+
return true;
|
|
77
|
+
return matchesAnyScope(dir, scope) || scope.some((prefix) => isUnderOrEqual(prefix, dir));
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* File-collect predicate for the scoped BFS walk, shared by both walkers.
|
|
81
|
+
* Collect `path` when there is no scope, or when `path` is in-scope. An absent
|
|
82
|
+
* or empty `scope` collects every supported file (collection unchanged).
|
|
83
|
+
*/
|
|
84
|
+
export function isInScope(path, scope) {
|
|
85
|
+
return !scope || scope.length === 0 || matchesAnyScope(path, scope);
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=scope-match.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scope-match.js","sourceRoot":"","sources":["../../src/utils/scope-match.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,wEAAwE;AACxE,EAAE;AACF,wEAAwE;AACxE,yEAAyE;AACzE,4EAA4E;AAC5E,8EAA8E;AAC9E,gFAAgF;AAChF,yEAAyE;AAEzE,OAAO,EAAE,UAAU,EAAE,GAAG,IAAI,QAAQ,EAAE,MAAM,WAAW,CAAA;AAEvD,0EAA0E;AAC1E,2EAA2E;AAC3E,SAAS,eAAe,CAAC,MAAc;IACrC,OAAO,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAA;AAC7E,CAAC;AAED,4EAA4E;AAC5E,6EAA6E;AAC7E,sEAAsE;AACtE,SAAS,uBAAuB,CAAC,MAAc,EAAE,SAAiB;IAChE,IAAI,GAAG,GAAG,MAAM,CAAC,MAAM,CAAA;IACvB,OAAO,GAAG,GAAG,CAAC,IAAI,MAAM,CAAC,GAAG,GAAG,CAAC,CAAC,KAAK,SAAS,EAAE,CAAC;QAChD,GAAG,EAAE,CAAA;IACP,CAAC;IACD,IAAI,GAAG,KAAK,CAAC,EAAE,CAAC;QACd,OAAO,SAAS,CAAA;IAClB,CAAC;IACD,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAA;AAC7B,CAAC;AAOD,0EAA0E;AAC1E,MAAM,UAAU,oBAAoB,CAAC,MAAc;IACjD,MAAM,SAAS,GAAG,eAAe,CAAC,MAAM,CAAC,CAAA;IACzC,MAAM,KAAK,GAAG,uBAAuB,CAAC,MAAM,EAAE,SAAS,CAAC,CAAA;IACxD,OAAO;QACL,KAAK;QACL,UAAU,EAAE,KAAK,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,GAAG,SAAS;KAClE,CAAA;AACH,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,cAAc,CAAC,IAAY,EAAE,MAAc;IACzD,MAAM,EAAE,KAAK,EAAE,UAAU,EAAE,GAAG,oBAAoB,CAAC,MAAM,CAAC,CAAA;IAC1D,OAAO,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC,CAAA;AACtD,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAC,IAAY,EAAE,QAAkB;IAC9D,OAAO,QAAQ,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,cAAc,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAA;AAChE,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,mBAAmB,CAAC,KAAe;IACjD,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAA;AACtD,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,cAAc,CAAC,GAAW,EAAE,KAAgB;IAC1D,IAAI,CAAC,KAAK,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAA;IAC7C,OAAO,eAAe,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,KAAK,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,cAAc,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAAA;AAC3F,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,SAAS,CAAC,IAAY,EAAE,KAAgB;IACtD,OAAO,CAAC,KAAK,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,IAAI,eAAe,CAAC,IAAI,EAAE,KAAK,CAAC,CAAA;AACrE,CAAC"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns the literal prefixes joined with their `realpath`-resolved forms.
|
|
3
|
+
* Without canonicalization macOS would let `/etc` (which realpaths to
|
|
4
|
+
* `/private/etc`) slip past once the resolver normalizes the path. The
|
|
5
|
+
* literal is always kept so a realpath failure cannot weaken the policy.
|
|
6
|
+
*/
|
|
7
|
+
export declare function buildSensitivePrefixes(realpathSyncFn?: (p: string) => string): string[];
|
|
8
|
+
/**
|
|
9
|
+
* Returns a user-facing error string when `value` resolves to a sensitive
|
|
10
|
+
* system or credential directory. Returns `undefined` when the path is
|
|
11
|
+
* acceptable.
|
|
12
|
+
*
|
|
13
|
+
* `flagName` is interpolated into the error message so the surfacing
|
|
14
|
+
* surface (CLI flag, env var, ...) is visible at the call site. The CLI uses
|
|
15
|
+
* `'--base-dir'`; the server entry point uses `'BASE_DIR'` or `'BASE_DIRS'`
|
|
16
|
+
* to attribute the rejection to the env var actually consulted.
|
|
17
|
+
*
|
|
18
|
+
* The trailing-separator check on system prefixes guards against sibling
|
|
19
|
+
* paths like `/etcetera`. Both the `~/.ssh` and the expanded form are
|
|
20
|
+
* rejected so the policy holds when `$HOME` is unset.
|
|
21
|
+
*/
|
|
22
|
+
export declare function checkSensitivePath(value: string, flagName: string): string | undefined;
|
|
23
|
+
//# sourceMappingURL=sensitive-path.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sensitive-path.d.ts","sourceRoot":"","sources":["../../src/utils/sensitive-path.ts"],"names":[],"mappings":"AA4BA;;;;;GAKG;AACH,wBAAgB,sBAAsB,CACpC,cAAc,GAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAqB,GACnD,MAAM,EAAE,CAcV;AAUD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CA0BtF"}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// Sensitive-path policy shared by the CLI and the MCP server entry point.
|
|
2
|
+
//
|
|
3
|
+
// Both entry points must refuse to use system or credential directories as
|
|
4
|
+
// document roots: pre-multi-root code only enforced this at the CLI surface,
|
|
5
|
+
// which left a gap where `BASE_DIRS=["/etc"]` in the MCP server's environment
|
|
6
|
+
// would be silently accepted. This module owns the single source of truth
|
|
7
|
+
// for the policy so the CLI (`cli/options.ts` and `cli/common.ts`) and the
|
|
8
|
+
// server entry point (`server-main.ts`) cannot drift.
|
|
9
|
+
//
|
|
10
|
+
// The policy is intentionally simple — a small allow-list-by-exclusion of
|
|
11
|
+
// system mount points and credential directories under `$HOME`. It is not a
|
|
12
|
+
// general-purpose sandboxing mechanism; the parser layer (`DocumentParser`)
|
|
13
|
+
// remains the authoritative path-traversal / symlink-escape boundary.
|
|
14
|
+
import { realpathSync } from 'node:fs';
|
|
15
|
+
import { homedir } from 'node:os';
|
|
16
|
+
// Normalize a path for comparison: forward-slash separators throughout and
|
|
17
|
+
// lower-case on Windows (case-insensitive filesystem). Used by the security
|
|
18
|
+
// boundary checks below so `C:\Users\me\.ssh` and `c:/users/me/.ssh` resolve
|
|
19
|
+
// to the same comparable form.
|
|
20
|
+
function toComparable(p) {
|
|
21
|
+
const slashed = p.replace(/\\/g, '/');
|
|
22
|
+
return process.platform === 'win32' ? slashed.toLowerCase() : slashed;
|
|
23
|
+
}
|
|
24
|
+
const SENSITIVE_PATH_LITERALS = ['/etc', '/usr', '/sys', '/proc', '/var'];
|
|
25
|
+
/**
|
|
26
|
+
* Returns the literal prefixes joined with their `realpath`-resolved forms.
|
|
27
|
+
* Without canonicalization macOS would let `/etc` (which realpaths to
|
|
28
|
+
* `/private/etc`) slip past once the resolver normalizes the path. The
|
|
29
|
+
* literal is always kept so a realpath failure cannot weaken the policy.
|
|
30
|
+
*/
|
|
31
|
+
export function buildSensitivePrefixes(realpathSyncFn = realpathSync) {
|
|
32
|
+
const set = new Set();
|
|
33
|
+
for (const literal of SENSITIVE_PATH_LITERALS) {
|
|
34
|
+
set.add(literal);
|
|
35
|
+
try {
|
|
36
|
+
const canonical = realpathSyncFn(literal);
|
|
37
|
+
if (typeof canonical === 'string' && canonical.length > 0) {
|
|
38
|
+
set.add(canonical);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// realpath unavailable on this platform; literal already retained.
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return [...set];
|
|
46
|
+
}
|
|
47
|
+
const SENSITIVE_PATH_PREFIXES = buildSensitivePrefixes();
|
|
48
|
+
/**
|
|
49
|
+
* Directories under `$HOME` that hold credentials and must never be opened
|
|
50
|
+
* as document roots even when the user expands the path themselves.
|
|
51
|
+
*/
|
|
52
|
+
const SENSITIVE_HOME_PREFIXES = ['.ssh', '.gnupg'];
|
|
53
|
+
/**
|
|
54
|
+
* Returns a user-facing error string when `value` resolves to a sensitive
|
|
55
|
+
* system or credential directory. Returns `undefined` when the path is
|
|
56
|
+
* acceptable.
|
|
57
|
+
*
|
|
58
|
+
* `flagName` is interpolated into the error message so the surfacing
|
|
59
|
+
* surface (CLI flag, env var, ...) is visible at the call site. The CLI uses
|
|
60
|
+
* `'--base-dir'`; the server entry point uses `'BASE_DIR'` or `'BASE_DIRS'`
|
|
61
|
+
* to attribute the rejection to the env var actually consulted.
|
|
62
|
+
*
|
|
63
|
+
* The trailing-separator check on system prefixes guards against sibling
|
|
64
|
+
* paths like `/etcetera`. Both the `~/.ssh` and the expanded form are
|
|
65
|
+
* rejected so the policy holds when `$HOME` is unset.
|
|
66
|
+
*/
|
|
67
|
+
export function checkSensitivePath(value, flagName) {
|
|
68
|
+
const home = process.env['HOME'] || homedir();
|
|
69
|
+
const expanded = value.startsWith('~/') ? `${home}/${value.slice(2)}` : value;
|
|
70
|
+
const valueCmp = toComparable(expanded);
|
|
71
|
+
for (const prefix of SENSITIVE_PATH_PREFIXES) {
|
|
72
|
+
const prefixCmp = toComparable(prefix);
|
|
73
|
+
if (valueCmp === prefixCmp || valueCmp.startsWith(`${prefixCmp}/`)) {
|
|
74
|
+
return `Refusing to use sensitive system path for ${flagName}: ${value}`;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
for (const dir of SENSITIVE_HOME_PREFIXES) {
|
|
78
|
+
if (home.length > 0) {
|
|
79
|
+
const homePathCmp = toComparable(`${home}/${dir}`);
|
|
80
|
+
if (valueCmp === homePathCmp || valueCmp.startsWith(`${homePathCmp}/`)) {
|
|
81
|
+
return `Refusing to use sensitive system path for ${flagName}: ${value}`;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// Unexpanded `~/...` form — caught even when `home` is empty.
|
|
85
|
+
if (value === `~/${dir}` || value.startsWith(`~/${dir}/`)) {
|
|
86
|
+
return `Refusing to use sensitive system path for ${flagName}: ${value}`;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=sensitive-path.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sensitive-path.js","sourceRoot":"","sources":["../../src/utils/sensitive-path.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve `path` into the absolute, normalized key sync uses to reconcile disk
|
|
3
|
+
* state against database state. Windows keys are case-folded because its
|
|
4
|
+
* filesystem is case-insensitive; POSIX keys keep their case.
|
|
5
|
+
*
|
|
6
|
+
* `platform` is a parameter rather than a direct `process.platform` read, and it
|
|
7
|
+
* selects the resolver as well as the case-fold, so the Windows branch is
|
|
8
|
+
* provable on a macOS/Linux machine — the host-bound `resolve()` would otherwise
|
|
9
|
+
* turn a Windows path into a cwd-relative POSIX one.
|
|
10
|
+
*
|
|
11
|
+
* Resolution is purely lexical, with no filesystem canonicalization: symbolic
|
|
12
|
+
* links are intentionally not followed, and a path that no longer exists on disk
|
|
13
|
+
* must still yield a key.
|
|
14
|
+
*
|
|
15
|
+
* The key is an internal reconciliation identity. It never replaces the verbatim
|
|
16
|
+
* stored `filePath` spellings used for deletion, nor the shared scope contract
|
|
17
|
+
* used by the query, list, and source-classification flows.
|
|
18
|
+
*/
|
|
19
|
+
export declare function toSyncPathKey(path: string, platform?: NodeJS.Platform): string;
|
|
20
|
+
//# sourceMappingURL=sync-path-key.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sync-path-key.d.ts","sourceRoot":"","sources":["../../src/utils/sync-path-key.ts"],"names":[],"mappings":"AAUA;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,GAAE,MAAM,CAAC,QAA2B,GAAG,MAAM,CAKhG"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Sync-only comparison-key generation.
|
|
2
|
+
//
|
|
3
|
+
// This module deliberately holds no boundary logic. Containment is delegated to
|
|
4
|
+
// the unchanged `isUnderOrEqual` in `scope-match.ts`, composed by the caller as
|
|
5
|
+
// `isUnderOrEqual(toSyncPathKey(candidate), toSyncPathKey(prefix))`, so the
|
|
6
|
+
// exact-or-descendant, separator-boundary, and trailing-separator semantics
|
|
7
|
+
// stay in exactly one place.
|
|
8
|
+
import { posix, win32 } from 'node:path';
|
|
9
|
+
/**
|
|
10
|
+
* Resolve `path` into the absolute, normalized key sync uses to reconcile disk
|
|
11
|
+
* state against database state. Windows keys are case-folded because its
|
|
12
|
+
* filesystem is case-insensitive; POSIX keys keep their case.
|
|
13
|
+
*
|
|
14
|
+
* `platform` is a parameter rather than a direct `process.platform` read, and it
|
|
15
|
+
* selects the resolver as well as the case-fold, so the Windows branch is
|
|
16
|
+
* provable on a macOS/Linux machine — the host-bound `resolve()` would otherwise
|
|
17
|
+
* turn a Windows path into a cwd-relative POSIX one.
|
|
18
|
+
*
|
|
19
|
+
* Resolution is purely lexical, with no filesystem canonicalization: symbolic
|
|
20
|
+
* links are intentionally not followed, and a path that no longer exists on disk
|
|
21
|
+
* must still yield a key.
|
|
22
|
+
*
|
|
23
|
+
* The key is an internal reconciliation identity. It never replaces the verbatim
|
|
24
|
+
* stored `filePath` spellings used for deletion, nor the shared scope contract
|
|
25
|
+
* used by the query, list, and source-classification flows.
|
|
26
|
+
*/
|
|
27
|
+
export function toSyncPathKey(path, platform = process.platform) {
|
|
28
|
+
if (platform === 'win32') {
|
|
29
|
+
return win32.resolve(path).toLowerCase();
|
|
30
|
+
}
|
|
31
|
+
return posix.resolve(path);
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=sync-path-key.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sync-path-key.js","sourceRoot":"","sources":["../../src/utils/sync-path-key.ts"],"names":[],"mappings":"AAAA,uCAAuC;AACvC,EAAE;AACF,gFAAgF;AAChF,gFAAgF;AAChF,4EAA4E;AAC5E,4EAA4E;AAC5E,6BAA6B;AAE7B,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,WAAW,CAAA;AAExC;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,aAAa,CAAC,IAAY,EAAE,QAAQ,GAAoB,OAAO,CAAC,QAAQ;IACtF,IAAI,QAAQ,KAAK,OAAO,EAAE,CAAC;QACzB,OAAO,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,WAAW,EAAE,CAAA;IAC1C,CAAC;IACD,OAAO,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;AAC5B,CAAC"}
|