@vellumai/credential-executor 0.10.7 → 0.10.8-dev.202607102228.5945895

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 (65) hide show
  1. package/Dockerfile +1 -1
  2. package/node_modules/@vellumai/service-contracts/package.json +1 -2
  3. package/node_modules/@vellumai/service-contracts/src/__tests__/attachment-naming.test.ts +104 -0
  4. package/node_modules/@vellumai/service-contracts/src/__tests__/contracts.test.ts +0 -2
  5. package/node_modules/@vellumai/service-contracts/src/attachment-naming.ts +118 -0
  6. package/node_modules/@vellumai/service-contracts/src/credential-rpc.ts +3 -5
  7. package/node_modules/@vellumai/service-contracts/src/index.ts +2 -4
  8. package/node_modules/@vellumai/service-contracts/src/rpc.ts +4 -447
  9. package/package.json +2 -3
  10. package/src/__tests__/bulk-set-credentials.test.ts +1 -1
  11. package/src/__tests__/local-standalone.test.ts +5 -36
  12. package/src/__tests__/managed-integration.test.ts +112 -91
  13. package/src/__tests__/managed-reconnect.test.ts +2 -2
  14. package/src/__tests__/transport.test.ts +23 -27
  15. package/src/cli.ts +1 -1
  16. package/src/index.ts +8 -88
  17. package/src/main.ts +228 -340
  18. package/src/paths.ts +4 -20
  19. package/src/server.ts +52 -469
  20. package/node_modules/@vellumai/service-contracts/src/__tests__/grants.test.ts +0 -686
  21. package/node_modules/@vellumai/service-contracts/src/grants.ts +0 -184
  22. package/node_modules/@vellumai/service-contracts/src/rendering.ts +0 -135
  23. package/src/__tests__/command-executor.test.ts +0 -1879
  24. package/src/__tests__/command-validator.test.ts +0 -1405
  25. package/src/__tests__/command-workspace.test.ts +0 -1050
  26. package/src/__tests__/grant-store.test.ts +0 -689
  27. package/src/__tests__/http-executor.test.ts +0 -1336
  28. package/src/__tests__/http-policy.test.ts +0 -1069
  29. package/src/__tests__/local-materializers.test.ts +0 -860
  30. package/src/__tests__/local-token-refresh.test.ts +0 -361
  31. package/src/__tests__/manage-secure-command-tool.test.ts +0 -134
  32. package/src/__tests__/managed-lazy-getters.test.ts +0 -359
  33. package/src/__tests__/managed-materializers.test.ts +0 -1028
  34. package/src/__tests__/managed-rejection.test.ts +0 -43
  35. package/src/__tests__/toolstore.test.ts +0 -773
  36. package/src/audit/store.ts +0 -188
  37. package/src/commands/auth-adapters.ts +0 -169
  38. package/src/commands/egress-hooks.ts +0 -203
  39. package/src/commands/executor.ts +0 -1155
  40. package/src/commands/output-scan.ts +0 -157
  41. package/src/commands/profiles.ts +0 -286
  42. package/src/commands/validator.ts +0 -702
  43. package/src/commands/workspace.ts +0 -550
  44. package/src/grants/index.ts +0 -17
  45. package/src/grants/persistent-store.ts +0 -309
  46. package/src/grants/rpc-handlers.ts +0 -293
  47. package/src/grants/temporary-store.ts +0 -289
  48. package/src/http/audit.ts +0 -84
  49. package/src/http/executor.ts +0 -684
  50. package/src/http/path-template.ts +0 -245
  51. package/src/http/policy.ts +0 -238
  52. package/src/http/response-filter.ts +0 -233
  53. package/src/managed-errors.ts +0 -9
  54. package/src/managed-lazy-getters.ts +0 -106
  55. package/src/managed-main.ts +0 -822
  56. package/src/materializers/local-oauth-lookup.ts +0 -98
  57. package/src/materializers/local-token-refresh.ts +0 -287
  58. package/src/materializers/local.ts +0 -316
  59. package/src/materializers/managed-platform.ts +0 -295
  60. package/src/subjects/local.ts +0 -177
  61. package/src/subjects/managed.ts +0 -311
  62. package/src/subjects/policy.ts +0 -79
  63. package/src/toolstore/integrity.ts +0 -94
  64. package/src/toolstore/manifest.ts +0 -154
  65. package/src/toolstore/publish.ts +0 -571
