@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 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
- See the [quick start](../../README.md#quick-start) for a policy and CLI example;
14
- [landstrip(1)](../landstrip/man/man1/landstrip.1) covers policy and platform limits.
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 = resolveFilesystemPatterns(filesystem.denyWrite, baseDirectory);
538
+ resolved.denyWrite = resolveWriteDenyPatterns(filesystem.denyWrite, baseDirectory, filesystem.allowWrite);
462
539
  }
463
540
  if (filesystem.denyWriteAlways !== undefined) {
464
- resolved.denyWriteAlways = resolveFilesystemPatterns(filesystem.denyWriteAlways, baseDirectory);
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
- if (filesystem.denyWriteAlways && matchesPathPattern(filePath, filesystem.denyWriteAlways, baseDirectory)) {
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 && matchesPathPattern(filePath, filesystem.denyWrite, baseDirectory)) {
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.10",
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.10",
36
+ "@landstrip/landstrip": "0.20.12",
37
37
  "ipaddr.js": "^2.5.0"
38
38
  },
39
39
  "publishConfig": {