@landstrip/landstrip-api 0.19.6 → 0.19.7

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/lib/index.d.ts CHANGED
@@ -84,6 +84,7 @@ export type LandstripPolicyErrorCode =
84
84
  | 'POLICY_IO_FAILED'
85
85
  | 'POLICY_UNRESTRICTED_READ'
86
86
  | 'POLICY_DENY_READ_ALWAYS_UNSUPPORTED'
87
+ | 'POLICY_DENY_WRITE_ALWAYS_UNSUPPORTED'
87
88
  | 'POLICY_TCP_BIND_UNSUPPORTED'
88
89
  | 'POLICY_UNIX_SOCKET_UNSUPPORTED'
89
90
  | 'POLICY_UNIX_SOCKET_PATH'
@@ -141,6 +142,114 @@ export interface LandstripControlResponse {
141
142
  action: 'allow' | 'deny';
142
143
  }
143
144
 
145
+ export interface LandstripFilesystemPolicy {
146
+ denyRead?: string[];
147
+ denyReadAlways?: string[];
148
+ allowRead?: string[];
149
+ allowWrite?: string[];
150
+ denyWrite?: string[];
151
+ denyWriteAlways?: string[];
152
+ }
153
+
154
+ export interface LandstripNetworkPolicy {
155
+ allowNetwork?: boolean;
156
+ allowLocalBinding?: boolean;
157
+ allowAllUnixSockets?: boolean;
158
+ allowUnixSockets?: string[];
159
+ httpProxyPort?: number;
160
+ socksProxyPort?: number;
161
+ }
162
+
163
+ export type LandstripWindowsAppContainerMode = 'lpac' | 'standard';
164
+
165
+ export interface LandstripWindowsPolicy {
166
+ appContainerMode?: LandstripWindowsAppContainerMode;
167
+ allowLoopback?: boolean;
168
+ }
169
+
170
+ export interface LandstripPolicy {
171
+ filesystem?: LandstripFilesystemPolicy;
172
+ network?: LandstripNetworkPolicy;
173
+ windows?: LandstripWindowsPolicy;
174
+ }
175
+
176
+ export interface SandboxFilesystemConfig {
177
+ denyRead: string[];
178
+ denyReadAlways: string[];
179
+ allowRead: string[];
180
+ allowWrite: string[];
181
+ denyWrite: string[];
182
+ denyWriteAlways: string[];
183
+ }
184
+
185
+ export type ShellReadAccess = 'host' | 'policy';
186
+
187
+ export interface SandboxShellConfig {
188
+ readAccess: ShellReadAccess;
189
+ }
190
+
191
+ export interface SandboxNetworkConfig {
192
+ allowNetwork: boolean;
193
+ allowLocalBinding: boolean;
194
+ allowAllUnixSockets: boolean;
195
+ allowUnixSockets: string[];
196
+ allowedDomains: string[];
197
+ deniedDomains: string[];
198
+ }
199
+
200
+ export interface SandboxWindowsConfig {
201
+ appContainerMode: LandstripWindowsAppContainerMode;
202
+ allowLoopback: boolean;
203
+ }
204
+
205
+ export interface SandboxConfig {
206
+ enabled: boolean;
207
+ shell?: SandboxShellConfig;
208
+ network: SandboxNetworkConfig;
209
+ filesystem: SandboxFilesystemConfig;
210
+ windows?: SandboxWindowsConfig;
211
+ }
212
+
213
+ export type SandboxFilesystemConfigFile = Partial<SandboxFilesystemConfig>;
214
+ export type SandboxShellConfigFile = Partial<SandboxShellConfig>;
215
+ export type SandboxNetworkConfigFile = Partial<SandboxNetworkConfig>;
216
+ export type SandboxWindowsConfigFile = Partial<SandboxWindowsConfig>;
217
+
218
+ export interface SandboxConfigFile {
219
+ enabled?: boolean;
220
+ shell?: SandboxShellConfigFile;
221
+ network?: SandboxNetworkConfigFile;
222
+ filesystem?: SandboxFilesystemConfigFile;
223
+ windows?: SandboxWindowsConfigFile;
224
+ }
225
+
226
+ export type SandboxConfigOverrides = SandboxConfigFile;
227
+
228
+
229
+ export type PathAccessDecision = 'allow' | 'ask' | 'deny' | 'denyAlways';
230
+
231
+ export type DomainAccessDecision = 'allow' | 'ask' | 'deny';
232
+
233
+ export interface EvaluateReadOptions {
234
+ baseDirectory?: string;
235
+ allowReadOverrides?: string[];
236
+ requireAllowMatch?: boolean;
237
+ }
238
+
239
+ export interface EvaluateWriteOptions {
240
+ baseDirectory?: string;
241
+ allowWriteOverrides?: string[];
242
+ }
243
+
244
+ export interface EvaluateDomainOptions {
245
+ allowedDomainsOverrides?: string[];
246
+ }
247
+
248
+
249
+ export interface ExtractDeniedPathOptions {
250
+ cwd?: string;
251
+ command?: string;
252
+ }
144
253
  /**
145
254
  * Path to the native landstrip binary for the running platform.
146
255
  *
@@ -150,3 +259,13 @@ export function binaryPath(platform?: string, arch?: string): string;
150
259
 
151
260
  /** Name of the binary package for the given platform. */
152
261
  export function packageName(platform?: string, arch?: string): string;
262
+
263
+
264
+ export type {
265
+ ProxyPortRange,
266
+ FilterProxyOptions,
267
+ FilterProxyHandle,
268
+ ProxyEnvironmentVariable,
269
+ ProxyCredentials,
270
+ CreateProxyEnvironmentOptions,
271
+ } from './proxy';
package/lib/proxy.d.ts CHANGED
@@ -20,3 +20,35 @@ export interface FilterProxyHandle {
20
20
  }
21
21
 
22
22
  export function startFilterProxy(options: FilterProxyOptions): Promise<FilterProxyHandle>;
