@markup-carve/carve-lsp 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/README.md +55 -0
  2. package/dist/analyze.d.ts +17 -1
  3. package/dist/analyze.js +57 -1
  4. package/dist/analyze.js.map +1 -1
  5. package/dist/analyze.test.js +95 -0
  6. package/dist/analyze.test.js.map +1 -1
  7. package/dist/definition.js +26 -2
  8. package/dist/definition.js.map +1 -1
  9. package/dist/definition.test.js +8 -0
  10. package/dist/definition.test.js.map +1 -1
  11. package/dist/folding.js +4 -4
  12. package/dist/folding.js.map +1 -1
  13. package/dist/hover.js +33 -16
  14. package/dist/hover.js.map +1 -1
  15. package/dist/include-directive.d.ts +61 -0
  16. package/dist/include-directive.js +158 -0
  17. package/dist/include-directive.js.map +1 -0
  18. package/dist/include-directive.test.d.ts +1 -0
  19. package/dist/include-directive.test.js +83 -0
  20. package/dist/include-directive.test.js.map +1 -0
  21. package/dist/include-path.d.ts +63 -0
  22. package/dist/include-path.js +135 -0
  23. package/dist/include-path.js.map +1 -0
  24. package/dist/include-path.test.d.ts +1 -0
  25. package/dist/include-path.test.js +189 -0
  26. package/dist/include-path.test.js.map +1 -0
  27. package/dist/include-security-conformance.test.d.ts +1 -0
  28. package/dist/include-security-conformance.test.js +100 -0
  29. package/dist/include-security-conformance.test.js.map +1 -0
  30. package/dist/include-settings.d.ts +50 -0
  31. package/dist/include-settings.js +135 -0
  32. package/dist/include-settings.js.map +1 -0
  33. package/dist/include-settings.test.d.ts +1 -0
  34. package/dist/include-settings.test.js +191 -0
  35. package/dist/include-settings.test.js.map +1 -0
  36. package/dist/includes.d.ts +78 -0
  37. package/dist/includes.js +158 -0
  38. package/dist/includes.js.map +1 -0
  39. package/dist/includes.test.d.ts +1 -0
  40. package/dist/includes.test.js +202 -0
  41. package/dist/includes.test.js.map +1 -0
  42. package/dist/inline-text.d.ts +24 -0
  43. package/dist/inline-text.js +34 -0
  44. package/dist/inline-text.js.map +1 -0
  45. package/dist/inline-text.test.d.ts +1 -0
  46. package/dist/inline-text.test.js +32 -0
  47. package/dist/inline-text.test.js.map +1 -0
  48. package/dist/position-encoding.test.d.ts +1 -0
  49. package/dist/position-encoding.test.js +143 -0
  50. package/dist/position-encoding.test.js.map +1 -0
  51. package/dist/position.d.ts +82 -0
  52. package/dist/position.js +137 -0
  53. package/dist/position.js.map +1 -0
  54. package/dist/position.test.d.ts +1 -0
  55. package/dist/position.test.js +61 -0
  56. package/dist/position.test.js.map +1 -0
  57. package/dist/references.js +7 -0
  58. package/dist/references.js.map +1 -1
  59. package/dist/semantic.d.ts +14 -0
  60. package/dist/semantic.js +113 -27
  61. package/dist/semantic.js.map +1 -1
  62. package/dist/semantic.test.d.ts +1 -0
  63. package/dist/semantic.test.js +93 -0
  64. package/dist/semantic.test.js.map +1 -0
  65. package/dist/server.js +50 -23
  66. package/dist/server.js.map +1 -1
  67. package/package.json +2 -2
