@landstrip/landstrip-api 0.20.10 → 0.20.12
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 +11 -2
- package/lib/shared.d.ts +8 -0
- package/lib/shared.js +95 -8
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -10,8 +10,17 @@ x64/arm64. This API package does not own the CLI or platform binary packages.
|
|
|
10
10
|
npm install --save-dev @landstrip/landstrip-api
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
npx landstrip run -p policy.json -- cargo test
|
|
17
|
+
npx landstrip check -p policy.json
|
|
18
|
+
npx landstrip doctor
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
See the [quick start](../../README.md#quick-start) for a policy example;
|
|
22
|
+
[landstrip(1)](../landstrip/man/man1/landstrip.1) covers policy and platform
|
|
23
|
+
limits.
|
|
15
24
|
|
|
16
25
|
## Node.js API
|
|
17
26
|
|
package/lib/shared.d.ts
CHANGED
|
@@ -173,6 +173,14 @@ export function matchesPathPattern(
|
|
|
173
173
|
baseDirectory?: string,
|
|
174
174
|
): boolean;
|
|
175
175
|
|
|
176
|
+
// Relative write denies apply at the policy base and every effective write root.
|
|
177
|
+
export function matchesWriteDenyPattern(
|
|
178
|
+
filePath: string,
|
|
179
|
+
patterns: LandstripPathSelector[],
|
|
180
|
+
baseDirectory?: string,
|
|
181
|
+
writeRoots?: LandstripPathSelector[],
|
|
182
|
+
): boolean;
|
|
183
|
+
|
|
176
184
|
export function matchPathSpecificity(
|
|
177
185
|
filePath: string,
|
|
178
186
|
patterns: LandstripPathSelector[],
|
package/lib/shared.js
CHANGED
|
@@ -5,6 +5,7 @@ const {
|
|
|
5
5
|
lstatSync,
|
|
6
6
|
mkdtempSync,
|
|
7
7
|
readlinkSync,
|
|
8
|
+
readdirSync,
|
|
8
9
|
realpathSync,
|
|
9
10
|
rmSync,
|
|
10
11
|
writeFileSync,
|
|
@@ -442,6 +443,82 @@ function resolveFilesystemPatterns(patterns, baseDirectory) {
|
|
|
442
443
|
return patterns.map((pattern) => resolvePathSelector(pattern, baseDirectory));
|
|
443
444
|
}
|
|
444
445
|
|
|
446
|
+
// Expand only glob roots; literal roots may not exist yet. Darwin also keeps lexical aliases.
|
|
447
|
+
function resolveWriteRoots(writeRoots, baseDirectory) {
|
|
448
|
+
const roots = new Set();
|
|
449
|
+
const opaqueErrors = new Set(['ENOENT', 'ENOTDIR', 'EACCES', 'EPERM', 'EIO', 'ENOTCONN']);
|
|
450
|
+
const addRoot = (root) => {
|
|
451
|
+
if (process.platform === 'darwin') roots.add(root);
|
|
452
|
+
roots.add(canonicalizePath(root, baseDirectory));
|
|
453
|
+
};
|
|
454
|
+
for (const selector of writeRoots) {
|
|
455
|
+
const raw = isRecord(selector) ? selector.literal ?? selector.glob : selector;
|
|
456
|
+
const lexical = expandPath(raw, baseDirectory);
|
|
457
|
+
const { isGlob } = resolveSelectorForMatch(selector, baseDirectory);
|
|
458
|
+
if (!isGlob) {
|
|
459
|
+
addRoot(lexical);
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
const wildcard = lexical.search(/[*?[\]]/);
|
|
463
|
+
const prefix = lexical.slice(0, wildcard);
|
|
464
|
+
const start = prefix.endsWith(sep) ? resolve(prefix) : dirname(prefix);
|
|
465
|
+
const matcher = globToRegExp(normalizePathForComparison(lexical));
|
|
466
|
+
const pending = [[start, 0]];
|
|
467
|
+
let matched = false;
|
|
468
|
+
while (pending.length > 0) {
|
|
469
|
+
const [current, depth] = pending.pop();
|
|
470
|
+
if (depth >= 40) throw new Error('Write-root glob traversal exceeds maximum depth (40)');
|
|
471
|
+
try {
|
|
472
|
+
const metadata = lstatSync(current);
|
|
473
|
+
if (matcher.test(normalizePathForComparison(current))) {
|
|
474
|
+
addRoot(current);
|
|
475
|
+
matched = true;
|
|
476
|
+
}
|
|
477
|
+
if (metadata.isDirectory() && !metadata.isSymbolicLink()) {
|
|
478
|
+
for (const entry of readdirSync(current)) {
|
|
479
|
+
pending.push([join(current, entry), depth + 1]);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
} catch (error) {
|
|
483
|
+
if (!opaqueErrors.has(error.code)) throw error;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
if (!matched && typeof selector === 'string') {
|
|
487
|
+
try {
|
|
488
|
+
lstatSync(lexical);
|
|
489
|
+
addRoot(lexical);
|
|
490
|
+
} catch (error) {
|
|
491
|
+
if (!opaqueErrors.has(error.code)) throw error;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
return roots;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// Preserve write-root anchoring when lowering relative denies to absolute selectors.
|
|
499
|
+
function resolveWriteDenyPatterns(patterns, baseDirectory, writeRoots = []) {
|
|
500
|
+
const resolved = resolveFilesystemPatterns(patterns, baseDirectory);
|
|
501
|
+
if (!baseDirectory) return resolved;
|
|
502
|
+
const lexicalBase = resolve(baseDirectory);
|
|
503
|
+
const baseRoot = canonicalizePath(baseDirectory);
|
|
504
|
+
let roots;
|
|
505
|
+
for (const pattern of patterns ?? []) {
|
|
506
|
+
const raw = isRecord(pattern) ? pattern.literal ?? pattern.glob : pattern;
|
|
507
|
+
if (isAbsolute(expandHomePath(raw))) continue;
|
|
508
|
+
roots ??= resolveWriteRoots(writeRoots, baseDirectory);
|
|
509
|
+
const { isGlob } = resolveSelectorForMatch(pattern, baseDirectory);
|
|
510
|
+
const relativePattern = normalizePathForComparison(relative(baseRoot, resolve(baseRoot, raw)));
|
|
511
|
+
for (const root of roots) {
|
|
512
|
+
const rootName = normalizePathForComparison(basename(root));
|
|
513
|
+
if (rootName && (isGlob ? globToRegExp(relativePattern).test(rootName) : relativePattern === rootName)) {
|
|
514
|
+
resolved.push({ literal: canonicalizePath(root, baseDirectory) });
|
|
515
|
+
}
|
|
516
|
+
if (root !== baseRoot && root !== lexicalBase) resolved.push(resolvePathSelector(pattern, root));
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
return resolved;
|
|
520
|
+
}
|
|
521
|
+
|
|
445
522
|
function resolveFilesystemPolicy(filesystem, baseDirectory) {
|
|
446
523
|
if (!filesystem) return {};
|
|
447
524
|
const resolved = {};
|
|
@@ -458,10 +535,10 @@ function resolveFilesystemPolicy(filesystem, baseDirectory) {
|
|
|
458
535
|
resolved.allowWrite = resolveFilesystemPatterns(filesystem.allowWrite, baseDirectory);
|
|
459
536
|
}
|
|
460
537
|
if (filesystem.denyWrite !== undefined) {
|
|
461
|
-
resolved.denyWrite =
|
|
538
|
+
resolved.denyWrite = resolveWriteDenyPatterns(filesystem.denyWrite, baseDirectory, filesystem.allowWrite);
|
|
462
539
|
}
|
|
463
540
|
if (filesystem.denyWriteAlways !== undefined) {
|
|
464
|
-
resolved.denyWriteAlways =
|
|
541
|
+
resolved.denyWriteAlways = resolveWriteDenyPatterns(filesystem.denyWriteAlways, baseDirectory, filesystem.allowWrite);
|
|
465
542
|
}
|
|
466
543
|
return resolved;
|
|
467
544
|
}
|
|
@@ -879,6 +956,15 @@ function matchesPathPattern(filePath, patterns, baseDirectory = process.cwd()) {
|
|
|
879
956
|
});
|
|
880
957
|
}
|
|
881
958
|
|
|
959
|
+
// Relative write denies apply at the policy base and every effective write root.
|
|
960
|
+
function matchesWriteDenyPattern(filePath, patterns, baseDirectory = process.cwd(), writeRoots = []) {
|
|
961
|
+
return matchesPathPattern(
|
|
962
|
+
filePath,
|
|
963
|
+
resolveWriteDenyPatterns(patterns, baseDirectory, writeRoots),
|
|
964
|
+
baseDirectory,
|
|
965
|
+
);
|
|
966
|
+
}
|
|
967
|
+
|
|
882
968
|
function matchPathSpecificity(filePath, patterns, baseDirectory = process.cwd()) {
|
|
883
969
|
if (!Array.isArray(patterns) || patterns.length === 0) return -1;
|
|
884
970
|
let best = -1;
|
|
@@ -973,18 +1059,18 @@ function evaluateWriteAccess(filePath, filesystemOrConfig, options, allowWriteOv
|
|
|
973
1059
|
const opts = normalizeWriteOptions(options, allowWriteOverrides);
|
|
974
1060
|
const baseDirectory = opts.baseDirectory;
|
|
975
1061
|
|
|
976
|
-
|
|
1062
|
+
const effectiveAllowWrite = opts.allowWriteOverrides.length > 0
|
|
1063
|
+
? mergeArray(filesystem.allowWrite ?? [], opts.allowWriteOverrides)
|
|
1064
|
+
: (filesystem.allowWrite ?? []);
|
|
1065
|
+
|
|
1066
|
+
if (filesystem.denyWriteAlways && matchesWriteDenyPattern(filePath, filesystem.denyWriteAlways, baseDirectory, effectiveAllowWrite)) {
|
|
977
1067
|
return 'denyAlways';
|
|
978
1068
|
}
|
|
979
1069
|
|
|
980
|
-
if (filesystem.denyWrite &&
|
|
1070
|
+
if (filesystem.denyWrite && matchesWriteDenyPattern(filePath, filesystem.denyWrite, baseDirectory, effectiveAllowWrite)) {
|
|
981
1071
|
return 'deny';
|
|
982
1072
|
}
|
|
983
1073
|
|
|
984
|
-
const effectiveAllowWrite = opts.allowWriteOverrides.length > 0
|
|
985
|
-
? mergeArray(filesystem.allowWrite ?? [], opts.allowWriteOverrides)
|
|
986
|
-
: (filesystem.allowWrite ?? []);
|
|
987
|
-
|
|
988
1074
|
if (effectiveAllowWrite.length > 0 && matchesPathPattern(filePath, effectiveAllowWrite, baseDirectory)) {
|
|
989
1075
|
return 'allow';
|
|
990
1076
|
}
|
|
@@ -1282,6 +1368,7 @@ module.exports = {
|
|
|
1282
1368
|
|
|
1283
1369
|
normalizePathForComparison,
|
|
1284
1370
|
matchesPathPattern,
|
|
1371
|
+
matchesWriteDenyPattern,
|
|
1285
1372
|
matchPathSpecificity,
|
|
1286
1373
|
isPathReadAllowed,
|
|
1287
1374
|
isPathWriteAllowed,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@landstrip/landstrip-api",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.12",
|
|
4
4
|
"description": "TypeScript APIs for the Landstrip sandbox",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://github.com/landstrip/landstrip#readme",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"LICENSE"
|
|
34
34
|
],
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@landstrip/landstrip": "0.20.
|
|
36
|
+
"@landstrip/landstrip": "0.20.12",
|
|
37
37
|
"ipaddr.js": "^2.5.0"
|
|
38
38
|
},
|
|
39
39
|
"publishConfig": {
|