23
+
24
+ export const PROXY_ENVIRONMENT_VARIABLES: readonly [
25
+ 'HTTP_PROXY',
26
+ 'HTTPS_PROXY',
27
+ 'ALL_PROXY',
28
+ 'http_proxy',
29
+ 'https_proxy',
30
+ 'all_proxy',
31
+ ];
32
+
33
+ export type ProxyEnvironmentVariable = (typeof PROXY_ENVIRONMENT_VARIABLES)[number];
34
+
35
+ export interface ProxyCredentials {
36
+ token: string;
37
+ authorization: string;
38
+ }
39
+
40
+ export function createProxyCredentials(token?: string): ProxyCredentials;
41
+
42
+ export function createProxyUrl(port: number, token?: string | null, host?: string): string;
43
+
44
+ export interface CreateProxyEnvironmentOptions {
45
+ token?: string | null;
46
+ host?: string;
47
+ baseEnv?: NodeJS.ProcessEnv;
48
+ }
49
+
50
+ export function createProxyEnvironment(
51
+ port?: number | null,
52
+ options?: CreateProxyEnvironmentOptions | string | null,
53
+ baseEnv?: NodeJS.ProcessEnv,
54
+ ): NodeJS.ProcessEnv;
package/lib/proxy.js CHANGED
@@ -1,6 +1,8 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
2
  'use strict';
3
3
 
4
+ const { randomBytes } = require('node:crypto');
5
+
4
6
  const { lookup } = require('node:dns/promises');
5
7
  const { Agent, createServer: createHttpServer, request: requestHttp } = require('node:http');
6
8
  const { connect, isIP } = require('node:net');
@@ -342,6 +344,60 @@ function startFilterProxy(options) {
342
344
  });
343
345
  }
344
346
 
347
+ const PROXY_ENVIRONMENT_VARIABLES = [
348
+ 'HTTP_PROXY',
349
+ 'HTTPS_PROXY',
350
+ 'ALL_PROXY',
351
+ 'http_proxy',
352
+ 'https_proxy',
353
+ 'all_proxy',
354
+ ];
355
+
356
+ function createProxyCredentials(tokenArg) {
357
+ const token =
358
+ typeof tokenArg === 'string' && tokenArg.length > 0
359
+ ? tokenArg
360
+ : randomBytes(32).toString('base64url');
361
+ const authorization = `Basic ${Buffer.from(`landstrip:${token}`).toString('base64')}`;
362
+ return { token, authorization };
363
+ }
364
+
365
+ function createProxyUrl(port, token, host = '127.0.0.1') {
366
+ const credentials = typeof token === 'string' && token.length > 0 ? `landstrip:${token}@` : '';
367
+ return `http://${credentials}${host}:${port}`;
368
+ }
369
+
370
+ function createProxyEnvironment(port, options, baseEnvArg) {
371
+ let token = null;
372
+ let host = '127.0.0.1';
373
+ let baseEnv = baseEnvArg;
374
+
375
+ if (typeof options === 'string') {
376
+ token = options;
377
+ } else if (options && typeof options === 'object') {
378
+ if (typeof options.token === 'string') token = options.token;
379
+ if (typeof options.host === 'string') host = options.host;
380
+ if (options.baseEnv && typeof options.baseEnv === 'object') baseEnv = options.baseEnv;
381
+ }
382
+
383
+ const result = baseEnv ? { ...baseEnv } : {};
384
+ if (!port || typeof port !== 'number' || port <= 0) {
385
+ return result;
386
+ }
387
+
388
+ const url = createProxyUrl(port, token, host);
389
+ for (const name of PROXY_ENVIRONMENT_VARIABLES) {
390
+ result[name] = url;
391
+ }
392
+ result.NO_PROXY = '';
393
+ result.no_proxy = '';
394
+ return result;
395
+ }
396
+
345
397
  module.exports = {
346
398
  startFilterProxy,
399
+ PROXY_ENVIRONMENT_VARIABLES,
400
+ createProxyCredentials,
401
+ createProxyUrl,
402
+ createProxyEnvironment,
347
403
  };
package/lib/shared.d.ts CHANGED
@@ -2,13 +2,44 @@
2
2
 
3
3
  import type {
4
4
  LandstripControlResponse,
5
+ LandstripFilesystemPolicy,
5
6
  LandstripFilesystemTrap,
7
+ LandstripNetworkPolicy,
6
8
  LandstripNetworkTrap,
9
+ LandstripPolicy,
7
10
  LandstripTrap,
11
+ LandstripWindowsPolicy,
12
+ SandboxConfig,
13
+ SandboxConfigFile,
14
+ SandboxConfigOverrides,
15
+ SandboxFilesystemConfig,
16
+ SandboxFilesystemConfigFile,
17
+ SandboxNetworkConfig,
18
+ SandboxNetworkConfigFile,
19
+ SandboxShellConfig,
20
+ SandboxShellConfigFile,
21
+ SandboxWindowsConfig,
22
+ SandboxWindowsConfigFile,
23
+ ShellReadAccess,
8
24
  } from './index.js';
9
25
 
10
26
  export type LandstripDenialTrap = LandstripFilesystemTrap | LandstripNetworkTrap;
11
27
 
28
+ export type {
29
+ SandboxConfig,
30
+ SandboxConfigFile,
31
+ SandboxConfigOverrides,
32
+ SandboxFilesystemConfig,
33
+ SandboxFilesystemConfigFile,
34
+ SandboxNetworkConfig,
35
+ SandboxNetworkConfigFile,
36
+ SandboxShellConfig,
37
+ SandboxShellConfigFile,
38
+ SandboxWindowsConfig,
39
+ SandboxWindowsConfigFile,
40
+ ShellReadAccess,
41
+ };
42
+
12
43
  export function isRecord(value: unknown): value is Record<string, unknown>;
13
44
 
14
45
  export function expandHomePath(path: string): string;
@@ -57,3 +88,144 @@ export function controlResponseLine(
57
88
  queryId: string,
58
89
  action: LandstripControlResponse['action'],
59
90
  ): string;