@@ -0,0 +1,135 @@
1
+ /**
2
+ * The single place an include path becomes a file read.
3
+ *
4
+ * PART 9 §19 puts four MUSTs on this function, and they are here rather than
5
+ * spread over the callers on purpose: a guard applied on one path and missed on
6
+ * another is the defect this repository's siblings keep hitting. Nothing else
7
+ * in this package joins, canonicalizes or opens an include target.
8
+ *
9
+ * - MUST resolve include paths only to files under the configured project root
10
+ * AFTER symlink resolution (rejecting `..`-traversal and absolute paths that
11
+ * escape the root)
12
+ * - MUST NOT fetch remote URLs unless an allowlist is explicitly configured
13
+ *
14
+ * The remaining two MUSTs (recursion depth, total expanded byte size) are
15
+ * properties of the include GRAPH rather than of one path, so they live in
16
+ * {@link ./includes.js} where the walk is.
17
+ */
18
+ import { closeSync, fstatSync, openSync, readSync, realpathSync } from 'node:fs';
19
+ import path from 'node:path';
20
+ /** A scheme with an authority (`https://…`), or a protocol-relative `//host/…`. */
21
+ const REMOTE_RE = /^(?:[A-Za-z][A-Za-z0-9+.-]*:)?\/\//;
22
+ function remoteHost(spec) {
23
+ const withScheme = spec.startsWith('//') ? `https:${spec}` : spec;
24
+ try {
25
+ return new URL(withScheme).hostname;
26
+ }
27
+ catch {
28
+ return '';
29
+ }
30
+ }
31
+ /**
32
+ * Filesystem resolver with canonical root containment, for a workspace the
33
+ * client reports as trusted.
34
+ *
35
+ * Canonicalize-then-contain: the candidate is resolved to its real path
36
+ * (symlinks followed) and only then compared against the real root.
37
+ *
38
+ * Deliberately NOT a lexical ban on `..`, which is both too strict and too
39
+ * weak. Too strict: `../shared/glossary.crv` from `chapters/ch1.crv` is a
40
+ * normal book layout whose canonical target is inside the root, and the
41
+ * conformance vector `i10-fs-dotdot-inside-root-allowed` requires it to
42
+ * resolve. Too weak: a symlink inside the root pointing out of it, or an
43
+ * absolute path, escapes without containing `..` at all
44
+ * (`i10-fs-symlink-dir-escape-denied`). Canonical containment subsumes both.
45
+ */
46
+ export function fileSystemResolver(includeRoot, opts = {}) {
47
+ const rootReal = realpathSync(includeRoot);
48
+ const allowedRemoteHosts = opts.allowedRemoteHosts ?? [];
49
+ const contains = (candidate) => {
50
+ const rel = path.relative(rootReal, candidate);
51
+ if (rel === '')
52
+ return true;
53
+ if (!rel || path.isAbsolute(rel))
54
+ return false;
55
+ // Segment-wise, so a directory legitimately named "..foo" is not read as
56
+ // an escape the way a startsWith('..') prefix test would.
57
+ return rel.split(path.sep)[0] !== '..';
58
+ };
59
+ return (includePath, ctx) => {
60
+ // GUARD 1 (§19: MUST NOT fetch remote URLs unless an allowlist is
61
+ // explicitly configured). Checked before any path join, so a URL can never
62
+ // be mistaken for a relative path and read off disk as `<root>/https:/…`.
63
+ if (REMOTE_RE.test(includePath)) {
64
+ const host = remoteHost(includePath);
65
+ if (!allowedRemoteHosts.includes(host)) {
66
+ return { ok: false, id: includePath, denial: 'remote-not-allowed' };
67
+ }
68
+ // Allowlisted, and still refused: this server has no fetcher. Reported
69
+ // as a denial rather than silently, so the author sees why.
70
+ return { ok: false, id: includePath, denial: 'remote-not-allowed' };
71
+ }
72
+ // GUARD 2 (§19: absolute paths). Denied outright by default; when the host
73
+ // opts in, an absolute path still has to survive GUARD 3.
74
+ if (!opts.allowAbsolute && path.isAbsolute(includePath)) {
75
+ return { ok: false, id: includePath, denial: 'absolute-denied' };
76
+ }
77
+ // The stack carries the canonical path of each ancestor, so a nested
78
+ // relative include resolves against its actual parent directory, not the
79
+ // root.
80
+ const parent = ctx.stack[ctx.stack.length - 1];
81
+ const base = parent ? path.dirname(path.resolve(rootReal, parent)) : rootReal;
82
+ const joined = path.isAbsolute(includePath)
83
+ ? includePath
84
+ : path.resolve(base, includePath);
85
+ let real;
86
+ try {
87
+ // Symlink resolution happens HERE, before containment, which is what
88
+ // makes a symlinked escape visible to GUARD 3 at all.
89
+ real = realpathSync(joined);
90
+ }
91
+ catch {
92
+ return { ok: false, id: includePath, denial: 'not-found' };
93
+ }
94
+ // GUARD 3 (§19: only files under the configured project root AFTER symlink
95
+ // resolution). Catches `..` that leaves the root, a symlink that points out
96
+ // of it, and an absolute path outside it.
97
+ if (!contains(real)) {
98
+ return { ok: false, id: includePath, denial: 'outside-root' };
99
+ }
100
+ let fd;
101
+ try {
102
+ fd = openSync(real, 'r');
103
+ }
104
+ catch {
105
+ return { ok: false, id: includePath, denial: 'not-found' };
106
+ }
107
+ try {
108
+ const stat = fstatSync(fd);
109
+ // Regular files only. A FIFO or a character device would block the read
110
+ // or stream without end, and neither is a document.
111
+ if (!stat.isFile())
112
+ return { ok: false, id: real, denial: 'not-a-file' };
113
+ const buffer = Buffer.allocUnsafe(stat.size);
114
+ let read = 0;
115
+ while (read < stat.size) {
116
+ const n = readSync(fd, buffer, read, stat.size - read, read);
117
+ if (n <= 0)
118
+ break;
119
+ read += n;
120
+ }
121
+ return {
122
+ ok: true,
123
+ id: real,
124
+ source: buffer.subarray(0, read).toString('utf8'),
125
+ // Encoded bytes, not JavaScript characters, so the budget counts what
126
+ // the include-bomb actually costs.
127
+ bytes: read,
128
+ };
129
+ }
130
+ finally {
131
+ closeSync(fd);
132
+ }
133
+ };
134
+ }
135
+ //# sourceMappingURL=include-path.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"include-path.js","sourceRoot":"","sources":["../src/include-path.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,189 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+ import { mkdirSync, mkdtempSync, realpathSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
4
+ import { tmpdir } from 'node:os';
5
+ import path from 'node:path';
6
+ import { fileSystemResolver } from './include-path.js';
7
+ // PART 9 §19 puts four MUSTs on path resolution. Each has a test here, and
8
+ // each was proven load-bearing by deleting its guard in `include-path.ts` and
9
+ // watching THIS test go red (see the pull request for the transcript).
10
+ //
11
+ // Expectations follow the cross-engine include-conformance vectors
12
+ // `i10-fs-*` in markup-carve/carve, which currently live only on the unmerged
13
+ // spec branch for markup-carve/carve#291. They are cited, not vendored.
14
+ const CTX = { stack: [], depth: 0 };
15
+ function fixture(tree) {
16
+ const dir = realpathSync(mkdtempSync(path.join(tmpdir(), 'carve-lsp-inc-')));
17
+ for (const [name, value] of Object.entries(tree)) {
18
+ const full = path.join(dir, name);
19
+ mkdirSync(path.dirname(full), { recursive: true });
20
+ if (typeof value === 'string')
21
+ writeFileSync(full, value);
22
+ else
23
+ symlinkSync(path.join(dir, value.symlink), full);
24
+ }
25
+ test.after(() => rmSync(dir, { recursive: true, force: true }));
26
+ return dir;
27
+ }
28
+ // ---------------------------------------------------------------------------
29
+ // MUST 1: resolve only under the root AFTER symlink resolution
30
+ // ---------------------------------------------------------------------------
31
+ test('a symlinked DIRECTORY escaping the root is denied (i10-fs-symlink-dir-escape-denied)', () => {
32
+ const dir = fixture({
33
+ 'root/main.crv': '{{ linkdir/secret.crv }}\n',
34
+ 'outside/secret.crv': 'TOP SECRET\n',
35
+ 'root/linkdir': { symlink: 'outside' },
36
+ });
37
+ const resolve = fileSystemResolver(path.join(dir, 'root'));
38
+ const result = resolve('linkdir/secret.crv', CTX);
39
+ assert.equal(result.ok, false);
40
+ assert.equal(result.ok === false && result.denial, 'outside-root');
41
+ // The dependency id is the path as written, never the real path behind it.
42
+ assert.equal(result.id, 'linkdir/secret.crv');
43
+ });
44
+ test('a symlinked FILE whose real target escapes the root is denied (i10-fs-symlink-file-and-dotdot-denied)', () => {
45
+ const dir = fixture({
46
+ 'root/main.crv': '{{ link.crv }}\n',
47
+ 'secret.crv': 'TOP SECRET\n',
48
+ 'root/link.crv': { symlink: 'secret.crv' },
49
+ });
50
+ const resolve = fileSystemResolver(path.join(dir, 'root'));
51
+ const result = resolve('link.crv', CTX);
52
+ assert.equal(result.ok, false);
53
+ assert.equal(result.ok === false && result.denial, 'outside-root');
54
+ });
55
+ // ---------------------------------------------------------------------------
56
+ // MUST 2: reject `..`-traversal that leaves the root
57
+ // ---------------------------------------------------------------------------
58
+ test('a ../ escape out of the root is denied (i10-fs-dotdot-escape-denied)', () => {
59
+ const dir = fixture({
60
+ 'root/main.crv': '{{ ../secret.crv }}\n',
61
+ 'secret.crv': 'TOP SECRET\n',
62
+ });
63
+ const resolve = fileSystemResolver(path.join(dir, 'root'));
64
+ const result = resolve('../secret.crv', CTX);
65
+ assert.equal(result.ok, false);
66
+ assert.equal(result.ok === false && result.denial, 'outside-root');
67
+ });
68
+ test('a ../ path whose canonical target stays INSIDE the root resolves (i10-fs-dotdot-inside-root-allowed)', () => {
69
+ // Control for the test above: the guard is canonical containment, not a
70
+ // lexical ban on `..`. A book layout that reaches a sibling directory is
71
+ // legal and must keep working. No mutation of the containment check breaks
72
+ // this case - deleting the check makes it pass just as well - so it is
73
+ // stated here as the boundary the guard must NOT overreach, not as proof.
74
+ const dir = fixture({
75
+ 'chapters/ch1.crv': 'Chapter one.\n',
76
+ 'shared/glossary.crv': 'Glossary body.\n',
77
+ });
78
+ const resolve = fileSystemResolver(dir);
79
+ const result = resolve('../shared/glossary.crv', {
80
+ stack: [path.join(dir, 'chapters/ch1.crv')],
81
+ depth: 1,
82
+ });
83
+ assert.equal(result.ok, true);
84
+ assert.equal(result.ok === true && result.source, 'Glossary body.\n');
85
+ });
86
+ // ---------------------------------------------------------------------------
87
+ // MUST 3: reject absolute paths that escape the root
88
+ // ---------------------------------------------------------------------------
89
+ test('an absolute path outside the root is denied even with allowAbsolute (i10-fs-absolute-path-denied)', () => {
90
+ // With absolute paths explicitly enabled, containment is the ONLY thing
91
+ // standing between the directive and the file, which is what makes this the
92
+ // test for the MUST rather than for the convenience default below.
93
+ const dir = fixture({
94
+ 'root/main.crv': 'x\n',
95
+ 'secret.crv': 'TOP SECRET\n',
96
+ });
97
+ const resolve = fileSystemResolver(path.join(dir, 'root'), { allowAbsolute: true });
98
+ const result = resolve(path.join(dir, 'secret.crv'), CTX);
99
+ assert.equal(result.ok, false);
100
+ assert.equal(result.ok === false && result.denial, 'outside-root');
101
+ });
102
+ test('an absolute path is denied by default, before containment is consulted', () => {
103
+ const dir = fixture({ 'root/main.crv': 'x\n', 'root/inside.crv': 'Inside.\n' });
104
+ const resolve = fileSystemResolver(path.join(dir, 'root'));
105
+ // Absolute AND inside the root: only the default-deny can reject this one,
106
+ // so it isolates that guard from containment.
107
+ const result = resolve(path.join(dir, 'root/inside.crv'), CTX);
108
+ assert.equal(result.ok, false);
109
+ assert.equal(result.ok === false && result.denial, 'absolute-denied');
110
+ });
111
+ test('an absolute path inside the root resolves once allowAbsolute is set', () => {
112
+ const dir = fixture({ 'root/inside.crv': 'Inside.\n' });
113
+ const resolve = fileSystemResolver(path.join(dir, 'root'), { allowAbsolute: true });
114
+ const result = resolve(path.join(dir, 'root/inside.crv'), CTX);
115
+ assert.equal(result.ok, true);
116
+ });
117
+ // ---------------------------------------------------------------------------
118
+ // MUST 4: never fetch a remote URL without an explicit allowlist
119
+ // ---------------------------------------------------------------------------
120
+ test('a remote URL is denied with no allowlist configured, and never touches the disk', () => {
121
+ const dir = fixture({ 'main.crv': 'x\n' });
122
+ const resolve = fileSystemResolver(dir);
123
+ for (const url of [
124
+ 'https://example.com/evil.crv',
125
+ 'http://example.com/evil.crv',
126
+ 'file://localhost/etc/passwd',
127
+ '//example.com/evil.crv',
128
+ ]) {
129
+ const result = resolve(url, CTX);
130
+ assert.equal(result.ok, false, url);
131
+ assert.equal(result.ok === false && result.denial, 'remote-not-allowed', url);
132
+ }
133
+ });
134
+ test('a remote URL is denied even when its host IS allowlisted, because nothing fetches', () => {
135
+ const dir = fixture({ 'main.crv': 'x\n' });
136
+ const resolve = fileSystemResolver(dir, { allowedRemoteHosts: ['example.com'] });
137
+ const result = resolve('https://example.com/ok.crv', CTX);
138
+ assert.equal(result.ok, false);
139
+ assert.equal(result.ok === false && result.denial, 'remote-not-allowed');
140
+ });
141
+ test('a URL-shaped path is not read off disk as a relative path', () => {
142
+ // Without the scheme check a URL falls through to path joining, and
143
+ // `<root>/https:/example.com/evil.crv` is a file an attacker can plant.
144
+ const dir = fixture({
145
+ 'https:/example.com/evil.crv': 'PLANTED\n',
146
+ });
147
+ const resolve = fileSystemResolver(dir);
148
+ const result = resolve('https://example.com/evil.crv', CTX);
149
+ assert.equal(result.ok, false);
150
+ assert.equal(result.ok === false && result.denial, 'remote-not-allowed');
151
+ });
152
+ // ---------------------------------------------------------------------------
153
+ // Ordinary resolution and non-file targets
154
+ // ---------------------------------------------------------------------------
155
+ test('a missing target is reported as not-found, not as a containment denial', () => {
156
+ const dir = fixture({ 'main.crv': 'x\n' });
157
+ const resolve = fileSystemResolver(dir);
158
+ const result = resolve('absent.crv', CTX);
159
+ assert.equal(result.ok, false);
160
+ assert.equal(result.ok === false && result.denial, 'not-found');
161
+ });
162
+ test('a directory is not a document', () => {
163
+ const dir = fixture({ 'sub/child.crv': 'x\n' });
164
+ const resolve = fileSystemResolver(dir);
165
+ const result = resolve('sub', CTX);
166
+ assert.equal(result.ok, false);
167
+ assert.equal(result.ok === false && result.denial, 'not-a-file');
168
+ });
169
+ test('bytes are counted as encoded UTF-8, not as JavaScript characters', () => {
170
+ const dir = fixture({ 'child.crv': 'éé\u{1f600}' });
171
+ const resolve = fileSystemResolver(dir);
172
+ const result = resolve('child.crv', CTX);
173
+ assert.equal(result.ok, true);
174
+ assert.equal(result.ok === true && result.bytes, Buffer.byteLength('éé\u{1f600}', 'utf8'));
175
+ });
176
+ test('a nested relative include resolves against its parent directory, not the root', () => {
177
+ const dir = fixture({
178
+ 'chapters/ch1.crv': 'Chapter one.\n',
179
+ 'chapters/notes.crv': 'Notes.\n',
180
+ });
181
+ const resolve = fileSystemResolver(dir);
182
+ const result = resolve('notes.crv', {
183
+ stack: [path.join(dir, 'chapters/ch1.crv')],
184
+ depth: 1,
185
+ });
186
+ assert.equal(result.ok, true);
187
+ assert.equal(result.ok === true && result.source, 'Notes.\n');
188
+ });
189
+ //# sourceMappingURL=include-path.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"include-path.test.js","sourceRoot":"","sources":["../src/include-path.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,100 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+ import { mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
4
+ import { tmpdir } from 'node:os';
5
+ import path from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { fileSystemResolver } from './include-path.js';
8
+ import { resolveIncludes } from './includes.js';
9
+ const sourceRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
10
+ const corpusPath = path.join(sourceRoot, 'tests/spec/tests/include-security-conformance/vectors.json');
11
+ const corpus = JSON.parse(readFileSync(corpusPath, 'utf8'));
12
+ function fixture(tree) {
13
+ const dir = realpathSync(mkdtempSync(path.join(tmpdir(), 'carve-lsp-security-')));
14
+ const links = [];
15
+ for (const [name, value] of Object.entries(tree ?? {})) {
16
+ const full = path.join(dir, name);
17
+ mkdirSync(path.dirname(full), { recursive: true });
18
+ if (typeof value === 'string')
19
+ writeFileSync(full, value);
20
+ else
21
+ links.push([full, value.symlink]);
22
+ }
23
+ for (const [full, target] of links)
24
+ symlinkSync(path.join(dir, target), full);
25
+ test.after(() => rmSync(dir, { recursive: true, force: true }));
26
+ return dir;
27
+ }
28
+ function graph(vector) {
29
+ const calls = [];
30
+ let maxVisitedDepth = 0;
31
+ const resolver = (request, context) => {
32
+ calls.push(request);
33
+ maxVisitedDepth = Math.max(maxVisitedDepth, context.depth + 1);
34
+ const source = vector.files?.[request];
35
+ return source === undefined
36
+ ? { ok: false, id: request, denial: 'not-found' }
37
+ : { ok: true, id: request, source, bytes: Buffer.byteLength(source, 'utf8') };
38
+ };
39
+ const result = resolveIncludes(vector.entry ?? '', {
40
+ resolver,
41
+ maxDepth: vector.maxDepth,
42
+ maxBytes: vector.maxBytes,
43
+ });
44
+ const warning = result.warnings.find((item) => item.rule === 'include-depth' || item.rule === 'include-budget');
45
+ return {
46
+ resolverCalls: calls,
47
+ maxVisitedDepth,
48
+ chargedBytes: result.bytes,
49
+ status: warning ? 'denied' : 'allowed',
50
+ denial: warning?.rule === 'include-depth' ? 'depth' : warning ? 'budget' : undefined,
51
+ };
52
+ }
53
+ function run(vector) {
54
+ if (vector.kind === 'activation') {
55
+ const calls = [];
56
+ const resolver = (request) => {
57
+ calls.push(request);
58
+ return { ok: false, id: request, denial: 'not-found' };
59
+ };
60
+ resolveIncludes(vector.entry ?? '', vector.enabled ? { resolver } : {});
61
+ return { resolverCalls: calls };
62
+ }
63
+ if (vector.kind === 'graph')
64
+ return graph(vector);
65
+ const dir = fixture(vector.tree ?? { 'root/main.crv': '' });
66
+ const root = realpathSync(path.join(dir, vector.root ?? 'root'));
67
+ const resolver = fileSystemResolver(root, {
68
+ allowAbsolute: vector.allowAbsolute,
69
+ allowedRemoteHosts: vector.allowedRemoteHosts,
70
+ });
71
+ const rawRequest = vector.request ?? '';
72
+ const request = rawRequest.replace(/^<ABS:([^>]+)>$/, (_match, rel) => path.join(dir, rel));
73
+ const context = vector.from
74
+ ? { stack: [realpathSync(path.join(dir, vector.from))], depth: 0 }
75
+ : { stack: [], depth: 0 };
76
+ const result = resolver(request, context);
77
+ if (vector.kind === 'remote') {
78
+ return {
79
+ status: result.ok ? 'allowed' : vector.allowedRemoteHosts?.length ? 'unsupported' : 'denied',
80
+ denial: result.ok ? undefined : result.denial,
81
+ remoteFetches: [],
82
+ };
83
+ }
84
+ return result.ok
85
+ ? { status: 'allowed', canonicalId: result.id.replace(root, '<ROOT>') }
86
+ : { status: 'denied', denial: result.denial };
87
+ }
88
+ test('executes every shared PART 9 section 19 security vector', async (t) => {
89
+ assert.equal(corpus.version, 1);
90
+ assert.equal(corpus.vectors.length, 12);
91
+ for (const vector of corpus.vectors) {
92
+ await t.test(vector.name, () => {
93
+ const actual = run(vector);
94
+ for (const [field, expected] of Object.entries(vector.expected)) {
95
+ assert.deepEqual(actual[field], expected, `${vector.name}: ${field}`);
96
+ }
97
+ });
98
+ }
99
+ });
100
+ //# sourceMappingURL=include-security-conformance.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"include-security-conformance.test.js","sourceRoot":"","sources":["../src/include-security-conformance.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,50 @@
1
+ import type { IncludeOptions } from './includes.js';
2
+ export interface IncludeSettings {
3
+ /**
4
+ * `auto` (the default) means ON ONLY for a workspace the client has reported
5
+ * as trusted. A client that says nothing about trust therefore gets includes
6
+ * OFF: silence is not consent.
7
+ */
8
+ enabled: 'auto' | 'on' | 'off';
9
+ /** Containment root override. Otherwise the workspace root, then the document's directory. */
10
+ includeRoot?: string;
11
+ /** Allow absolute include paths, still subject to root containment. */
12
+ allowAbsolute?: boolean;
13
+ /** Hosts a remote include may name. Empty means none; this server never fetches. */
14
+ allowedRemoteHosts?: string[];
15
+ maxDepth?: number;
16
+ maxBytes?: number;
17
+ }
18
+ export declare const DEFAULT_INCLUDE_SETTINGS: IncludeSettings;
19
+ /**
20
+ * Read `carve.includes` out of `initializationOptions` or a
21
+ * `workspace/didChangeConfiguration` payload. Anything unrecognized falls back
22
+ * to the default rather than being trusted as given.
23
+ */
24
+ export declare function readIncludeSettings(raw: unknown): IncludeSettings;
25
+ /** Client-reported workspace trust. Absent means untrusted. */
26
+ export declare function readWorkspaceTrusted(raw: unknown): boolean;
27
+ export declare function fsPath(uri: string): string | undefined;
28
+ export interface IncludeGateInput {
29
+ uri: string;
30
+ settings: IncludeSettings;
31
+ workspaceTrusted: boolean;
32
+ /**
33
+ * Every workspace folder the client reported, in the order it reported them.
34
+ * A multi-root session gets one root per folder, not the first folder for
35
+ * everything: rooting a document from the second folder at the first would
36
+ * reject its ordinary relative includes as escapes.
37
+ */
38
+ workspaceRoots?: string[];
39
+ }
40
+ /**
41
+ * Build the include options for one document, or undefined to leave its
42
+ * directives literal.
43
+ *
44
+ * The containment root is the explicit override, else the workspace root, else
45
+ * the document's own directory for a file opened outside any workspace. It is
46
+ * NEVER the process working directory: a language server is commonly spawned
47
+ * from the user's home or from `/`, and rooting there would make containment
48
+ * meaningless.
49
+ */
50
+ export declare function includeOptionsFor(input: IncludeGateInput): IncludeOptions | undefined;
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Client-facing include settings and the decision of whether a document gets
3
+ * include resolution at all (PART 9 §19: "MUST treat includes as opt-in (off
4
+ * for untrusted input)").
5
+ *
6
+ * Kept out of `server.ts` so the opt-in decision is testable on its own. It is
7
+ * the gate in front of a file-read capability, which is not something to leave
8
+ * exercised only by hand.
9
+ */
10
+ import { realpathSync } from 'node:fs';
11
+ import path, { dirname } from 'node:path';
12
+ import { fileURLToPath } from 'node:url';
13
+ import { fileSystemResolver } from './include-path.js';
14
+ export const DEFAULT_INCLUDE_SETTINGS = { enabled: 'auto' };
15
+ /**
16
+ * Read `carve.includes` out of `initializationOptions` or a
17
+ * `workspace/didChangeConfiguration` payload. Anything unrecognized falls back
18
+ * to the default rather than being trusted as given.
19
+ */
20
+ export function readIncludeSettings(raw) {
21
+ const source = raw?.carve
22
+ ?.includes;
23
+ if (!source || typeof source !== 'object')
24
+ return DEFAULT_INCLUDE_SETTINGS;
25
+ const enabled = source['enabled'];
26
+ const settings = {
27
+ enabled: enabled === 'on' || enabled === 'off' ? enabled : 'auto',
28
+ };
29
+ if (typeof source['includeRoot'] === 'string')
30
+ settings.includeRoot = source['includeRoot'];
31
+ if (typeof source['allowAbsolute'] === 'boolean')
32
+ settings.allowAbsolute = source['allowAbsolute'];
33
+ if (Array.isArray(source['allowedRemoteHosts'])) {
34
+ settings.allowedRemoteHosts = source['allowedRemoteHosts'].filter((host) => typeof host === 'string');
35
+ }
36
+ if (typeof source['maxDepth'] === 'number')
37
+ settings.maxDepth = source['maxDepth'];
38
+ if (typeof source['maxBytes'] === 'number')
39
+ settings.maxBytes = source['maxBytes'];
40
+ return settings;
41
+ }
42
+ /** Client-reported workspace trust. Absent means untrusted. */
43
+ export function readWorkspaceTrusted(raw) {
44
+ return raw?.workspaceTrusted === true;
45
+ }
46
+ export function fsPath(uri) {
47
+ if (!uri.startsWith('file:'))
48
+ return undefined;
49
+ try {
50
+ return fileURLToPath(uri);
51
+ }
52
+ catch {
53
+ return undefined;
54
+ }
55
+ }
56
+ /** True when `candidate` is `root` or sits underneath it, segment-wise. */
57
+ function contains(root, candidate) {
58
+ const rel = path.relative(root, candidate);
59
+ if (rel === '')
60
+ return true;
61
+ if (!rel || path.isAbsolute(rel))
62
+ return false;
63
+ return rel.split(path.sep)[0] !== '..';
64
+ }
65
+ /**
66
+ * The workspace folder this document belongs to: the deepest one containing
67
+ * it. A document in no folder at all has no workspace root, and falls back to
68
+ * its own directory rather than to somebody else's folder.
69
+ */
70
+ function workspaceRootFor(documentPath, roots) {
71
+ let best;
72
+ for (const root of roots ?? []) {
73
+ if (!contains(root, documentPath))
74
+ continue;
75
+ if (best === undefined || root.length > best.length)
76
+ best = root;
77
+ }
78
+ return best;
79
+ }
80
+ /**
81
+ * Build the include options for one document, or undefined to leave its
82
+ * directives literal.
83
+ *
84
+ * The containment root is the explicit override, else the workspace root, else
85
+ * the document's own directory for a file opened outside any workspace. It is
86
+ * NEVER the process working directory: a language server is commonly spawned
87
+ * from the user's home or from `/`, and rooting there would make containment
88
+ * meaningless.
89
+ */
90
+ export function includeOptionsFor(input) {
91
+ // GUARD 0 (§19: MUST treat includes as opt-in, off for untrusted input).
92
+ if (input.settings.enabled === 'off')
93
+ return undefined;
94
+ if (input.settings.enabled === 'auto' && !input.workspaceTrusted)
95
+ return undefined;
96
+ const documentPath = fsPath(input.uri);
97
+ // A document with no filesystem identity (untitled:, a remote scheme) has no
98
+ // directory to resolve against and gets no capability.
99
+ if (documentPath === undefined)
100
+ return undefined;
101
+ const configured = input.settings.includeRoot ??
102
+ workspaceRootFor(documentPath, input.workspaceRoots) ??
103
+ dirname(documentPath);
104
+ // Canonicalize the root here as well as inside the resolver, so that the
105
+ // root a caller sees on `includeRoot` is in the same coordinate system as
106
+ // the ids the resolver returns. Without it, a root reached through a symlink
107
+ // (or given relatively) makes every child id look like it sits outside the
108
+ // root, and a diagnostic naming that child would print an absolute path.
109
+ let root;
110
+ try {
111
+ root = realpathSync(configured);
112
+ }
113
+ catch {
114
+ return undefined;
115
+ }
116
+ let resolver;
117
+ try {
118
+ resolver = fileSystemResolver(root, {
119
+ allowAbsolute: input.settings.allowAbsolute ?? false,
120
+ allowedRemoteHosts: input.settings.allowedRemoteHosts ?? [],
121
+ });
122
+ }
123
+ catch {
124
+ // A root that is not a real directory yields no capability at all, rather
125
+ // than a resolver that silently falls back to somewhere wider.
126
+ return undefined;
127
+ }
128
+ const options = { resolver, sourcePath: documentPath, includeRoot: root };
129
+ if (input.settings.maxDepth !== undefined)
130
+ options.maxDepth = input.settings.maxDepth;
131
+ if (input.settings.maxBytes !== undefined)
132
+ options.maxBytes = input.settings.maxBytes;
133
+ return options;
134
+ }
135
+ //# sourceMappingURL=include-settings.js.map