@@ -1,571 +0,0 @@
1
- /**
2
- * Immutable toolstore publisher.
3
- *
4
- * Downloads (or re-fetches) secure command bundles inside CES, verifies
5
- * their digest against the declared expected value, and writes them into
6
- * content-addressed directories within the CES-private data root.
7
- *
8
- * ## Invariants
9
- *
10
- * 1. **CES-only writes** — All bundle content is written to the CES
11
- * toolstore directory, which lives under the CES-private data root.
12
- * The assistant process cannot read or write this path.
13
- *
14
- * 2. **Immutable publications** — Once a digest directory exists, it is
15
- * never overwritten. Re-publishing the same digest is a deduplicated
16
- * no-op that returns success.
17
- *
18
- * 3. **Digest verification before write** — Downloaded bytes are verified
19
- * against the expected digest before any file is created. A mismatch
20
- * is a hard error; no partial writes occur.
21
- *
22
- * 4. **No credential grants** — Publishing a bundle into the toolstore
23
- * is a pure content operation. It does not create, modify, or imply
24
- * any credential-use grant.
25
- *
26
- * 5. **No workspace-origin bundles** — Source URLs that point to the
27
- * workspace directory or use non-HTTPS schemes are rejected.
28
- */
29
-
30
- import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, readlinkSync, realpathSync, renameSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
31
- import { join, resolve } from "node:path";
32
-
33
- import { getCesToolStoreDir, type CesMode } from "../paths.js";
34
- import type { SecureCommandManifest } from "../commands/profiles.js";
35
- import { validateManifest } from "../commands/validator.js";
36
- import { verifyDigest } from "./integrity.js";
37
- import {
38
- isValidSha256Hex,
39
- isWorkspaceOriginPath,
40
- validateSourceUrl,
41
- type ToolstoreManifest,
42
- } from "./manifest.js";
43
-
44
- // ---------------------------------------------------------------------------
45
- // Publication result
46
- // ---------------------------------------------------------------------------
47
-
48
- export interface PublishResult {
49
- /** Whether the publication succeeded. */
50
- success: boolean;
51
-
52
- /**
53
- * Whether this was a deduplicated no-op (the digest directory already
54
- * existed from a previous publication).
55
- */
56
- deduplicated: boolean;
57
-
58
- /** The content-addressed directory path where the bundle is stored. */
59
- bundlePath: string;
60
-
61
- /** Error message if publication failed (undefined on success). */
62
- error?: string;
63
- }
64
-
65
- // ---------------------------------------------------------------------------
66
- // Publish request
67
- // ---------------------------------------------------------------------------
68
-
69
- export interface PublishRequest {
70
- /** Raw bundle bytes to publish. */
71
- bundleBytes: Buffer | Uint8Array;
72
-
73
- /** Expected SHA-256 hex digest of the bundle bytes. */
74
- expectedDigest: string;
75
-
76
- /** Unique identifier for the command bundle. */
77
- bundleId: string;
78
-
79
- /** Semantic version of the bundle. */
80
- version: string;
81
-
82
- /** HTTPS URL from which the bundle was fetched. */
83
- sourceUrl: string;
84
-
85
- /** The secure command manifest for this bundle. */
86
- secureCommandManifest: SecureCommandManifest;
87
-
88
- /** CES mode override (defaults to auto-detection). */
89
- cesMode?: CesMode;
90
- }
91
-
92
- // ---------------------------------------------------------------------------
93
- // Content-addressed path helpers
94
- // ---------------------------------------------------------------------------
95
-
96
- /** Manifest filename within a content-addressed bundle directory. */
97
- const MANIFEST_FILENAME = "toolstore-manifest.json";
98
-
99
- /** Bundle content filename within a content-addressed bundle directory. */
100
- const BUNDLE_FILENAME = "bundle.bin";
101
-
102
- /**
103
- * Return the content-addressed directory path for a given digest.
104
- *
105
- * Layout: `<toolstoreDir>/<digest>/`
106
- *
107
- * @throws {Error} if digest is not a valid SHA-256 hex string.
108
- */
109
- export function getBundleDir(
110
- toolstoreDir: string,
111
- digest: string,
112
- ): string {
113
- if (!isValidSha256Hex(digest)) {
114
- throw new Error(`Invalid digest "${digest}": must be a 64-character lowercase hex SHA-256 digest.`);
115
- }
116
- return join(toolstoreDir, digest);
117
- }
118
-
119
- /**
120
- * Return the manifest file path for a given digest.
121
- *
122
- * @throws {Error} if digest is not a valid SHA-256 hex string.
123
- */
124
- export function getBundleManifestPath(
125
- toolstoreDir: string,
126
- digest: string,
127
- ): string {
128
- return join(getBundleDir(toolstoreDir, digest), MANIFEST_FILENAME);
129
- }
130
-
131
- /**
132
- * Return the bundle content file path for a given digest.
133
- *
134
- * @throws {Error} if digest is not a valid SHA-256 hex string.
135
- */
136
- export function getBundleContentPath(
137
- toolstoreDir: string,
138
- digest: string,
139
- ): string {
140
- return join(getBundleDir(toolstoreDir, digest), BUNDLE_FILENAME);
141
- }
142
-
143
- // ---------------------------------------------------------------------------
144
- // Publisher
145
- // ---------------------------------------------------------------------------
146
-
147
- /**
148
- * Publish a secure command bundle into the CES-private immutable
149
- * toolstore.
150
- *
151
- * This function:
152
- * 1. Validates the source URL (HTTPS only, no workspace origins).
153
- * 2. Validates the expected digest format.
154
- * 3. Validates the secure command manifest.
155
- * 4. Verifies the bundle bytes match the expected digest.
156
- * 5. Checks for deduplication (returns early if already published).
157
- * 6. Writes bundle contents and manifest atomically.
158
- *
159
- * Returns a {@link PublishResult} describing the outcome.
160
- */
161
- export function publishBundle(request: PublishRequest): PublishResult {
162
- const {
163
- bundleBytes,
164
- expectedDigest,
165
- bundleId,
166
- version,
167
- sourceUrl,
168
- secureCommandManifest,
169
- cesMode,
170
- } = request;
171
-
172
- const toolstoreDir = getCesToolStoreDir(cesMode);
173
-
174
- // -- Validate source URL ------------------------------------------------
175
- const urlError = validateSourceUrl(sourceUrl);
176
- if (urlError) {
177
- return {
178
- success: false,
179
- deduplicated: false,
180
- bundlePath: "",
181
- error: `Invalid source URL: ${urlError}`,
182
- };
183
- }
184
-
185
- // -- Reject workspace-origin paths in the URL ---------------------------
186
- try {
187
- const parsedUrl = new URL(sourceUrl);
188
- const rawPathname = parsedUrl.pathname;
189
-
190
- // Always check the raw pathname — this must not be skipped.
191
- if (isWorkspaceOriginPath(rawPathname)) {
192
- return {
193
- success: false,
194
- deduplicated: false,
195
- bundlePath: "",
196
- error: `Source URL path "${rawPathname}" appears to be a workspace-origin path. ` +
197
- `Workspace-origin binaries are never publishable.`,
198
- };
199
- }
200
-
201
- // Also check the decoded pathname for percent-encoded traversals.
202
- try {
203
- const decodedPathname = decodeURIComponent(rawPathname);
204
- if (decodedPathname !== rawPathname && isWorkspaceOriginPath(decodedPathname)) {
205
- return {
206
- success: false,
207
- deduplicated: false,
208
- bundlePath: "",
209
- error: `Source URL path "${decodedPathname}" (decoded from "${rawPathname}") appears to be a workspace-origin path. ` +
210
- `Workspace-origin binaries are never publishable.`,
211
- };
212
- }
213
- } catch {
214
- // decodeURIComponent threw URIError on malformed sequences (e.g. %C0%AF).
215
- // The raw pathname was already checked above, so we're safe.
216
- }
217
- } catch {
218
- // URL parsing already validated above
219
- }
220
-
221
- // -- Validate digest format ---------------------------------------------
222
- if (!isValidSha256Hex(expectedDigest)) {
223
- return {
224
- success: false,
225
- deduplicated: false,
226
- bundlePath: "",
227
- error: `Invalid expectedDigest "${expectedDigest}". ` +
228
- `Must be a 64-character lowercase hex SHA-256 digest.`,
229
- };
230
- }
231
-
232
- // -- Validate secure command manifest -----------------------------------
233
- const manifestValidation = validateManifest(secureCommandManifest);
234
- if (!manifestValidation.valid) {
235
- return {
236
- success: false,
237
- deduplicated: false,
238
- bundlePath: "",
239
- error: `Invalid secure command manifest: ${manifestValidation.errors.join("; ")}`,
240
- };
241
- }
242
-
243
- // -- Validate manifest metadata matches request -------------------------
244
- const metadataMismatches: string[] = [];
245
- if (secureCommandManifest.bundleDigest !== expectedDigest) {
246
- metadataMismatches.push(
247
- `bundleDigest "${secureCommandManifest.bundleDigest}" does not match expectedDigest "${expectedDigest}"`,
248
- );
249
- }
250
- if (secureCommandManifest.bundleId !== bundleId) {
251
- metadataMismatches.push(
252
- `bundleId "${secureCommandManifest.bundleId}" does not match request bundleId "${bundleId}"`,
253
- );
254
- }
255
- if (secureCommandManifest.version !== version) {
256
- metadataMismatches.push(
257
- `version "${secureCommandManifest.version}" does not match request version "${version}"`,
258
- );
259
- }
260
- if (metadataMismatches.length > 0) {
261
- return {
262
- success: false,
263
- deduplicated: false,
264
- bundlePath: "",
265
- error: `Manifest metadata mismatch: ${metadataMismatches.join("; ")}`,
266
- };
267
- }
268
-
269
- // -- Verify bundle digest -----------------------------------------------
270
- const digestResult = verifyDigest(bundleBytes, expectedDigest);
271
- if (!digestResult.valid) {
272
- return {
273
- success: false,
274
- deduplicated: false,
275
- bundlePath: "",
276
- error: digestResult.error!,
277
- };
278
- }
279
-
280
- // -- Deduplication check ------------------------------------------------
281
- const bundleDir = getBundleDir(toolstoreDir, expectedDigest);
282
- const manifestPath = getBundleManifestPath(toolstoreDir, expectedDigest);
283
-
284
- if (existsSync(bundleDir) && existsSync(manifestPath)) {
285
- // Already published — deduplicated no-op
286
- return {
287
- success: true,
288
- deduplicated: true,
289
- bundlePath: bundleDir,
290
- };
291
- }
292
-
293
- // -- Ensure toolstore directory exists -----------------------------------
294
- mkdirSync(toolstoreDir, { recursive: true });
295
-
296
- // -- Write bundle atomically --------------------------------------------
297
- //
298
- // Write to a staging directory first, then rename to the final
299
- // content-addressed path. This prevents partial writes from being
300
- // visible to readers.
301
- const stagingDir = join(toolstoreDir, `.staging-${expectedDigest}-${Date.now()}`);
302
- mkdirSync(stagingDir, { recursive: true });
303
-
304
- try {
305
- // Write bundle content to a unique staging filename so that if the
306
- // archive itself contains a root-level "bundle.bin", extraction won't
307
- // collide with the staging archive file.
308
- const stagingArchiveName = `.ces-bundle-staging-${Date.now()}.tar.gz`;
309
- const stagingBundlePath = join(stagingDir, stagingArchiveName);
310
- writeFileSync(stagingBundlePath, bundleBytes, { mode: 0o444 });
311
-
312
- // Extract the tar.gz archive into the staging directory.
313
- // The archive is expected to contain the runnable bundle contents,
314
- // including the entrypoint declared in the manifest.
315
- const extractProc = Bun.spawnSync(
316
- ["tar", "xzf", stagingBundlePath, "-C", stagingDir],
317
- { stdout: "pipe", stderr: "pipe" },
318
- );
319
- if (extractProc.exitCode !== 0) {
320
- const stderr = extractProc.stderr
321
- ? new TextDecoder().decode(extractProc.stderr).trim()
322
- : "unknown error";
323
- throw new Error(
324
- `Bundle extraction failed (exit code ${extractProc.exitCode}): ${stderr}`,
325
- );
326
- }
327
-
328
- // Remove the raw archive — the extracted contents replace it
329
- try {
330
- unlinkSync(stagingBundlePath);
331
- } catch {
332
- // Best effort — the file may have been overwritten by extraction
333
- }
334
-
335
- // Scan for symlinks that escape the bundle directory.
336
- // A symlink pointing outside the staging directory could allow the
337
- // entrypoint to execute arbitrary binaries on the host.
338
- const escapingSymlinks = findEscapingSymlinks(stagingDir);
339
- if (escapingSymlinks.length > 0) {
340
- throw new Error(
341
- `Bundle contains symlinks that point outside the bundle directory: ${escapingSymlinks.join(", ")}. ` +
342
- `Symlink escape is not allowed.`,
343
- );
344
- }
345
-
346
- // Validate that the declared entrypoint resolves inside the staging
347
- // directory. A manifest with an absolute or `../`-traversal entrypoint
348
- // could make chmod (or later execution) touch files outside the bundle.
349
- const realStagingDir = realpathSync(stagingDir);
350
- const extractedEntrypoint = resolve(realStagingDir, secureCommandManifest.entrypoint);
351
- if (extractedEntrypoint !== realStagingDir && !extractedEntrypoint.startsWith(realStagingDir + "/")) {
352
- throw new Error(
353
- `Entrypoint "${secureCommandManifest.entrypoint}" resolves outside the bundle directory. ` +
354
- `Path traversal in entrypoint values is not allowed.`,
355
- );
356
- }
357
-
358
- // Check that the entrypoint itself is not a symlink (use lstat to avoid following)
359
- try {
360
- const entrypointStat = lstatSync(extractedEntrypoint);
361
- if (entrypointStat.isSymbolicLink()) {
362
- throw new Error(
363
- `Entrypoint "${secureCommandManifest.entrypoint}" is a symbolic link. ` +
364
- `Symlink entrypoints are not allowed — the entrypoint must be a regular file.`,
365
- );
366
- }
367
- } catch (err) {
368
- if (err instanceof Error && err.message.includes("Symlink entrypoints")) {
369
- throw err;
370
- }
371
- // lstat failed — the file doesn't exist, fall through to existsSync check
372
- }
373
-
374
- if (!existsSync(extractedEntrypoint)) {
375
- throw new Error(
376
- `Entrypoint "${secureCommandManifest.entrypoint}" not found in extracted bundle contents. ` +
377
- `The archive must contain the declared entrypoint path.`,
378
- );
379
- }
380
-
381
- // Make the entrypoint executable
382
- chmodSync(extractedEntrypoint, 0o555);
383
-
384
- // Build and write toolstore manifest
385
- const toolstoreManifest: ToolstoreManifest = {
386
- digest: expectedDigest,
387
- bundleId,
388
- version,
389
- origin: {
390
- sourceUrl,
391
- fetchedAt: new Date().toISOString(),
392
- },
393
- declaredProfiles: Object.keys(secureCommandManifest.commandProfiles),
394
- secureCommandManifest,
395
- publishedAt: new Date().toISOString(),
396
- };
397
-
398
- const stagingManifestPath = join(stagingDir, MANIFEST_FILENAME);
399
- writeFileSync(
400
- stagingManifestPath,
401
- JSON.stringify(toolstoreManifest, null, 2) + "\n",
402
- { mode: 0o444 },
403
- );
404
-
405
- // Rename staging directory to final content-addressed path
406
- //
407
- // On POSIX, rename() is atomic within a filesystem. Since the
408
- // staging dir is in the same parent as the final dir, this is
409
- // a same-filesystem rename.
410
- renameSync(stagingDir, bundleDir);
411
- } catch (err) {
412
- // Clean up staging directory on failure
413
- try {
414
- rmSync(stagingDir, { recursive: true, force: true });
415
- } catch {
416
- // Best effort cleanup
417
- }
418
- // Re-check: another process may have published the same digest concurrently
419
- if (existsSync(bundleDir) && existsSync(manifestPath)) {
420
- return {
421
- success: true,
422
- deduplicated: true,
423
- bundlePath: bundleDir,
424
- };
425
- }
426
- return {
427
- success: false,
428
- deduplicated: false,
429
- bundlePath: "",
430
- error: `Failed to write bundle to toolstore: ${err instanceof Error ? err.message : String(err)}`,
431
- };
432
- }
433
-
434
- return {
435
- success: true,
436
- deduplicated: false,
437
- bundlePath: bundleDir,
438
- };
439
- }
440
-
441
- // ---------------------------------------------------------------------------
442
- // Reader helpers
443
- // ---------------------------------------------------------------------------
444
-
445
- /**
446
- * Read a published toolstore manifest by digest.
447
- *
448
- * Returns null if no bundle with the given digest is published or if the
449
- * digest is not a valid SHA-256 hex string.
450
- */
451
- export function readPublishedManifest(
452
- digest: string,
453
- cesMode?: CesMode,
454
- ): ToolstoreManifest | null {
455
- if (!isValidSha256Hex(digest)) {
456
- return null;
457
- }
458
-
459
- const toolstoreDir = getCesToolStoreDir(cesMode);
460
- const manifestPath = getBundleManifestPath(toolstoreDir, digest);
461
-
462
- if (!existsSync(manifestPath)) {
463
- return null;
464
- }
465
-
466
- try {
467
- const raw = readFileSync(manifestPath, "utf-8");
468
- return JSON.parse(raw) as ToolstoreManifest;
469
- } catch {
470
- return null;
471
- }
472
- }
473
-
474
- /**
475
- * Delete a published bundle from the toolstore by digest.
476
- *
477
- * Removes the entire content-addressed directory for the given digest.
478
- * Returns true if the directory existed and was deleted, false if it
479
- * did not exist (or the digest was invalid).
480
- *
481
- * This is used during tool unregistration to ensure that a previously
482
- * published bundle cannot be executed after the tool is removed from
483
- * the in-memory registry.
484
- */
485
- export function deleteBundleFromToolstore(
486
- digest: string,
487
- cesMode?: CesMode,
488
- ): boolean {
489
- if (!isValidSha256Hex(digest)) {
490
- return false;
491
- }
492
-
493
- const toolstoreDir = getCesToolStoreDir(cesMode);
494
- const bundleDir = getBundleDir(toolstoreDir, digest);
495
-
496
- if (!existsSync(bundleDir)) {
497
- return false;
498
- }
499
-
500
- rmSync(bundleDir, { recursive: true, force: true });
501
- return true;
502
- }
503
-
504
- /**
505
- * Check if a bundle with the given digest is published in the toolstore.
506
- *
507
- * Returns false if the digest is not a valid SHA-256 hex string.
508
- */
509
- export function isBundlePublished(
510
- digest: string,
511
- cesMode?: CesMode,
512
- ): boolean {
513
- if (!isValidSha256Hex(digest)) {
514
- return false;
515
- }
516
-
517
- const toolstoreDir = getCesToolStoreDir(cesMode);
518
- const bundleDir = getBundleDir(toolstoreDir, digest);
519
- const manifestPath = getBundleManifestPath(toolstoreDir, digest);
520
- return existsSync(bundleDir) && existsSync(manifestPath);
521
- }
522
-
523
- // ---------------------------------------------------------------------------
524
- // Symlink escape detection
525
- // ---------------------------------------------------------------------------
526
-
527
- /**
528
- * Recursively scan a directory for symlinks that point outside of it.
529
- *
530
- * Returns an array of relative paths (from `rootDir`) of symlinks whose
531
- * resolved target falls outside `rootDir`.
532
- */
533
- function findEscapingSymlinks(rootDir: string): string[] {
534
- const escaping: string[] = [];
535
- const realRoot = realpathSync(rootDir);
536
-
537
- function walk(dir: string): void {
538
- let entries: string[];
539
- try {
540
- entries = readdirSync(dir);
541
- } catch {
542
- return;
543
- }
544
-
545
- for (const entry of entries) {
546
- const fullPath = join(dir, entry);
547
- let stat;
548
- try {
549
- stat = lstatSync(fullPath);
550
- } catch {
551
- continue;
552
- }
553
-
554
- if (stat.isSymbolicLink()) {
555
- // Resolve the symlink target relative to its parent directory
556
- const linkTarget = readlinkSync(fullPath);
557
- const resolvedTarget = resolve(dir, linkTarget);
558
- // Check if the resolved target is outside the root directory
559
- if (!resolvedTarget.startsWith(realRoot + "/") && resolvedTarget !== realRoot) {
560
- const relativePath = fullPath.slice(realRoot.length + 1);
561
- escaping.push(relativePath);
562
- }
563
- } else if (stat.isDirectory()) {
564
- walk(fullPath);
565
- }
566
- }
567
- }
568
-
569
- walk(realRoot);
570
- return escaping;
571
- }