91
+
92
+
93
+ export interface BuildLandstripPolicyOptions {
94
+ filesystem?: LandstripFilesystemPolicy;
95
+ network?: LandstripNetworkPolicy;
96
+ windows?: LandstripWindowsPolicy;
97
+ baseDirectory?: string;
98
+ httpProxyPort?: number | null;
99
+ socksProxyPort?: number | null;
100
+ }
101
+
102
+ export function resolveFilesystemPatterns(
103
+ patterns: string[] | undefined,
104
+ baseDirectory?: string,
105
+ ): string[];
106
+
107
+ export function resolveFilesystemPolicy(
108
+ filesystem?: LandstripFilesystemPolicy,
109
+ baseDirectory?: string,
110
+ ): LandstripFilesystemPolicy;
111
+
112
+ export function buildLandstripPolicy(
113
+ options: BuildLandstripPolicyOptions,
114
+ ): LandstripPolicy;
115
+
116
+ export function serializeLandstripPolicy(policy: LandstripPolicy): string;
117
+
118
+
119
+ export function mergeArray(base: string[], override?: string[]): string[];
120
+
121
+ export function parseSandboxConfig(value: unknown): SandboxConfigFile;
122
+
123
+ export function deepMergeSandboxConfig<T extends SandboxConfig = SandboxConfig>(
124
+ base: T,
125
+ overrides?: SandboxConfigFile,
126
+ ): T;
127
+
128
+
129
+ export type PathAccessDecision = 'allow' | 'ask' | 'deny' | 'denyAlways';
130
+
131
+ export type DomainAccessDecision = 'allow' | 'ask' | 'deny';
132
+
133
+ export interface EvaluateReadOptions {
134
+ baseDirectory?: string;
135
+ allowReadOverrides?: string[];
136
+ requireAllowMatch?: boolean;
137
+ }
138
+
139
+ export interface EvaluateWriteOptions {
140
+ baseDirectory?: string;
141
+ allowWriteOverrides?: string[];
142
+ }
143
+
144
+ export interface EvaluateDomainOptions {
145
+ allowedDomainsOverrides?: string[];
146
+ }
147
+
148
+ export function matchesPathPattern(
149
+ filePath: string,
150
+ patterns: string[],
151
+ baseDirectory?: string,
152
+ ): boolean;
153
+
154
+ export function matchPathSpecificity(
155
+ filePath: string,
156
+ patterns: string[],
157
+ baseDirectory?: string,
158
+ ): number;
159
+
160
+ export function isPathReadAllowed(
161
+ filePath: string,
162
+ allowRead: string[],
163
+ denyRead: string[],
164
+ baseDirectory?: string,
165
+ options?: { requireAllowMatch?: boolean },
166
+ ): boolean;
167
+
168
+ export function isPathWriteAllowed(
169
+ filePath: string,
170
+ allowWrite: string[],
171
+ baseDirectory?: string,
172
+ ): boolean;
173
+
174
+ export function shouldPromptForWrite(
175
+ filePath: string,
176
+ allowWrite: string[],
177
+ baseDirectory?: string,
178
+ ): boolean;
179
+
180
+ export function evaluateReadAccess(
181
+ filePath: string,
182
+ filesystem: SandboxFilesystemConfig | SandboxFilesystemConfigFile | SandboxConfig | SandboxConfigFile,
183
+ options?: EvaluateReadOptions | string,
184
+ allowReadOverrides?: string[],
185
+ ): 'allow' | 'ask' | 'deny';
186
+
187
+ export function evaluateWriteAccess(
188
+ filePath: string,
189
+ filesystem: SandboxFilesystemConfig | SandboxFilesystemConfigFile | SandboxConfig | SandboxConfigFile,
190
+ options?: EvaluateWriteOptions | string,
191
+ allowWriteOverrides?: string[],
192
+ ): PathAccessDecision;
193
+
194
+ export function isDomainAllowed(
195
+ domain: string,
196
+ network: SandboxNetworkConfig | SandboxNetworkConfigFile | SandboxConfig | SandboxConfigFile,
197
+ allowedDomainsOverrides?: string[],
198
+ ): boolean;
199
+
200
+ export function evaluateDomainAccess(
201
+ domain: string,
202
+ network: SandboxNetworkConfig | SandboxNetworkConfigFile | SandboxConfig | SandboxConfigFile,
203
+ options?: EvaluateDomainOptions | string[],
204
+ ): DomainAccessDecision;
205
+
206
+
207
+ export function normalizeBlockedPath(filePath: string, cwd?: string): string;
208
+
209
+ export function extractTrapBlockedPath(
210
+ trapOutput: string,
211
+ cwd?: string,
212
+ operation?: 'read' | 'write',
213
+ ): string | null;
214
+ export function extractDomainsFromCommand(command: string): string[];
215
+
216
+ export function extractCandidatePaths(command: string): string[];
217
+
218
+ export function extractNativeDeniedPath(output: string, cwd?: string): string | null;
219
+
220
+ export function extractNativeWriteDeniedPath(output: string, cwd?: string): string | null;
221
+
222
+ export interface ExtractDeniedPathOptions {
223
+ cwd?: string;
224
+ command?: string;
225
+ }
226
+
227
+ export function extractDeniedPath(
228
+ output: string,
229
+ options?: ExtractDeniedPathOptions | string,
230
+ command?: string,
231
+ ): string | null;
package/lib/shared.js CHANGED
@@ -347,6 +347,635 @@ function controlResponseLine(queryId, action) {
347
347
  return JSON.stringify({ query_id: queryId, action }) + '\n';
348
348
  }
349
349
 
350
+ function resolveFilesystemPatterns(patterns, baseDirectory) {
351
+ if (!patterns || !Array.isArray(patterns)) return [];
352
+ if (!baseDirectory) return [...patterns];
353
+ return patterns.map((pattern) =>
354
+ /[*?[\]]/.test(pattern)
355
+ ? canonicalizeGlobPattern(pattern, baseDirectory)
356
+ : canonicalizePath(pattern, baseDirectory),
357
+ );
358
+ }
359
+
360
+ function resolveFilesystemPolicy(filesystem, baseDirectory) {
361
+ if (!filesystem) return {};
362
+ const resolved = {};
363
+ if (filesystem.denyRead !== undefined) {
364
+ resolved.denyRead = resolveFilesystemPatterns(filesystem.denyRead, baseDirectory);
365
+ }
366
+ if (filesystem.denyReadAlways !== undefined) {
367
+ resolved.denyReadAlways = resolveFilesystemPatterns(filesystem.denyReadAlways, baseDirectory);
368
+ }
369
+ if (filesystem.allowRead !== undefined) {
370
+ resolved.allowRead = resolveFilesystemPatterns(filesystem.allowRead, baseDirectory);
371
+ }
372
+ if (filesystem.allowWrite !== undefined) {
373
+ resolved.allowWrite = resolveFilesystemPatterns(filesystem.allowWrite, baseDirectory);
374
+ }
375
+ if (filesystem.denyWrite !== undefined) {
376
+ resolved.denyWrite = resolveFilesystemPatterns(filesystem.denyWrite, baseDirectory);
377
+ }
378
+ if (filesystem.denyWriteAlways !== undefined) {
379
+ resolved.denyWriteAlways = resolveFilesystemPatterns(filesystem.denyWriteAlways, baseDirectory);
380
+ }
381
+ return resolved;
382
+ }
383
+
384
+ function buildLandstripPolicy(options) {
385
+ const policy = {};
386
+ if (options.network) {
387
+ const net = {
388
+ allowNetwork: Boolean(options.network.allowNetwork),
389
+ allowLocalBinding: Boolean(options.network.allowLocalBinding),
390
+ allowAllUnixSockets: Boolean(options.network.allowAllUnixSockets),
391
+ allowUnixSockets: Array.isArray(options.network.allowUnixSockets)
392
+ ? [...options.network.allowUnixSockets]
393
+ : [],
394
+ };
395
+ const httpProxyPort =
396
+ options.httpProxyPort !== undefined
397
+ ? options.httpProxyPort
398
+ : options.network.httpProxyPort;
399
+ if (httpProxyPort !== null && httpProxyPort !== undefined) {
400
+ net.httpProxyPort = httpProxyPort;
401
+ }
402
+ const socksProxyPort =
403
+ options.socksProxyPort !== undefined
404
+ ? options.socksProxyPort
405
+ : options.network.socksProxyPort;
406
+ if (socksProxyPort !== null && socksProxyPort !== undefined) {
407
+ net.socksProxyPort = socksProxyPort;
408
+ }
409
+ policy.network = net;
410
+ }
411
+ if (options.filesystem) {
412
+ policy.filesystem = resolveFilesystemPolicy(
413
+ options.filesystem,
414
+ options.baseDirectory,
415
+ );
416
+ }
417
+ if (options.windows) {
418
+ policy.windows = {
419
+ appContainerMode: options.windows.appContainerMode ?? 'lpac',
420
+ allowLoopback: Boolean(options.windows.allowLoopback),
421
+ };
422
+ }
423
+ return policy;
424
+ }
425
+
426
+ function serializeLandstripPolicy(policy) {
427
+ return JSON.stringify(policy, null, 2) + '\n';
428
+ }
429
+
430
+ function requireSandboxObject(value, label) {
431
+ if (!isRecord(value)) {
432
+ throw new Error(`${label} must be an object`);
433
+ }
434
+ }
435
+
436
+ function rejectUnknownSandboxFields(value, allowed, prefix = '') {
437
+ for (const key of Object.keys(value)) {
438
+ if (!allowed.includes(key)) {
439
+ throw new Error(`unknown sandbox field ${prefix}${key}`);
440
+ }
441
+ }
442
+ }
443
+
444
+ function validateBooleanFields(value, fields, prefix = '') {
445
+ for (const field of fields) {
446
+ if (value[field] !== undefined && typeof value[field] !== 'boolean') {
447
+ throw new Error(`${prefix}${field} must be a boolean`);
448
+ }
449
+ }
450
+ }
451
+
452
+ function validateStringArrayFields(value, fields, prefix = '') {
453
+ for (const field of fields) {
454
+ const entry = value[field];
455
+ if (entry === undefined) continue;
456
+ if (!Array.isArray(entry) || entry.some((item) => typeof item !== 'string')) {
457
+ throw new Error(`${prefix}${field} must be an array of strings`);
458
+ }
459
+ }
460
+ }
461
+
462
+ function mergeArray(base, override) {
463
+ if (!override) return base;
464
+ return [...new Set([...(base ?? []), ...override])];
465
+ }
466
+
467
+ function parseSandboxConfig(value) {
468
+ requireSandboxObject(value, 'sandbox config');
469
+ rejectUnknownSandboxFields(value, ['enabled', 'shell', 'network', 'filesystem', 'windows']);
470
+ validateBooleanFields(value, ['enabled']);
471
+
472
+ const config = {};
473
+ if (value.enabled !== undefined) {
474
+ config.enabled = value.enabled;
475
+ }
476
+
477
+ if (value.shell !== undefined) {
478
+ requireSandboxObject(value.shell, 'shell');
479
+ rejectUnknownSandboxFields(value.shell, ['readAccess'], 'shell.');
480
+ if (
481
+ value.shell.readAccess !== undefined &&
482
+ value.shell.readAccess !== 'host' &&
483
+ value.shell.readAccess !== 'policy'
484
+ ) {
485
+ throw new Error('shell.readAccess must be host or policy');
486
+ }
487
+ config.shell = { ...value.shell };
488
+ }
489
+
490
+ if (value.network !== undefined) {
491
+ requireSandboxObject(value.network, 'network');
492
+ rejectUnknownSandboxFields(
493
+ value.network,
494
+ [
495
+ 'allowNetwork',
496
+ 'allowLocalBinding',
497
+ 'allowAllUnixSockets',
498
+ 'allowUnixSockets',
499
+ 'allowedDomains',
500
+ 'deniedDomains',
501
+ ],
502
+ 'network.',
503
+ );
504
+ validateBooleanFields(
505
+ value.network,
506
+ ['allowNetwork', 'allowLocalBinding', 'allowAllUnixSockets'],
507
+ 'network.',
508
+ );
509
+ validateStringArrayFields(
510
+ value.network,
511
+ ['allowUnixSockets', 'allowedDomains', 'deniedDomains'],
512
+ 'network.',
513
+ );
514
+ config.network = {};
515
+ if (value.network.allowNetwork !== undefined) {
516
+ config.network.allowNetwork = value.network.allowNetwork;
517
+ }
518
+ if (value.network.allowLocalBinding !== undefined) {
519
+ config.network.allowLocalBinding = value.network.allowLocalBinding;
520
+ }
521
+ if (value.network.allowAllUnixSockets !== undefined) {
522
+ config.network.allowAllUnixSockets = value.network.allowAllUnixSockets;
523
+ }
524
+ if (value.network.allowUnixSockets !== undefined) {
525
+ config.network.allowUnixSockets = [...value.network.allowUnixSockets];
526
+ }
527
+ if (value.network.allowedDomains !== undefined) {
528
+ config.network.allowedDomains = [...value.network.allowedDomains];
529
+ }
530
+ if (value.network.deniedDomains !== undefined) {
531
+ config.network.deniedDomains = [...value.network.deniedDomains];
532
+ }
533
+ }
534
+
535
+ if (value.filesystem !== undefined) {
536
+ requireSandboxObject(value.filesystem, 'filesystem');
537
+ rejectUnknownSandboxFields(
538
+ value.filesystem,
539
+ ['denyRead', 'denyReadAlways', 'allowRead', 'allowWrite', 'denyWrite', 'denyWriteAlways'],
540
+ 'filesystem.',
541
+ );
542
+ validateStringArrayFields(
543
+ value.filesystem,
544
+ ['denyRead', 'denyReadAlways', 'allowRead', 'allowWrite', 'denyWrite', 'denyWriteAlways'],
545
+ 'filesystem.',
546
+ );
547
+ config.filesystem = {};
548
+ if (value.filesystem.denyRead !== undefined) {
549
+ config.filesystem.denyRead = [...value.filesystem.denyRead];
550
+ }
551
+ if (value.filesystem.denyReadAlways !== undefined) {
552
+ config.filesystem.denyReadAlways = [...value.filesystem.denyReadAlways];
553
+ }
554
+ if (value.filesystem.allowRead !== undefined) {
555
+ config.filesystem.allowRead = [...value.filesystem.allowRead];
556
+ }
557
+ if (value.filesystem.allowWrite !== undefined) {
558
+ config.filesystem.allowWrite = [...value.filesystem.allowWrite];
559
+ }
560
+ if (value.filesystem.denyWrite !== undefined) {
561
+ config.filesystem.denyWrite = [...value.filesystem.denyWrite];
562
+ }
563
+ if (value.filesystem.denyWriteAlways !== undefined) {
564
+ config.filesystem.denyWriteAlways = [...value.filesystem.denyWriteAlways];
565
+ }
566
+ }
567
+
568
+ if (value.windows !== undefined) {
569
+ requireSandboxObject(value.windows, 'windows');
570
+ rejectUnknownSandboxFields(value.windows, ['appContainerMode', 'allowLoopback'], 'windows.');
571
+ if (
572
+ value.windows.appContainerMode !== undefined &&
573
+ value.windows.appContainerMode !== 'standard' &&
574
+ value.windows.appContainerMode !== 'lpac'
575
+ ) {
576
+ throw new Error('windows.appContainerMode must be lpac or standard');
577
+ }
578
+ validateBooleanFields(value.windows, ['allowLoopback'], 'windows.');
579
+ config.windows = {};
580
+ if (value.windows.appContainerMode !== undefined) {
581
+ config.windows.appContainerMode = value.windows.appContainerMode;
582
+ }
583
+ if (value.windows.allowLoopback !== undefined) {
584
+ config.windows.allowLoopback = value.windows.allowLoopback;
585
+ }
586
+ }
587
+
588
+ return config;
589
+ }
590
+
591
+ function deepMergeSandboxConfig(base, overrides) {
592
+ if (!overrides) return { ...base };
593
+
594
+ const shell = overrides.shell;
595
+ const network = overrides.network;
596
+ const filesystem = overrides.filesystem;
597
+ const windows = overrides.windows;
598
+
599
+ const result = {
600
+ enabled: overrides.enabled ?? base.enabled,
601
+ network: {
602
+ allowNetwork: network?.allowNetwork ?? base.network?.allowNetwork ?? false,
603
+ allowLocalBinding: network?.allowLocalBinding ?? base.network?.allowLocalBinding ?? false,
604
+ allowAllUnixSockets: network?.allowAllUnixSockets ?? base.network?.allowAllUnixSockets ?? false,
605
+ allowUnixSockets: mergeArray(base.network?.allowUnixSockets, network?.allowUnixSockets),
606
+ allowedDomains: mergeArray(base.network?.allowedDomains, network?.allowedDomains),
607
+ deniedDomains: mergeArray(base.network?.deniedDomains, network?.deniedDomains),
608
+ },
609
+ filesystem: {
610
+ denyRead: mergeArray(base.filesystem?.denyRead, filesystem?.denyRead),
611
+ denyReadAlways: mergeArray(base.filesystem?.denyReadAlways, filesystem?.denyReadAlways),
612
+ allowRead: mergeArray(base.filesystem?.allowRead, filesystem?.allowRead),
613
+ allowWrite: mergeArray(base.filesystem?.allowWrite, filesystem?.allowWrite),
614
+ denyWrite: mergeArray(base.filesystem?.denyWrite, filesystem?.denyWrite),
615
+ denyWriteAlways: mergeArray(base.filesystem?.denyWriteAlways, filesystem?.denyWriteAlways),
616
+ },
617
+ };
618
+
619
+ if (base.shell !== undefined || shell !== undefined) {
620
+ result.shell = {
621
+ readAccess: shell?.readAccess ?? base.shell?.readAccess ?? 'host',
622
+ };
623
+ }
624
+
625
+ if (base.windows !== undefined || windows !== undefined) {
626
+ result.windows = {
627
+ appContainerMode: windows?.appContainerMode ?? base.windows?.appContainerMode ?? 'standard',
628
+ allowLoopback: windows?.allowLoopback ?? base.windows?.allowLoopback ?? false,
629
+ };
630
+ }
631
+
632
+ return result;
633
+ }
634
+
635
+
636
+ function normalizePathForComparison(filePath) {
637
+ const normalized = normalizePathSeparators(filePath);
638
+ return process.platform === 'win32' ? normalized.toLowerCase() : normalized;
639
+ }
640
+
641
+ function matchesPathPattern(filePath, patterns, baseDirectory = process.cwd()) {
642
+ if (!Array.isArray(patterns) || patterns.length === 0) return false;
643
+ const abs = normalizePathForComparison(canonicalizePath(filePath, baseDirectory));
644
+
645
+ return patterns.some((pattern) => {
646
+ const absPattern = normalizePathForComparison(canonicalizeGlobPattern(pattern, baseDirectory));
647
+
648
+ if (/[*?[\]]/.test(pattern)) {
649
+ const matcher = globToRegExp(absPattern);
650
+ for (let candidate = abs; ;) {
651
+ if (matcher.test(candidate)) return true;
652
+ const parent = normalizePathSeparators(dirname(candidate));
653
+ if (parent === candidate) break;
654
+ candidate = process.platform === 'win32' ? parent.toLowerCase() : parent;
655
+ }
656
+ return false;
657
+ }
658
+
659
+ const sepChar = absPattern.endsWith('/') ? '' : '/';
660
+ return abs === absPattern || abs.startsWith(absPattern + sepChar);
661
+ });
662
+ }
663
+
664
+ function matchPathSpecificity(filePath, patterns, baseDirectory = process.cwd()) {
665
+ if (!Array.isArray(patterns) || patterns.length === 0) return -1;
666
+ let best = -1;
667
+ for (const pattern of patterns) {
668
+ if (!matchesPathPattern(filePath, [pattern], baseDirectory)) continue;
669
+ const canonical = canonicalizeGlobPattern(pattern, baseDirectory);
670
+ if (canonical.length > best) {
671
+ best = canonical.length;
672
+ }
673
+ }
674
+ return best;
675
+ }
676
+
677
+ function isPathReadAllowed(
678
+ filePath,
679
+ allowRead,
680
+ denyRead,
681
+ baseDirectory = process.cwd(),
682
+ options,
683
+ ) {
684
+ const allowPatterns = Array.isArray(allowRead) ? allowRead : [];
685
+ const denyPatterns = Array.isArray(denyRead) ? denyRead : [];
686
+ const allow = matchPathSpecificity(filePath, allowPatterns, baseDirectory);
687
+ const deny = matchPathSpecificity(filePath, denyPatterns, baseDirectory);
688
+
689
+ if (options && options.requireAllowMatch) {
690
+ return allow >= 0 && allow >= deny;
691
+ }
692
+ if (deny < 0) return true;
693
+ return allow >= deny;
694
+ }
695
+
696
+ function isPathWriteAllowed(filePath, allowWrite, baseDirectory = process.cwd()) {
697
+ const allowPatterns = Array.isArray(allowWrite) ? allowWrite : [];
698
+ return allowPatterns.length > 0 && matchesPathPattern(filePath, allowPatterns, baseDirectory);
699
+ }
700
+
701
+ function shouldPromptForWrite(filePath, allowWrite, baseDirectory = process.cwd()) {
702
+ return !isPathWriteAllowed(filePath, allowWrite, baseDirectory);
703
+ }
704
+
705
+ function normalizeReadOptions(options, allowReadOverrides) {
706
+ if (typeof options === 'string') {
707
+ return {
708
+ baseDirectory: options,
709
+ allowReadOverrides: Array.isArray(allowReadOverrides) ? allowReadOverrides : [],
710
+ requireAllowMatch: false,
711
+ };
712
+ }
713
+ return {
714
+ baseDirectory: options?.baseDirectory,
715
+ allowReadOverrides: options?.allowReadOverrides ?? (Array.isArray(allowReadOverrides) ? allowReadOverrides : []),
716
+ requireAllowMatch: options?.requireAllowMatch ?? false,
717
+ };
718
+ }
719
+
720
+ function evaluateReadAccess(filePath, filesystemOrConfig, options, allowReadOverrides) {
721
+ const filesystem = filesystemOrConfig && 'filesystem' in filesystemOrConfig ? filesystemOrConfig.filesystem : (filesystemOrConfig ?? {});
722
+ const opts = normalizeReadOptions(options, allowReadOverrides);
723
+ const baseDirectory = opts.baseDirectory;
724
+
725
+ if (filesystem.denyReadAlways && matchesPathPattern(filePath, filesystem.denyReadAlways, baseDirectory)) {
726
+ return 'deny';
727
+ }
728
+
729
+ const effectiveAllowRead = opts.allowReadOverrides.length > 0
730
+ ? mergeArray(filesystem.allowRead ?? [], opts.allowReadOverrides)
731
+ : (filesystem.allowRead ?? []);
732
+
733
+ if (isPathReadAllowed(filePath, effectiveAllowRead, filesystem.denyRead ?? [], baseDirectory, { requireAllowMatch: opts.requireAllowMatch })) {
734
+ return 'allow';
735
+ }
736
+
737
+ return 'ask';
738
+ }
739
+
740
+ function normalizeWriteOptions(options, allowWriteOverrides) {
741
+ if (typeof options === 'string') {
742
+ return {
743
+ baseDirectory: options,
744
+ allowWriteOverrides: Array.isArray(allowWriteOverrides) ? allowWriteOverrides : [],
745
+ };
746
+ }
747
+ return {
748
+ baseDirectory: options?.baseDirectory,
749
+ allowWriteOverrides: options?.allowWriteOverrides ?? (Array.isArray(allowWriteOverrides) ? allowWriteOverrides : []),
750
+ };
751
+ }
752
+
753
+ function evaluateWriteAccess(filePath, filesystemOrConfig, options, allowWriteOverrides) {
754
+ const filesystem = filesystemOrConfig && 'filesystem' in filesystemOrConfig ? filesystemOrConfig.filesystem : (filesystemOrConfig ?? {});
755
+ const opts = normalizeWriteOptions(options, allowWriteOverrides);
756
+ const baseDirectory = opts.baseDirectory;
757
+
758
+ if (filesystem.denyWriteAlways && matchesPathPattern(filePath, filesystem.denyWriteAlways, baseDirectory)) {
759
+ return 'denyAlways';
760
+ }
761
+
762
+ if (filesystem.denyWrite && matchesPathPattern(filePath, filesystem.denyWrite, baseDirectory)) {
763
+ return 'deny';
764
+ }
765
+
766
+ const effectiveAllowWrite = opts.allowWriteOverrides.length > 0
767
+ ? mergeArray(filesystem.allowWrite ?? [], opts.allowWriteOverrides)
768
+ : (filesystem.allowWrite ?? []);
769
+
770
+ if (effectiveAllowWrite.length > 0 && matchesPathPattern(filePath, effectiveAllowWrite, baseDirectory)) {
771
+ return 'allow';
772
+ }
773
+
774
+ return 'ask';
775
+ }
776
+
777
+ function isDomainAllowed(domain, networkOrConfig, allowedDomainsOverrides) {
778
+ const network = networkOrConfig && 'network' in networkOrConfig ? networkOrConfig.network : (networkOrConfig ?? {});
779
+ if (network.allowNetwork) return true;
780
+ const denied = network.deniedDomains ?? [];
781
+ if (domainMatchesAny(domain, denied)) return false;
782
+ const effectiveAllowed = Array.isArray(allowedDomainsOverrides) && allowedDomainsOverrides.length > 0
783
+ ? mergeArray(network.allowedDomains ?? [], allowedDomainsOverrides)
784
+ : (network.allowedDomains ?? []);
785
+ return domainMatchesAny(domain, effectiveAllowed);
786
+ }
787
+
788
+ function evaluateDomainAccess(domain, networkOrConfig, options) {
789
+ const network = networkOrConfig && 'network' in networkOrConfig ? networkOrConfig.network : (networkOrConfig ?? {});
790
+ if (network.allowNetwork) return 'allow';
791
+ const overrides = Array.isArray(options) ? options : options?.allowedDomainsOverrides;
792
+ const denied = network.deniedDomains ?? [];
793
+ if (domainMatchesAny(domain, denied)) return 'deny';
794
+ const effectiveAllowed = Array.isArray(overrides) && overrides.length > 0
795
+ ? mergeArray(network.allowedDomains ?? [], overrides)
796
+ : (network.allowedDomains ?? []);
797
+ if (domainMatchesAny(domain, effectiveAllowed)) return 'allow';
798
+ return 'ask';
799
+ }
800
+
801
+
802
+ function isPathLike(value) {
803
+ if (typeof value !== 'string') return false;
804
+ const trimmed = value.trim();
805
+ return (
806
+ trimmed === '~' ||
807
+ trimmed.startsWith('/') ||
808
+ trimmed.startsWith('\\\\') ||
809
+ /^[a-zA-Z]:[\\/]/.test(trimmed) ||
810
+ trimmed.startsWith('~/') ||
811
+ trimmed.startsWith('~\\') ||
812
+ trimmed.startsWith('./') ||
813
+ trimmed.startsWith('.\\') ||
814
+ trimmed.startsWith('../') ||
815
+ trimmed.startsWith('..\\') ||
816
+ trimmed.startsWith('.') ||
817
+ trimmed.includes('/') ||
818
+ trimmed.includes('\\')
819
+ );
820
+ }
821
+
822
+ function normalizeBlockedPath(filePath, cwd = process.cwd()) {
823
+ const nativePath =
824
+ process.platform === 'win32' && /^\/[a-zA-Z](?:\/|$)/.test(filePath)
825
+ ? `${filePath[1]}:${filePath.slice(2)}`
826
+ : filePath;
827
+ return canonicalizePath(isAbsolute(nativePath) ? nativePath : join(cwd, nativePath), cwd);
828
+ }
829
+
830
+ function normalizePathMatch(value, cwd) {
831
+ return isPathLike(value) ? normalizeBlockedPath(value, cwd) : null;
832
+ }
833
+
834
+ function extractDomainsFromCommand(command) {
835
+ if (typeof command !== 'string') return [];
836
+ const domains = [];
837
+ const urlRegex = /https?:\/\/([^\s/:?#'"]+)(?::\d+)?(?:[/?#]|\s|$)/g;
838
+ let match;
839
+ while ((match = urlRegex.exec(command)) !== null) {
840
+ const domain = match[1].toLowerCase();
841
+ if (!domains.includes(domain)) {
842
+ domains.push(domain);
843
+ }
844
+ }
845
+ return domains;
846
+ }
847
+
848
+ function extractCandidatePaths(command) {
849
+ if (typeof command !== 'string') return [];
850
+ const paths = [];
851
+ const tokens = command.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) ?? [];
852
+ for (const rawToken of tokens) {
853
+ const token = rawToken.replace(/^["']|["']$/g, '');
854
+ const clean = token.replace(/^[<>&|;]+|[<>&|;]+$/g, '');
855
+ if (
856
+ clean.length > 0 &&
857
+ !clean.startsWith('-') &&
858
+ (clean.startsWith('/') ||
859
+ clean.startsWith('./') ||
860
+ clean.startsWith('../') ||
861
+ clean.startsWith('~/') ||
862
+ clean.startsWith('\\\\') ||
863
+ clean.startsWith('~\\') ||
864
+ clean.startsWith('.\\') ||
865
+ clean.startsWith('..\\') ||
866
+ /^[a-zA-Z]:[\\/]/.test(clean))
867
+ ) {
868
+ if (!paths.includes(clean)) {
869
+ paths.push(clean);
870
+ }
871
+ }
872
+ }
873
+ return paths;
874
+ }
875
+
876
+ function extractNativeDeniedPath(output, cwd = process.cwd()) {
877
+ if (typeof output !== 'string') return null;
878
+ const denial = String.raw`(?:Access is denied\.?|Operation not permitted|Permission denied)`;
879
+ let match = output.match(new RegExp(String.raw`['"]([^'"\n]+)['"][^\r\n]{0,120}${denial}`, 'i'));
880
+ if (match) return normalizePathMatch(match[1], cwd);
881
+
882
+ // bash/sh and native Windows tools: line X: /path: Permission denied,
883
+ // or cmd: C:\path: Access is denied.
884
+ match = output.match(
885
+ new RegExp(
886
+ String.raw`(?:^|:\s+)((?:[a-zA-Z]:[\\/]|\\\\|/|\.{1,2}[\\/])[^:\r\n]*?):\s+${denial}$`,
887
+ 'im',
888
+ ),
889
+ );
890
+ if (match) return normalizePathMatch(match[1], cwd);
891
+
892
+ // ls/cat/cp: cannot open/access/stat '/path': Permission denied
893
+ match = output.match(
894
+ new RegExp(
895
+ String.raw`^[a-zA-Z0-9_-]+: cannot (?:open|access|stat|create)(?: directory)? '?([^'\n]+?)'?(?: for (?:reading|writing))?: ${denial}$`,
896
+ 'im',
897
+ ),
898
+ );
899
+ if (match) return normalizePathMatch(match[1], cwd);
900
+
901
+ return null;
902
+ }
903
+
904
+ function extractNativeWriteDeniedPath(output, cwd = process.cwd()) {
905
+ if (typeof output !== 'string') return null;
906
+ const denial = String.raw`(?:Access is denied\.?|Operation not permitted|Permission denied)`;
907
+ let match = output.match(
908
+ new RegExp(
909
+ String.raw`(?:[Uu]nable to create|failed to create(?: directory)?|cannot (?:create|touch|mkdir|remove|unlink|rename)|for writing)[^'"\x60\r\n]*['"\x60]([^'"\x60\r\n]+)['"\x60](?:[^\r\n]*\r?\n){0,3}[^\r\n]{0,120}${denial}`,
910
+ 'im',
911
+ ),
912
+ );
913
+ if (match) return normalizePathMatch(match[1], cwd);
914
+
915
+ match = output.match(
916
+ new RegExp(
917
+ String.raw`^[a-zA-Z0-9_-]+: cannot create(?: directory)? '?([^'\n]+?)'?(?: for writing)?: ${denial}$`,
918
+ 'im',
919
+ ),
920
+ );
921
+ if (match) return normalizePathMatch(match[1], cwd);
922
+
923
+
924
+ match = output.match(
925
+ new RegExp(
926
+ String.raw`^[a-zA-Z0-9_-]+: couldn't open temporary file ((?:[a-zA-Z]:[\\/]|\\\\|/)[^:\r\n]*?): ${denial}$`,
927
+ 'im',
928
+ ),
929
+ );
930
+ if (match) return normalizePathMatch(match[1], cwd);
931
+
932
+ return null;
933
+ }
934
+
935
+ function extractTrapBlockedPath(trapOutput, cwd = process.cwd(), operation) {
936
+ if (typeof trapOutput !== 'string') return null;
937
+ const traps = parseLandstripTraps(trapOutput);
938
+ for (const trap of traps) {
939
+ if (isFilesystemTrap(trap) && (!operation || trap.operation === operation)) {
940
+ return normalizeBlockedPath(trap.path, cwd);
941
+ }
942
+ }
943
+ return null;
944
+ }
945
+
946
+ function extractDeniedPath(output, options, commandArg) {
947
+ if (typeof output !== 'string') return null;
948
+ let cwd = process.cwd();
949
+ let command = commandArg;
950
+ if (typeof options === 'string') {
951
+ cwd = options;
952
+ } else if (options && typeof options === 'object') {
953
+ if (typeof options.cwd === 'string') cwd = options.cwd;
954
+ if (typeof options.command === 'string') command = options.command;
955
+ }
956
+
957
+ // 1. Structured traps
958
+ const landstripTraps = parseLandstripTraps(output);
959
+ const trapped = extractTrapBlockedPath(output, cwd);
960
+ if (trapped) return trapped;
961
+
962
+ // 2. Native shell/tool error messages
963
+ const nativeMatch = extractNativeDeniedPath(output, cwd);
964
+ if (nativeMatch) return nativeMatch;
965
+
966
+ // 3. Fallback: if landstrip trapped but path wasn't specified, inspect command candidates
967
+ if (
968
+ command &&
969
+ landstripTraps.some((trap) => trap.kind === 'filesystem' || trap.kind === 'internal')
970
+ ) {
971
+ for (const candidate of extractCandidatePaths(command)) {
972
+ return normalizeBlockedPath(candidate, cwd);
973
+ }
974
+ }
975
+
976
+ return null;
977
+ }
978
+
350
979
  module.exports = {
351
980
  isRecord,
352
981
  expandHomePath,
@@ -371,4 +1000,30 @@ module.exports = {
371
1000
  formatLandstripTrap,
372
1001
  formatLandstripTraps,
373
1002
  controlResponseLine,
1003
+ resolveFilesystemPatterns,
1004
+ resolveFilesystemPolicy,
1005
+ buildLandstripPolicy,
1006
+ serializeLandstripPolicy,
1007
+ mergeArray,
1008
+ parseSandboxConfig,
1009
+ deepMergeSandboxConfig,
1010
+
1011
+ normalizePathForComparison,
1012
+ matchesPathPattern,
1013
+ matchPathSpecificity,
1014
+ isPathReadAllowed,
1015
+ isPathWriteAllowed,
1016
+ shouldPromptForWrite,
1017
+ evaluateReadAccess,
1018
+ evaluateWriteAccess,
1019
+ isDomainAllowed,
1020
+ evaluateDomainAccess,
1021
+
1022
+ normalizeBlockedPath,
1023
+ extractTrapBlockedPath,
1024
+ extractDomainsFromCommand,
1025
+ extractCandidatePaths,
1026
+ extractNativeDeniedPath,
1027
+ extractNativeWriteDeniedPath,
1028
+ extractDeniedPath,
374
1029
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@landstrip/landstrip-api",
3
- "version": "0.19.6",
3
+ "version": "0.19.7",
4
4
  "description": "Sandbox runner using Landlock, Seatbelt, and AppContainer",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://github.com/landstrip/landstrip#readme",
@@ -40,12 +40,12 @@
40
40
  "ipaddr.js": "^2.5.0"
41
41
  },
42
42
  "optionalDependencies": {
43
- "@landstrip/landstrip-darwin-arm64": "0.19.6",
44
- "@landstrip/landstrip-darwin-x64": "0.19.6",
45
- "@landstrip/landstrip-linux-x64": "0.19.6",
46
- "@landstrip/landstrip-linux-arm64": "0.19.6",
47
- "@landstrip/landstrip-win32-x64": "0.19.6",
48
- "@landstrip/landstrip-win32-arm64": "0.19.6"
43
+ "@landstrip/landstrip-darwin-arm64": "0.19.7",
44
+ "@landstrip/landstrip-darwin-x64": "0.19.7",
45
+ "@landstrip/landstrip-linux-x64": "0.19.7",
46
+ "@landstrip/landstrip-linux-arm64": "0.19.7",
47
+ "@landstrip/landstrip-win32-x64": "0.19.7",
48
+ "@landstrip/landstrip-win32-arm64": "0.19.7"
49
49
  },
50
50
  "publishConfig": {
51
51
  "access": "public"